mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-15 07:04:26 +10:00
Compare commits
293 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9957e1fc1f | |||
| e38d03a32e | |||
| ea657a964a | |||
| 760d8e1db3 | |||
| 880df84828 | |||
| 815534d409 | |||
| becfbedc99 | |||
| 7da4e68384 | |||
| 5ea6b17b16 | |||
| 3707acb124 | |||
| 51d6210920 | |||
| 6a31d304d3 | |||
| 26fdf74d69 | |||
| b1de1b4bf0 | |||
| 3c26ce2d93 | |||
| cba1e5950c | |||
| 743ef7f124 | |||
| 13d3a107da | |||
| dfdcafb071 | |||
| 33b3b04294 | |||
| 6a7dd981a2 | |||
| d68f755677 | |||
| 264d2bcc9a | |||
| 8dd385beed | |||
| 2328848400 | |||
| 0287de1862 | |||
| 4dd472e3c3 | |||
| ed0e6b6554 | |||
| 6d258154a0 | |||
| bac82dc9e8 | |||
| 8682e6ea67 | |||
| 30a1a36ee4 | |||
| 3fe5cdf55d | |||
| 9ada4ea601 | |||
| 7a823654df | |||
| 425229fce8 | |||
| c4b3971bdd | |||
| a50c0d0b2d | |||
| 72448f67d0 | |||
| bc5f299350 | |||
| 6d97ad6855 | |||
| 1fbe1823cb | |||
| f941f0dbfa | |||
| 352a6c427e | |||
| 5f9259e41f | |||
| 75ae903b99 | |||
| 8892823337 | |||
| e738664f89 | |||
| e37616fa15 | |||
| 2763d83fe4 | |||
| 77018dc358 | |||
| 1f6b2dacf9 | |||
| 21c58d4e13 | |||
| 3af97ff6dd | |||
| 703d5a1ec4 | |||
| d2a6fbe182 | |||
| e801a497f8 | |||
| e92a66ff28 | |||
| 6900e5c3db | |||
| 966a8d0d2c | |||
| 3ec3b05fb8 | |||
| 14a93e9bf5 | |||
| c229b0369e | |||
| 9f332e93be | |||
| b472ea8c70 | |||
| a67c6d81c3 | |||
| 91ae4dab90 | |||
| 08ac60a408 | |||
| d4da34fcf7 | |||
| 74840d3baf | |||
| 4a72fbd1ad | |||
| dbe0a5411b | |||
| dc3325ec46 | |||
| a92e57bb64 | |||
| e21f3106d0 | |||
| 0dcb5f05f0 | |||
| f501d11ec6 | |||
| dfcf13a97b | |||
| ccd23c4b81 | |||
| 00636c9084 | |||
| accec1681b | |||
| 67238468ce | |||
| bc5b12f1ef | |||
| c09af98bef | |||
| ae32e76563 | |||
| 5572c9ee75 | |||
| f6cc000788 | |||
| 75b0d198bc | |||
| 1947cd9f3e | |||
| f1d93bd5e8 | |||
| f63d50f0da | |||
| eb597b6c68 | |||
| efe21c4e87 | |||
| 38fece3313 | |||
| 3af3cce606 | |||
| 026ec6f7de | |||
| eb50249b93 | |||
| ca6058eccd | |||
| 99c0ab7e22 | |||
| 2950a9a687 | |||
| 1b3de54873 | |||
| 20a9ef3c2b | |||
| a741e12ad1 | |||
| e54f30d6fb | |||
| e1d23ad2c7 | |||
| f07993b367 | |||
| 0e5f1a45c4 | |||
| f10aeaeba8 | |||
| 00e4f52d75 | |||
| 3ea2e4763e | |||
| 94d9afe8b1 | |||
| 7db3a12723 | |||
| fcf10b4a73 | |||
| 7f353490cf | |||
| 46683e0ec2 | |||
| 4e368d562d | |||
| 38f6e42796 | |||
| f56c28a27d | |||
| 92d3009eb4 | |||
| 7a4ac9ae9b | |||
| f8d00caae0 | |||
| e03d80b71f | |||
| b7d0db8d1c | |||
| 6ae3f612ae | |||
| 40d3941aab | |||
| e53c493e78 | |||
| 54e0dae172 | |||
| 066aba7c5d | |||
| 5e446207c6 | |||
| 609d0c8dbc | |||
| 820bac0db0 | |||
| d3c7d8e43a | |||
| 0c1e163b88 | |||
| d0d6a34fb5 | |||
| 16ce1359d7 | |||
| 9fe4a3710d | |||
| 8611adab1f | |||
| 7d457cb863 | |||
| 297516fc80 | |||
| 6b6a881c7a | |||
| 8ef8a38495 | |||
| ddcda4ba5a | |||
| 5cfe45b953 | |||
| b572314ae9 | |||
| e97fb9bd24 | |||
| 1c9c089a53 | |||
| cb3b5a84eb | |||
| a4bbeffddc | |||
| 37ec8f2f05 | |||
| 39cd6d5514 | |||
| 44eb4fad58 | |||
| 1a209cbcfc | |||
| 33a8f34463 | |||
| ce8e8f0d5b | |||
| aa2d0f1927 | |||
| 0757c8e53a | |||
| add4731d05 | |||
| 7dc162d968 | |||
| 8ba4bbfbc5 | |||
| cac6abfef1 | |||
| 5354acb1d3 | |||
| fae416fb34 | |||
| 69433b6d89 | |||
| ea3b9609fc | |||
| 20a9939314 | |||
| c7b7deb0f6 | |||
| 82e04e8090 | |||
| f299608296 | |||
| 7dcec5b4ee | |||
| e4684b585a | |||
| 8386f262e1 | |||
| 45cd8a56a3 | |||
| 754f8a6c62 | |||
| c4f54efd77 | |||
| 637e08d22c | |||
| 32dc0fca22 | |||
| b5aa294fc1 | |||
| 26516baf67 | |||
| 4879b136f8 | |||
| bdd7fc0cdd | |||
| 5ea044af10 | |||
| 9d20be1c06 | |||
| 9436c2d45a | |||
| 17e55e96bb | |||
| e4cfbb57b4 | |||
| d9f9ff58b4 | |||
| a059f1be45 | |||
| 9e46f8b44c | |||
| a934781009 | |||
| 5fe6738f25 | |||
| c1bcf261d7 | |||
| b570539a2d | |||
| 89a14c2719 | |||
| 4ad01ed43c | |||
| ffaa4033ae | |||
| 1a4fd1b477 | |||
| e1555ce380 | |||
| c7933d363b | |||
| 08ffb978cf | |||
| c5ec60638c | |||
| 75ec3b6116 | |||
| 45c9823c6f | |||
| 45658a7612 | |||
| a14833494e | |||
| 457b44de3a | |||
| 36d4a10396 | |||
| 77566b0fe1 | |||
| 7633327f45 | |||
| 6b4b2d7ce6 | |||
| 10b63e0df2 | |||
| ba6d751346 | |||
| 96d222a580 | |||
| 01ad8471cc | |||
| 2b826757cb | |||
| 9bf649e2c6 | |||
| c7a2bf9a95 | |||
| 82adbd761b | |||
| 9a8bdf00dc | |||
| 8b30342113 | |||
| 817c60a155 | |||
| f08e86cf97 | |||
| a6bb9490a1 | |||
| e4e8bfa4ef | |||
| d1e45fc2ba | |||
| 32fa96431e | |||
| 1e9508d401 | |||
| 36697c6e61 | |||
| c9145c99d3 | |||
| 6b6d9caeeb | |||
| d0e3767db6 | |||
| f9cb0c80a5 | |||
| a26d14bd46 | |||
| 411cd3f8d2 | |||
| 38f4de80b6 | |||
| 7de07c023f | |||
| c272c60f9a | |||
| eca78453d6 | |||
| 3754cf14ea | |||
| 834850fb51 | |||
| e7e2bb91b8 | |||
| 4c492f69ef | |||
| 50f2a8b439 | |||
| 2c8a15538e | |||
| 68eeefa04e | |||
| ebbc367fec | |||
| 2da8995d0b | |||
| 1c376b0056 | |||
| da70d5fc08 | |||
| f63bc4b787 | |||
| 9b1f1e1994 | |||
| 5f475fce4d | |||
| 7eff1df6e2 | |||
| bd030153c1 | |||
| 5140ff383d | |||
| dc57f9b9c0 | |||
| 53cd3f4461 | |||
| 35e296f1cd | |||
| 532401cc94 | |||
| 5321974cbb | |||
| 7c16dde989 | |||
| 9a75c912af | |||
| 14f3429eb5 | |||
| e49e80d330 | |||
| d07372c7e0 | |||
| 990f2bd33d | |||
| 29660d520e | |||
| 4f609f160f | |||
| e313bea3fc | |||
| 77be2b8e6f | |||
| c81c3efe7c | |||
| cac0cc15eb | |||
| b88e5e647a | |||
| 87d11c2e6b | |||
| 7b3c099736 | |||
| 11cb14a925 | |||
| d2df2b0bed | |||
| 723bf7293c | |||
| 0ef2194fb0 | |||
| 3664ae34cd | |||
| 1b94442ab6 | |||
| 3ae14781f0 | |||
| ecc496f2af | |||
| 87b25655d0 | |||
| c47a4cb622 | |||
| a30fc439f3 | |||
| afcc4db405 | |||
| 87bcb6a6a3 | |||
| 68bb031bb6 | |||
| c4f5c7b171 | |||
| 2bce14224d | |||
| fd305fd55b | |||
| d0dd805244 | |||
| 8668564464 |
+9
-1
@@ -33,6 +33,9 @@ migrate_working_dir/
|
||||
pubspec.lock
|
||||
/build/
|
||||
/coverage/
|
||||
# fvm project files
|
||||
.fvm/
|
||||
.fvmrc
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
@@ -58,6 +61,7 @@ secrets.dart
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
macos/Flutter/GeneratedPluginRegistrant.swift
|
||||
|
||||
# iOS
|
||||
**/ios/Pods/
|
||||
@@ -83,6 +87,10 @@ keystore.properties
|
||||
# IDE
|
||||
.vscode/launch.json
|
||||
.vscode/settings.json
|
||||
.contextstream/
|
||||
|
||||
# Cloudflare Wrangler
|
||||
.wrangler
|
||||
.wrangler
|
||||
|
||||
# Claude Code local working dir (worktrees, jobs, settings)
|
||||
.claude/
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## BLE Frames & Protocol Notes
|
||||
- Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`.
|
||||
- Discovery: scans for device name prefix `MeshCore-` and filters by `platformName`/`advertisementData.advName`.
|
||||
- Discovery: scans for device names matching known prefixes and filters by `platformName`/`advertisementData.advName`.
|
||||
- Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`.
|
||||
- Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57.
|
||||
- Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MeshCore Open - Flutter Client
|
||||
|
||||
Open-source Flutter client for MeshCore LoRa mesh networking devices.
|
||||
Open-source Flutter client for MeshCore LoRa mesh networking devices. Connects to MeshCore-compatible radios over **BLE, TCP, or USB serial** and provides direct/channel chat, contact and channel management, on-map node tracking, repeater administration, and on-device message translation.
|
||||
|
||||
## Build Commands
|
||||
|
||||
@@ -17,6 +17,9 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices.
|
||||
# Build iOS
|
||||
~/flutter/bin/flutter build ios
|
||||
|
||||
# Build versioned web release (uses build_pipe)
|
||||
~/flutter/bin/dart run build_pipe
|
||||
|
||||
# Run static analysis
|
||||
~/flutter/bin/flutter analyze
|
||||
|
||||
@@ -28,43 +31,132 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices.
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart # App entry point, MaterialApp setup with Provider
|
||||
├── connector/
|
||||
│ └── meshcore_connector.dart # BLE communication layer (MeshCoreConnector)
|
||||
├── screens/
|
||||
│ ├── scanner_screen.dart # BLE device scanning (home screen)
|
||||
│ ├── device_screen.dart # Connected device hub with navigation
|
||||
│ ├── chat_screen.dart # Chat interface (placeholder)
|
||||
│ ├── contacts_screen.dart # Contacts list (placeholder)
|
||||
│ └── settings_screen.dart # Device info and app settings
|
||||
└── widgets/
|
||||
└── device_tile.dart # Device list item with signal strength
|
||||
├── main.dart # Entry point: MultiProvider wiring, locale + theme, initial route
|
||||
├── connector/ # Unified BLE/TCP/USB transport layer
|
||||
│ ├── meshcore_connector.dart # Central state holder + ChangeNotifier (all transports)
|
||||
│ ├── meshcore_connector_tcp.dart # TCP transport helper
|
||||
│ ├── meshcore_connector_usb.dart # USB serial transport helper
|
||||
│ ├── meshcore_protocol.dart # Frame size + version constants
|
||||
│ └── meshcore_uuids.dart # Nordic UART UUIDs + scan name prefixes
|
||||
├── models/ # Plain data classes (Contact, Channel, Message, Community, …)
|
||||
├── services/ # ChangeNotifier services + IO services (retry, translation, ML, …)
|
||||
├── storage/ # SharedPreferences-backed stores, scoped per device key
|
||||
├── helpers/ # Pure utilities (Smaz compression, GIF parsing, scroll helpers, path hop resolution)
|
||||
├── utils/ # Platform / IO / UX utilities (logger, GPX export, dialogs)
|
||||
├── theme/ # MeshPalette (defined, not yet wired in main.dart)
|
||||
├── l10n/ # ARB localization for 18 locales
|
||||
├── icons/ # Custom icon widgets
|
||||
├── widgets/ # Reusable widgets (AppBar, BatteryUi, QR, jump-to-bottom, …)
|
||||
└── screens/ # ~26 screens — see Screens section below
|
||||
```
|
||||
|
||||
## Screens
|
||||
|
||||
All screens are fully implemented (no remaining placeholders).
|
||||
|
||||
### Connection / Scanning
|
||||
| Screen | Purpose |
|
||||
|---|---|
|
||||
| `scanner_screen.dart` | BLE device scan and connect — main entry point |
|
||||
| `tcp_screen.dart` | Connect to a MeshCore device over TCP/IP |
|
||||
| `usb_screen.dart` | Connect to a MeshCore device over USB serial |
|
||||
| `discovery_screen.dart` | Browse all discovered (non-contact) mesh nodes |
|
||||
| `chrome_required_screen.dart` | Web gate for non-Chrome browsers (BLE unavailable) |
|
||||
|
||||
### Chat / Messaging
|
||||
| Screen | Purpose |
|
||||
|---|---|
|
||||
| `chat_screen.dart` | Direct (private) messaging with a contact |
|
||||
| `channel_chat_screen.dart` | Group messaging inside a named channel |
|
||||
| `channels_screen.dart` | List and manage channels (add/edit/delete) |
|
||||
| `channel_message_path_screen.dart` | Hop-by-hop route a channel message took, with map overlay |
|
||||
|
||||
### Contacts / Neighbors
|
||||
| Screen | Purpose |
|
||||
|---|---|
|
||||
| `contacts_screen.dart` | Full contacts list with previews and management |
|
||||
| `neighbors_screen.dart` | Nodes directly heard by the connected radio (one-hop) |
|
||||
|
||||
### Repeater Management
|
||||
| Screen | Purpose |
|
||||
|---|---|
|
||||
| `repeater_hub_screen.dart` | Top-level repeater hub; navigates to sub-screens |
|
||||
| `repeater_status_screen.dart` | Live status of a managed repeater node |
|
||||
| `repeater_cli_screen.dart` | Raw command-line interface to a repeater |
|
||||
| `repeater_settings_screen.dart` | Full radio/node settings editor for a repeater |
|
||||
|
||||
### Map / Location
|
||||
| Screen | Purpose |
|
||||
|---|---|
|
||||
| `map_screen.dart` | Main map view of contacts/nodes with live GPS positions |
|
||||
| `line_of_sight_map_screen.dart` | Terrain LOS analysis between configurable endpoints |
|
||||
| `path_trace_map.dart` | Animates the hop path a direct message traveled |
|
||||
| `map_cache_screen.dart` | Download/clear offline map tile cache |
|
||||
| `community_qr_scanner_screen.dart` | Scan QR to join a mesh community/channel |
|
||||
|
||||
### Settings / Debug / Diagnostics
|
||||
| Screen | Purpose |
|
||||
|---|---|
|
||||
| `settings_screen.dart` | Connected device settings: radio params, identity, GPS |
|
||||
| `app_settings_screen.dart` | App preferences: theme, units, map source, notifications |
|
||||
| `app_debug_log_screen.dart` | In-app log viewer (app-layer messages) |
|
||||
| `ble_debug_log_screen.dart` | In-app log viewer (raw BLE frame traffic) |
|
||||
| `companion_radio_stats_screen.dart` | RF stats (RSSI, SNR, packet counts) for paired radio |
|
||||
| `telemetry_screen.dart` | Battery / sensor / environmental telemetry for a contact |
|
||||
|
||||
## Architecture
|
||||
|
||||
### State Management
|
||||
- **Provider** with `ChangeNotifier` pattern
|
||||
- `MeshCoreConnector` is the central state holder for BLE connection
|
||||
- Screens use `Consumer<MeshCoreConnector>` for reactive UI updates
|
||||
|
||||
`Provider` with `ChangeNotifier`. `main.dart` wires a `MultiProvider` with the following:
|
||||
|
||||
| Provider | Role |
|
||||
|---|---|
|
||||
| `MeshCoreConnector` | Active transport (BLE/TCP/USB), connection state, frame I/O |
|
||||
| `MessageRetryService` | ACK tracking and retry scheduling with backoff |
|
||||
| `PathHistoryService` | Per-contact routing history (LRU cache, 50 contacts) |
|
||||
| `AppSettingsService` | App preferences (theme, units, locale, notifications) |
|
||||
| `BleDebugLogService` | Raw BLE frame log buffer |
|
||||
| `AppDebugLogService` | Structured app log buffer |
|
||||
| `ChatTextScaleService` | Pinch-to-zoom text scale for chat screens |
|
||||
| `TranslationService` | On-device LLM translation (llamadart) |
|
||||
| `UiViewStateService` | Contacts/channels sort/filter/search state |
|
||||
| `TimeoutPredictionService` | ML linear regression for ACK timeout prediction |
|
||||
| `StorageService` | Path history + delivery observation persistence |
|
||||
| `MapTileCacheService` | OSM tile pre-cache |
|
||||
|
||||
Screens consume these via `Consumer<T>` (or `context.watch<T>()` / `context.read<T>()`) for reactive UI.
|
||||
|
||||
### Storage / Persistence
|
||||
|
||||
All stores in `lib/storage/` use `PrefsManager` (a `SharedPreferences` singleton initialized in `main()`). Most stores **scope keys by the first 10 hex chars of the connected device's public key**, so per-radio data is isolated.
|
||||
|
||||
| Store | Persists |
|
||||
|---|---|
|
||||
| `message_store`, `channel_message_store` | Direct + channel messages |
|
||||
| `contact_store`, `contact_discovery_store` | Known + discovered contacts |
|
||||
| `channel_store`, `channel_order_store`, `channel_settings_store` | Channels, display order, per-channel Smaz toggle |
|
||||
| `community_store` | Communities (32-byte shared secrets) |
|
||||
| `contact_group_store`, `contact_settings_store` | Groups, per-contact Smaz toggle |
|
||||
| `unread_store` | Per-contact unread counts (debounced writes) |
|
||||
|
||||
GGUF translation models are stored as files (not SharedPreferences) via `translation_file_store`.
|
||||
|
||||
### Theming
|
||||
- Material 3 design (`useMaterial3: true`)
|
||||
- System-based dark/light mode (`ThemeMode.system`)
|
||||
- Blue color scheme seed
|
||||
- `lib/theme/mesh_theme.dart` defines a warm-dark `MeshPalette` (phosphor-green accents) but is **not currently wired** in `main.dart` — available for a future redesign
|
||||
|
||||
## BLE Protocol
|
||||
### Localization
|
||||
|
||||
### Nordic UART Service (NUS)
|
||||
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
|
||||
- **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device)
|
||||
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
|
||||
18 locales supported via Flutter's standard ARB pipeline (`lib/l10n/`): en, de, es, fr, it, pt, ru, uk, bg, hu, ja, ko, nl, pl, sk, sl, sv, zh. Language override comes from `AppSettingsService.settings.languageOverride`. Use the `context.l10n` extension (`lib/l10n/l10n.dart`) for translated strings; contact-type names live in `contact_localization.dart`.
|
||||
|
||||
### Device Discovery
|
||||
- Scans for devices with name prefix `MeshCore-`
|
||||
- Filters by `platformName` or `advertisementData.advName`
|
||||
## Transports
|
||||
|
||||
### Connection States
|
||||
`MeshCoreConnector` unifies all three transports under one `ChangeNotifier`. There is **no shared base class** — selection is via the `MeshCoreTransportType { bluetooth, usb, tcp }` enum, and BLE/TCP/USB share the same connection-state enum, send/receive API, and frame protocol.
|
||||
|
||||
### Connection State
|
||||
```dart
|
||||
enum MeshCoreConnectionState {
|
||||
disconnected,
|
||||
@@ -75,28 +167,137 @@ enum MeshCoreConnectionState {
|
||||
}
|
||||
```
|
||||
|
||||
### Frame I/O
|
||||
- **Send**: `MeshCoreConnector.sendFrame(Uint8List data)`
|
||||
- **Receive**: `MeshCoreConnector.receivedFrames` stream of `Uint8List`
|
||||
### Frame I/O (all transports)
|
||||
- **Send**: `MeshCoreConnector.sendFrame(Uint8List data, {String? channelSendQueueId, bool expectsGenericAck})`
|
||||
- **Receive**: `Stream<Uint8List> get receivedFrames`
|
||||
- **Protocol constants** (`meshcore_protocol.dart`): `maxFrameSize = 172`, `maxTextPayloadBytes = 160`, `appProtocolVersion = 4`
|
||||
|
||||
### BLE — Nordic UART Service (NUS)
|
||||
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
|
||||
- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
|
||||
- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
|
||||
- **Discovery**: scans for devices whose name starts with `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, or `LowMesh_MC_` (filters on both `platformName` and `advertisementData.advName`)
|
||||
- **Linux**: `linux_ble_pairing_service.dart` falls back to `bluetoothctl` when BlueZ agent prompts fail
|
||||
|
||||
### TCP
|
||||
- Manual host/port entry, persisted via `AppSettingsService` (`tcpServerAddress`, `tcpServerPort`)
|
||||
- UI hint: `192.168.40.10` / port `5000`
|
||||
- Disabled on web (`PlatformInfo.isWeb`)
|
||||
- API: `MeshCoreConnector.connectTcp(host: ..., port: ...)`
|
||||
|
||||
### USB Serial (flserial)
|
||||
- Default baud rate: `115200`
|
||||
- Port enumeration: `MeshCoreConnector.listUsbPorts()`
|
||||
- COBS-framed packets via `usb_serial_frame_codec.dart`
|
||||
- macOS device-name resolution via `ioreg` (`utils/macos_usb_device_names.dart`)
|
||||
- API: `MeshCoreConnector.connectUsb(portName: ..., baudRate: 115200)`
|
||||
|
||||
## Dependencies
|
||||
|
||||
App version: `9.5.0+13` — Dart SDK constraint: `^3.9.2`
|
||||
|
||||
**Connectivity**
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| flutter_blue_plus | ^2.1.0 | BLE communication |
|
||||
| provider | ^6.1.5+1 | State management |
|
||||
| cupertino_icons | ^1.0.8 | iOS-style icons |
|
||||
| flutter_blue_plus | ^2.1.0 | BLE scanning, connecting, and UART data transfer |
|
||||
| flutter_blue_plus_platform_interface | ^9.0.2 | Platform-interface layer required by flutter_blue_plus |
|
||||
| flserial | git (MeshEnvy fork) | USB serial transport for wired device connections (TODO: upstream pending) |
|
||||
|
||||
**State / Storage**
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| provider | ^6.1.5+1 | ChangeNotifier-based state management across screens |
|
||||
| shared_preferences | ^2.2.2 | Persistent key-value storage for user settings |
|
||||
| path_provider | ^2.1.5 | Locates platform-appropriate directories for file I/O |
|
||||
|
||||
**Crypto**
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| crypto | ^3.0.3 | SHA/HMAC hashing used in message authentication |
|
||||
| pointycastle | ^4.0.0 | AES encryption/decryption for channel and direct messages |
|
||||
| uuid | ^4.3.3 | Generates UUIDs for message and contact identity |
|
||||
|
||||
**Maps & Location**
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| flutter_map | ^8.2.2 | Interactive tile map for node positions and path traces |
|
||||
| latlong2 | ^0.9.1 | LatLng coordinate type used throughout map and GPS code |
|
||||
| gpx | ^2.3.0 | Export node paths as GPX track files |
|
||||
|
||||
**UI**
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| material_symbols_icons | ^4.2928.1 | Extended Material Symbols icon set (line-of-sight, etc.) |
|
||||
| flutter_svg | ^2.0.10+1 | Renders SVG assets (custom icons such as LoS indicator) |
|
||||
| cached_network_image | ^3.4.1 | Caches map tile images downloaded over the network |
|
||||
| flutter_cache_manager | ^3.4.1 | Underlying cache manager used by cached_network_image |
|
||||
| flutter_linkify | ^6.0.0 | Auto-detects and makes URLs tappable in chat messages |
|
||||
| mobile_scanner | ^7.1.4 | QR/barcode scanning for contact and channel import |
|
||||
| qr_flutter | ^4.1.0 | Generates QR codes for sharing contacts and channels |
|
||||
| cupertino_icons | ^1.0.8 | iOS-style icon font (bundled for completeness) |
|
||||
| characters | ^1.4.0 | Unicode-aware string operations for message text handling |
|
||||
|
||||
**Notifications / Background**
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| flutter_local_notifications | ^22.0.0 | Shows local push notifications for incoming messages |
|
||||
| flutter_foreground_task | ^9.2.0 | Keeps the app alive in background to maintain BLE/USB connection |
|
||||
|
||||
**ML / AI**
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| ml_algo | ^16.0.0 | OLS regression used in `timeout_prediction_service.dart` to predict message ACK timeouts |
|
||||
| ml_dataframe | ^1.0.0 | DataFrame input format required by ml_algo |
|
||||
| llamadart | ^0.8.0 | On-device LLM inference used in `translation_service.dart` for message translation |
|
||||
| flutter_langdetect | ^0.0.1 | Detects a message's source language in `translation_service.dart` before translating |
|
||||
|
||||
**Misc**
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| http | ^1.2.0 | Fetches tile URLs and any remote API calls |
|
||||
| url_launcher | ^6.3.0 | Opens URLs in the system browser from linkified chat text |
|
||||
| share_plus | ^13.1.0 | Shares files (e.g. exported GPX tracks) via the system share sheet |
|
||||
| package_info_plus | ^10.1.0 | Reads app version/build number displayed in settings |
|
||||
| web | ^1.1.1 | Web-platform APIs for USB serial and browser detection on Flutter Web |
|
||||
| intl | any | Internationalization and locale formatting (required by flutter_localizations) |
|
||||
| build_pipe | ^0.3.1 | CI/CD build pipeline configuration (web release builds with versioned assets) |
|
||||
|
||||
## Platform Configuration
|
||||
|
||||
### Android (`android/app/src/main/AndroidManifest.xml`)
|
||||
- `BLUETOOTH`, `BLUETOOTH_ADMIN` (API 30 and below)
|
||||
- `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `BLUETOOTH_ADVERTISE` (API 31+)
|
||||
- `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` (for BLE scanning)
|
||||
- `INTERNET` (map tiles, translation model downloads)
|
||||
- `BLUETOOTH`, `BLUETOOTH_ADMIN` (API ≤ 30)
|
||||
- `BLUETOOTH_SCAN` (with `neverForLocation`), `BLUETOOTH_CONNECT`, `BLUETOOTH_ADVERTISE` (API 31+)
|
||||
- `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` (BLE scanning on API ≤ 30)
|
||||
- `POST_NOTIFICATIONS` (API 33+)
|
||||
- `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_CONNECTED_DEVICE` (background BLE/USB connection)
|
||||
- `WAKE_LOCK`
|
||||
- `CAMERA` (QR scanning, declared as optional feature)
|
||||
- USB host hardware feature (optional)
|
||||
|
||||
`flutter_foreground_task` registers a `ForegroundService` with `foregroundServiceType="connectedDevice"` and `stopWithTask="false"`.
|
||||
|
||||
**Build config (`android/app/build.gradle.kts`)**: `applicationId = com.meshcore.meshcore_open`, NDK `29.0.14206865`, Java 8 core-library desugaring (`desugar_jdk_libs:2.1.4`), release signing via `key.properties` (debug fallback).
|
||||
|
||||
### iOS (`ios/Runner/Info.plist`)
|
||||
- `NSBluetoothAlwaysUsageDescription`
|
||||
- `NSBluetoothPeripheralUsageDescription`
|
||||
- `NSBluetoothAlwaysUsageDescription`, `NSBluetoothPeripheralUsageDescription`
|
||||
- `NSCameraUsageDescription` (QR scanning to join communities)
|
||||
- Background modes: `bluetooth-central`
|
||||
- `LSApplicationQueriesSchemes`: `http`, `https`
|
||||
|
||||
### Web (`web/`)
|
||||
PWA scaffold present but boilerplate (`manifest.json` and `index.html` are unmodified Flutter defaults). BLE is unsupported in browsers; TCP and Web Serial USB may work in Chrome only. `ChromeRequiredScreen` gates non-Chrome web users. Versioned releases are produced via `build_pipe` (`?v=<pubspec version>` cache busting, no service worker).
|
||||
|
||||
### Desktop
|
||||
`linux/`, `windows/`, and `macos/` directories are present as Flutter scaffolds. No app-specific native config has been added; BLE on desktop has not been validated.
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
@@ -123,14 +324,14 @@ enum MeshCoreConnectionState {
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `lib/connector/meshcore_connector.dart` | All BLE logic - scanning, connecting, data transfer |
|
||||
| `lib/screens/scanner_screen.dart` | Entry point UI, device list |
|
||||
| `lib/main.dart` | App configuration, theme, Provider setup |
|
||||
| `pubspec.yaml` | Dependencies and project metadata |
|
||||
|
||||
## Placeholder Screens
|
||||
|
||||
The following screens are implemented as placeholders and need full implementation:
|
||||
- `chat_screen.dart` - Mesh chat functionality
|
||||
- `contacts_screen.dart` - Contact management
|
||||
- `settings_screen.dart` - Radio settings, node identity, location (partially implemented)
|
||||
| `lib/main.dart` | App configuration, MultiProvider setup, theme, locale, initial route |
|
||||
| `lib/connector/meshcore_connector.dart` | Unified BLE/TCP/USB transport state holder |
|
||||
| `lib/connector/meshcore_protocol.dart` | Frame size limits and protocol version |
|
||||
| `lib/connector/meshcore_uuids.dart` | NUS UUIDs and BLE scan name prefixes |
|
||||
| `lib/services/app_settings_service.dart` | App-wide settings (`AppSettings` JSON in SharedPreferences) |
|
||||
| `lib/services/storage_service.dart` | Path history + delivery observation persistence |
|
||||
| `lib/services/message_retry_service.dart` | ACK tracking + retry scheduling |
|
||||
| `lib/services/translation_service.dart` | On-device LLM translation (llamadart) |
|
||||
| `lib/storage/prefs_manager.dart` | SharedPreferences singleton initialized in `main()` |
|
||||
| `lib/screens/scanner_screen.dart` | Home screen — BLE scan and connect |
|
||||
| `pubspec.yaml` | Dependencies and project metadata (current version `9.5.0+13`) |
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# How to contribute to Meshcore Open
|
||||
|
||||
Before submitting any pull requests (PR), please review the following information.
|
||||
|
||||
Unsolicited PRs without previous discussion or open issues may be
|
||||
rejected. As may changes that are too broad (i.e. 100 files changed) or that
|
||||
cover too many separate changes. If the changes are clearly AI generated they
|
||||
may also be rejected. [See more](#ai-use)
|
||||
|
||||
## First Step Checklist
|
||||
|
||||
### **Did you find a bug?**
|
||||
|
||||
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/zjs81/meshcore-open/issues).
|
||||
|
||||
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/zjs81/meshcore-open/issues/new).
|
||||
Be sure to include a **title and clear description**, as much relevant
|
||||
information as possible, and a **code sample** or an **executable test case**
|
||||
demonstrating the expected behavior that is not occurring. You can also include
|
||||
screenshots or video.
|
||||
|
||||
* DO NOT start work and submit a PR at this time, please discuss the issue and
|
||||
your implementation plan first.
|
||||
|
||||
### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
|
||||
|
||||
Changes that are cosmetic in nature and do not add anything substantial to the
|
||||
stability, functionality, or testability of the application will generally not
|
||||
be accepted.
|
||||
|
||||
### **Do you intend to add a new feature or change an existing one?**
|
||||
|
||||
* Suggest your change in a new issue as a feature request.
|
||||
|
||||
* DO NOT start work and submit a PR at this time, please discuss the change and
|
||||
your implementation plan first.
|
||||
|
||||
* After it is generally decided that the feature or change fits the goals of the
|
||||
project you can start work or open a PR if you have already started.
|
||||
|
||||
## Submitting your patch
|
||||
|
||||
* All changes should be based on the `dev` branch. When creating your PR please
|
||||
be sure to change the target to merge into dev, and when starting work on a new
|
||||
branch be sure to start on latest `dev`.
|
||||
|
||||
* Ensure the PR description clearly describes the problem and solution. Include
|
||||
the relevant issue number if applicable.
|
||||
|
||||
* The PR should contain **one commit** only, the commit message should have a
|
||||
clear title followed by a new line and then brief description if needed. PR with
|
||||
multiple commits will be squashed into one before merging if required. See
|
||||
[Git Mastery](https://git-mastery.org/lessons/commitMessage/) for more
|
||||
information on good commit messages.
|
||||
|
||||
* **Before committing changes** on your branch, be sure to run both
|
||||
`dart format .` and `flutter analyze`. The continuous development checks will
|
||||
fail if issues here are not addressed before hand.
|
||||
|
||||
## AI-use
|
||||
|
||||
Everyone loves some help, AI agents are a tool in many of our belts. The project
|
||||
is not anti-AI.
|
||||
|
||||
There are some limits to acceptable use however. Generally:
|
||||
|
||||
* All code generated by AI should be thoroughly reviewed by the contributor.
|
||||
* The changes should be tightly controlled to not change anything out of scope
|
||||
for the patch, bug fix, etc.
|
||||
* The contributor should have a good understanding of what the code does and how
|
||||
the application works in order to effectively be able to manage the agent.
|
||||
@@ -6,6 +6,8 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices.
|
||||
|
||||
MeshCore Open is a cross-platform mobile application for communicating with MeshCore LoRa mesh network devices via Bluetooth Low Energy (BLE). The app enables long-range, off-grid communication through peer-to-peer messaging, public channels, and mesh networking capabilities.
|
||||
|
||||
**Website:** [meshcoreopen.org](https://meshcoreopen.org/)
|
||||
|
||||
<a href="http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/zjs81/meshcore-open">
|
||||
<img src="assets/badges/badge_obtainium.png" height="80" align="center" alt="Get it on Obtainium"/>
|
||||
</a>
|
||||
@@ -92,12 +94,12 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
|---------|---------|
|
||||
| flutter_blue_plus | Bluetooth Low Energy communication |
|
||||
| provider | State management |
|
||||
| sqflite | Local database storage |
|
||||
| shared_preferences | Local key-value storage (scoped per device) |
|
||||
| flutter_map | Interactive map display |
|
||||
| latlong2 | Geographic coordinate handling |
|
||||
| flutter_local_notifications | Background notification support |
|
||||
| smaz | Message compression |
|
||||
| pointycastle | Cryptographic operations |
|
||||
| llamadart | On-device LLM message translation |
|
||||
| intl | Internationalization and date formatting |
|
||||
|
||||
## Getting Started
|
||||
@@ -150,7 +152,8 @@ lib/
|
||||
├── main.dart # App entry point
|
||||
├── connector/
|
||||
│ ├── meshcore_connector.dart # BLE communication & state management
|
||||
│ └── meshcore_protocol.dart # Protocol definitions & frame parsing
|
||||
│ ├── meshcore_protocol.dart # Protocol definitions & frame parsing
|
||||
│ └── meshcore_uuids.dart # Device names and IDs (add prefixes here!)
|
||||
├── screens/
|
||||
│ ├── scanner_screen.dart # Device scanning (home screen)
|
||||
│ ├── contacts_screen.dart # Contact list
|
||||
@@ -184,7 +187,16 @@ lib/
|
||||
|
||||
### Device Discovery
|
||||
|
||||
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-`
|
||||
Devices are discovered by scanning for BLE advertisements with known MeshCore device name prefixes. These are currently:
|
||||
- `MeshCore-`
|
||||
- `Whisper-`
|
||||
- `WisCore-`
|
||||
- `HT-`
|
||||
- `LowMesh_MC_`
|
||||
- `NRF52`
|
||||
|
||||
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
|
||||
|
||||
|
||||
### Message Format
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
@@ -21,7 +21,12 @@ The MeshCore BLE protocol implements a binary frame-based communication system u
|
||||
|
||||
### Connection Flow
|
||||
|
||||
1. **Scan** for devices with name prefix `MeshCore-`
|
||||
1. **Scan** for devices with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
|
||||
- `MeshCore-`
|
||||
- `Whisper-`
|
||||
- `WisCore-`
|
||||
- `HT-`
|
||||
- `LowMesh_MC_`
|
||||
2. **Connect** with 15-second timeout
|
||||
3. **Request MTU** of 185 bytes (falls back to default if unsupported)
|
||||
4. **Discover services** and locate NUS characteristics
|
||||
|
||||
@@ -25,8 +25,8 @@ A bottom sheet with a search field and a grid of GIF thumbnails.
|
||||
### How to Access
|
||||
App Settings → Appearance → Language
|
||||
|
||||
### Supported Languages (15)
|
||||
English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian
|
||||
### Supported Languages (18)
|
||||
English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean
|
||||
|
||||
### How It Works
|
||||
- All UI strings go through Flutter's ARB localization system
|
||||
@@ -185,3 +185,78 @@ An ML-based service that predicts expected delivery timeouts:
|
||||
- Blends per-contact statistics with ML predictions
|
||||
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
|
||||
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
|
||||
|
||||
---
|
||||
|
||||
## On-Device Message Translation
|
||||
|
||||
### What It Is
|
||||
An optional on-device translation service powered by an embedded LLM (llamadart, running GGUF models). Translation runs entirely on-device — no data leaves the app.
|
||||
|
||||
### How to Access
|
||||
Tap the translate button on any received message. On first use, the GGUF model file is downloaded and cached locally.
|
||||
|
||||
### How It Works
|
||||
- Model files are managed by `TranslationFileStore`; download progress is shown in-place
|
||||
- Before translating, the source language is automatically detected using the `flutter_langdetect` package. If the detected language already matches the target language, translation is skipped
|
||||
- Translation runs via `TranslationService` using the llamadart CPU backend (arm64 and x64 on Android)
|
||||
- Translated text is shown in `TranslatedMessageContent` as an inline overlay on the original message bubble
|
||||
- Each translation is cached; re-tapping shows the cached result without re-running inference
|
||||
|
||||
---
|
||||
|
||||
## Emoji Reactions
|
||||
|
||||
### How to Access
|
||||
Long-press a message bubble in any direct or channel chat, then select a reaction emoji.
|
||||
|
||||
### What the User Sees
|
||||
An emoji picker inline with common reactions. Selected reactions appear below the message bubble with a count.
|
||||
|
||||
### How It Works
|
||||
- Implemented via `emoji_picker.dart` and `reaction_helper.dart`
|
||||
- Reactions are transmitted as a special message type visible to all participants with MeshCore Open
|
||||
|
||||
---
|
||||
|
||||
## Linkification
|
||||
|
||||
### What It Does
|
||||
URLs and `meshcore://` URIs in received messages are automatically detected and rendered as tappable links.
|
||||
|
||||
### How It Works
|
||||
- Powered by the `flutter_linkify` package via `link_handler.dart`
|
||||
- Tapping a URL opens the system browser; tapping a `meshcore://` URI imports the contact
|
||||
|
||||
---
|
||||
|
||||
## GPX Export
|
||||
|
||||
### How to Access
|
||||
Settings → Export section (three options: Export Repeaters, Export Contacts, Export All).
|
||||
|
||||
### What It Does
|
||||
Exports contacts with GPS coordinates to a `.gpx` file via the OS share sheet. Not available on web.
|
||||
|
||||
---
|
||||
|
||||
## Pinch-to-Zoom Chat Text
|
||||
|
||||
### What It Does
|
||||
Users can pinch to scale all chat text up or down within a session.
|
||||
|
||||
### How It Works
|
||||
- Implemented via `ChatTextScaleService` and `ChatZoomWrapper`
|
||||
- Scale range: 0.8× to 1.8×
|
||||
- The chosen scale persists across the session via the service
|
||||
|
||||
---
|
||||
|
||||
## Background Service (Android)
|
||||
|
||||
### What It Does
|
||||
On Android, a foreground service (`background_service.dart`) keeps the BLE connection and message handling alive when the app is in the background. On other platforms this is a no-op.
|
||||
|
||||
### User Impact
|
||||
- A persistent notification appears while the service is running
|
||||
- Messages are received and retry logic continues even when the app is not in the foreground
|
||||
|
||||
@@ -33,7 +33,6 @@ RX (device → host): [0x3E][len_lo][len_hi][payload...]
|
||||
- Length: 2-byte little-endian, payload only
|
||||
- Max payload: 172 bytes
|
||||
- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving
|
||||
- USB: 10ms post-write delay between frames
|
||||
|
||||
## Connection State Machine
|
||||
|
||||
@@ -49,8 +48,16 @@ enum MeshCoreConnectionState {
|
||||
|
||||
## BLE Connection Lifecycle
|
||||
|
||||
1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]`
|
||||
2. **Connect** with 15-second timeout
|
||||
1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
|
||||
- `MeshCore-`
|
||||
- `Whisper-`
|
||||
- `WisCore-`
|
||||
- `Seeed`
|
||||
- `Lilygo`
|
||||
- `HT-`
|
||||
- `LowMesh_MC_`
|
||||
- `NRF52`
|
||||
2. **Connect** with 15-second timeout (6 seconds on Linux)
|
||||
3. **Request MTU** 185 bytes (non-web only)
|
||||
4. **Discover services** and locate NUS
|
||||
5. **Enable TX notifications** (up to 3 attempts on native)
|
||||
@@ -74,7 +81,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||
| Max path size | 64 bytes | Maximum path data |
|
||||
| Max name size | 32 bytes | Maximum node name |
|
||||
| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` |
|
||||
| App protocol version | 3 | Sent in device query |
|
||||
| App protocol version | 4 | Sent in device query |
|
||||
| Contact frame size | 148 bytes | Fixed-size contact record |
|
||||
|
||||
## Command Codes (App → Device)
|
||||
@@ -109,13 +116,15 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||
| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
|
||||
| 36 | CMD_SEND_TRACE_PATH | Request path trace |
|
||||
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
|
||||
| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry |
|
||||
| 39 | CMD_SEND_TELEMETRY_REQ | Request sensor telemetry |
|
||||
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
|
||||
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
|
||||
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
|
||||
| 56 | CMD_GET_STATS | Request companion radio stats |
|
||||
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
|
||||
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
|
||||
| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
|
||||
| 61 | CMD_SET_PATH_HASH_MODE | Set path hash width (bytes per hop) |
|
||||
|
||||
## Response / Push Codes (Device → App)
|
||||
|
||||
@@ -139,6 +148,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||
| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
|
||||
| 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
|
||||
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
|
||||
| 24 | RESP_CODE_STATS | Companion radio stats |
|
||||
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
|
||||
| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
|
||||
| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
|
||||
|
||||
+11
-21
@@ -4,7 +4,7 @@
|
||||
|
||||
Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh.
|
||||
|
||||
Up to 8 channels (indices 0–7) can be active simultaneously on one device.
|
||||
The number of active channels is determined by the firmware (default 40); the device reports its actual limit at login.
|
||||
|
||||
## How to Access
|
||||
|
||||
@@ -17,7 +17,7 @@ QuickSwitchBar tab 1 (middle) from any main screen.
|
||||
| Public | Globe | Green | Fixed well-known PSK; any device can join |
|
||||
| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention |
|
||||
| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key |
|
||||
| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret |
|
||||
| Community | Groups/Tag | Magenta | PSK derived via HMAC-SHA256 from a community's shared secret |
|
||||
|
||||
## Channels List Screen
|
||||
|
||||
@@ -26,13 +26,12 @@ QuickSwitchBar tab 1 (middle) from any main screen.
|
||||
- **Search bar** with live text filtering (300ms debounce)
|
||||
- **Sort/filter button**
|
||||
- **Scrollable list of channel cards**, each showing:
|
||||
- Type icon with color coding (purple badge overlay for community channels)
|
||||
- Type icon with color coding (magenta badge overlay for community channels)
|
||||
- Channel name (or "Channel N" if unnamed)
|
||||
- Subtitle: "Public channel", "Hashtag channel", "Private channel", or "Community channel - {name}"
|
||||
- Unread badge (if messages are unread)
|
||||
- Drag handle (when manual sort is active)
|
||||
- **"+" FAB** to add a new channel
|
||||
- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings
|
||||
- **Overflow menu**: Disconnect, Manage Communities, Settings
|
||||
|
||||
If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown.
|
||||
|
||||
@@ -60,7 +59,7 @@ Tap the "+" FAB to open a dialog with six options:
|
||||
|
||||
| Action | Description |
|
||||
|---|---|
|
||||
| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) |
|
||||
| Edit | Change name, PSK (with a dice icon to generate a random PSK), SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit), or Cyr2Lat encoding toggle (transliterates Cyrillic to Latin for compatibility) |
|
||||
| Mute / Unmute | Toggle push notification suppression for this channel |
|
||||
| Delete | Remove the channel from the device (confirmation required) |
|
||||
|
||||
@@ -70,9 +69,9 @@ Tap a channel card to open the channel chat screen.
|
||||
|
||||
### App Bar
|
||||
|
||||
- Type icon (public/private/hashtag)
|
||||
- Type icon: globe for public channels, tag (#) for all other channel types
|
||||
- Channel name
|
||||
- Subtitle: "{type} - {N} unread"
|
||||
- Subtitle: "{Public|Private} • {N} unread" (e.g., "Public • 3 unread")
|
||||
|
||||
### Message Display
|
||||
|
||||
@@ -101,8 +100,7 @@ Tap a channel card to open the channel chat screen.
|
||||
|
||||
### Message Path Viewing
|
||||
|
||||
- **Mobile**: Tap a message bubble to view its routing path
|
||||
- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop)
|
||||
- **All platforms**: Long-press (or right-click on desktop) a message bubble → "Path"
|
||||
- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
|
||||
|
||||
### Context Actions (Long-Press / Right-Click)
|
||||
@@ -110,20 +108,12 @@ Tap a channel card to open the channel chat screen.
|
||||
| Action | Availability | Description |
|
||||
|---|---|---|
|
||||
| Reply | All messages | Triggers reply mode |
|
||||
| Path | Desktop only | Opens message path view |
|
||||
| Path | All messages | Opens message path view |
|
||||
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
|
||||
| Copy | All messages | Copies text to clipboard |
|
||||
| Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread |
|
||||
| Delete | All messages | Removes locally (not from mesh) |
|
||||
|
||||
### Message Path Viewing
|
||||
|
||||
Tap a message bubble to open the Channel Message Path Screen, which shows:
|
||||
- Each hop in the path as a visual chain
|
||||
- Known contacts identified by name at each hop
|
||||
- Observed vs. declared hop counts
|
||||
- Alternative path variants (if received via multiple paths)
|
||||
- Map view buttons for geographic path visualization
|
||||
|
||||
## Communities
|
||||
|
||||
Communities are a layer above channels that provide a private namespace.
|
||||
@@ -151,7 +141,7 @@ From the channels screen overflow menu → "Manage Communities". Opens a draggab
|
||||
- **Tap a community** to directly show its QR code for sharing
|
||||
- **Popup menu** per community:
|
||||
- **Show QR** — displays the QR code for sharing with new members
|
||||
- **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
|
||||
- **Leave Community** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
|
||||
|
||||
## How Channels Differ from Direct Messages
|
||||
|
||||
|
||||
@@ -18,17 +18,15 @@ From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
|
||||
|
||||
- **Title**: Contact name
|
||||
- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details.
|
||||
- **Action buttons**:
|
||||
- **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing
|
||||
- **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries.
|
||||
- **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle
|
||||
- **Action button**:
|
||||
- **Overflow menu** (⋮ icon): Contains Routing, Info, Telemetry, Settings, and Clear Chat. Routing opens the routing sheet where you can switch between Auto, Direct, and Flood routing and manage recent paths (hop count, round-trip time, age, success count, color-coded by repeater). Info shows a dialog with contact type, path, GPS coordinates, and public key.
|
||||
|
||||
### Message List
|
||||
|
||||
- Scrollable list with newest messages at the bottom
|
||||
- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background
|
||||
- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name)
|
||||
- Bubble width capped at 65% of screen width
|
||||
- Bubble width capped at 72% of screen width
|
||||
- Hyperlinks rendered as tappable green underlined text
|
||||
- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset
|
||||
- **Jump to bottom**: Floating button appears when scrolled away from the bottom
|
||||
@@ -37,6 +35,7 @@ From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
|
||||
### Input Bar
|
||||
|
||||
- **GIF button** (left): Opens GIF picker bottom sheet
|
||||
- **Translation button** (optional, between GIF and text field): Shown only when translation is enabled in App Settings. Tap to configure outgoing-message translation language and on/off toggle.
|
||||
- **Text field** (center): Auto-capitalization, enforces UTF-8 byte limit in real-time
|
||||
- **Send button** (right): Submits the message
|
||||
- On desktop: Enter/Numpad Enter also submits
|
||||
@@ -66,8 +65,8 @@ Outgoing messages display a status indicator:
|
||||
|
||||
When enabled in App Settings, additional metadata appears inside each bubble:
|
||||
- Timestamp (HH:MM)
|
||||
- Retry count (e.g., "Retry 2 of 4")
|
||||
- Status icon
|
||||
- Retry count (e.g., "Retry 2 of 4") — only shown for outgoing messages where at least one retry has occurred
|
||||
- Status icon (outgoing only)
|
||||
- Round-trip time in seconds (if delivered)
|
||||
|
||||
## Message Length Limits
|
||||
@@ -86,7 +85,7 @@ When a direct message is sent:
|
||||
|
||||
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
|
||||
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
|
||||
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise `3000 + 3000 × path_length` milliseconds (15000ms for flood)
|
||||
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise from the device's own `est_timeout` in `RESP_CODE_SENT` (clamped to the physics range); otherwise calculated from LoRa airtime physics: `500 + (airtime × 6 + 250) × (pathLength + 1)` ms for direct paths, `500 + 16 × airtime` ms for flood (airtime is estimated from the radio's current spreading factor, bandwidth, and coding rate). The result is capped at 45 seconds.
|
||||
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
|
||||
5. **Max retries**: Configurable (default 5, range 2–10)
|
||||
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
|
||||
@@ -113,8 +112,10 @@ Add emoji reactions to incoming messages (not your own):
|
||||
| Action | Availability | Description |
|
||||
|---|---|---|
|
||||
| Add reaction | Incoming messages only | Opens emoji picker |
|
||||
| View path | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path |
|
||||
| View path | All platforms: long-press/right-click menu | Shows message routing path |
|
||||
| Copy | All messages | Copies text to clipboard |
|
||||
| Translate | Incoming messages only (when translation is enabled and not yet translated) | Translates the message on-demand using the on-device model |
|
||||
| Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread |
|
||||
| Delete | All messages | Removes locally (not from mesh) |
|
||||
| Retry | Failed outgoing messages | Re-sends the message |
|
||||
| Open chat with sender | Room server chats | Opens 1:1 chat with the message sender |
|
||||
|
||||
+17
-17
@@ -6,18 +6,17 @@ The Contacts screen is the primary hub for managing mesh nodes your radio has a
|
||||
|
||||
## How to Access
|
||||
|
||||
- Automatically shown after connecting to a device
|
||||
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens
|
||||
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens (Channels is shown first after connecting)
|
||||
- Back navigation from Chat or Settings screens
|
||||
|
||||
## Contact Types
|
||||
|
||||
| Type | Avatar Color | Icon | Description |
|
||||
|---|---|---|---|
|
||||
| Chat | Blue | Chat bubble | Another user's mesh radio |
|
||||
| Repeater | Orange | Cell tower | A mesh repeater/relay node |
|
||||
| Room | Purple | Group | A room server for group chat |
|
||||
| Sensor | Green | Sensors | A sensor device |
|
||||
| Chat | Blue | Initials / emoji | Another user's mesh radio |
|
||||
| Repeater | Amber | Cell tower | A mesh repeater/relay node |
|
||||
| Room | Magenta | Meeting room | A room server for group chat |
|
||||
| Sensor | Teal | Sensors | A sensor device |
|
||||
|
||||
## Contact List
|
||||
|
||||
@@ -73,41 +72,42 @@ Groups are stored per radio identity (scoped by public key).
|
||||
|
||||
| Action | Availability | Description |
|
||||
|---|---|---|
|
||||
| Path Trace / Ping | Repeaters, Rooms (always); Chat if `pathLength > 0` | Opens PathTraceMapScreen. Label shows "Ping" when no path bytes are known, "Path Trace" otherwise |
|
||||
| Ping | Repeaters only | Opens PathTraceMapScreen targeting the repeater |
|
||||
| Path Trace | Rooms (always); Chat/Sensor only if `pathLength > 0` | Opens PathTraceMapScreen. For rooms, label shows "Ping" when no path bytes are known, "Path Trace" when path bytes are available |
|
||||
| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
|
||||
| Room Login | Rooms only | Login dialog → ChatScreen |
|
||||
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
|
||||
| Open Chat | Chat/Sensor | Same as single tap |
|
||||
| Add/Remove Favorite | All types | Toggles the favorite flag |
|
||||
| Share Contact | All types | Copies `meshcore://<hex>` URI to clipboard |
|
||||
| Share Contact | All types | Requests advert from device → copies `meshcore://<hex>` URI to clipboard |
|
||||
| Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop |
|
||||
| Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
|
||||
|
||||
## App Bar Menus
|
||||
|
||||
The Contacts screen has **two separate popup menus** in the app bar:
|
||||
The Contacts screen has a single **three-dot overflow menu** (`⋮`) in the app bar:
|
||||
|
||||
**Antenna icon menu** (contact sharing):
|
||||
- Discovered Contacts — opens the DiscoveryScreen
|
||||
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
|
||||
- *(divider)*
|
||||
- Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes
|
||||
- Flood Advert — broadcasts across the full mesh network
|
||||
- Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally
|
||||
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
|
||||
|
||||
**Three-dot overflow menu**:
|
||||
- *(divider)*
|
||||
- Disconnect — disconnects from the device
|
||||
- Discovered Contacts — opens the DiscoveryScreen
|
||||
- Settings — opens the Settings screen
|
||||
|
||||
A **floating action button** (person-add icon) provides a shortcut sheet to "Add Contact from Clipboard" or "Discovered Contacts".
|
||||
|
||||
## Adding Contacts
|
||||
|
||||
### Automatic (Passive)
|
||||
When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings).
|
||||
|
||||
### Import from Clipboard
|
||||
Antenna menu → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
|
||||
Overflow menu (or the FAB shortcut) → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
|
||||
|
||||
### Import from Discovered Contacts
|
||||
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
|
||||
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
|
||||
|
||||
## Contact Sharing Format
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ All contacts with known GPS coordinates are plotted:
|
||||
| Room | Purple | Meeting room |
|
||||
| Sensor | Orange | Sensors |
|
||||
|
||||
Node name labels appear automatically at zoom level 12 and above.
|
||||
Node name labels appear automatically at zoom level 14 and above.
|
||||
|
||||
### Shared Map Pins (Flag Icons)
|
||||
Location pins shared in chat messages are displayed as flags:
|
||||
@@ -35,9 +35,9 @@ Location pins shared in chat messages are displayed as flags:
|
||||
|
||||
Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
|
||||
|
||||
### Predicted / Guessed Locations (Semi-Transparent)
|
||||
### Predicted / Guessed Locations
|
||||
|
||||
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers.
|
||||
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as markers with a `not_listed_location` icon and a muted grey or colored border, visually distinct from confirmed-location markers.
|
||||
|
||||
#### Why guessed locations exist
|
||||
|
||||
@@ -55,19 +55,19 @@ In a mesh network, every message hops through one or more repeaters on its way t
|
||||
|
||||
5. **Compute the estimated position**:
|
||||
- **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other.
|
||||
- **Two or more anchors**: The position is the average (centroid) of all anchor coordinates, with a smaller offset radius (80–120m) applied for visual separation.
|
||||
- **Two or more anchors**: The position is a weighted average of all anchor coordinates (each subsequent anchor weighted at half the previous one, biasing toward the first), with a smaller offset radius (120m for 2 anchors, 80m for 3+) applied for visual separation.
|
||||
|
||||
6. **Assign confidence level**:
|
||||
- **High confidence** (2+ anchors): Displayed at 55% opacity.
|
||||
- **Low confidence** (1 anchor): Displayed at 30% opacity.
|
||||
- **High confidence** (2+ anchors): The marker border uses the node's type color (brighter border).
|
||||
- **Low confidence** (1 anchor): The marker border is rendered in a muted grey.
|
||||
|
||||
7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild.
|
||||
|
||||
#### How to read guessed locations on the map
|
||||
|
||||
- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix.
|
||||
- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
|
||||
- **More transparent** (30%): Lower confidence — based on a single repeater anchor only.
|
||||
- **Marker with `not_listed_location` icon**: This is a guessed position, not a confirmed GPS fix.
|
||||
- **Colored border** (type color): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
|
||||
- **Grey border**: Lower confidence — based on a single repeater anchor only.
|
||||
- Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated.
|
||||
- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
|
||||
|
||||
@@ -88,10 +88,10 @@ Shows a bottom sheet with:
|
||||
- **Set as my location**: Updates your device's advertised location
|
||||
|
||||
### Filter Dialog (FAB)
|
||||
Toggle visibility of: chat nodes, repeaters, other nodes, guessed locations, discovery contacts.
|
||||
Toggle visibility of: chat nodes, repeaters, other nodes, guessed locations, discovery contacts, overlapping markers (stacked markers at similar coordinates), and shared map pins (flag markers).
|
||||
Additional filters:
|
||||
- **Key prefix filter**: Show only contacts whose public key starts with a given prefix
|
||||
- **Last-seen time slider**: From 1 hour to "all time"
|
||||
- **Last-seen time slider**: Exponential scale from near-zero to 6 months, with "all time" at the top end
|
||||
|
||||
### Legend Card (Top-Right)
|
||||
Shows node count and pin count. Tappable to expand a legend of all marker types.
|
||||
@@ -110,9 +110,16 @@ A map with a polyline showing the route from your node through repeater hops to
|
||||
- **Green circles**: Hops with known GPS coordinates
|
||||
- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
|
||||
- **Red endpoint**: Target contact with known GPS
|
||||
- **Purple semi-transparent endpoint**: Target with guessed position
|
||||
- **Magenta endpoint**: Target with guessed position
|
||||
|
||||
A legend card at the bottom lists each hop pair with SNR quality icons and total path distance.
|
||||
A bottom panel shows each hop pair with SNR quality icons and total path distance. When multiple observed paths are available, a **Single / Combined** toggle appears at the top of the map. In Combined view, all paths are overlaid; shared segments are highlighted with a white halo and a path count badge appears on shared nodes.
|
||||
|
||||
The bottom panel also provides **packet animation controls**:
|
||||
- **Animation toggle** (on/off)
|
||||
- **Step back / Play / Step forward / Replay** buttons
|
||||
- **Follow packet lock** — keeps the map camera centered on the moving packet dot
|
||||
- **Speed selector** (0.5×, 1×, 2×, 4×)
|
||||
- A live **"Hop x of y · from → to"** label that tracks the active segment
|
||||
|
||||
### How It Works
|
||||
Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte.
|
||||
@@ -125,17 +132,17 @@ Sends a trace request frame over the mesh. The repeater network traces the path
|
||||
From the main map, tap the terrain/antenna icon.
|
||||
|
||||
### What the User Sees
|
||||
A full-screen map with a collapsible control panel containing:
|
||||
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow)
|
||||
- **Status**: Clear (green) or blocked (red) with distance and minimum clearance
|
||||
- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button
|
||||
A full-screen map with a draggable bottom sheet containing:
|
||||
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow); obstruction points are marked as clickable dots on the chart
|
||||
- **Status summary**: Clear (green), Marginal (amber, within 5 m of obstruction), or Blocked (red) with distance and clearance/obstruction amount
|
||||
- **Options section** (collapsible): Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button
|
||||
|
||||
### Key Interactions
|
||||
- **Long-press the map** to add custom endpoints (orange pushpin markers, renameable/deleteable)
|
||||
- **Long-press the map** to add custom endpoints (pushpin markers, renameable/deleteable)
|
||||
- **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set
|
||||
- **Antenna heights** are adjustable for both endpoints
|
||||
- **Map line** between endpoints is colored green (clear) or red (blocked)
|
||||
- Terrain elevation is fetched from the Open-Meteo API (21–81 sample points, cached 24 hours)
|
||||
- **Map line** between endpoints is colored green (clear), amber (marginal), or red (blocked)
|
||||
- Terrain elevation is fetched from the Open-Meteo API (21, 41, or 81 sample points depending on link distance, cached 24 hours)
|
||||
- K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz
|
||||
|
||||
---
|
||||
@@ -149,7 +156,7 @@ Settings → App Settings → Map Display → Offline Map Cache
|
||||
- Map with a blue polygon overlay showing previously selected cache bounds
|
||||
- Bounding box coordinates card
|
||||
- **Cache Area** controls: "Use Current View" and Clear buttons
|
||||
- **Zoom Range** slider (3–18) with estimated tile count
|
||||
- **Zoom Range** range slider (3–18, dual-handle for min and max) with estimated tile count
|
||||
- **Download progress** bar (when downloading)
|
||||
- **Download Tiles** and **Clear Cache** buttons
|
||||
|
||||
|
||||
+12
-27
@@ -5,7 +5,7 @@
|
||||
The app follows this general flow:
|
||||
|
||||
```
|
||||
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts Screen
|
||||
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Channels Screen
|
||||
```
|
||||
|
||||
After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**.
|
||||
@@ -22,42 +22,27 @@ The QuickSwitchBar is a Material 3 `NavigationBar` with a frosted-glass visual t
|
||||
|
||||
Tapping a tab replaces the current screen with a subtle fade + slight horizontal nudge transition (220ms forward, 200ms reverse). The back button is suppressed on all three main screens — navigation between them is flat, not stacked. All icons use outline variants (`people_outline`, `tag`, `map_outlined`) following Material 3 conventions.
|
||||
|
||||
## Device Screen
|
||||
## Disconnection
|
||||
|
||||
The Device Screen is a transitional hub that shows after connection. In practice, the app navigates directly to Contacts after connecting, but the Device Screen is reachable via the QuickSwitchBar.
|
||||
|
||||
### What the User Sees
|
||||
|
||||
**App Bar**:
|
||||
- Left: Battery indicator chip (tappable — toggles between percentage and voltage display). Icon changes based on level: `battery_unknown` when data unavailable, `battery_alert` (orange) at 15% or below, `battery_full` otherwise
|
||||
- Left-aligned title (`centerTitle: false`): Two-line layout — small grey "MeshCore" label above the device name in bold
|
||||
- Right: Disconnect button (`bluetooth_disabled` crossed-out icon) and Settings button (tune icon)
|
||||
|
||||
**Body**:
|
||||
- **Connection Card**: Device avatar, device name, device ID, "Connected" chip, and battery chip
|
||||
- **Quick Switch** section: The QuickSwitchBar widget for navigating to Contacts/Channels/Map
|
||||
|
||||
### Disconnection
|
||||
|
||||
- The disconnect button shows a confirmation dialog before disconnecting
|
||||
- The disconnect button (available in the overflow menu of each main screen) shows a confirmation dialog before disconnecting
|
||||
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
|
||||
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
|
||||
|
||||
## Theme and Locale
|
||||
|
||||
- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
|
||||
- **Language** can be overridden to one of 15 supported languages, or follow the system locale
|
||||
- **Language** can be overridden to one of 18 supported languages, or follow the system locale
|
||||
- On web, if a non-Chromium browser is detected, the app shows a `ChromeRequiredScreen` instead of the Scanner (Web Bluetooth requires Chromium)
|
||||
|
||||
## Full Navigation Graph
|
||||
|
||||
```
|
||||
ScannerScreen (root, always on stack)
|
||||
├─ [BLE connect] → push → ContactsScreen
|
||||
├─ [TCP FAB] → push → TcpScreen
|
||||
│ └─ [TCP connected] → pushReplacement → ContactsScreen
|
||||
└─ [USB FAB] → push → UsbScreen
|
||||
└─ [USB connected] → pushReplacement → ContactsScreen
|
||||
├─ [BLE connect] → push → ChannelsScreen
|
||||
├─ [TCP icon button] → push → TcpScreen
|
||||
│ └─ [TCP connected] → pushReplacement → ChannelsScreen
|
||||
└─ [USB icon button] → push → UsbScreen
|
||||
└─ [USB connected] → pushReplacement → ChannelsScreen
|
||||
|
||||
ContactsScreen (selected=0)
|
||||
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
||||
@@ -75,9 +60,9 @@ ChannelsScreen (selected=1)
|
||||
MapScreen (selected=2)
|
||||
├─ [quick-switch 0] → pushReplacement → ContactsScreen
|
||||
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
||||
├─ [radar button] → push → PathTraceMapScreen
|
||||
├─ [terrain button] → push → LineOfSightMapScreen
|
||||
└─ [long-press] → share marker / set location
|
||||
├─ [radar menu item] → enters in-map path trace mode (push → PathTraceMapScreen after path is built)
|
||||
├─ [terrain menu item] → push → LineOfSightMapScreen
|
||||
└─ [long-press] → share marker sheet
|
||||
|
||||
Settings (push from any main screen)
|
||||
└─ [App Settings] → push → AppSettingsScreen
|
||||
|
||||
@@ -22,7 +22,7 @@ MeshCore Open provides both **system notifications** (push-style OS alerts) and
|
||||
|
||||
### 3. Advertisement Notifications
|
||||
- **Triggered when**: A new node is discovered on the mesh for the first time
|
||||
- **Title**: "New [type] discovered" (e.g., "New chat node discovered")
|
||||
- **Title**: "New [type] discovered" (e.g., "New Chat discovered")
|
||||
- **Body**: Contact's name
|
||||
- **Priority**: Default
|
||||
- **Android channel**: `adverts`
|
||||
@@ -43,7 +43,7 @@ Red numeric badges appear throughout the UI:
|
||||
- **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages
|
||||
- **Channels list**: Each channel row shows an unread badge
|
||||
- **Chat screen subtitle**: Shows unread count inline
|
||||
- Badges cap at "99+" for display
|
||||
- Badges cap at "9999+" for display
|
||||
|
||||
### How Unread Counts Work
|
||||
|
||||
@@ -73,7 +73,7 @@ There is no per-contact muting.
|
||||
|
||||
The notification system prevents notification storms:
|
||||
- **Minimum interval**: 3 seconds between individual notifications
|
||||
- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`): "MeshCore Activity — 2 messages, 1 channel message, 3 new nodes". Note: batch summaries are Android-only; on Apple platforms individual notifications are shown
|
||||
- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`). The title is "MeshCore Activity" and the body lists the grouped counts (e.g., "2 messages, 1 channel message, 3 new nodes"). Batch summaries are Android-only; queued notifications that overflow the batch window are silently dropped on other platforms
|
||||
|
||||
## Notification Clearing
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ From the Contacts screen:
|
||||
- Password field with show/hide toggle
|
||||
- "Save password" checkbox (persists for future logins). If a saved password exists, it is pre-filled and the checkbox is pre-checked, making login one-tap
|
||||
- Routing mode selector and "Manage Paths" link are available directly in the dialog (configure routing before login)
|
||||
- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password stops immediately after the first attempt — only timeouts trigger retries
|
||||
- After 5 failed attempts, further login attempts are blocked
|
||||
- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password (explicit failure response) stops immediately — only timeouts trigger retries
|
||||
- If auto-clock-sync is enabled for this repeater (configured in Repeater Settings), a `clock sync` command is sent automatically on successful login
|
||||
|
||||
---
|
||||
|
||||
@@ -28,15 +28,17 @@ The central management screen showing:
|
||||
|
||||
- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
|
||||
- **Battery chemistry selector**: NMC / LiFePO4 / LiPo (saved per repeater)
|
||||
- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" based on contact type:
|
||||
- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" (admin) or "Repeater Guest" / "Room Guest" (guest) based on contact type and login result:
|
||||
|
||||
| Card | Destination |
|
||||
|---|---|
|
||||
| Status | Repeater Status Screen |
|
||||
| Telemetry | Telemetry Screen |
|
||||
| CLI | Repeater CLI Screen |
|
||||
| Neighbors | Neighbors Screen |
|
||||
| Settings | Repeater Settings Screen |
|
||||
| Card | Destination | Visibility |
|
||||
|---|---|---|
|
||||
| Status | Repeater Status Screen | All users |
|
||||
| Telemetry | Telemetry Screen | All users |
|
||||
| Neighbors | Neighbors Screen | All users |
|
||||
| CLI | Repeater CLI Screen | Admin only |
|
||||
| Settings | Repeater Settings Screen | Admin only |
|
||||
|
||||
The battery chemistry selector and CLI/Settings cards are hidden from guest users.
|
||||
|
||||
---
|
||||
|
||||
@@ -47,26 +49,28 @@ The central management screen showing:
|
||||
Three information cards:
|
||||
|
||||
**System Information**:
|
||||
- Battery percentage
|
||||
- Uptime
|
||||
- Queue length
|
||||
- Error flags
|
||||
- Battery percentage and voltage (e.g. "85% / 3.95V"), using the battery chemistry set in the hub screen
|
||||
- Clock at login time
|
||||
- Uptime (days/hours/minutes/seconds)
|
||||
- Queue length
|
||||
- Debug flags (error event count)
|
||||
|
||||
**Radio Statistics**:
|
||||
- Last RSSI and SNR
|
||||
- Noise floor
|
||||
- TX and RX airtime
|
||||
- TX airtime and RX airtime
|
||||
|
||||
**Packet Statistics**:
|
||||
- Packets sent, received, and duplicates
|
||||
- Broken down by flood vs. direct
|
||||
- Packets sent and received, each broken down by flood vs. direct
|
||||
- Duplicates, broken down by flood vs. direct
|
||||
- Channel utilization (% of uptime used by TX + RX)
|
||||
|
||||
### Key Interactions
|
||||
- Auto-queries the repeater on open; shows a loading spinner until data arrives
|
||||
- On timeout: red snackbar error. On success: data appears with a green snackbar confirmation
|
||||
- Pull-to-refresh or refresh button to re-query
|
||||
- On timeout: red snackbar error. On success: data appears in-place (no extra snackbar)
|
||||
- Pull-to-refresh or refresh button in the app bar to re-query
|
||||
- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
|
||||
- Accepts both binary `RESP_CODE_STATUS_RESPONSE` frames and legacy JSON text responses
|
||||
|
||||
---
|
||||
|
||||
@@ -76,7 +80,7 @@ A terminal-style interface for sending commands directly to the repeater.
|
||||
|
||||
### What the User Sees
|
||||
|
||||
- **Quick-command bar** (horizontal scroll): Shortcut buttons for common commands (get name, get radio, get tx, neighbors, ver, advert, clock)
|
||||
- **Quick-command bar** (horizontal scroll): Shortcut buttons for 9 common commands (advert, get name, get radio, get tx, discover.neighbors, neighbors, ver, clock, clock sync)
|
||||
- **Command history list**: Sent commands in primary color, responses in secondary color
|
||||
- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
|
||||
|
||||
@@ -85,16 +89,20 @@ A terminal-style interface for sending commands directly to the repeater.
|
||||
- Type a command and press send (or Enter on desktop)
|
||||
- Up/down arrows navigate through command history
|
||||
- Quick-command buttons populate and send common commands
|
||||
- Bug report icon: Shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
|
||||
- Overflow menu (three-dot icon): "Debug next command" option shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
|
||||
- Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately
|
||||
- Clear icon: Wipes the command/response history
|
||||
- Failed/timed-out commands are automatically retried once
|
||||
|
||||
### Available CLI Commands
|
||||
|
||||
**General**: `advert`, `reboot`, `clock`, `password`, `ver`, `clear stats`
|
||||
The in-app help reference (help icon) documents all known commands. Categories:
|
||||
|
||||
**Settings**: `set name`, `set af`, `set tx`, `set repeat`, `set allow.read.only`, `set flood.max`, `set int.thresh`, `set agc.reset.interval`, `set multi.acks`, `set advert.interval`, `set flood.advert.interval`, `set guest.password`, `set lat`, `set lon`, `set radio`, `set rxdelay`, `set txdelay`, `set direct.txdelay`, `set bridge.*`, `set adc.multiplier`, `tempradio`, `setperm`
|
||||
**General**: `advert`, `advert.zerohop`, `reboot`, `clock`, `clock sync`, `password`, `ver`, `clear stats`, `erase`, `poweroff`, `shutdown`, `clkreboot`, `start ota`, `time`, `board`, `discover.neighbors`, `powersaving`, `stats-packets`, `stats-radio`, `stats-core`
|
||||
|
||||
**Get**: `get name`, `get role`, `get public.key`, `get prv.key`, `get repeat`, `get tx`, `get freq`, `get radio`, `get radio.rxgain`, `get af`, `get dutycycle`, `get int.thresh`, `get agc.reset.interval`, `get multi.acks`, `get allow.read.only`, `get advert.interval`, `get flood.advert.interval`, `get guest.password`, `get lat`, `get lon`, `get rxdelay`, `get txdelay`, `get direct.txdelay`, `get flood.max`, `get owner.info`, `get path.hash.mode`, `get loop.detect`, `get acl`, `get bridge.*`, `get adc.multiplier`, `get bootloader.ver`
|
||||
|
||||
**Set**: `set name`, `set af`, `set tx`, `set repeat`, `set allow.read.only`, `set flood.max`, `set int.thresh`, `set agc.reset.interval`, `set multi.acks`, `set advert.interval`, `set flood.advert.interval`, `set guest.password`, `set lat`, `set lon`, `set freq`, `set radio`, `set rxdelay`, `set txdelay`, `set direct.txdelay`, `set radio.rxgain`, `set dutycycle`, `set loop.detect`, `set path.hash.mode`, `set owner.info`, `set prv.key`, `set bridge.*`, `set adc.multiplier`, `tempradio`, `setperm`
|
||||
|
||||
**Bridge**: `get bridge.type`
|
||||
|
||||
@@ -102,9 +110,13 @@ A terminal-style interface for sending commands directly to the repeater.
|
||||
|
||||
**Neighbors**: `neighbors`, `neighbor.remove`
|
||||
|
||||
**Region Management**: `region`, `region load/get/put/remove/allowf/denyf/home/save`
|
||||
**Power Management**: `get pwrmgt.support`, `get pwrmgt.source`, `get pwrmgt.bootreason`, `get pwrmgt.bootmv`
|
||||
|
||||
**GPS**: `gps`, `gps on/off/sync/setloc/advert`
|
||||
**Sensors**: `sensor get {key}`, `sensor set {key} {value}`, `sensor list [start]`
|
||||
|
||||
**GPS Management**: `gps`, `gps {on|off}`, `gps sync`, `gps setloc`, `gps advert`, `gps advert {none|share|prefs}`
|
||||
|
||||
**Region Management**: `region`, `region load`, `region get`, `region put`, `region remove`, `region allowf`, `region denyf`, `region home`, `region save`, `region default`, `region list allowed`, `region list denied`
|
||||
|
||||
---
|
||||
|
||||
@@ -147,40 +159,63 @@ A card titled "Repeater's Neighbors - N" listing each neighbor as:
|
||||
|
||||
### What the User Sees
|
||||
|
||||
Five configuration cards:
|
||||
Nine configuration cards, each with its own per-field refresh button(s):
|
||||
|
||||
**1. Basic Settings**
|
||||
- Name field
|
||||
- Admin password field
|
||||
- Guest password field
|
||||
- Admin password field (write-only; always sent when non-empty)
|
||||
- Guest password field (write-only; always sent when non-empty)
|
||||
|
||||
**2. Radio Settings**
|
||||
- Frequency (MHz)
|
||||
- TX Power (dBm)
|
||||
- TX Power (dBm) — has its own independent refresh button
|
||||
- Bandwidth dropdown (kHz)
|
||||
- Spreading Factor (SF5–SF12)
|
||||
- Coding Rate (4/5–4/8)
|
||||
- RX Gain boost toggle
|
||||
|
||||
**3. Location Settings**
|
||||
- Latitude and longitude fields
|
||||
- Latitude and longitude fields, each with an independent refresh button
|
||||
|
||||
**4. Features**
|
||||
- Packet forwarding toggle
|
||||
- Guest access toggle
|
||||
- Packet forwarding toggle (`set repeat`)
|
||||
- Guest access toggle (`set allow.read.only`)
|
||||
- Multi-ACKs toggle (`set multi.acks`)
|
||||
- Auto clock sync after login toggle (local app setting only, not sent to repeater)
|
||||
|
||||
**5. Advertisement Settings**
|
||||
**5. Network Health**
|
||||
- Loop detection dropdown (off / minimal / moderate / strict; `set loop.detect`)
|
||||
- Duty cycle slider (1–100%; `set dutycycle`)
|
||||
|
||||
**6. Advertisement Settings**
|
||||
- Local advert interval slider (60–240 minutes) with enable/disable toggle
|
||||
- Flood advert interval slider (3–168 hours) with enable/disable toggle
|
||||
- Flood max hops slider (0–64; `set flood.max`)
|
||||
|
||||
**6. Danger Zone** (red-styled card)
|
||||
- Reboot repeater
|
||||
- Erase filesystem (serial-only warning)
|
||||
**7. Owner Info**
|
||||
- Multi-line text field for operator contact info (`set owner.info`); newlines sent as `|`
|
||||
|
||||
**8. Actions** (one-tap, no save needed)
|
||||
- Send Advertisement (`advert`)
|
||||
- Send Zero-Hop Advertisement (`advert.zerohop`)
|
||||
- Clock Sync (`clock sync`)
|
||||
|
||||
**9. Advanced** (collapsed by default)
|
||||
- Path hash mode dropdown (0–2; `set path.hash.mode`)
|
||||
- TX delay field (`set txdelay`)
|
||||
- Direct TX delay field (`set direct.txdelay`)
|
||||
- Interference threshold field (`set int.thresh`)
|
||||
- AGC reset interval slider (0–240s in multiples of 4; `set agc.reset.interval`)
|
||||
|
||||
**Danger Zone** (red-styled card)
|
||||
- Reboot repeater (sends `reboot` with confirmation dialog)
|
||||
- Erase filesystem (serial-only; shows a confirmation dialog, then an informational snackbar — no command is sent over the air)
|
||||
|
||||
### Key Interactions
|
||||
- **Settings are NOT auto-fetched on open**. Only name and location are pre-filled from locally cached contact data. You must tap each section's refresh button to fetch live values from the repeater
|
||||
- TX Power has its own separate refresh button, independent from the main Radio Settings refresh
|
||||
- Save button appears when changes are detected
|
||||
- Settings are sent sequentially with 200ms delays between commands (fire-and-forget, no per-command acknowledgment wait)
|
||||
- Validation prevents invalid values (e.g., frequency range, LoRa parameter compatibility)
|
||||
- **Settings are NOT auto-fetched on open**. Name is pre-filled from cached contact data. Each section has its own refresh button to fetch live values from the repeater
|
||||
- TX Power, RX Gain, latitude, longitude, and advanced fields each have independent inline refresh buttons
|
||||
- Save button in app bar appears when any change is detected; failed commands keep those fields dirty for retry
|
||||
- Settings are sent sequentially with 200ms delays between commands; firmware responses are checked and partial failures are reported in a snackbar
|
||||
- Some changes (e.g. radio frequency) require a reboot; the firmware response triggers an orange "reboot needed" snackbar
|
||||
- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
|
||||
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access. It is effectively non-functional when connected wirelessly
|
||||
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access
|
||||
|
||||
@@ -28,9 +28,11 @@ The BLE Scanner is the app's home screen, displayed immediately on launch.
|
||||
|
||||
**Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets.
|
||||
|
||||
**Bottom FAB Row**: Up to three floating action buttons:
|
||||
- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
|
||||
- **TCP/IP** button - Opens TCP connection screen (all non-web platforms)
|
||||
**App Bar Actions**: Icon buttons in the top-right corner of the app bar:
|
||||
- **USB** icon button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
|
||||
- **TCP/IP** icon button - Opens TCP connection screen (all non-web platforms)
|
||||
|
||||
**Bottom FAB**: A single floating action button:
|
||||
- **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off
|
||||
|
||||
### Device Tile
|
||||
@@ -51,7 +53,7 @@ Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon s
|
||||
|
||||
### How Scanning Works
|
||||
|
||||
- Filters for devices with names starting with `MeshCore-` or `Whisper-`
|
||||
- Filters for devices advertising the Nordic UART Service UUID (so community forks with non-standard names are still found). Known name prefixes used by stock firmware builds for reference: `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, `LowMesh_MC_`, `NRF52`
|
||||
- Uses low-latency scan mode on Android
|
||||
- Scans for 10 seconds then auto-stops
|
||||
- On iOS/macOS, waits for BLE adapter initialization before starting
|
||||
@@ -61,11 +63,11 @@ Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon s
|
||||
|
||||
Tap a device tile or its Connect button:
|
||||
1. The connector stops scanning and transitions to "connecting"
|
||||
2. Connects to the device with a 15-second timeout
|
||||
2. Connects to the device with a 15-second timeout (6 seconds on Linux)
|
||||
3. Requests MTU 185 bytes for optimal throughput
|
||||
4. Discovers BLE services and locates the Nordic UART Service
|
||||
5. Subscribes to TX notifications for receiving data
|
||||
6. On success, automatically navigates to the Contacts screen
|
||||
6. On success, automatically navigates to the Channels screen
|
||||
7. On failure, shows a red error snackbar
|
||||
|
||||
---
|
||||
@@ -74,7 +76,7 @@ Tap a device tile or its Connect button:
|
||||
|
||||
### How to Access
|
||||
|
||||
From the Scanner screen, tap the **USB** FAB button.
|
||||
From the Scanner screen, tap the **USB** icon button in the app bar.
|
||||
|
||||
### What the User Sees
|
||||
|
||||
@@ -82,15 +84,15 @@ From the Scanner screen, tap the **USB** FAB button.
|
||||
- A list of detected USB serial ports, each showing:
|
||||
- Friendly display name
|
||||
- Raw port name (subtitle, only shown when it differs from the display name)
|
||||
- "Connect" button
|
||||
- FABs at the bottom to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
|
||||
- Chevron trailing icon (the entire tile is tappable to connect)
|
||||
- Transport switcher buttons (outlined, not FABs) to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
|
||||
|
||||
### Key Interactions
|
||||
|
||||
- On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected)
|
||||
- On mobile: tap the "Scan" FAB to manually refresh
|
||||
- Tap a port or its Connect button to connect
|
||||
- On successful connection, navigates to Contacts screen
|
||||
- Tap a port tile to connect
|
||||
- On successful connection, navigates to Channels screen
|
||||
- On connection failure, the port list automatically refreshes
|
||||
- Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more)
|
||||
|
||||
@@ -100,7 +102,7 @@ From the Scanner screen, tap the **USB** FAB button.
|
||||
|
||||
### How to Access
|
||||
|
||||
From the Scanner screen, tap the **TCP/IP** FAB button.
|
||||
From the Scanner screen, tap the **TCP/IP** icon button in the app bar.
|
||||
|
||||
### What the User Sees
|
||||
|
||||
@@ -108,7 +110,7 @@ From the Scanner screen, tap the **TCP/IP** FAB button.
|
||||
- **Host address** text field
|
||||
- **Port number** text field
|
||||
- **Connect** button
|
||||
- FABs at the bottom to switch to USB or BLE
|
||||
- Transport switcher buttons (outlined, not FABs) to switch to USB or BLE
|
||||
|
||||
### Key Interactions
|
||||
|
||||
@@ -119,6 +121,6 @@ From the Scanner screen, tap the **TCP/IP** FAB button.
|
||||
- Validation errors are shown as red snackbars
|
||||
- The Connect button shows a spinner and "Connecting..." label while in progress
|
||||
- The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000")
|
||||
- On success, navigates to Contacts screen and saves the host/port to settings
|
||||
- On success, navigates to Channels screen and saves the host/port to settings
|
||||
- On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000")
|
||||
- Error messages for timeout, unsupported platform, and connection failures
|
||||
|
||||
+92
-64
@@ -12,12 +12,13 @@ Settings are only accessible while a device is connected.
|
||||
The settings screen is a scrollable list of cards:
|
||||
|
||||
1. [Device Info](#device-info)
|
||||
2. [App Settings](#app-settings) (link to sub-screen)
|
||||
3. [Node Settings](#node-settings)
|
||||
4. [Actions](#actions)
|
||||
5. [Debug](#debug)
|
||||
2. [Node Settings](#node-settings)
|
||||
3. [Location](#location)
|
||||
4. [App Settings](#app-settings) (link to sub-screen)
|
||||
5. [Actions](#actions)
|
||||
6. [Export](#export)
|
||||
7. [About](#about)
|
||||
7. [Debug](#debug)
|
||||
8. [About](#about)
|
||||
|
||||
---
|
||||
|
||||
@@ -40,46 +41,6 @@ Battery shows an alert icon and orange text when at 15% or below. The toggle onl
|
||||
|
||||
---
|
||||
|
||||
## App Settings
|
||||
|
||||
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
|
||||
|
||||
### Appearance
|
||||
- **Theme**: System / Light / Dark
|
||||
- **Language**: System default or one of 15 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian)
|
||||
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
|
||||
|
||||
### Notifications
|
||||
- **Master enable/disable**: Requests OS permission when enabling
|
||||
- **Message notifications**: New direct message alerts
|
||||
- **Channel message notifications**: New channel message alerts
|
||||
- **Advertisement notifications**: New node discovery alerts
|
||||
|
||||
### Messaging
|
||||
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
|
||||
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
|
||||
- Max Route Weight (1–10, default 5, integer steps)
|
||||
- Initial Route Weight (0.5–5.0, default 3.0)
|
||||
- Success Increment (0.1–2.0, default 0.5, 0.1 steps)
|
||||
- Failure Decrement (0.1–2.0, default 0.2, 0.1 steps)
|
||||
- Max Message Retries (2–10, default 5)
|
||||
|
||||
### Battery
|
||||
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
|
||||
|
||||
### Map Display
|
||||
- **Show Repeaters**: Toggle repeater markers on map
|
||||
- **Show Chat Nodes**: Toggle chat node markers
|
||||
- **Show Other Nodes**: Toggle room/sensor markers
|
||||
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
|
||||
- **Units**: Metric / Imperial
|
||||
- **Offline Map Cache**: Navigate to tile download screen
|
||||
|
||||
### Debug
|
||||
- **App Debug Logging**: Enable the in-app debug log
|
||||
|
||||
---
|
||||
|
||||
## Node Settings
|
||||
|
||||
These settings are sent directly to the connected device firmware.
|
||||
@@ -91,7 +52,7 @@ These settings are sent directly to the connected device firmware.
|
||||
|
||||
### Radio Settings
|
||||
Opens a dialog pre-populated with the device's current radio settings. Contains:
|
||||
- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA/WA/QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918
|
||||
- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam
|
||||
- **Frequency** (MHz): Free text, validated 300–2500 MHz
|
||||
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
|
||||
- **Spreading Factor**: SF5–SF12
|
||||
@@ -99,6 +60,13 @@ Opens a dialog pre-populated with the device's current radio settings. Contains:
|
||||
- **TX Power** (dBm): Validated 0 to device max (typically 22 dBm)
|
||||
- **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies
|
||||
|
||||
### Companion Radio Stats
|
||||
Opens the RF statistics screen (RSSI, SNR, packet counts) for the paired radio. Only enabled when connected to a device that supports companion radio stats.
|
||||
|
||||
---
|
||||
|
||||
## Location
|
||||
|
||||
### Location
|
||||
Opens a dialog pre-populated with the device's current coordinates (if known):
|
||||
- Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value
|
||||
@@ -115,8 +83,68 @@ Five toggles controlling which node types are auto-added when heard:
|
||||
- Auto-add Sensors
|
||||
- Overwrite Oldest (when contact list is full)
|
||||
|
||||
### Privacy Mode
|
||||
Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements.
|
||||
### Privacy
|
||||
Opens a dialog with controls for how the node shares telemetry and location data:
|
||||
- **Advert Location**: Toggle whether the node broadcasts its location in advertisements
|
||||
- **Multi-Ack**: Toggle multi-ack delivery confirmations
|
||||
- **Telemetry Base Mode**: Deny All / Allow by Contact / Allow All
|
||||
- **Telemetry Location Mode**: Deny All / Allow by Contact / Allow All
|
||||
- **Telemetry Environment Mode**: Deny All / Allow by Contact / Allow All
|
||||
|
||||
Settings take effect when saved. A snackbar confirms the update.
|
||||
|
||||
---
|
||||
|
||||
## App Settings
|
||||
|
||||
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
|
||||
|
||||
### Appearance
|
||||
- **Theme**: System / Light / Dark
|
||||
- **Language**: System default or one of 18 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean)
|
||||
|
||||
### Notifications
|
||||
- **Master enable/disable**: Requests OS permission when enabling
|
||||
- **Message notifications**: New direct message alerts
|
||||
- **Channel message notifications**: New channel message alerts
|
||||
- **Advertisement notifications**: New node discovery alerts
|
||||
|
||||
### Messaging
|
||||
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
|
||||
- **Jump to Oldest Unread**: When opening a chat, scrolls to the oldest unread message instead of the newest
|
||||
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
|
||||
- Max Route Weight (1–10, default 5, integer steps)
|
||||
- Initial Route Weight (0.5–5.0, default 3.0)
|
||||
- Success Increment (0.1–2.0, default 0.5, 0.1 steps)
|
||||
- Failure Decrement (0.1–2.0, default 0.2, 0.1 steps)
|
||||
- Max Message Retries (2–10, default 5)
|
||||
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
|
||||
|
||||
### Battery
|
||||
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
|
||||
|
||||
### Map Display
|
||||
- **Show Repeaters**: Toggle repeater markers on map
|
||||
- **Show Chat Nodes**: Toggle chat node markers
|
||||
- **Show Other Nodes**: Toggle room/sensor markers
|
||||
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
|
||||
- **Units**: Metric / Imperial
|
||||
- **Offline Map Cache**: Navigate to tile download screen
|
||||
|
||||
### Translation
|
||||
Not shown on web. Controls on-device message translation powered by a locally-downloaded ML model:
|
||||
- **Enable Translation**: Translates incoming messages into the selected target language
|
||||
- **Translate Composer**: Translates outgoing messages from the target language back before sending
|
||||
- **Target Language**: Language to translate into (searchable list; defaults to the app language)
|
||||
- **Downloaded Model**: Dropdown to select among already-downloaded translation models
|
||||
- **Preset Model**: Download a curated preset model with one tap
|
||||
- **Custom Model URL**: Enter a URL to download a custom GGUF-format model; shows download progress and a cancel button
|
||||
|
||||
### Cyrillic-to-Latin (Cyr2Lat)
|
||||
Controls character substitution profiles used to render Cyrillic text in Latin characters. A dropdown selects the active profile; Add, Edit, and Delete buttons manage the profile list (the last remaining profile cannot be deleted). Each profile stores a JSON character map.
|
||||
|
||||
### Debug
|
||||
- **App Debug Logging**: Enable the in-app debug log
|
||||
|
||||
---
|
||||
|
||||
@@ -126,10 +154,24 @@ One-tap device operations:
|
||||
|
||||
| Action | Description |
|
||||
|---|---|
|
||||
| Send Advertisement | Floods the mesh with your node's advertisement |
|
||||
| Sync Time | Sends current Unix timestamp to the device |
|
||||
| Refresh Contacts | Re-requests the full contact list |
|
||||
| Reboot Device | Confirmation dialog → reboots the device (shown in orange) |
|
||||
| Reboot Device | Confirmation dialog → reboots the device (shown in warning color) |
|
||||
| Delete All Paths | Confirmation dialog → clears all stored routing paths (shown in alert color) |
|
||||
|
||||
---
|
||||
|
||||
## Export
|
||||
|
||||
Three GPX export options (not available on web):
|
||||
|
||||
| Option | Exports |
|
||||
|---|---|
|
||||
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
|
||||
| Export Contacts | Chat contacts with GPS coordinates |
|
||||
| Export All | All contacts with GPS coordinates |
|
||||
|
||||
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
|
||||
|
||||
---
|
||||
|
||||
@@ -150,20 +192,6 @@ Structured log entries (Info / Warning / Error), with tag, message, and timestam
|
||||
|
||||
---
|
||||
|
||||
## Export
|
||||
|
||||
Three GPX export options (not available on web):
|
||||
|
||||
| Option | Exports |
|
||||
|---|---|
|
||||
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
|
||||
| Export Contacts | Chat contacts with GPS coordinates |
|
||||
| Export All | All contacts with GPS coordinates |
|
||||
|
||||
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
Shows the standard Flutter about dialog with app name, version, and legal notice.
|
||||
|
||||
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
platform :ios, '15.5'
|
||||
platform :ios, '16.4'
|
||||
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@@ -32,5 +32,8 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.4'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+13
-84
@@ -7,57 +7,13 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleMLKit/BarcodeScanning (7.0.0):
|
||||
- GoogleMLKit/MLKitCore
|
||||
- MLKitBarcodeScanning (~> 6.0.0)
|
||||
- GoogleMLKit/MLKitCore (7.0.0):
|
||||
- MLKitCommon (~> 12.0.0)
|
||||
- GoogleToolboxForMac/Defines (4.2.1)
|
||||
- GoogleToolboxForMac/Logger (4.2.1):
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- GoogleUtilities/Environment (8.1.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.0)
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- MLImage (1.0.0-beta6)
|
||||
- MLKitBarcodeScanning (6.0.0):
|
||||
- MLKitCommon (~> 12.0)
|
||||
- MLKitVision (~> 8.0)
|
||||
- MLKitCommon (12.0.0):
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLKitVision (8.0.0):
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLImage (= 1.0.0-beta6)
|
||||
- MLKitCommon (~> 12.0)
|
||||
- mobile_scanner (6.0.2):
|
||||
- mobile_scanner (7.0.0):
|
||||
- Flutter
|
||||
- GoogleMLKit/BarcodeScanning (~> 7.0.0)
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -66,34 +22,18 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
||||
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- GoogleDataTransport
|
||||
- GoogleMLKit
|
||||
- GoogleToolboxForMac
|
||||
- GoogleUtilities
|
||||
- GTMSessionFetcher
|
||||
- MLImage
|
||||
- MLKitBarcodeScanning
|
||||
- MLKitCommon
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
@@ -105,41 +45,30 @@ EXTERNAL SOURCES:
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
mobile_scanner:
|
||||
:path: ".symlinks/plugins/mobile_scanner/ios"
|
||||
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
|
||||
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
|
||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
|
||||
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
|
||||
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
|
||||
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
|
||||
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: 570da2a631486c6bd6496bed1e605e63e2471be5
|
||||
PODFILE CHECKSUM: e42b502c78c33aa1ed9d42eaea8960ce2139504b
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -179,6 +179,7 @@
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */,
|
||||
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
@@ -299,6 +300,22 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}",
|
||||
);
|
||||
name = "Fix Native Asset Minimum OS";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n";
|
||||
};
|
||||
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -414,7 +431,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -540,7 +557,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -591,7 +608,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
||||
@@ -2,12 +2,15 @@ import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
|
||||
+40
-19
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -22,8 +24,46 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app uses the camera to scan QR codes for joining communities.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>bluetooth-central</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -41,24 +81,5 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>bluetooth-central</string>
|
||||
</array>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app uses the camera to scan QR codes for joining communities.</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+1902
-357
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ class MeshCoreUsbManager {
|
||||
String? get activePortKey => _activePortKey;
|
||||
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
|
||||
bool get isConnected => _service.isConnected;
|
||||
Object? get lastError => _service.lastError;
|
||||
Stream<Uint8List> get frameStream => _service.frameStream;
|
||||
|
||||
// --- Configuration ---
|
||||
|
||||
@@ -202,13 +202,15 @@ const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdSendTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
const int cmdGetStats = 56;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdSetAutoAddConfig = 58;
|
||||
const int cmdGetAutoAddConfig = 59;
|
||||
const int cmdSetPathHashMode = 61;
|
||||
|
||||
// Text message types
|
||||
const int txtTypePlain = 0;
|
||||
@@ -222,6 +224,12 @@ const int reqTypeGetTelemetry = 0x03;
|
||||
const int reqTypeGetAccessList = 0x05;
|
||||
const int reqTypeGetNeighbors = 0x06;
|
||||
|
||||
Uint8List buildTelemetryBinaryPayload() {
|
||||
// Room servers/repeaters read byte 1 as an inverse telemetry permission mask.
|
||||
// Zero means "request every telemetry field allowed for this contact".
|
||||
return Uint8List.fromList([reqTypeGetTelemetry, 0x00, 0x00, 0x00, 0x00]);
|
||||
}
|
||||
|
||||
// Repeater response codes
|
||||
const int respServerLoginOk = 0;
|
||||
|
||||
@@ -245,6 +253,11 @@ const int respCodeChannelMsgRecvV3 = 17;
|
||||
const int respCodeChannelInfo = 18;
|
||||
const int respCodeCustomVars = 21;
|
||||
const int respCodeAutoAddConfig = 25;
|
||||
const int respCodeStats = 24;
|
||||
|
||||
const int statsTypeCore = 0;
|
||||
const int statsTypeRadio = 1;
|
||||
const int statsTypePackets = 2;
|
||||
|
||||
// Push codes (async from device)
|
||||
const int pushCodeAdvert = 0x80;
|
||||
@@ -313,7 +326,7 @@ const int maxPathSize = 64;
|
||||
const int pathHashSize = 1;
|
||||
const int maxNameSize = 32;
|
||||
const int maxFrameSize = 172;
|
||||
const int appProtocolVersion = 3;
|
||||
const int appProtocolVersion = 4;
|
||||
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
||||
const int maxTextPayloadBytes = 160;
|
||||
const int _sendTextMsgOverheadBytes =
|
||||
@@ -444,8 +457,13 @@ String pubKeyToHex(Uint8List pubKey) {
|
||||
|
||||
// Helper to convert hex string to public key
|
||||
Uint8List hexToPubKey(String hex) {
|
||||
if (hex.length != pubKeySize * 2) {
|
||||
throw FormatException(
|
||||
'Public key hex must be ${pubKeySize * 2} chars, got ${hex.length}',
|
||||
);
|
||||
}
|
||||
final result = Uint8List(pubKeySize);
|
||||
for (int i = 0; i < pubKeySize && i * 2 + 1 < hex.length; i++) {
|
||||
for (int i = 0; i < pubKeySize; i++) {
|
||||
result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
|
||||
}
|
||||
return result;
|
||||
@@ -554,6 +572,17 @@ Uint8List buildGetBattAndStorageFrame() {
|
||||
return Uint8List.fromList([cmdGetBattAndStorage]);
|
||||
}
|
||||
|
||||
/// Companion radio stats: [56][statsType] where statsType is statsTypeCore/Radio/Packets.
|
||||
Uint8List buildGetStatsFrame(int statsType) {
|
||||
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
|
||||
}
|
||||
|
||||
/// Path hash width on air: [61][0][mode], mode 0..2 → (mode+1) bytes per hop hash.
|
||||
Uint8List buildSetPathHashModeFrame(int mode) {
|
||||
final m = mode.clamp(0, 2);
|
||||
return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
|
||||
}
|
||||
|
||||
// Build CMD_SET_DEVICE_TIME frame
|
||||
Uint8List buildSetDeviceTimeFrame(int timestamp) {
|
||||
final writer = BufferWriter();
|
||||
@@ -702,25 +731,19 @@ Uint8List buildUpdateContactPathFrame(
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(timestamp);
|
||||
|
||||
if ((lat == null || lon == null) && lastModified != null) {
|
||||
// If lat/lon not provided, write zeros
|
||||
writer.writeInt32LE(0);
|
||||
writer.writeInt32LE(0);
|
||||
} else {
|
||||
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
|
||||
// Latitude
|
||||
final latitude = lat ?? 0.0;
|
||||
writer.writeInt32LE((latitude * 1e6).round());
|
||||
|
||||
// Longitude
|
||||
final longitude = lon ?? 0.0;
|
||||
writer.writeInt32LE((longitude * 1e6).round());
|
||||
}
|
||||
|
||||
if (lastModified != null) {
|
||||
// Last modified
|
||||
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||
// Optional [Lat x4, Lon x4][timestamp x4] tail per the doc comment above.
|
||||
// Emit 8 bytes of position (zero-filled when only lastModified is provided)
|
||||
// followed by an optional 4-byte timestamp. Earlier code emitted the
|
||||
// position block twice, which corrupted the tail and caused the firmware
|
||||
// to parse the second lat as the timestamp. See #427.
|
||||
final hasLocation = lat != null && lon != null;
|
||||
if (hasLocation || lastModified != null) {
|
||||
writer.writeInt32LE(hasLocation ? (lat * 1e6).round() : 0);
|
||||
writer.writeInt32LE(hasLocation ? (lon * 1e6).round() : 0);
|
||||
if (lastModified != null) {
|
||||
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
return writer.toBytes();
|
||||
@@ -933,7 +956,7 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
|
||||
writer.writeBytes(Uint8List(3)); // reserved bytes
|
||||
writer.writeBytes(pubKey);
|
||||
} else {
|
||||
writer.writeBytes(Uint8List(4)); // reserved bytes
|
||||
writer.writeBytes(Uint8List(3)); // reserved bytes
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
class MeshCoreUuids {
|
||||
static const String service = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
|
||||
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
|
||||
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
|
||||
|
||||
/// Known advertised-name prefixes used by stock MeshCore firmware builds.
|
||||
/// Discovery no longer filters on these (it filters on the [service] UUID so
|
||||
/// that community forks with custom names are still found); kept for
|
||||
/// reference and possible future display heuristics.
|
||||
static const List<String> deviceNamePrefixes = [
|
||||
"MeshCore-",
|
||||
"Whisper-",
|
||||
"WisCore-",
|
||||
"Seeed",
|
||||
"Lilygo",
|
||||
"HT-",
|
||||
"LowMesh_MC_",
|
||||
"NRF52",
|
||||
];
|
||||
}
|
||||
@@ -96,6 +96,34 @@ class CayenneLpp {
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case lppDigitalInput:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppDigitalOutput:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppAnalogInput:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 100,
|
||||
});
|
||||
break;
|
||||
case lppAnalogOutput:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 100,
|
||||
});
|
||||
break;
|
||||
case lppGenericSensor:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
@@ -131,6 +159,17 @@ class CayenneLpp {
|
||||
'value': buffer.readUInt8() / 2,
|
||||
});
|
||||
break;
|
||||
case lppAccelerometer:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': {
|
||||
'x': buffer.readInt16BE() / 1000,
|
||||
'y': buffer.readInt16BE() / 1000,
|
||||
'z': buffer.readInt16BE() / 1000,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case lppBarometricPressure:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
@@ -138,6 +177,13 @@ class CayenneLpp {
|
||||
'value': buffer.readUInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppAltitude:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppVoltage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
@@ -152,6 +198,13 @@ class CayenneLpp {
|
||||
'value': buffer.readInt16BE() / 1000,
|
||||
});
|
||||
break;
|
||||
case lppFrequency:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt32BE(),
|
||||
});
|
||||
break;
|
||||
case lppPercentage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
@@ -173,6 +226,56 @@ class CayenneLpp {
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppDistance:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt32BE() / 1000,
|
||||
});
|
||||
break;
|
||||
case lppEnergy:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt32BE() / 1000,
|
||||
});
|
||||
break;
|
||||
case lppDirection:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppUnixTime:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt32BE(),
|
||||
});
|
||||
break;
|
||||
case lppGyrometer:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': {
|
||||
'x': buffer.readInt16BE() / 100,
|
||||
'y': buffer.readInt16BE() / 100,
|
||||
'z': buffer.readInt16BE() / 100,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case lppColour:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': {
|
||||
'red': buffer.readUInt8(),
|
||||
'green': buffer.readUInt8(),
|
||||
'blue': buffer.readUInt8(),
|
||||
},
|
||||
});
|
||||
break;
|
||||
case lppGps:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
@@ -184,6 +287,24 @@ class CayenneLpp {
|
||||
},
|
||||
});
|
||||
break;
|
||||
case lppSwitch:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppPolyline:
|
||||
final size = buffer.readUInt8();
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': {
|
||||
'size': size,
|
||||
'data': _bytesToHex(_readPolylinePayload(buffer, size)),
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return telemetry;
|
||||
}
|
||||
@@ -216,6 +337,19 @@ class CayenneLpp {
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case lppDigitalInput:
|
||||
channelData['values']['digitalInput'] = buffer.readUInt8();
|
||||
break;
|
||||
case lppDigitalOutput:
|
||||
channelData['values']['digitalOutput'] = buffer.readUInt8();
|
||||
break;
|
||||
case lppAnalogInput:
|
||||
channelData['values']['analogInput'] = buffer.readInt16BE() / 100.0;
|
||||
break;
|
||||
case lppAnalogOutput:
|
||||
channelData['values']['analogOutput'] =
|
||||
buffer.readInt16BE() / 100.0;
|
||||
break;
|
||||
case lppGenericSensor:
|
||||
channelData['values']['generic'] = buffer.readUInt32BE();
|
||||
break;
|
||||
@@ -231,15 +365,29 @@ class CayenneLpp {
|
||||
case lppRelativeHumidity:
|
||||
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
|
||||
break;
|
||||
case lppAccelerometer:
|
||||
channelData['values']['accelerometer'] = {
|
||||
'x': buffer.readInt16BE() / 1000.0,
|
||||
'y': buffer.readInt16BE() / 1000.0,
|
||||
'z': buffer.readInt16BE() / 1000.0,
|
||||
};
|
||||
break;
|
||||
case lppBarometricPressure:
|
||||
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
|
||||
break;
|
||||
case lppAltitude:
|
||||
// MeshCore encodes standalone barometric altitude as LPP type 121.
|
||||
channelData['values']['altitude'] = buffer.readInt16BE();
|
||||
break;
|
||||
case lppVoltage:
|
||||
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
|
||||
break;
|
||||
case lppCurrent:
|
||||
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
|
||||
break;
|
||||
case lppFrequency:
|
||||
channelData['values']['frequency'] = buffer.readUInt32BE();
|
||||
break;
|
||||
case lppPercentage:
|
||||
channelData['values']['percentage'] = buffer.readUInt8();
|
||||
break;
|
||||
@@ -249,6 +397,32 @@ class CayenneLpp {
|
||||
case lppPower:
|
||||
channelData['values']['power'] = buffer.readUInt16BE();
|
||||
break;
|
||||
case lppDistance:
|
||||
channelData['values']['distance'] = buffer.readUInt32BE() / 1000.0;
|
||||
break;
|
||||
case lppEnergy:
|
||||
channelData['values']['energy'] = buffer.readUInt32BE() / 1000.0;
|
||||
break;
|
||||
case lppDirection:
|
||||
channelData['values']['direction'] = buffer.readUInt16BE();
|
||||
break;
|
||||
case lppUnixTime:
|
||||
channelData['values']['time'] = buffer.readUInt32BE();
|
||||
break;
|
||||
case lppGyrometer:
|
||||
channelData['values']['gyrometer'] = {
|
||||
'x': buffer.readInt16BE() / 100.0,
|
||||
'y': buffer.readInt16BE() / 100.0,
|
||||
'z': buffer.readInt16BE() / 100.0,
|
||||
};
|
||||
break;
|
||||
case lppColour:
|
||||
channelData['values']['colour'] = {
|
||||
'red': buffer.readUInt8(),
|
||||
'green': buffer.readUInt8(),
|
||||
'blue': buffer.readUInt8(),
|
||||
};
|
||||
break;
|
||||
case lppGps:
|
||||
channelData['values']['gps'] = {
|
||||
'latitude': buffer.readInt24BE() / 10000.0,
|
||||
@@ -256,22 +430,48 @@ class CayenneLpp {
|
||||
'altitude': buffer.readInt24BE() / 100.0,
|
||||
};
|
||||
break;
|
||||
// Add more types as needed...
|
||||
case lppSwitch:
|
||||
channelData['values']['switch'] = buffer.readUInt8() != 0;
|
||||
break;
|
||||
case lppPolyline:
|
||||
final size = buffer.readUInt8();
|
||||
channelData['values']['polyline'] = {
|
||||
'size': size,
|
||||
'data': _bytesToHex(_readPolylinePayload(buffer, size)),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
//Stopped parsing to avoid misalignment
|
||||
return channels.values.toList();
|
||||
// Stop parsing to avoid losing alignment on an unknown LPP type.
|
||||
return _sortedChannelValues(channels);
|
||||
}
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||
return channelsOut;
|
||||
return _sortedChannelValues(channels);
|
||||
} catch (e) {
|
||||
// Handle parsing errors, possibly due to malformed data
|
||||
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||
return <
|
||||
Map<String, dynamic>
|
||||
>[]; // Return an empty list on error to avoid crashing the app
|
||||
// Preserve any fields parsed before the malformed value.
|
||||
return _sortedChannelValues(channels);
|
||||
}
|
||||
}
|
||||
|
||||
static Uint8List _readPolylinePayload(BufferReader buffer, int size) {
|
||||
final declaredPayloadSize = size > 0 ? size - 1 : 0;
|
||||
final availablePayloadSize = declaredPayloadSize <= buffer.remaining
|
||||
? declaredPayloadSize
|
||||
: buffer.remaining;
|
||||
return buffer.readBytes(availablePayloadSize);
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> _sortedChannelValues(
|
||||
Map<int, Map<String, dynamic>> channels,
|
||||
) {
|
||||
final channelsOut = channels.values.toList();
|
||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||
return channelsOut;
|
||||
}
|
||||
|
||||
static String _bytesToHex(Uint8List bytes) {
|
||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,25 @@ class ChatScrollController extends ScrollController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Jumps toward an off-screen message so that lazy ListView.builder builds
|
||||
/// items near it. Only visible + cacheExtent items have real heights, so we
|
||||
/// use proportion of maxScrollExtent (itself an estimate from built items'
|
||||
/// avg height). Call [onJumped] on the next frame to ensureVisible/scroll
|
||||
/// to the exact target.
|
||||
void jumpToEstimatedOffset({
|
||||
required int unreadCount,
|
||||
required int totalMessages,
|
||||
required VoidCallback onJumped,
|
||||
}) {
|
||||
if (!hasClients || totalMessages == 0) return;
|
||||
final maxExtent = position.maxScrollExtent;
|
||||
final jumpOffset = maxExtent * (unreadCount / totalMessages);
|
||||
if (jumpOffset > 100) {
|
||||
jumpTo(jumpOffset);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => onJumped());
|
||||
}
|
||||
|
||||
void scrollToBottomIfAtBottom() {
|
||||
// Only scroll if jump button is NOT showing (i.e., already at bottom)
|
||||
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
|
||||
IconData contactTypeIcon(int type) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return Icons.chat;
|
||||
case advTypeRepeater:
|
||||
return Icons.cell_tower;
|
||||
case advTypeRoom:
|
||||
return Icons.group;
|
||||
case advTypeSensor:
|
||||
return Icons.sensors;
|
||||
default:
|
||||
return Icons.device_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Color contactTypeColor(int type) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return Colors.blue;
|
||||
case advTypeRepeater:
|
||||
return Colors.orange;
|
||||
case advTypeRoom:
|
||||
return Colors.purple;
|
||||
case advTypeSensor:
|
||||
return Colors.green;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
Color colorForName(String name) {
|
||||
const colors = [
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
Colors.pink,
|
||||
Colors.teal,
|
||||
Colors.indigo,
|
||||
Colors.cyan,
|
||||
Colors.amber,
|
||||
Colors.deepOrange,
|
||||
];
|
||||
return colors[name.hashCode.abs() % colors.length];
|
||||
}
|
||||
|
||||
String firstCharacterOrEmoji(String name) {
|
||||
if (name.isEmpty) return '?';
|
||||
final emoji = firstEmoji(name);
|
||||
if (emoji != null) return emoji;
|
||||
final runes = name.runes.toList();
|
||||
if (runes.isEmpty) return '?';
|
||||
return String.fromCharCode(runes[0]).toUpperCase();
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
class Cyr2Lat {
|
||||
static Map<String, String> _charMap = {
|
||||
'А': 'A',
|
||||
'В': 'B',
|
||||
'Е': 'E',
|
||||
'Ё': 'E',
|
||||
'З': '3',
|
||||
'К': 'K',
|
||||
'М': 'M',
|
||||
'Н': 'H',
|
||||
'О': 'O',
|
||||
'Р': 'P',
|
||||
'С': 'C',
|
||||
'Т': 'T',
|
||||
'Х': 'X',
|
||||
'Ь': 'b',
|
||||
'а': 'a',
|
||||
'е': 'e',
|
||||
'ё': 'e',
|
||||
'о': 'o',
|
||||
'р': 'p',
|
||||
'с': 'c',
|
||||
'у': 'y',
|
||||
'х': 'x',
|
||||
};
|
||||
|
||||
static final RegExp _prefixRegExp = RegExp(r'\@\[[\S\s]+\] ');
|
||||
|
||||
static void setCharMap(Map<String, String> charMap) {
|
||||
_charMap = Map.from(charMap);
|
||||
}
|
||||
|
||||
static String encode(String text) {
|
||||
if (text.isEmpty) return text;
|
||||
final buffer = StringBuffer();
|
||||
|
||||
final senderName = extractSenderName(text);
|
||||
final msgText = removeSenderName(text);
|
||||
|
||||
for (final rune in msgText.runes) {
|
||||
final char = String.fromCharCode(rune);
|
||||
buffer.write(_charMap[char] ?? char);
|
||||
}
|
||||
|
||||
return senderName + buffer.toString();
|
||||
}
|
||||
|
||||
static String removeSenderName(String text) {
|
||||
final match = _prefixRegExp.matchAsPrefix(text);
|
||||
if (match != null) {
|
||||
return text.substring(match.end);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static String extractSenderName(String text) {
|
||||
final match = _prefixRegExp.matchAsPrefix(text);
|
||||
if (match != null) {
|
||||
return match.group(0) ?? '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
class GifHelper {
|
||||
/// Parse a known GIF format, which can be any of:
|
||||
/// g:GIFID
|
||||
/// https://media.giphy.com/media/GIFID/giphy.gif
|
||||
/// https://giphy.com/gifs/Optional-title-with-dashes-GIFID
|
||||
///
|
||||
/// GIFID is a Giphy GIF ID. The https:// is optional (and
|
||||
/// can also be http://). The giphy.com/gifs form can also
|
||||
/// include a trailing slash.
|
||||
///
|
||||
/// Returns null if text is not a valid GIF format
|
||||
static String? parseGif(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||
if (match != null) {
|
||||
return match.group(1);
|
||||
}
|
||||
final directUrlMatch = RegExp(
|
||||
r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$',
|
||||
).firstMatch(trimmed);
|
||||
if (directUrlMatch != null) {
|
||||
return directUrlMatch.group(1);
|
||||
}
|
||||
// Giphy understands page URLs with just the ID, or any string and a
|
||||
// dash before the ID, and redirects to a page with a dash-separated
|
||||
// title, a dash, and the ID. IDs in this form *probably* can't
|
||||
// contain dashes.
|
||||
final pageMatch = RegExp(
|
||||
r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$',
|
||||
).firstMatch(trimmed);
|
||||
return pageMatch?.group(1);
|
||||
}
|
||||
|
||||
/// Encode a GIF in a format that parseGif() can parse.
|
||||
static String encodeGif(String gifId) {
|
||||
return 'g:$gifId';
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,17 @@ import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class LinkHandler {
|
||||
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
final orange = brightness == Brightness.dark
|
||||
? const Color(0xFFFFB74D)
|
||||
: const Color(0xFFE65100);
|
||||
return base.copyWith(color: orange, decoration: TextDecoration.underline);
|
||||
}
|
||||
|
||||
/// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile.
|
||||
static Widget buildLinkifyText({
|
||||
required BuildContext context,
|
||||
@@ -12,14 +21,9 @@ class LinkHandler {
|
||||
required TextStyle style,
|
||||
TextStyle? linkStyle,
|
||||
}) {
|
||||
final effectiveLinkStyle =
|
||||
linkStyle ??
|
||||
style.copyWith(
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
|
||||
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
||||
const linkifiers = [UrlLinkifier()];
|
||||
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
|
||||
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
||||
|
||||
if (PlatformInfo.isDesktop) {
|
||||
@@ -90,21 +94,19 @@ class LinkHandler {
|
||||
final uri = Uri.parse(url);
|
||||
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_couldNotOpenLink(url)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_couldNotOpenLink(url)),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_invalidLink),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_invalidLink),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,24 +8,29 @@ class PathHelper {
|
||||
.join(',');
|
||||
}
|
||||
|
||||
static String hopHex(int byte) {
|
||||
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
}
|
||||
|
||||
static String? hopName(int byte, List<Contact> allContacts) {
|
||||
final matches = allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKey.first == byte &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||
)
|
||||
.toList();
|
||||
if (matches.isEmpty) return null;
|
||||
if (matches.length == 1) return matches.first.name;
|
||||
return matches.map((c) => c.name).join(' | ');
|
||||
}
|
||||
|
||||
static String resolvePathNames(
|
||||
List<int> pathBytes,
|
||||
List<Contact> allContacts,
|
||||
) {
|
||||
return pathBytes
|
||||
.map((b) {
|
||||
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final matches = allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKey.first == b &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||
)
|
||||
.toList();
|
||||
if (matches.isEmpty) return hex;
|
||||
if (matches.length == 1) return matches.first.name;
|
||||
return matches.map((c) => c.name).join(' | ');
|
||||
})
|
||||
.map((b) => hopName(b, allContacts) ?? hopHex(b))
|
||||
.join(' \u2192 ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/contact.dart';
|
||||
|
||||
class PathHopResolver {
|
||||
const PathHopResolver._();
|
||||
|
||||
static List<Contact?> resolve({
|
||||
required List<int> pathBytes,
|
||||
required List<Contact> contacts,
|
||||
LatLng? endpoint,
|
||||
bool resolveFromEnd = false,
|
||||
}) {
|
||||
final candidatesByPrefix = <int, List<Contact>>{};
|
||||
for (final contact in contacts) {
|
||||
if (contact.publicKey.isEmpty) continue;
|
||||
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
||||
continue;
|
||||
}
|
||||
candidatesByPrefix
|
||||
.putIfAbsent(contact.publicKey.first, () => <Contact>[])
|
||||
.add(contact);
|
||||
}
|
||||
for (final candidates in candidatesByPrefix.values) {
|
||||
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||
}
|
||||
|
||||
final resolved = List<Contact?>.filled(pathBytes.length, null);
|
||||
final indexes = resolveFromEnd
|
||||
? List<int>.generate(pathBytes.length, (i) => pathBytes.length - 1 - i)
|
||||
: List<int>.generate(pathBytes.length, (i) => i);
|
||||
final distance = Distance();
|
||||
var previousPosition = endpoint;
|
||||
|
||||
for (final index in indexes) {
|
||||
final candidates = candidatesByPrefix[pathBytes[index]];
|
||||
if (candidates == null || candidates.isEmpty) continue;
|
||||
|
||||
var bestIndex = 0;
|
||||
if (previousPosition != null && candidates.length > 1) {
|
||||
double? nearestDistance;
|
||||
for (var i = 0; i < candidates.length; i++) {
|
||||
final position = _positionOf(candidates[i]);
|
||||
if (position == null) continue;
|
||||
final candidateDistance = distance(previousPosition, position);
|
||||
if (nearestDistance == null || candidateDistance < nearestDistance) {
|
||||
nearestDistance = candidateDistance;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final contact = candidates.removeAt(bestIndex);
|
||||
resolved[index] = contact;
|
||||
previousPosition = _positionOf(contact) ?? previousPosition;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
static LatLng? _positionOf(Contact contact) {
|
||||
if (!contact.hasLocation ||
|
||||
contact.latitude == null ||
|
||||
contact.longitude == null) {
|
||||
return null;
|
||||
}
|
||||
return LatLng(contact.latitude!, contact.longitude!);
|
||||
}
|
||||
}
|
||||
@@ -109,4 +109,9 @@ class ReactionHelper {
|
||||
|
||||
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
||||
}
|
||||
|
||||
/// Encode a reaction message that parseReaction() can parse.
|
||||
static String encodeReaction(String hash, String emojiIndex) {
|
||||
return 'r:$hash:$emojiIndex';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// showDismissibleSnackBar shows a [SnackBar] with tap to dismiss
|
||||
// all other properties are default and optional
|
||||
void showDismissibleSnackBar(
|
||||
BuildContext context, {
|
||||
Key? key,
|
||||
required Widget content,
|
||||
Color? backgroundColor,
|
||||
double? elevation,
|
||||
EdgeInsetsGeometry? margin,
|
||||
EdgeInsetsGeometry? padding,
|
||||
double? width,
|
||||
ShapeBorder? shape,
|
||||
HitTestBehavior? hitTestBehavior,
|
||||
SnackBarBehavior? behavior,
|
||||
SnackBarAction? action,
|
||||
double? actionOverflowThreshold,
|
||||
bool? showCloseIcon,
|
||||
Color? closeIconColor,
|
||||
Duration? duration,
|
||||
bool? persist,
|
||||
Animation<double>? animation,
|
||||
void Function()? onVisible,
|
||||
DismissDirection? dismissDirection,
|
||||
Clip? clipBehavior,
|
||||
}) {
|
||||
// Callers often reach here after an async gap; the context may already be
|
||||
// unmounted, or deactivated (popped but not yet disposed) — ancestor
|
||||
// lookups on a deactivated element throw. Showing nothing is the right
|
||||
// outcome in both cases.
|
||||
if (!context.mounted) return;
|
||||
var isActive = true;
|
||||
assert(() {
|
||||
isActive = (context as Element).debugIsActive;
|
||||
return true;
|
||||
}());
|
||||
if (!isActive) return;
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
if (messenger == null) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
key: key,
|
||||
content: GestureDetector(
|
||||
onTap: () => messenger.hideCurrentSnackBar(),
|
||||
child: content,
|
||||
),
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
width: width,
|
||||
shape: shape,
|
||||
hitTestBehavior: hitTestBehavior,
|
||||
behavior: behavior,
|
||||
action: action,
|
||||
actionOverflowThreshold: actionOverflowThreshold,
|
||||
showCloseIcon: showCloseIcon,
|
||||
closeIconColor: closeIconColor,
|
||||
duration: duration ?? const Duration(seconds: 4),
|
||||
persist: persist,
|
||||
animation: animation,
|
||||
onVisible: onVisible,
|
||||
dismissDirection: dismissDirection ?? DismissDirection.down,
|
||||
clipBehavior: clipBehavior ?? Clip.hardEdge,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,14 @@ import 'package:flutter/services.dart';
|
||||
|
||||
class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
final int maxBytes;
|
||||
final String Function(String)? encoder;
|
||||
|
||||
const Utf8LengthLimitingTextInputFormatter(this.maxBytes);
|
||||
const Utf8LengthLimitingTextInputFormatter(this.maxBytes, {this.encoder});
|
||||
|
||||
int _effectiveByteLength(String text) {
|
||||
final effective = encoder != null ? encoder!(text) : text;
|
||||
return utf8.encode(effective).length;
|
||||
}
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
@@ -13,8 +19,7 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
if (maxBytes <= 0) return oldValue;
|
||||
final bytes = utf8.encode(newValue.text);
|
||||
if (bytes.length <= maxBytes) return newValue;
|
||||
if (_effectiveByteLength(newValue.text) <= maxBytes) return newValue;
|
||||
|
||||
final truncated = _truncateToMaxBytes(newValue.text, maxBytes);
|
||||
return TextEditingValue(
|
||||
@@ -25,6 +30,14 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
}
|
||||
|
||||
String _truncateToMaxBytes(String text, int limit) {
|
||||
if (encoder != null) {
|
||||
final runes = text.runes.toList();
|
||||
while (runes.isNotEmpty &&
|
||||
_effectiveByteLength(String.fromCharCodes(runes)) > maxBytes) {
|
||||
runes.removeLast();
|
||||
}
|
||||
return String.fromCharCodes(runes);
|
||||
}
|
||||
final buffer = StringBuffer();
|
||||
var used = 0;
|
||||
for (final rune in text.runes) {
|
||||
|
||||
+780
-181
File diff suppressed because it is too large
Load Diff
+970
-265
File diff suppressed because it is too large
Load Diff
+718
-98
File diff suppressed because it is too large
Load Diff
+935
-230
File diff suppressed because it is too large
Load Diff
+912
-179
File diff suppressed because it is too large
Load Diff
+2582
File diff suppressed because it is too large
Load Diff
+988
-208
File diff suppressed because it is too large
Load Diff
+2684
File diff suppressed because it is too large
Load Diff
+2681
File diff suppressed because it is too large
Load Diff
+2194
-254
File diff suppressed because it is too large
Load Diff
+1462
-318
File diff suppressed because it is too large
Load Diff
+1552
-405
File diff suppressed because it is too large
Load Diff
+1296
-162
File diff suppressed because it is too large
Load Diff
+1501
-354
File diff suppressed because it is too large
Load Diff
+1468
-324
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1490
-341
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1397
-252
File diff suppressed because it is too large
Load Diff
+1571
-403
File diff suppressed because it is too large
Load Diff
+1332
-186
File diff suppressed because it is too large
Load Diff
+1317
-165
File diff suppressed because it is too large
Load Diff
+1342
-192
File diff suppressed because it is too large
Load Diff
+1331
-183
File diff suppressed because it is too large
Load Diff
+1326
-185
File diff suppressed because it is too large
Load Diff
+1483
-334
File diff suppressed because it is too large
Load Diff
+1213
-141
File diff suppressed because it is too large
Load Diff
+704
-105
File diff suppressed because it is too large
Load Diff
+903
-266
File diff suppressed because it is too large
Load Diff
+637
-38
@@ -33,6 +33,8 @@
|
||||
"common_remove": "Remover",
|
||||
"common_enable": "Ativar",
|
||||
"common_disable": "Desativar",
|
||||
"common_autoRefresh": "Atualização automática",
|
||||
"common_interval": "Intervalo",
|
||||
"common_reboot": "Reiniciar",
|
||||
"common_loading": "Carregando...",
|
||||
"common_notAvailable": "—",
|
||||
@@ -52,7 +54,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_title": "MeshCore Open",
|
||||
"scanner_title": "MeshCore: Versão aberta",
|
||||
"scanner_scanning": "Procurando por dispositivos...",
|
||||
"scanner_connecting": "Conectando...",
|
||||
"scanner_disconnecting": "Desconectando...",
|
||||
@@ -104,6 +106,8 @@
|
||||
"settings_privacyModeEnabled": "Modo de privacidade ativado",
|
||||
"settings_privacyModeDisabled": "Modo de privacidade desativado",
|
||||
"settings_actions": "Ações",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Enviar Publicidade",
|
||||
"settings_sendAdvertisementSubtitle": "Presença de transmissão agora",
|
||||
"settings_advertisementSent": "Anúncio enviado",
|
||||
@@ -133,12 +137,12 @@
|
||||
"settings_aboutDescription": "Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.",
|
||||
"settings_infoName": "Nome",
|
||||
"settings_infoId": "ID",
|
||||
"settings_infoStatus": "Status",
|
||||
"settings_infoStatus": "Estado",
|
||||
"settings_infoBattery": "Bateria",
|
||||
"settings_infoPublicKey": "Chave Pública",
|
||||
"settings_infoContactsCount": "Número de Contatos",
|
||||
"settings_infoChannelCount": "Número do Canal",
|
||||
"settings_presets": "Presets",
|
||||
"settings_presets": "Configurações pré-definidas",
|
||||
"settings_frequency": "Frequência (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)",
|
||||
@@ -164,19 +168,19 @@
|
||||
"appSettings_themeDark": "Escuro",
|
||||
"appSettings_language": "Idioma",
|
||||
"appSettings_languageSystem": "Padrão do sistema",
|
||||
"appSettings_languageEn": "English",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageDe": "Deutsch",
|
||||
"appSettings_languagePl": "Polski",
|
||||
"appSettings_languageSl": "Slovenščina",
|
||||
"appSettings_languageEn": "Inglês",
|
||||
"appSettings_languageFr": "Francês",
|
||||
"appSettings_languageEs": "Espanhol",
|
||||
"appSettings_languageDe": "Alemão",
|
||||
"appSettings_languagePl": "Polonês",
|
||||
"appSettings_languageSl": "Esloveno",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageIt": "Italiano",
|
||||
"appSettings_languageZh": "中文",
|
||||
"appSettings_languageSv": "Svenska",
|
||||
"appSettings_languageNl": "Nederlands",
|
||||
"appSettings_languageSk": "Slovenčina",
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_languageZh": "Chinês",
|
||||
"appSettings_languageSv": "Sueco",
|
||||
"appSettings_languageNl": "Holandês",
|
||||
"appSettings_languageSk": "Esloveno",
|
||||
"appSettings_languageBg": "Búlgaro",
|
||||
"appSettings_notifications": "Notificações",
|
||||
"appSettings_enableNotifications": "Ativar Notificações",
|
||||
"appSettings_enableNotificationsSubtitle": "Receber notificações para mensagens e anúncios",
|
||||
@@ -337,11 +341,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_hashtagChannel": "Canal com hashtag",
|
||||
"channels_public": "Público",
|
||||
"channels_private": "Privado",
|
||||
"channels_publicChannel": "Canal público",
|
||||
"channels_privateChannel": "Canal privado",
|
||||
"channels_editChannel": "Editar canal",
|
||||
"channels_muteChannel": "Silenciar canal",
|
||||
"channels_unmuteChannel": "Ativar canal",
|
||||
@@ -388,6 +389,22 @@
|
||||
}
|
||||
},
|
||||
"channels_smazCompression": "Compressão SMAZ",
|
||||
"channels_cyr2latCompression": "Compressão Cyr2Lat",
|
||||
"channels_cyr2latCompressionDscr": "Substitui alguns caracteres cirílicos por caracteres latinos ao enviar.",
|
||||
"channels_cyr2latSettingsHeading": "Configuração do Cyr2Lat",
|
||||
"channels_cyr2latSettingsSubheading": "Lista de substituições",
|
||||
"channels_cyr2latSettingsDscr": "Editar a configuração JSON de substituição de caracteres",
|
||||
"channels_cyr2latSettingsDialogHint": "Mapa de substituições JSON",
|
||||
"channels_cyr2latSettingsDialogWrongJSON": "JSON incorreto: {error}",
|
||||
"settings_cyr2latProfileAdd": "Adicionar perfil Cyr2Lat",
|
||||
"settings_cyr2latProfileName": "Nome do perfil",
|
||||
"settings_cyr2latProfileNameEmpty": "O nome do perfil não pode estar vazio",
|
||||
"settings_cyr2latProfileAdded": "Perfil adicionado com sucesso",
|
||||
"settings_cyr2latProfileUpdated": "Perfil atualizado com sucesso",
|
||||
"settings_cyr2latProfileEdit": "Editar perfil Cyr2Lat",
|
||||
"settings_cyr2latProfileDelete": "Eliminar perfil Cyr2Lat",
|
||||
"settings_cyr2latProfileDeleted": "Perfil eliminado com sucesso",
|
||||
"settings_cyr2latProfileDeleteDscr": "Tem a certeza de que deseja eliminar o perfil \"{name}\"?",
|
||||
"channels_channelUpdated": "Canal \"{name}\" atualizado",
|
||||
"@channels_channelUpdated": {
|
||||
"placeholders": {
|
||||
@@ -399,7 +416,7 @@
|
||||
"channels_publicChannelAdded": "Canal público adicionado",
|
||||
"channels_sortBy": "Ordenar por",
|
||||
"channels_sortManual": "Manual",
|
||||
"channels_sortAZ": "A-Z",
|
||||
"channels_sortAZ": "De A a Z",
|
||||
"channels_sortLatestMessages": "Últimas mensagens",
|
||||
"channels_sortUnread": "Não lido",
|
||||
"chat_noMessages": "Ainda não existem mensagens.",
|
||||
@@ -504,7 +521,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"debugFrame_timestamp": "- Timestamp: {timestamp}",
|
||||
"debugFrame_timestamp": "- Carimbo: {timestamp}",
|
||||
"@debugFrame_timestamp": {
|
||||
"placeholders": {
|
||||
"timestamp": {
|
||||
@@ -531,7 +548,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"debugFrame_textTypeCli": "CLI",
|
||||
"debugFrame_textTypeCli": "Interface de Linha de Comando",
|
||||
"debugFrame_textTypePlain": "Simples",
|
||||
"debugFrame_text": "- Texto: \"{text}\"",
|
||||
"@debugFrame_text": {
|
||||
@@ -550,7 +567,7 @@
|
||||
"chat_pathHistoryFull": "O histórico está cheio. Remova entradas para adicionar novas.",
|
||||
"chat_hopSingular": "pule",
|
||||
"chat_hopPlural": "salta",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{salto} other{saltos}}",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -819,7 +836,7 @@
|
||||
"login_autoUseSavedPath": "Auto (usar caminho salvo)",
|
||||
"login_forceFloodMode": "Modo de Inundação Forçado",
|
||||
"login_managePaths": "Gerenciar Caminhos",
|
||||
"login_login": "Login",
|
||||
"login_login": "Entrar",
|
||||
"login_attempt": "Tentar {current}/{max}",
|
||||
"@login_attempt": {
|
||||
"placeholders": {
|
||||
@@ -879,11 +896,11 @@
|
||||
"path_setPath": "Definir Caminho",
|
||||
"repeater_management": "Gerenciamento de Repetidor",
|
||||
"repeater_managementTools": "Ferramentas de Gerenciamento",
|
||||
"repeater_status": "Status",
|
||||
"repeater_status": "Estado",
|
||||
"repeater_statusSubtitle": "Visualizar status do repetidor, estatísticas e vizinhos.",
|
||||
"repeater_telemetry": "Telemetria",
|
||||
"repeater_telemetrySubtitle": "Visualizar telemetria de sensores e estatísticas do sistema",
|
||||
"repeater_cli": "CLI",
|
||||
"repeater_cli": "Interface de Linha de Comando",
|
||||
"repeater_cliSubtitle": "Enviar comandos ao repetidor",
|
||||
"repeater_settings": "Configurações",
|
||||
"repeater_settingsSubtitle": "Configurar parâmetros do repetidor",
|
||||
@@ -993,7 +1010,7 @@
|
||||
"repeater_radioSettings": "Configurações de Rádio",
|
||||
"repeater_frequencyMhz": "Frequência (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
"repeater_txPower": "TX Power",
|
||||
"repeater_txPower": "Energia da TX",
|
||||
"repeater_txPowerHelper": "1-30 dBm",
|
||||
"repeater_bandwidth": "Largura de banda",
|
||||
"repeater_spreadingFactor": "Fator de Dispersão",
|
||||
@@ -1059,6 +1076,81 @@
|
||||
},
|
||||
"repeater_confirm": "Confirmar",
|
||||
"repeater_settingsSaved": "Configurações salvas com sucesso",
|
||||
"repeater_rxGain": "Aumento do ganho do RX",
|
||||
"repeater_rxGainHelper": "Maior sensibilidade, maior consumo de corrente (apenas para SX1262/SX1268)",
|
||||
"repeater_refreshRxGain": "Reforçar o ganho do RX",
|
||||
"repeater_multiAcks": "Múltiplas respostas de confirmação",
|
||||
"repeater_multiAcksSubtitle": "Reconheça mensagens através de múltiplos caminhos para uma melhor entrega.",
|
||||
"repeater_refreshMultiAcks": "Reiniciar múltiplas confirmações",
|
||||
"repeater_networkHealth": "Saúde da rede",
|
||||
"repeater_loopDetect": "Detecção de loops",
|
||||
"repeater_loopDetectHelper": "Envie pacotes que pareçam ser loops de roteamento.",
|
||||
"repeater_loopDetectOff": "Desligado",
|
||||
"repeater_loopDetectMinimal": "Mínimo",
|
||||
"repeater_loopDetectModerate": "Moderado",
|
||||
"repeater_loopDetectStrict": "Rígido",
|
||||
"repeater_dutyCycle": "Ciclo de operação",
|
||||
"repeater_dutyCycleHelper": "Porcentagem máxima de tempo de transmissão",
|
||||
"repeater_dutyCyclePercent": "{percent}%",
|
||||
"@repeater_dutyCyclePercent": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_ownerInfo": "Informações sobre o operador",
|
||||
"repeater_ownerInfoHelper": "Metadados públicos para este repetidor",
|
||||
"repeater_refreshOwnerInfo": "Atualizar informações do operador",
|
||||
"repeater_floodMax": "Número máximo de saltos em caso de inundação",
|
||||
"repeater_floodMaxHelper": "Número máximo de saltos que um pacote de inundação pode percorrer (0-64)",
|
||||
"repeater_advancedSettings": "Avançado",
|
||||
"repeater_advancedSettingsSubtitle": "Controles de ajuste para operadores experientes",
|
||||
"repeater_pathHashMode": "Modo de hash de caminho",
|
||||
"repeater_pathHashModeHelper": "Bytes utilizados para codificar o ID deste repetidor nas tags de caminho/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). As versões 1.13 e anteriores do firmware não suportam caminhos multi-byte — apenas funcionam uma vez após a ativação da rede (a partir da versão 1.14+).",
|
||||
"repeater_txDelay": "Atraso na entrega em Flood, TX",
|
||||
"repeater_txDelayHelper": "Ajuste de espaçamento para tráfego de inundações, como um multiplicador do tempo de transmissão (0-2, padrão 0,5). Quanto maior, menos colisões, mas uma entrega mais lenta.",
|
||||
"repeater_directTxDelay": "Atraso direto no sinal TX",
|
||||
"repeater_directTxDelayHelper": "Intervalo de retransmissão para tráfego direto (não em enxame), como um multiplicador do tempo de transmissão do pacote (0-2, padrão 0,3).",
|
||||
"repeater_intThresh": "Limite de interferência",
|
||||
"repeater_intThreshHelper": "O limite é definido para o nível de ruído do rádio, de modo que ele rejeite interferências acima desse nível. 0 desativa – aumente apenas se você observar erros de RX em uma faixa de frequência com ruído.",
|
||||
"repeater_agcResetInterval": "Intervalo de reinicialização do AGC",
|
||||
"repeater_agcResetIntervalHelper": "Com que frequência redefinir o controle automático de ganho do rádio para recuperar de um estado em que o ganho está travado. Segundos, reduzidos a um múltiplo de 4. 0 desativa as redefinições periódicas.",
|
||||
"repeater_actionsTitle": "Ações",
|
||||
"repeater_sendAdvert": "Envie anúncio sobre inundações",
|
||||
"repeater_sendAdvertSubtitle": "Transmita um anúncio sobre inundações pela rede.",
|
||||
"repeater_sendAdvertZeroHop": "Enviar anúncio sem intermediários",
|
||||
"repeater_sendAdvertZeroHopSubtitle": "Transmita um anúncio de um único salto (sem repetição).",
|
||||
"repeater_clockSync": "Sincronize o relógio agora",
|
||||
"repeater_clockSyncSubtitle": "Envie a hora do seu telefone para o repetidor.",
|
||||
"repeater_actionSucceeded": "{action} succeeded",
|
||||
"@repeater_actionSucceeded": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_actionFailed": "{action} failed: {error}",
|
||||
"@repeater_actionFailed": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
},
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsSavedRebootNeeded": "Configurações salvas — reinicie o repetidor para aplicar as alterações.",
|
||||
"repeater_settingsPartialFailure": "Algumas configurações falharam: {failures}",
|
||||
"@repeater_settingsPartialFailure": {
|
||||
"placeholders": {
|
||||
"failures": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_errorSavingSettings": "Erro ao salvar as configurações: {error}",
|
||||
"@repeater_errorSavingSettings": {
|
||||
"placeholders": {
|
||||
@@ -1070,11 +1162,9 @@
|
||||
"repeater_refreshBasicSettings": "Atualizar Configurações Básicas",
|
||||
"repeater_refreshRadioSettings": "Atualizar Configurações de Rádio",
|
||||
"repeater_refreshTxPower": "Atualizar TX de energia",
|
||||
"repeater_refreshLocationSettings": "Atualizar Configurações de Localização",
|
||||
"repeater_refreshPacketForwarding": "Atualizar Roteamento de Pacotes",
|
||||
"repeater_refreshGuestAccess": "Atualizar Acesso de Convidados",
|
||||
"repeater_refreshPrivacyMode": "Atualizar Modo Privacidade",
|
||||
"repeater_refreshAdvertisementSettings": "Atualizar Configurações do Anúncio",
|
||||
"repeater_refreshed": "{label} atualizado",
|
||||
"@repeater_refreshed": {
|
||||
"placeholders": {
|
||||
@@ -1192,6 +1282,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_digitalInputLabel": "Entrada digital",
|
||||
"telemetry_digitalOutputLabel": "Saída digital",
|
||||
"telemetry_analogInputLabel": "Entrada analógica",
|
||||
"telemetry_analogOutputLabel": "Saída analógica",
|
||||
"telemetry_genericLabel": "Sensor genérico",
|
||||
"telemetry_luminosityLabel": "Luminosidade",
|
||||
"telemetry_presenceLabel": "Presença",
|
||||
"telemetry_humidityLabel": "Humidade",
|
||||
"telemetry_accelerometerLabel": "Acelerómetro",
|
||||
"telemetry_pressureLabel": "Pressão",
|
||||
"telemetry_altitudeLabel": "Altitude",
|
||||
"telemetry_frequencyLabel": "Frequência",
|
||||
"telemetry_percentageLabel": "Percentagem",
|
||||
"telemetry_concentrationLabel": "Concentração",
|
||||
"telemetry_powerLabel": "Potência",
|
||||
"telemetry_distanceLabel": "Distância",
|
||||
"telemetry_energyLabel": "Energia",
|
||||
"telemetry_directionLabel": "Direção",
|
||||
"telemetry_timeLabel": "Hora",
|
||||
"telemetry_gyrometerLabel": "Girómetro",
|
||||
"telemetry_colourLabel": "Cor",
|
||||
"telemetry_gpsLabel": "GPS",
|
||||
"telemetry_switchLabel": "Interruptor",
|
||||
"telemetry_polylineLabel": "Polilinha",
|
||||
"telemetry_altitudeValue": "{meters} m",
|
||||
"telemetry_frequencyValue": "{hertz} Hz",
|
||||
"telemetry_pressureValue": "{hpa} hPa",
|
||||
"telemetry_luminosityValue": "{lux} lx",
|
||||
"telemetry_powerValue": "{watts} W",
|
||||
"telemetry_distanceValue": "{meters} m",
|
||||
"telemetry_energyValue": "{kilowattHours} kWh",
|
||||
"telemetry_directionValue": "{degrees}°",
|
||||
"telemetry_concentrationValue": "{ppm} ppm",
|
||||
"telemetry_percentageValue": "{percent}%",
|
||||
"telemetry_analogValue": "{value}",
|
||||
"telemetry_autoFetchQuantity": "Número de solicitações",
|
||||
"telemetry_error": "Não foi possível obter os dados",
|
||||
"telemetry_noData": "Não estão disponíveis dados de telemetria.",
|
||||
"telemetry_channelTitle": "Canal {channel}",
|
||||
"@telemetry_channelTitle": {
|
||||
@@ -1347,7 +1474,7 @@
|
||||
"listFilter_sortBy": "Ordenar por",
|
||||
"listFilter_latestMessages": "Últimas mensagens",
|
||||
"listFilter_heardRecently": "Ouvido recentemente",
|
||||
"listFilter_az": "A-Z",
|
||||
"listFilter_az": "De A a Z",
|
||||
"listFilter_filters": "Filtros",
|
||||
"listFilter_all": "Tudo",
|
||||
"listFilter_users": "Usuários",
|
||||
@@ -1461,7 +1588,7 @@
|
||||
},
|
||||
"community_title": "Comunidade",
|
||||
"community_createDesc": "Crie uma nova comunidade e compartilhe via código QR.",
|
||||
"common_ok": "OK",
|
||||
"common_ok": "Tudo bem",
|
||||
"community_create": "Criar Comunidade",
|
||||
"community_join": "Junte-se",
|
||||
"community_joinTitle": "Junte-se à Comunidade",
|
||||
@@ -1922,13 +2049,6 @@
|
||||
"contact_telemetry": "Telemetria",
|
||||
"contact_settings": "Configurações de Contato",
|
||||
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso Inicial da Rota",
|
||||
"appSettings_maxRouteWeight": "Peso Máximo da Rota",
|
||||
"appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.",
|
||||
@@ -1941,5 +2061,484 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
}
|
||||
"settings_multiAck": "Multi-ACKs",
|
||||
"map_showOverlaps": "Sobreposições da Chave Repeater",
|
||||
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_jumpToOldestUnread": "Vá para a mensagem mais antiga não lida",
|
||||
"chat_sendCooldown": "Por favor, aguarde um momento antes de reenviar.",
|
||||
"appSettings_languageHu": "Húngaro",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.",
|
||||
"appSettings_languageJa": "Japonês",
|
||||
"appSettings_languageKo": "Coreano",
|
||||
"radioStats_tooltip": "Estatísticas de rádio e malha",
|
||||
"radioStats_screenTitle": "Estatísticas de rádio",
|
||||
"radioStats_notConnected": "Conecte-se a um dispositivo para visualizar estatísticas de rádio.",
|
||||
"radioStats_firmwareTooOld": "As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.",
|
||||
"radioStats_waiting": "Aguardando dados…",
|
||||
"radioStats_noiseFloor": "Nível de ruído: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Último SNR: {snr} dB",
|
||||
"radioStats_txAir": "Tempo de transmissão da TX (total): {seconds} s",
|
||||
"radioStats_rxAir": "Tempo de uso do RX (total): {seconds} s",
|
||||
"radioStats_chartCaption": "Nível de ruído (dBm) em amostras recentes.",
|
||||
"radioStats_stripNoise": "Nível de ruído: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Obtendo estatísticas de rádio…",
|
||||
"radioStats_settingsTile": "Estatísticas de rádio",
|
||||
"radioStats_settingsSubtitle": "Nível de ruído, RSSI, SNR e tempo de transmissão",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Traduza antes de enviar",
|
||||
"translation_enableSubtitle": "Traduzir mensagens recebidas e permitir a tradução antes do envio.",
|
||||
"translation_enableTitle": "Ativar a tradução",
|
||||
"translation_title": "Tradução",
|
||||
"translation_composerSubtitle": "Controla o estado padrão do ícone de tradução do compositor.",
|
||||
"translation_autoIncomingTitle": "Traduzir mensagens automaticamente",
|
||||
"translation_autoIncomingSubtitle": "Traduz automaticamente mensagens para notificações e para chats ou canais.",
|
||||
"translation_translateMessage": "Traduzir mensagem",
|
||||
"translation_targetLanguage": "Língua-alvo",
|
||||
"translation_useAppLanguage": "Utilize o idioma da aplicação",
|
||||
"translation_downloadedModelLabel": "Modelo baixado",
|
||||
"translation_presetModelLabel": "Modelo pré-definido da Hugging Face",
|
||||
"translation_manualUrlLabel": "URL do modelo manual",
|
||||
"translation_downloading": "Baixando...",
|
||||
"translation_downloadModel": "Baixar modelo",
|
||||
"translation_working": "Trabalhando...",
|
||||
"translation_stop": "Pare",
|
||||
"translation_mergingChunks": "Combinando os fragmentos baixados em um único arquivo...",
|
||||
"translation_downloadedModels": "Modelos baixados",
|
||||
"translation_deleteModel": "Excluir modelo",
|
||||
"translation_modelDownloaded": "Modelo de tradução baixado.",
|
||||
"translation_downloadStopped": "Download interrompido.",
|
||||
"translation_downloadFailed": "Falha na descarga: {error}",
|
||||
"translation_enterUrlFirst": "Insira primeiro a URL do modelo.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_messageTranslation": "Tradução da mensagem",
|
||||
"translation_translateBeforeSending": "Traduzir antes de enviar",
|
||||
"translation_composerEnabledHint": "As mensagens serão traduzidas antes de serem enviadas.",
|
||||
"translation_composerDisabledHint": "Envie mensagens no idioma original, conforme digitado.",
|
||||
"translation_translateTo": "Traduzir para {language}",
|
||||
"translation_translationOptions": "Opções de tradução",
|
||||
"translation_systemLanguage": "Idioma do sistema",
|
||||
"scanner_linuxPairingShowPin": "Mostrar PIN",
|
||||
"scanner_linuxPairingHidePin": "Ocultar PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).",
|
||||
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth",
|
||||
"repeater_cliQuickClockSync": "Sincronização do Relógio",
|
||||
"repeater_cliQuickDiscovery": "Descobrir Vizinhos",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.",
|
||||
"repeater_clockSyncAfterLogin": "Sincronização do relógio após o login",
|
||||
"room_guest": "Informações do Servidor",
|
||||
"chat_sendMessage": "Enviar mensagem",
|
||||
"repeater_guest": "Informações sobre repetidores",
|
||||
"repeater_guestTools": "Ferramentas para hóspedes",
|
||||
"repeater_getCategory": "Obter valores",
|
||||
"repeater_powerMgmt": "Gerenciamento de energia",
|
||||
"repeater_sensors": "Sensores",
|
||||
"repeater_cliHelpPowerOff": "Desliga o dispositivo. (não se espera resposta)",
|
||||
"repeater_cliHelpClkReboot": "Redefine o relógio para uma data conhecida e reinicia o dispositivo.",
|
||||
"repeater_cliHelpAdvertZeroHop": "Envia um anúncio sem \"salto\" (apenas para vizinhos próximos).",
|
||||
"repeater_cliHelpStartOta": "Inicia uma atualização de firmware via rádio em placas compatíveis.",
|
||||
"repeater_cliHelpTime": "Define o relógio do dispositivo para os segundos da época Unix especificados. O relógio não pode retroceder.",
|
||||
"repeater_cliHelpBoard": "Indica o fabricante da placa / identificador de hardware.",
|
||||
"repeater_cliHelpDiscoverNeighbors": "Envia uma solicitação de descoberta de nós para os vizinhos próximos. (Apenas para repetidores)",
|
||||
"repeater_cliHelpPowersaving": "Indica se o modo de economia de energia está ativado ou desativado.",
|
||||
"repeater_cliHelpPowersavingOnOff": "Habilita ou desabilita o modo de economia de energia (quando disponível).",
|
||||
"repeater_cliHelpErase": "(Apenas para dispositivos) Formata o sistema de arquivos do dispositivo. Apaga todas as configurações e contatos.",
|
||||
"repeater_cliHelpSetDutyCycle": "Define o ciclo de transmissão máximo permitido como uma porcentagem (1-100). Ajusta internamente o fator de tempo de transmissão.",
|
||||
"repeater_cliHelpSetPrvKey": "(Apenas para uso em série) Substitui a chave privada de identificação do dispositivo. É necessário reiniciar o dispositivo para aplicar a alteração. Gera uma nova chave pública.",
|
||||
"repeater_cliHelpSetRadioRxGain": "(Apenas para SX126x) Alterna o ganho amplificado do receptor (RX) para melhorar a sensibilidade em condições de corrente mais elevada.",
|
||||
"repeater_cliHelpSetOwnerInfo": "Define a string com as informações de contato do proprietário, que será incluída nos anúncios. Utilize '|' para indicar novas linhas.",
|
||||
"repeater_cliHelpSetPathHashMode": "Define o modo de hash de caminho. 0 = modo legado, 1 = modo padrão, 2 = modo rigoroso. Afeta a forma como os caminhos de roteamento são correspondidos.",
|
||||
"repeater_cliHelpSetLoopDetect": "Define o nível de sensibilidade para a detecção de loops de roteamento: desligado, mínimo, moderado ou estrito.",
|
||||
"repeater_cliHelpSetFreq": "(Apenas para rádio) Define rapidamente a frequência. É necessário reiniciar o dispositivo. Recomenda-se usar a opção \"configurar rádio\" para definir todos os parâmetros do rádio.",
|
||||
"repeater_cliHelpSetBridgeChannel": "(Apenas para a ponte ESPNow) Define o canal Wi-Fi (1-14) utilizado pela ponte.",
|
||||
"repeater_cliHelpGetName": "Mostra o nome do nó configurado.",
|
||||
"repeater_cliHelpGetRole": "Mostra o papel do firmware (Repetidor, Servidor de Sala, etc.).",
|
||||
"repeater_cliHelpGetPublicKey": "Exibe a chave pública do dispositivo.",
|
||||
"repeater_cliHelpGetPrvKey": "(Apenas para uso em série) Exibe a chave privada do dispositivo. Trate-a como uma informação confidencial.",
|
||||
"repeater_cliHelpGetRepeat": "Indica se a função de encaminhamento de pacotes (função de repetidor) está ativada ou desativada.",
|
||||
"repeater_cliHelpGetTx": "Mostra a potência atual em dBm.",
|
||||
"repeater_cliHelpGetFreq": "Mostra a frequência de rádio configurada em MHz.",
|
||||
"repeater_cliHelpGetRadio": "Exibe todos os parâmetros de rádio: frequência, largura de banda, fator de espalhamento, taxa de codificação.",
|
||||
"repeater_cliHelpGetRadioRxGain": "(Apenas para SX126x) Mostra o estado do ganho amplificado do RX.",
|
||||
"repeater_cliHelpGetAf": "Mostra o fator de tempo de transmissão atual.",
|
||||
"repeater_cliHelpGetDutyCycle": "Mostra o ciclo de trabalho atual permitido em porcentagem.",
|
||||
"repeater_cliHelpGetIntThresh": "Mostra o limite de interferência do canal em dB.",
|
||||
"repeater_cliHelpGetAgcResetInterval": "Mostra o intervalo de reinicialização do AGC em segundos.",
|
||||
"repeater_cliHelpGetMultiAcks": "Indica se o modo de confirmação dupla está ativado (1) ou desativado (0).",
|
||||
"repeater_cliHelpGetAllowReadOnly": "Indica se o acesso somente de leitura para os convidados está habilitado.",
|
||||
"repeater_cliHelpGetAdvertInterval": "Indica o intervalo de publicidade local em minutos.",
|
||||
"repeater_cliHelpGetFloodAdvertInterval": "Mostra o intervalo de tempo da publicidade relacionada às inundações, em horas.",
|
||||
"repeater_cliHelpGetGuestPassword": "Mostra a senha de convidado configurada.",
|
||||
"repeater_cliHelpGetLat": "Mostra a latitude configurada.",
|
||||
"repeater_cliHelpGetLon": "Mostra a longitude configurada.",
|
||||
"repeater_cliHelpGetRxDelay": "Mostra o valor base do atraso de resposta.",
|
||||
"repeater_cliHelpGetTxDelay": "Mostra o fator de atraso em modo de inundação.",
|
||||
"repeater_cliHelpGetDirectTxDelay": "Mostra o fator de atraso direto.",
|
||||
"repeater_cliHelpGetFloodMax": "Mostra o número máximo de saltos devido às inundações.",
|
||||
"repeater_cliHelpGetOwnerInfo": "Exibe a string de informações de contato do proprietário.",
|
||||
"repeater_cliHelpGetPathHashMode": "Mostra o modo de hash de caminho (0/1/2).",
|
||||
"repeater_cliHelpGetLoopDetect": "Demonstra a sensibilidade na detecção de loops.",
|
||||
"repeater_cliHelpGetAcl": "(Apenas para séries) Lista as entradas de controle de acesso em um repetidor.",
|
||||
"repeater_cliHelpGetBridgeEnabled": "Indica se a ponte está habilitada.",
|
||||
"repeater_cliHelpGetBridgeDelay": "Mostra o atraso da ponte em milissegundos.",
|
||||
"repeater_cliHelpGetBridgeSource": "Indica se a ponte está enviando ou recebendo pacotes RX ou TX.",
|
||||
"repeater_cliHelpGetBridgeBaud": "(Apenas para ponte RS232) Exibe a taxa de baud da ponte.",
|
||||
"repeater_cliHelpGetBridgeChannel": "(Apenas para a ponte ESPNow) Exibe o canal WiFi da ponte.",
|
||||
"repeater_cliHelpGetBridgeSecret": "(Apenas para a ponte ESPNow) Exibe o segredo compartilhado pela ponte.",
|
||||
"repeater_cliHelpGetBootloaderVer": "(Apenas para NRF52) Exibe a versão do bootloader.",
|
||||
"repeater_cliHelpGetAdcMultiplier": "Mostra o multiplicador do ADC (escalonamento da tensão da bateria).",
|
||||
"repeater_cliHelpGetPwrMgtSupport": "Indica se o sistema possui suporte para gerenciamento de energia.",
|
||||
"repeater_cliHelpGetPwrMgtSource": "Indica a fonte de energia atual: externa ou bateria.",
|
||||
"repeater_cliHelpGetPwrMgtBootReason": "Mostra as razões mais recentes para a reinicialização e desligamento.",
|
||||
"repeater_cliHelpGetPwrMgtBootMv": "Mostra a tensão da bateria no momento da inicialização, em milivolts (mV).",
|
||||
"repeater_cliHelpSensorGet": "Lê uma configuração de sensor personalizada através de uma chave.",
|
||||
"repeater_cliHelpSensorSet": "Cria uma configuração personalizada para um sensor.",
|
||||
"repeater_cliHelpSensorList": "Lista todas as configurações de sensores personalizadas, organizadas em páginas a partir de um índice de início opcional.",
|
||||
"repeater_cliHelpRegionDefault": "Mostra o escopo de região padrão atual.",
|
||||
"repeater_cliHelpRegionDefaultSet": "Define o escopo regional padrão. Use \"<null>\" para limpar.",
|
||||
"repeater_cliHelpRegionListAllowed": "Lista as regiões que permitem o tráfego em áreas de risco de inundações.",
|
||||
"repeater_cliHelpRegionListDenied": "Lista as regiões que restringem o tráfego em áreas de risco de inundações.",
|
||||
"repeater_cliHelpStatsPackets": "(Apenas para séries) Apresenta estatísticas em nível de pacotes.",
|
||||
"repeater_cliHelpStatsRadio": "(Apenas para transmissões em série) Exibe estatísticas de rádio.",
|
||||
"repeater_cliHelpStatsCore": "(Apenas para dispositivos em série) Exibe estatísticas básicas do firmware.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "Compartilhado",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Locais ocupados",
|
||||
"losBlockedSpotsHint": "Toque em um ponto bloqueado para destacá-lo no mapa.",
|
||||
"losSelectedObstructionTitle": "Obstrução selecionada",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||
"settings_companionDebugLog": "Registro de depuração auxiliar",
|
||||
"settings_companionDebugLogSubtitle": "Comandos, respostas e dados brutos para protocolos BLE/TCP/USB",
|
||||
"chat_markAsUnread": "Marcar como não lido",
|
||||
"chat_newMessages": "Novas mensagens",
|
||||
"repeater_chanUtil": "Utilização do canal",
|
||||
"@routing_lastWorked": {
|
||||
"placeholders": {
|
||||
"when": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@routing_deliveryCounts": {
|
||||
"placeholders": {
|
||||
"successes": {
|
||||
"type": "int"
|
||||
},
|
||||
"failures": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_hopCounter": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_invalidTokens": {
|
||||
"placeholders": {
|
||||
"tokens": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_communityShortId": {
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_undo": "Desfazer",
|
||||
"messageStatus_sent": "Enviado",
|
||||
"messageStatus_pending": "Enviar",
|
||||
"messageStatus_delivered": "Entregue",
|
||||
"messageStatus_failed": "Falhou ao enviar",
|
||||
"messageStatus_repeated": "Ouvi repetidamente",
|
||||
"contacts_moreOptions": "Mais opções",
|
||||
"contacts_searchOpen": "Pesquisar contatos",
|
||||
"contacts_searchClose": "Pesquisa avançada",
|
||||
"routing_title": "Rotas",
|
||||
"routing_modeAuto": "Carro",
|
||||
"routing_modeFlood": "Inundação",
|
||||
"routing_modeManual": "Manual",
|
||||
"routing_modeAutoHint": "Seleciona automaticamente o caminho mais conhecido, e, se nenhum caminho conhecido for encontrado, utiliza a estratégia de \"inundação\".",
|
||||
"routing_modeFloodHint": "Transmissão através de todos os repetidores. É a opção mais confiável, mas utiliza mais tempo de transmissão.",
|
||||
"routing_modeManualHint": "Sempre segue exatamente o caminho que você define.",
|
||||
"routing_currentRoute": "Rota atual",
|
||||
"routing_directNoHops": "Direto – sem saltos de repetidor",
|
||||
"routing_noPathYet": "Ainda não há um caminho definido. A mensagem continua a ser enviada até que uma rota seja encontrada.",
|
||||
"routing_floodBroadcast": "Transmissão através de todos os repetidores",
|
||||
"routing_editPath": "Editar caminho",
|
||||
"routing_forgetPath": "Esqueça o caminho",
|
||||
"routing_knownPaths": "Rotas conhecidas",
|
||||
"routing_knownPathsHint": "Toque em um caminho para alternar para ele.",
|
||||
"routing_inUse": "Em uso",
|
||||
"routing_qualityStrong": "Primeiro salto notável",
|
||||
"routing_qualityGood": "Primeiro salto bem-sucedido",
|
||||
"routing_qualityFair": "Primeira etapa bem-sucedida",
|
||||
"routing_qualityWorked": "Foi entregue",
|
||||
"routing_qualityFlood": "Informação obtida através de relatos generalizados.",
|
||||
"routing_qualityUntested": "Não testado",
|
||||
"routing_neverWorked": "nunca confirmado",
|
||||
"routing_floodDelivery": "Entrega em áreas afetadas por inundações",
|
||||
"pathEditor_title": "Criar Caminho",
|
||||
"pathEditor_hopCounter": "{count} de 64 gramas de lúpulo",
|
||||
"pathEditor_noHops": "Ainda não há lúpulos adicionados. Clique nos repetidores abaixo para adicioná-los na ordem desejada, ou salve sem adicionar lúpulos para enviar diretamente.",
|
||||
"pathEditor_addHops": "Adicione os lúpulos na seguinte ordem.",
|
||||
"pathEditor_searchRepeaters": "Encontrar repetidores",
|
||||
"pathEditor_advancedHex": "Avançado: caminho hexadecimal bruto",
|
||||
"pathEditor_hexLabel": "Prefixos hexadecimais",
|
||||
"pathEditor_hexHelper": "Dois caracteres hexadecimais por salto, separados por vírgulas.",
|
||||
"pathEditor_invalidTokens": "Inválido: {tokens}",
|
||||
"routing_lastWorked": "worked {when}",
|
||||
"pathEditor_tooManyHops": "Máximo de 64 saltos",
|
||||
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
|
||||
"pathEditor_usePath": "Utilize este caminho.",
|
||||
"pathEditor_removeHop": "Remova o lúpulo",
|
||||
"pathEditor_unknownHop": "Repetidor desconhecido",
|
||||
"map_zoomIn": "Ampliar",
|
||||
"map_zoomOut": "Ampliar",
|
||||
"map_centerMap": "Mapa do centro",
|
||||
"chrome_bluetoothRequiresChromium": "O Web Bluetooth requer um navegador Chromium.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS confirmado",
|
||||
"pathTrace_legendInferred": "Posição inferida",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_online": "Online",
|
||||
"map_activity": "Atividade",
|
||||
"scanner_bluetoothWebUnsupported": "A funcionalidade Bluetooth não está disponível no navegador. Conecte-se via USB em vez disso.",
|
||||
"map_searchHint": "Pesquisar por nome ou ID do nó",
|
||||
"map_recent": "Recente",
|
||||
"map_stale": "Vencido",
|
||||
"map_visible": "Visível",
|
||||
"map_hidden": "Escondido",
|
||||
"map_centerOnNode": "Centralizar no nó",
|
||||
"map_details": "Detalhes",
|
||||
"map_noGps": "Sem GPS",
|
||||
"map_noResults": "Nenhum nó encontrado",
|
||||
"pathMap_viewSingle": "Único",
|
||||
"pathMap_viewCombined": "Combinado",
|
||||
"pathMap_play": "Reproduzir",
|
||||
"pathMap_pause": "Pausa",
|
||||
"pathMap_stepBack": "Salto anterior",
|
||||
"pathMap_replay": "Repetir",
|
||||
"pathMap_stepForward": "Próximo salto",
|
||||
"pathMap_animationOn": "Exibir animação do pacote",
|
||||
"pathMap_animationOff": "Ocultar a animação do pacote",
|
||||
"pathMap_hopOf": "Salto {current} de {total}",
|
||||
"pathMap_observedPaths": "Caminhos observados: {count}",
|
||||
"pathMap_primary": "Primário",
|
||||
"pathMap_alternate": "Alt {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 salto} other{{count} saltos}}",
|
||||
"pathMap_legendShared": "Segmento compartilhado",
|
||||
"pathMap_legendEstimated": "Segmento estimado",
|
||||
"pathMap_sharedNodeCount": "Utilizado em {count} caminhos",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 salto não tem localização — o caminho mostrado é parcial} other{{count} saltos não têm localização — o caminho mostrado é parcial}}",
|
||||
"pathMap_showAllPaths": "Mostrar tudo",
|
||||
"pathMap_hidePath": "Esconder caminho",
|
||||
"pathMap_showPath": "Mostrar o caminho",
|
||||
"pathMap_collapsePanel": "Recolher painel",
|
||||
"pathMap_expandPanel": "Expandir painel",
|
||||
"pathMap_noLocation": "Sem localização",
|
||||
"pathMap_followPacket": "Fixar vista no pacote",
|
||||
"pathMap_unfollowPacket": "Liberar vista do pacote",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
}
|
||||
|
||||
+674
-12
@@ -39,6 +39,8 @@
|
||||
"common_notAvailable": "—",
|
||||
"common_voltageValue": "{volts} В",
|
||||
"common_percentValue": "{percent}%",
|
||||
"common_autoRefresh": "Автообновление",
|
||||
"common_interval": "Интервал",
|
||||
"scanner_title": "MeshCore Open",
|
||||
"scanner_scanning": "Поиск устройств...",
|
||||
"scanner_connecting": "Подключение...",
|
||||
@@ -81,6 +83,8 @@
|
||||
"settings_privacyModeEnabled": "Режим конфиденциальности включен",
|
||||
"settings_privacyModeDisabled": "Режим конфиденциальности выключен",
|
||||
"settings_actions": "Действия",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Отправить анонсирование",
|
||||
"settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас",
|
||||
"settings_advertisementSent": "Анонсирование отправлено",
|
||||
@@ -229,11 +233,8 @@
|
||||
"channels_searchChannels": "Поиск каналов...",
|
||||
"channels_noChannelsFound": "Каналы не найдены",
|
||||
"channels_channelIndex": "Канал {index}",
|
||||
"channels_hashtagChannel": "Хэштег-канал",
|
||||
"channels_public": "Публичный",
|
||||
"channels_private": "Приватный",
|
||||
"channels_publicChannel": "Публичный канал",
|
||||
"channels_privateChannel": "Приватный канал",
|
||||
"channels_editChannel": "Изменить канал",
|
||||
"channels_muteChannel": "Отключить уведомления канала",
|
||||
"channels_unmuteChannel": "Включить уведомления канала",
|
||||
@@ -252,6 +253,22 @@
|
||||
"channels_channelAdded": "Канал \"{name}\" добавлен",
|
||||
"channels_editChannelTitle": "Изменить канал {index}",
|
||||
"channels_smazCompression": "Сжатие SMAZ",
|
||||
"channels_cyr2latCompression": "Сжатие Cyr2Lat",
|
||||
"channels_cyr2latCompressionDscr": "Заменяет некоторые кириллические символы на латиницу при отправке.",
|
||||
"channels_cyr2latSettingsHeading": "Настройка Cyr2Lat",
|
||||
"channels_cyr2latSettingsSubheading": "Список замен",
|
||||
"channels_cyr2latSettingsDscr": "Редактировать JSON-конфигурацию замены символов",
|
||||
"channels_cyr2latSettingsDialogHint": "JSON-карта замен",
|
||||
"channels_cyr2latSettingsDialogWrongJSON": "Некорректный JSON: {error}",
|
||||
"settings_cyr2latProfileAdd": "Добавить профиль Cyr2Lat",
|
||||
"settings_cyr2latProfileName": "Название профиля",
|
||||
"settings_cyr2latProfileNameEmpty": "Название профиля не может быть пустым",
|
||||
"settings_cyr2latProfileAdded": "Профиль добавлен",
|
||||
"settings_cyr2latProfileUpdated": "Профиль успешно обновлен",
|
||||
"settings_cyr2latProfileEdit": "Редактировать профиль Cyr2Lat",
|
||||
"settings_cyr2latProfileDelete": "Удалить профиль Cyr2Lat",
|
||||
"settings_cyr2latProfileDeleted": "Профиль успешно удален",
|
||||
"settings_cyr2latProfileDeleteDscr": "Вы действительно хотите удалить профиль \"{name}\"?",
|
||||
"channels_channelUpdated": "Канал \"{name}\" обновлён",
|
||||
"channels_publicChannelAdded": "Публичный канал добавлен",
|
||||
"channels_sortBy": "Сортировка",
|
||||
@@ -358,6 +375,8 @@
|
||||
"chat_direct": "Прямой",
|
||||
"chat_poiShared": "Точка интереса отправлена",
|
||||
"chat_unread": "Непрочитанных: {count}",
|
||||
"chat_markAsUnread": "Пометить как непрочитанные",
|
||||
"chat_newMessages": "Новые сообщения",
|
||||
"map_title": "Карта нод",
|
||||
"map_noNodesWithLocation": "Нет нод с данными о местоположении",
|
||||
"map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте",
|
||||
@@ -669,6 +688,43 @@
|
||||
"telemetry_voltageValue": "{volts}В",
|
||||
"telemetry_currentValue": "{amps}А",
|
||||
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
|
||||
"telemetry_digitalInputLabel": "Цифровой вход",
|
||||
"telemetry_digitalOutputLabel": "Цифровой выход",
|
||||
"telemetry_analogInputLabel": "Аналоговый вход",
|
||||
"telemetry_analogOutputLabel": "Аналоговый выход",
|
||||
"telemetry_genericLabel": "Общий датчик",
|
||||
"telemetry_luminosityLabel": "Освещённость",
|
||||
"telemetry_presenceLabel": "Присутствие",
|
||||
"telemetry_humidityLabel": "Влажность",
|
||||
"telemetry_accelerometerLabel": "Акселерометр",
|
||||
"telemetry_pressureLabel": "Давление",
|
||||
"telemetry_altitudeLabel": "Высота",
|
||||
"telemetry_frequencyLabel": "Частота",
|
||||
"telemetry_percentageLabel": "Процент",
|
||||
"telemetry_concentrationLabel": "Концентрация",
|
||||
"telemetry_powerLabel": "Мощность",
|
||||
"telemetry_distanceLabel": "Расстояние",
|
||||
"telemetry_energyLabel": "Энергия",
|
||||
"telemetry_directionLabel": "Направление",
|
||||
"telemetry_timeLabel": "Время",
|
||||
"telemetry_gyrometerLabel": "Гирометр",
|
||||
"telemetry_colourLabel": "Цвет",
|
||||
"telemetry_gpsLabel": "GPS",
|
||||
"telemetry_switchLabel": "Переключатель",
|
||||
"telemetry_polylineLabel": "Полилиния",
|
||||
"telemetry_altitudeValue": "{meters} м",
|
||||
"telemetry_frequencyValue": "{hertz} Гц",
|
||||
"telemetry_pressureValue": "{hpa} гПа",
|
||||
"telemetry_luminosityValue": "{lux} лк",
|
||||
"telemetry_powerValue": "{watts} Вт",
|
||||
"telemetry_distanceValue": "{meters} м",
|
||||
"telemetry_energyValue": "{kilowattHours} кВт⋅ч",
|
||||
"telemetry_directionValue": "{degrees}°",
|
||||
"telemetry_concentrationValue": "{ppm} ppm",
|
||||
"telemetry_percentageValue": "{percent}%",
|
||||
"telemetry_analogValue": "{value}",
|
||||
"telemetry_autoFetchQuantity": "Количество запросов",
|
||||
"telemetry_error": "Не удалось получить данные",
|
||||
"neighbors_receivedData": "Полученные данные о соседях",
|
||||
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
|
||||
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
|
||||
@@ -1162,13 +1218,6 @@
|
||||
"contact_clearChat": "Очистить чат",
|
||||
"contact_lastSeen": "Последний раз видели",
|
||||
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
|
||||
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
|
||||
@@ -1181,5 +1230,618 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
|
||||
"settings_multiAck": "Мульти-ACK: {value}"
|
||||
}
|
||||
"map_showOverlaps": "Перекрытия ключа повтора",
|
||||
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.",
|
||||
"appSettings_jumpToOldestUnread": "Перейти к самому старому непрочитанному сообщению",
|
||||
"appSettings_languageHu": "Венгерский",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.",
|
||||
"appSettings_languageJa": "Японский",
|
||||
"appSettings_languageKo": "Корейский",
|
||||
"radioStats_tooltip": "Статистика радио и беспроводной сети",
|
||||
"radioStats_screenTitle": "Статистика радиовещания",
|
||||
"radioStats_notConnected": "Подключитесь к устройству, чтобы просмотреть статистику радио.",
|
||||
"radioStats_firmwareTooOld": "Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.",
|
||||
"radioStats_waiting": "Ожидаем данных…",
|
||||
"radioStats_noiseFloor": "Уровень шума: {noiseDbm} дБм",
|
||||
"radioStats_lastRssi": "Последнее значение RSSI: {rssiDbm} дБм",
|
||||
"radioStats_lastSnr": "Последнее значение SNR: {snr} дБ",
|
||||
"radioStats_txAir": "Время эфира на телеканале TX (общее): {seconds} секунд",
|
||||
"radioStats_rxAir": "Общее время использования RX (в секундах): {seconds} с",
|
||||
"radioStats_chartCaption": "Уровень шума (дБм) на основе последних измерений.",
|
||||
"radioStats_stripNoise": "Уровень шума: {noiseDbm} дБм",
|
||||
"radioStats_stripWaiting": "Получение данных о радио…",
|
||||
"radioStats_settingsTile": "Статистика радиовещания",
|
||||
"radioStats_settingsSubtitle": "Уровень шума, RSSI, SNR и время передачи",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Переводить входящие сообщения и позволять предварительный перевод перед отправкой.",
|
||||
"translation_composerTitle": "Переводить перед отправкой",
|
||||
"translation_title": "Перевод",
|
||||
"translation_enableTitle": "Включить перевод",
|
||||
"translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.",
|
||||
"translation_autoIncomingTitle": "Автоматически переводить сообщения",
|
||||
"translation_autoIncomingSubtitle": "Автоматически переводит сообщения для уведомлений, а также для чатов и каналов.",
|
||||
"translation_translateMessage": "Перевести сообщение",
|
||||
"translation_targetLanguage": "Целевой язык",
|
||||
"translation_useAppLanguage": "Используйте язык приложения",
|
||||
"translation_downloadedModelLabel": "Загруженная модель",
|
||||
"translation_presetModelLabel": "Предопределенная модель от Hugging Face",
|
||||
"translation_manualUrlLabel": "Ссылка на руководство",
|
||||
"translation_downloadModel": "Скачать модель",
|
||||
"translation_downloading": "Загрузка...",
|
||||
"translation_stop": "Прекратите",
|
||||
"translation_working": "Работа...",
|
||||
"translation_mergingChunks": "Объединение скачанных фрагментов в один финальный файл...",
|
||||
"translation_downloadedModels": "Загруженные модели",
|
||||
"translation_deleteModel": "Удалить модель",
|
||||
"translation_modelDownloaded": "Модель перевода загружена.",
|
||||
"translation_downloadStopped": "Процесс загрузки был прерван.",
|
||||
"translation_downloadFailed": "Не удалось скачать: {error}",
|
||||
"translation_enterUrlFirst": "Сначала введите URL модели.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translateBeforeSending": "Перевести перед отправкой",
|
||||
"translation_composerEnabledHint": "Сообщения будут переведены перед отправкой.",
|
||||
"translation_messageTranslation": "Перевод сообщения",
|
||||
"translation_composerDisabledHint": "Отправляйте сообщения на языке, в котором они были изначально набраны.",
|
||||
"translation_translateTo": "Перевести на {language}",
|
||||
"translation_translationOptions": "Варианты перевода",
|
||||
"translation_systemLanguage": "Язык системы",
|
||||
"scanner_linuxPairingShowPin": "Показать PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).",
|
||||
"scanner_linuxPairingHidePin": "Скрыть PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth",
|
||||
"repeater_cliQuickDiscovery": "Обнаружить Соседей",
|
||||
"repeater_cliQuickClockSync": "Синхронизация часов",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "Синхронизация часов после входа в систему",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.",
|
||||
"chat_sendMessage": "Отправить сообщение",
|
||||
"repeater_guest": "Информация о ретрансляторе",
|
||||
"room_guest": "Информация о сервере",
|
||||
"repeater_guestTools": "Инструменты для гостей",
|
||||
"common_done": "Готово",
|
||||
"background_serviceTitle": "MeshCore работает",
|
||||
"background_serviceText": "Поддерживает BLE-соединение",
|
||||
"appSettings_translationModelDeleted": "Удалено {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Не удалось удалить: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Не удалось обновить канал: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Тип",
|
||||
"map_path": "Путь",
|
||||
"map_location": "Местоположение",
|
||||
"map_estLocation": "Прибл. местоположение",
|
||||
"map_publicKey": "Публичный ключ",
|
||||
"map_publicKeyPrefixHint": "напр. ab12",
|
||||
"contact_typeChat": "Чат",
|
||||
"contact_typeRepeater": "Ретранслятор",
|
||||
"contact_typeRoom": "Комната",
|
||||
"contact_typeSensor": "Датчик",
|
||||
"contact_typeUnknown": "Неизвестно",
|
||||
"channels_via": "через {path}",
|
||||
"chat_score": "Оценка",
|
||||
"settings_multiAck": "Несколько подтверждений",
|
||||
"map_sharedAt": "Поделено",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "Щелкните по заблокированной области, чтобы выделить ее на карте.",
|
||||
"losBlockedSpotsTitle": "Зарезервированные места",
|
||||
"losSelectedObstructionTitle": "Выбранный объект, препятствующий движению",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||
"repeater_rxGain": "Увеличенная эффективность RX",
|
||||
"repeater_rxGainHelper": "Более высокая чувствительность, больший ток потребления (только для SX1262/SX1268)",
|
||||
"repeater_refreshRxGain": "Обновите усиление RX",
|
||||
"repeater_multiAcks": "Несколько подтверждений",
|
||||
"repeater_multiAcksSubtitle": "Обеспечьте доставку сообщений по нескольким каналам для повышения эффективности.",
|
||||
"repeater_refreshMultiAcks": "Обновление нескольких подтверждений",
|
||||
"repeater_networkHealth": "Состояние сети",
|
||||
"repeater_loopDetect": "Обнаружение циклов",
|
||||
"repeater_loopDetectHelper": "Создайте пакеты данных, которые выглядят как циклы маршрутизации.",
|
||||
"repeater_loopDetectOff": "Отключено",
|
||||
"repeater_loopDetectMinimal": "Минимальный",
|
||||
"repeater_loopDetectModerate": "Умеренный",
|
||||
"repeater_loopDetectStrict": "Строгий",
|
||||
"repeater_dutyCycle": "Цикл работы",
|
||||
"repeater_dutyCycleHelper": "Максимальный процент времени, выделенного на трансляцию.",
|
||||
"repeater_dutyCyclePercent": "{percent}%",
|
||||
"@repeater_dutyCyclePercent": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_ownerInfo": "Информация о операторе",
|
||||
"repeater_ownerInfoHelper": "Общая метаинформация для этого ретранслятора",
|
||||
"repeater_refreshOwnerInfo": "Обновить информацию о операторе",
|
||||
"repeater_floodMax": "Максимальное количество прыжков при наводнении",
|
||||
"repeater_floodMaxHelper": "Максимальное количество пакетов, которые могут быть отправлены в одном потоке (0-64)",
|
||||
"repeater_advancedSettings": "Продвинутый",
|
||||
"repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов",
|
||||
"repeater_pathHashMode": "Режим хеширования пути",
|
||||
"repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.",
|
||||
"repeater_txDelay": "Задержка в работе системы Flood TX",
|
||||
"repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.",
|
||||
"repeater_directTxDelay": "Прямая задержка сигнала TX",
|
||||
"repeater_directTxDelayHelper": "Передача промежуточных данных для прямого (немассового) трафика, в качестве коэффициента, равного времени передачи пакета (от 0 до 2, по умолчанию 0,3).",
|
||||
"repeater_intThresh": "Пороговое значение помех",
|
||||
"repeater_intThreshHelper": "Порог устанавливается для калибровки уровня шума радио, чтобы оно отсеивало помехи, превышающие этот уровень. Значение \"0\" означает отключение – используйте только в случае, если вы наблюдаете ошибки при приеме сигнала в шумном диапазоне.",
|
||||
"repeater_agcResetInterval": "Интервал сброса AGC",
|
||||
"repeater_agcResetIntervalHelper": "Как часто следует сбрасывать автоматическую регулировку усиления радио, чтобы вернуться к нормальному состоянию после заклинивания? Интервал сброса составляет несколько секунд, кратный 4. Отключение периодического сброса осуществляется с помощью параметра 0.",
|
||||
"repeater_actionsTitle": "Действия",
|
||||
"repeater_sendAdvert": "Отправить объявление о наводнении",
|
||||
"repeater_sendAdvertSubtitle": "Разместите рекламу о наводнении в эфире по всей сети.",
|
||||
"repeater_sendAdvertZeroHop": "Опубликуйте рекламу, не требующую промежуточного распространения.",
|
||||
"repeater_sendAdvertZeroHopSubtitle": "Разместите рекламу, распространяемую одним способом (без использования ретрансляторов).",
|
||||
"repeater_clockSync": "Синхронизировать время сейчас",
|
||||
"repeater_clockSyncSubtitle": "Установите время на вашем телефоне, чтобы оно совпадало со временем ретранслятора.",
|
||||
"repeater_actionSucceeded": "{action} succeeded",
|
||||
"@repeater_actionSucceeded": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_actionFailed": "{action} failed: {error}",
|
||||
"@repeater_actionFailed": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
},
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsSavedRebootNeeded": "Настройки сохранены — перезагрузите ретранслятор, чтобы применить их.",
|
||||
"repeater_settingsPartialFailure": "Некоторые настройки не удалось применить: {failures}",
|
||||
"@repeater_settingsPartialFailure": {
|
||||
"placeholders": {
|
||||
"failures": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@common_percentValue": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@settings_aboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@telemetry_temperatureValue": {
|
||||
"placeholders": {
|
||||
"celsius": {
|
||||
"type": "String"
|
||||
},
|
||||
"fahrenheit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channelPath_timeWithDate": {
|
||||
"placeholders": {
|
||||
"day": {
|
||||
"type": "int"
|
||||
},
|
||||
"month": {
|
||||
"type": "int"
|
||||
},
|
||||
"time": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channelPath_timeOnly": {
|
||||
"placeholders": {
|
||||
"time": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channelPath_selectedPathLabel": {
|
||||
"placeholders": {
|
||||
"label": {
|
||||
"type": "String"
|
||||
},
|
||||
"prefixes": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_getCategory": "Получить значения",
|
||||
"repeater_powerMgmt": "Управление энергопотреблением",
|
||||
"repeater_sensors": "Датчики",
|
||||
"repeater_cliHelpPowerOff": "Отключает устройство. (ожидается отсутствие ответа).",
|
||||
"repeater_cliHelpClkReboot": "Сбрасывает часы до известной эпохи и перезапускает устройство.",
|
||||
"repeater_cliHelpAdvertZeroHop": "Отправляет рекламу, распространяемую только среди ближайших соседей (без промежуточных узлов).",
|
||||
"repeater_cliHelpStartOta": "Запускает обновление прошивки по воздуху на поддерживаемых устройствах.",
|
||||
"repeater_cliHelpTime": "Устанавливает время устройства в соответствии с заданными секундами от начала эпохи Unix. Время не может сброситься назад.",
|
||||
"repeater_cliHelpBoard": "Отображает информацию о производителе платы / идентификатор аппаратного обеспечения.",
|
||||
"repeater_cliHelpDiscoverNeighbors": "Отправляет запрос на обнаружение соседних узлов. (Только для ретранслятора)",
|
||||
"repeater_cliHelpPowersavingOnOff": "Включает или выключает режим экономии энергии (если он поддерживается).",
|
||||
"repeater_cliHelpPowersaving": "Показывает, включен ли режим экономии энергии.",
|
||||
"repeater_cliHelpErase": "(Только для серийного использования) Форматирует файловую систему устройства. Удаляет все настройки и контакты.",
|
||||
"repeater_cliHelpSetDutyCycle": "Устанавливает максимальный допустимый цикл передачи данных в процентах (от 1 до 100). Внутренне корректирует коэффициент времени передачи.",
|
||||
"repeater_cliHelpSetPrvKey": "(Только для серийного использования) Заменяет приватный ключ, идентифицирующий устройство. Требуется перезагрузка для применения. Генерирует новый публичный ключ.",
|
||||
"repeater_cliHelpSetRadioRxGain": "(Только для SX126x) Переключает усиление RX для повышения чувствительности при больших токах потребления.",
|
||||
"repeater_cliHelpSetOwnerInfo": "Указывает строку с контактной информацией владельца, которая должна быть включена в объявления. Используйте '|' для переносов строк.",
|
||||
"repeater_cliHelpSetPathHashMode": "Устанавливает режим хеширования пути. 0 = устаревший, 1 = стандартный, 2 = строгий. Влияет на то, как определяются маршруты.",
|
||||
"repeater_cliHelpSetLoopDetect": "Устанавливает чувствительность обнаружения циклов маршрутизации: \"выключено\", \"минимальная\", \"умеренная\" или \"строгая\".",
|
||||
"repeater_cliHelpSetFreq": "(Только для настройки) Быстро устанавливает только частоту. Требуется перезагрузка. Рекомендуется использовать функцию \"настройка радио\" для полного набора параметров.",
|
||||
"repeater_cliHelpSetBridgeChannel": "(Только для моста ESPNow) Устанавливает канал Wi-Fi (от 1 до 14), используемый мостом.",
|
||||
"repeater_cliHelpGetName": "Отображает имя настроенного узла.",
|
||||
"repeater_cliHelpGetRole": "Отображает роль прошивки (ретранслятор, сервер для комнаты и т.д.).",
|
||||
"repeater_cliHelpGetPublicKey": "Отображает открытый ключ устройства.",
|
||||
"repeater_cliHelpGetPrvKey": "(Только для серийного использования) Отображает приватный ключ устройства. Рассматривайте его как секретную информацию.",
|
||||
"repeater_cliHelpGetRepeat": "Отображает, включена ли функция перенаправления пакетов (функция ретранслятора) или нет.",
|
||||
"repeater_cliHelpGetTx": "Отображает текущую мощность передатчика в дБм.",
|
||||
"repeater_cliHelpGetFreq": "Отображает настроенную частоту радиосигнала в мегагерцах.",
|
||||
"repeater_cliHelpGetRadio": "Отображает все параметры радиосигнала: частоту, полосу пропускания, коэффициент модуляции, скорость кодирования.",
|
||||
"repeater_cliHelpGetRadioRxGain": "(Только для SX126x) Отображает состояние усиления сигнала на входе RX.",
|
||||
"repeater_cliHelpGetAf": "Отображает текущий коэффициент времени эфира.",
|
||||
"repeater_cliHelpGetDutyCycle": "Отображает текущий допустимый цикл работы в процентах.",
|
||||
"repeater_cliHelpGetIntThresh": "Отображает порог помех в децибелах.",
|
||||
"repeater_cliHelpGetAgcResetInterval": "Отображает интервал сброса автоматической регулировки усиления в секундах.",
|
||||
"repeater_cliHelpGetMultiAcks": "Показывает, включен ли режим двойной подтверждения (1) или выключен (0).",
|
||||
"repeater_cliHelpGetAllowReadOnly": "Отображает, разрешен ли доступ для чтения только для гостей.",
|
||||
"repeater_cliHelpGetAdvertInterval": "Отображает продолжительность рекламного блока в минутах.",
|
||||
"repeater_cliHelpGetFloodAdvertInterval": "Отображает интервал времени показа рекламного ролика в часах.",
|
||||
"repeater_cliHelpGetGuestPassword": "Отображает установленный пароль для гостя.",
|
||||
"repeater_cliHelpGetLat": "Отображает заданную широту.",
|
||||
"repeater_cliHelpGetLon": "Отображает заданную долготу.",
|
||||
"repeater_cliHelpGetRxDelay": "Отображает базовое значение задержки.",
|
||||
"repeater_cliHelpGetTxDelay": "Отображает коэффициент задержки при работе в режиме затопления.",
|
||||
"repeater_cliHelpGetDirectTxDelay": "Отображает коэффициент задержки в режиме прямого подключения.",
|
||||
"repeater_cliHelpGetFloodMax": "Отображает максимальное количество переходов при затоплении.",
|
||||
"repeater_cliHelpGetOwnerInfo": "Отображает строку с контактной информацией владельца.",
|
||||
"repeater_cliHelpGetPathHashMode": "Отображает режим работы с хэшем пути (0/1/2).",
|
||||
"repeater_cliHelpGetLoopDetect": "Отображает чувствительность к обнаружению циклов.",
|
||||
"repeater_cliHelpGetAcl": "(Только для серий) Перечисляет записи управления доступом на ретрансляторе.",
|
||||
"repeater_cliHelpGetBridgeEnabled": "Показывает, включена ли функция моста.",
|
||||
"repeater_cliHelpGetBridgeDelay": "Отображает задержку в миллисекундах.",
|
||||
"repeater_cliHelpGetBridgeSource": "Отображает, какие пакеты RX или TX передаются через мост.",
|
||||
"repeater_cliHelpGetBridgeBaud": "(Только для интерфейса RS232) Отображает скорость передачи данных на интерфейсе RS232.",
|
||||
"repeater_cliHelpGetBridgeChannel": "(Только для моста ESPNow) Отображает канал WiFi, используемый мостом.",
|
||||
"repeater_cliHelpGetBridgeSecret": "(Только для моста ESPNow) Отображает общий секрет, используемый мостом.",
|
||||
"repeater_cliHelpGetBootloaderVer": "(Только для NRF52) Отображает версию загрузчика.",
|
||||
"repeater_cliHelpGetAdcMultiplier": "Отображает коэффициент умножения аналого-цифрового преобразователя (масштабирование напряжения от батареи).",
|
||||
"repeater_cliHelpGetPwrMgtSupport": "Сообщает, есть ли у совета поддержки функций управления питанием.",
|
||||
"repeater_cliHelpGetPwrMgtSource": "Отображает текущий источник питания: внешний или аккумулятор.",
|
||||
"repeater_cliHelpGetPwrMgtBootReason": "Отображает последние причины сброса и выключения.",
|
||||
"repeater_cliHelpGetPwrMgtBootMv": "Отображает напряжение батареи при запуске системы в милливольтах (мВ).",
|
||||
"repeater_cliHelpSensorGet": "Считывает пользовательское значение для датчика по указанному ключу.",
|
||||
"repeater_cliHelpSensorSet": "Создает пользовательские настройки для датчика.",
|
||||
"repeater_cliHelpSensorList": "Перечисляет все пользовательские настройки датчиков, разбитые на страницы с возможностью указания начального индекса.",
|
||||
"repeater_cliHelpRegionDefault": "Отображает текущий область действия по умолчанию.",
|
||||
"repeater_cliHelpRegionDefaultSet": "Устанавливает значение региона по умолчанию. Используйте \"<null>\", чтобы сбросить значение.",
|
||||
"repeater_cliHelpRegionListAllowed": "Перечисляет регионы, где разрешено движение транспорта во время наводнений.",
|
||||
"repeater_cliHelpRegionListDenied": "Перечисляет регионы, где запрещено движение транспорта во время наводнений.",
|
||||
"repeater_cliHelpStatsPackets": "(Только для серийной версии) Отображает статистику на уровне пакетов.",
|
||||
"repeater_cliHelpStatsRadio": "(Только для серий) Отображает статистику радио.",
|
||||
"repeater_cliHelpStatsCore": "(Только для серийного оборудования) Отображает основные статистические данные прошивки.",
|
||||
"settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.",
|
||||
"repeater_chanUtil": "Использование канала",
|
||||
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)",
|
||||
"@routing_lastWorked": {
|
||||
"placeholders": {
|
||||
"when": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@routing_deliveryCounts": {
|
||||
"placeholders": {
|
||||
"successes": {
|
||||
"type": "int"
|
||||
},
|
||||
"failures": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_hopCounter": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_invalidTokens": {
|
||||
"placeholders": {
|
||||
"tokens": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_communityShortId": {
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"messageStatus_pending": "Отправка",
|
||||
"common_undo": "Отменить",
|
||||
"messageStatus_delivered": "Доставлено",
|
||||
"messageStatus_sent": "Отправлено",
|
||||
"messageStatus_failed": "Не удалось отправить",
|
||||
"messageStatus_repeated": "Услышал несколько раз",
|
||||
"contacts_moreOptions": "Больше вариантов",
|
||||
"contacts_searchOpen": "Найти контакты",
|
||||
"contacts_searchClose": "Закрыть поиск",
|
||||
"routing_title": "Маршрутизация",
|
||||
"routing_modeAuto": "Авто",
|
||||
"routing_modeFlood": "Наводнение",
|
||||
"routing_modeManual": "Инструкция",
|
||||
"routing_modeAutoHint": "Автоматически выбирает наиболее известный путь, и если такой путь неизвестен, использует алгоритм поиска пути.",
|
||||
"routing_modeFloodHint": "Передача сигнала через все ретрансляторы. Самый надежный способ, но требует больше времени на передачу.",
|
||||
"routing_modeManualHint": "Всегда следует точно по указанному вами маршруту.",
|
||||
"routing_currentRoute": "Текущий маршрут",
|
||||
"routing_directNoHops": "Прямое соединение – без использования ретрансляторов",
|
||||
"routing_noPathYet": "Пока нет пути. Следующее сообщение будет отправлено до тех пор, пока не будет обнаружен маршрут.",
|
||||
"routing_floodBroadcast": "Транслируется через все ретрансляторы",
|
||||
"routing_editPath": "Изменить путь",
|
||||
"routing_forgetPath": "Забудьте о маршруте",
|
||||
"routing_knownPaths": "Известные маршруты",
|
||||
"routing_knownPathsHint": "Создайте маршрут для переключения на этот пункт.",
|
||||
"routing_inUse": "В эксплуатации",
|
||||
"routing_qualityStrong": "Сильный первый скачок",
|
||||
"routing_qualityGood": "Хорошее начало",
|
||||
"routing_qualityFair": "Первый хороший урожай",
|
||||
"routing_qualityWorked": "Осуществлено",
|
||||
"routing_qualityFlood": "Узнал из новостей, распространяющихся в интернете.",
|
||||
"routing_qualityUntested": "Непроверенный",
|
||||
"routing_neverWorked": "никогда не было подтверждено",
|
||||
"routing_floodDelivery": "Доставка при затоплении",
|
||||
"pathEditor_title": "Создать маршрут",
|
||||
"pathEditor_hopCounter": "{count} из 64 хмеля",
|
||||
"pathEditor_noHops": "На данный момент хмель еще не добавлен. Чтобы добавить его, нажмите на соответствующие кнопки ниже в нужном порядке, или сохраните рецепт без хмеля, чтобы отправить его напрямую.",
|
||||
"pathEditor_addHops": "Добавляйте хмель в соответствии с указанным порядком.",
|
||||
"pathEditor_searchRepeaters": "Поиск повторителей",
|
||||
"pathEditor_advancedHex": "Продвинутый уровень: прямой путь в шестнадцатеричном формате",
|
||||
"pathEditor_hexLabel": "Префиксы шестнадцатеричной системы",
|
||||
"pathEditor_hexHelper": "Два шестнадцатеричных символа на каждом шаге, разделенные запятыми.",
|
||||
"pathEditor_invalidTokens": "Неверно: {tokens}",
|
||||
"routing_lastWorked": "worked {when}",
|
||||
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
|
||||
"pathEditor_tooManyHops": "Максимальное количество ингредиентов – 64",
|
||||
"pathEditor_usePath": "Используйте этот путь",
|
||||
"pathEditor_removeHop": "Удалить хмель",
|
||||
"pathEditor_unknownHop": "Неизвестный ретранслятор",
|
||||
"map_zoomIn": "Увеличить масштаб",
|
||||
"map_zoomOut": "Увеличить масштаб",
|
||||
"map_centerMap": "Карта центра",
|
||||
"chrome_bluetoothRequiresChromium": "Для работы Web Bluetooth требуется браузер на основе Chromium.",
|
||||
"channels_communityShortId": "Идентификатор: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS подтверждено",
|
||||
"pathTrace_legendInferred": "Выведенная позиция",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_searchHint": "Поиск по имени или ID узла",
|
||||
"map_online": "Онлайн",
|
||||
"scanner_bluetoothWebUnsupported": "Bluetooth недоступен в браузере. Подключитесь через USB.",
|
||||
"map_activity": "Активность",
|
||||
"map_recent": "Недавно",
|
||||
"map_stale": "Устаревший",
|
||||
"map_visible": "Видимый",
|
||||
"map_hidden": "Скрытый",
|
||||
"map_centerOnNode": "Центрировать на узле",
|
||||
"map_details": "Детали",
|
||||
"map_noGps": "Без GPS",
|
||||
"map_noResults": "Не найдено соответствующих узлов",
|
||||
"pathMap_viewSingle": "Одиночный",
|
||||
"pathMap_viewCombined": "Объединённые",
|
||||
"pathMap_play": "Воспроизвести",
|
||||
"pathMap_pause": "Пауза",
|
||||
"pathMap_replay": "Повтор",
|
||||
"pathMap_stepBack": "Предыдущий хоп",
|
||||
"pathMap_stepForward": "Следующий хоп",
|
||||
"pathMap_animationOn": "Показать анимацию пакета",
|
||||
"pathMap_animationOff": "Скрыть анимацию пакета",
|
||||
"pathMap_hopOf": "Хоп {current} из {total}",
|
||||
"pathMap_observedPaths": "Наблюдаемые маршруты: {count}",
|
||||
"pathMap_primary": "Основной",
|
||||
"pathMap_alternate": "Альт {index}",
|
||||
"pathMap_hopCount": "{count, plural, one{{count} хоп} few{{count} хопа} many{{count} хопов} other{{count} хопов}}",
|
||||
"pathMap_legendShared": "Общий сегмент",
|
||||
"pathMap_legendEstimated": "Расчётный сегмент",
|
||||
"pathMap_sharedNodeCount": "Используется в {count} маршрутах",
|
||||
"pathMap_partialAnimation": "{count, plural, one{{count} хоп не имеет координат — показанный путь неполный} few{{count} хопа не имеют координат — показанный путь неполный} many{{count} хопов не имеют координат — показанный путь неполный} other{{count} хопов не имеют координат — показанный путь неполный}}",
|
||||
"pathMap_showAllPaths": "Показать всё",
|
||||
"pathMap_hidePath": "Скрыть путь",
|
||||
"pathMap_collapsePanel": "Скрыть панель",
|
||||
"pathMap_showPath": "Показать маршрут",
|
||||
"pathMap_expandPanel": "Расширить панель",
|
||||
"pathMap_noLocation": "Нет координат",
|
||||
"pathMap_followPacket": "Следить за пакетом",
|
||||
"pathMap_unfollowPacket": "Не следить за пакетом",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
}
|
||||
|
||||
+643
-44
@@ -33,6 +33,8 @@
|
||||
"common_remove": "Odstrániť",
|
||||
"common_enable": "Povolit",
|
||||
"common_disable": "Zakázať",
|
||||
"common_autoRefresh": "Automatické obnovenie",
|
||||
"common_interval": "Časový interval",
|
||||
"common_reboot": "Restartovať",
|
||||
"common_loading": "Načítavanie...",
|
||||
"common_notAvailable": "—",
|
||||
@@ -52,7 +54,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_title": "MeshCore Open",
|
||||
"scanner_title": "MeshCore – Verzia pre verejnosť",
|
||||
"scanner_scanning": "Skrívania zariadení...",
|
||||
"scanner_connecting": "Pripojujem sa...",
|
||||
"scanner_disconnecting": "Odpojuje sa...",
|
||||
@@ -104,6 +106,8 @@
|
||||
"settings_privacyModeEnabled": "Ochranný režim je povolený.",
|
||||
"settings_privacyModeDisabled": "Ochranný režim je vypnutý",
|
||||
"settings_actions": "Možné akcie",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Odoslať reklamu",
|
||||
"settings_sendAdvertisementSubtitle": "Momentálne priezornejšie.",
|
||||
"settings_advertisementSent": "Reklama odeslaná",
|
||||
@@ -121,7 +125,7 @@
|
||||
"settings_appDebugLog": "Záznam ladenia aplikácie",
|
||||
"settings_appDebugLogSubtitle": "Správy z ladenia aplikácie",
|
||||
"settings_about": "O nás",
|
||||
"settings_aboutVersion": "MeshCore Open v{version}",
|
||||
"settings_aboutVersion": "MeshCore, verzia {version}",
|
||||
"@settings_aboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
@@ -132,8 +136,8 @@
|
||||
"settings_aboutLegalese": "MeshCore Open Source Projekt 2024",
|
||||
"settings_aboutDescription": "Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.",
|
||||
"settings_infoName": "Meno",
|
||||
"settings_infoId": "ID",
|
||||
"settings_infoStatus": "Status",
|
||||
"settings_infoId": "Identifikátor",
|
||||
"settings_infoStatus": "Stav",
|
||||
"settings_infoBattery": "Batéria",
|
||||
"settings_infoPublicKey": "Verejný kľúč",
|
||||
"settings_infoContactsCount": "Počet kontaktov",
|
||||
@@ -146,7 +150,7 @@
|
||||
"settings_spreadingFactor": "Rozptýľovací faktor",
|
||||
"settings_codingRate": "Cenový kurz pre programovanie",
|
||||
"settings_txPower": "TX Výkon (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerHelper": "0 – 22",
|
||||
"settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)",
|
||||
"settings_error": "Chyba: {message}",
|
||||
"@settings_error": {
|
||||
@@ -164,19 +168,19 @@
|
||||
"appSettings_themeDark": "Tmavé",
|
||||
"appSettings_language": "Jazyk",
|
||||
"appSettings_languageSystem": "Predvolený systém",
|
||||
"appSettings_languageEn": "English",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageDe": "Deutsch",
|
||||
"appSettings_languagePl": "Polski",
|
||||
"appSettings_languageSl": "Slovenščina",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageIt": "Italiano",
|
||||
"appSettings_languageZh": "中文",
|
||||
"appSettings_languageSv": "Svenska",
|
||||
"appSettings_languageNl": "Nederlands",
|
||||
"appSettings_languageEn": "Anglicky",
|
||||
"appSettings_languageFr": "Francúzština",
|
||||
"appSettings_languageEs": "Španielsky",
|
||||
"appSettings_languageDe": "Nemecky",
|
||||
"appSettings_languagePl": "Poľský",
|
||||
"appSettings_languageSl": "Slovenčina",
|
||||
"appSettings_languagePt": "Portugalčina",
|
||||
"appSettings_languageIt": "Taliančina",
|
||||
"appSettings_languageZh": "Čínština",
|
||||
"appSettings_languageSv": "Švédska",
|
||||
"appSettings_languageNl": "Niderlandsky",
|
||||
"appSettings_languageSk": "Slovenčina",
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_languageBg": "Българština",
|
||||
"appSettings_notifications": "Upozornenia",
|
||||
"appSettings_enableNotifications": "Povolte Notifikácie",
|
||||
"appSettings_enableNotificationsSubtitle": "Zísť o upozornenia na správy a inzeráty",
|
||||
@@ -337,11 +341,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_hashtagChannel": "Kanál s hashtagom",
|
||||
"channels_public": "Veľké verejné",
|
||||
"channels_private": "Osobné",
|
||||
"channels_publicChannel": "Veľké verejne kanály",
|
||||
"channels_privateChannel": "Osobné kanál",
|
||||
"channels_editChannel": "Upraviť kanál",
|
||||
"channels_muteChannel": "Stlmiť kanál",
|
||||
"channels_unmuteChannel": "Zrušiť stlmenie kanála",
|
||||
@@ -388,6 +389,22 @@
|
||||
}
|
||||
},
|
||||
"channels_smazCompression": "Odstránenie kompresie SMAZ",
|
||||
"channels_cyr2latCompression": "Odstránenie kompresie Cyr2Lat",
|
||||
"channels_cyr2latCompressionDscr": "Pri odosielaní nahradí niektoré znaky cyriliky latinskými znakmi.",
|
||||
"channels_cyr2latSettingsHeading": "Nastavenia Cyr2Lat",
|
||||
"channels_cyr2latSettingsSubheading": "Zoznam nahradení",
|
||||
"channels_cyr2latSettingsDscr": "Upravte konfiguráciu JSON pre nahradenie znakov",
|
||||
"channels_cyr2latSettingsDialogHint": "JSON mapa nahradení",
|
||||
"channels_cyr2latSettingsDialogWrongJSON": "Nesprávny JSON: {error}",
|
||||
"settings_cyr2latProfileAdd": "Pridať profil Cyr2Lat",
|
||||
"settings_cyr2latProfileName": "Názov profilu",
|
||||
"settings_cyr2latProfileNameEmpty": "Názov profilu nesmie byť prázdny",
|
||||
"settings_cyr2latProfileAdded": "Profil bol úspešne pridaný",
|
||||
"settings_cyr2latProfileUpdated": "Profil bol úspešne aktualizovaný",
|
||||
"settings_cyr2latProfileEdit": "Upraviť profil Cyr2Lat",
|
||||
"settings_cyr2latProfileDelete": "Odstrániť profil Cyr2Lat",
|
||||
"settings_cyr2latProfileDeleted": "Profil bol úspešne odstránený",
|
||||
"settings_cyr2latProfileDeleteDscr": "Naozaj chcete odstrániť profil \"{name}\"?",
|
||||
"channels_channelUpdated": "Kanál \"{name}\" bol aktualizovaný",
|
||||
"@channels_channelUpdated": {
|
||||
"placeholders": {
|
||||
@@ -399,7 +416,7 @@
|
||||
"channels_publicChannelAdded": "Veľký kanál pridaný",
|
||||
"channels_sortBy": "Triediť podľa",
|
||||
"channels_sortManual": "Ručne",
|
||||
"channels_sortAZ": "A-Z",
|
||||
"channels_sortAZ": "Od A po Z",
|
||||
"channels_sortLatestMessages": "Posledné správy",
|
||||
"channels_sortUnread": "Nezriadené",
|
||||
"chat_noMessages": "Zatiaľ žiadne správy.",
|
||||
@@ -477,7 +494,7 @@
|
||||
"debugLog_noEntries": "Zatiaľ neboli zaznamenané žiadne debug logy.",
|
||||
"debugLog_enableInSettings": "Povolte ladicové logy v nastaveniach",
|
||||
"debugLog_frames": "Rámce",
|
||||
"debugLog_rawLogRx": "Raw Log-RX",
|
||||
"debugLog_rawLogRx": "Čistý log – RX",
|
||||
"debugLog_noBleActivity": "Zatiaľ žiadna aktivita BLE.",
|
||||
"debugFrame_length": "Dĺžka rámca: {count} bajtov",
|
||||
"@debugFrame_length": {
|
||||
@@ -531,7 +548,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"debugFrame_textTypeCli": "CLI",
|
||||
"debugFrame_textTypeCli": "CLI (Command Line Interface)",
|
||||
"debugFrame_textTypePlain": "Jednoduché",
|
||||
"debugFrame_text": "- Text: \"{text}\"",
|
||||
"@debugFrame_text": {
|
||||
@@ -541,7 +558,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"debugFrame_hexDump": "Hex Dump:",
|
||||
"debugFrame_hexDump": "Hexová analýza:",
|
||||
"chat_pathManagement": "Správa ciest",
|
||||
"chat_routingMode": "Režim trasy",
|
||||
"chat_autoUseSavedPath": "Použiť uloženú cestu",
|
||||
@@ -550,7 +567,7 @@
|
||||
"chat_pathHistoryFull": "História ciest je plná. Odstráňte záznamy, aby ste mohli pridať nové.",
|
||||
"chat_hopSingular": "Skok",
|
||||
"chat_hopPlural": "Skákať",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{skok} other{skoky}}",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -879,11 +896,11 @@
|
||||
"path_setPath": "Nastaviť cestu",
|
||||
"repeater_management": "Správa opakérov",
|
||||
"repeater_managementTools": "Nástroje na správu",
|
||||
"repeater_status": "Status",
|
||||
"repeater_status": "Stav",
|
||||
"repeater_statusSubtitle": "Zobraziť stav, štatistiky a susedov repeatera",
|
||||
"repeater_telemetry": "Telemetria",
|
||||
"repeater_telemetrySubtitle": "Zobraziť telemetriu senzorov a systémových štatistík",
|
||||
"repeater_cli": "CLI",
|
||||
"repeater_cli": "CLI (Command Line Interface)",
|
||||
"repeater_cliSubtitle": "Pošlite príkazy opakovaču",
|
||||
"repeater_settings": "Nastavenia",
|
||||
"repeater_settingsSubtitle": "Konfigurujte parametre opakovača",
|
||||
@@ -992,7 +1009,7 @@
|
||||
"repeater_guestPasswordHelper": "Prístupový heslo iba na čítanie",
|
||||
"repeater_radioSettings": "Nastavenia rádia",
|
||||
"repeater_frequencyMhz": "Frekvencia (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
"repeater_frequencyHelper": "300–2500 MHz",
|
||||
"repeater_txPower": "TX Power",
|
||||
"repeater_txPowerHelper": "1-30 dBm",
|
||||
"repeater_bandwidth": "Šírka pásma",
|
||||
@@ -1059,6 +1076,81 @@
|
||||
},
|
||||
"repeater_confirm": "Potvrdiť",
|
||||
"repeater_settingsSaved": "Nastavenia boli uložené úspešne.",
|
||||
"repeater_rxGain": "Zvýšený zisk RX",
|
||||
"repeater_rxGainHelper": "Vyššia citlivosť, vyšší príkon (platí len pre modely SX1262/SX1268)",
|
||||
"repeater_refreshRxGain": "Obnovte zvýšený zisk z RX",
|
||||
"repeater_multiAcks": "Víťazné potvrdenia (víťazné ACK)",
|
||||
"repeater_multiAcksSubtitle": "Potvrďte správy prostredníctvom viacerých trás pre lepšiu doručenie.",
|
||||
"repeater_refreshMultiAcks": "Opätovne potvrďte viacero ACK signálov",
|
||||
"repeater_networkHealth": "Zdravie siete",
|
||||
"repeater_loopDetect": "Detekcia slučiek",
|
||||
"repeater_loopDetectHelper": "Vytvorte balíčky, ktoré vizuálne pripomínajú slučky v síti.",
|
||||
"repeater_loopDetectOff": "Vypnuté",
|
||||
"repeater_loopDetectMinimal": "Minimálny",
|
||||
"repeater_loopDetectModerate": "Stredný, mierny",
|
||||
"repeater_loopDetectStrict": "Prísne",
|
||||
"repeater_dutyCycle": "Cyklus činnosti",
|
||||
"repeater_dutyCycleHelper": "Maximálna percentáľ dostupného času vysielania",
|
||||
"repeater_dutyCyclePercent": "{percent}%",
|
||||
"@repeater_dutyCyclePercent": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_ownerInfo": "Informácie o poskytovateľovi",
|
||||
"repeater_ownerInfoHelper": "Veľké dátové informácie pre tento vysielací zdroj",
|
||||
"repeater_refreshOwnerInfo": "Zísť informácie o operátore",
|
||||
"repeater_floodMax": "Maximálny počet skokov pri povodni",
|
||||
"repeater_floodMaxHelper": "Maximálny počet paketov, ktoré môžu preletieť cez jeden hop (0-64)",
|
||||
"repeater_advancedSettings": "Pokročilé",
|
||||
"repeater_advancedSettingsSubtitle": "Ovládacie knopy pre skúsených operátorov",
|
||||
"repeater_pathHashMode": "Režim hashovania cesty",
|
||||
"repeater_pathHashModeHelper": "Byty použité na zakódovanie ID tohto opakovača v tagoch pre trasu/detekciu slučky. 0 = 1 bytu (256 ID, až 64 skokov), 1 = 2 byty (65 000 ID, až 32 skokov), 2 = 3 byty (16 miliónov ID, až 21 skokov). Verzie 1.13 a staršie nepodporujú viacbytové trasy – fungujú len, keď je sieť aktivovaná.",
|
||||
"repeater_txDelay": "Zpoždanie v Flood, TX",
|
||||
"repeater_txDelayHelper": "Nastavenie pre opakované vysielanie pre dopravu počas povodní, ako násobok času, ktorý paket využije (0-2, výchoce hodnota 0,5). Vyššia hodnota znamená menej kolízii, ale pomalšie doručovanie.",
|
||||
"repeater_directTxDelay": "Priame oneskorenie TX",
|
||||
"repeater_directTxDelayHelper": "Nastavenie pre retransmisiu pre priame (nie pre plnú sieť), ako násobok času prenosu paketov (0-2, výchoce 0,3).",
|
||||
"repeater_intThresh": "Hranica, pri ktorej dochádza k rušeniu",
|
||||
"repeater_intThreshHelper": "Hranica je nastavená tak, aby odfiltrovala šum nad touto úrovňou. Hodnota 0 znamená, že sa nebude nič odfiltrovať – nastavte ju len v prípade, že zaznamenáte chyby pri prijímaní signálu v šumnej frekvencii.",
|
||||
"repeater_agcResetInterval": "Interval reštartu AGC",
|
||||
"repeater_agcResetIntervalHelper": "Ako často by ste mali reštartovať automatické ovládanie zosilnenia, aby ste sa vrátili do normálneho stavu, ak je zosilnenie zablokované? Nastavenie „4.0“ vypne pravidelné reštarty.",
|
||||
"repeater_actionsTitle": "Opatrenia",
|
||||
"repeater_sendAdvert": "Odoslať inzerát o povodňovej situácii",
|
||||
"repeater_sendAdvertSubtitle": "Zverejnite reklamu na povodňu prostredníctvom siete.",
|
||||
"repeater_sendAdvertZeroHop": "Odoslať reklamu bez prenosu",
|
||||
"repeater_sendAdvertZeroHopSubtitle": "Zverejnite reklamnú správu, ktorá sa prenáša len raz (bez prenosov).",
|
||||
"repeater_clockSync": "Synchronizujte hodiny teraz",
|
||||
"repeater_clockSyncSubtitle": "Nastavte čas na vašom telefóne, aby odpovedal na volania z vysielacieho zariadenia.",
|
||||
"repeater_actionSucceeded": "{action} succeeded",
|
||||
"@repeater_actionSucceeded": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_actionFailed": "{action} failed: {error}",
|
||||
"@repeater_actionFailed": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
},
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsSavedRebootNeeded": "Nastavenia uložené – reštartujte vysielací prístroj, aby sa nastavenia aplikovali.",
|
||||
"repeater_settingsPartialFailure": "Niektoré nastavenia neúspešné: {failures}",
|
||||
"@repeater_settingsPartialFailure": {
|
||||
"placeholders": {
|
||||
"failures": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_errorSavingSettings": "Chyba pri ukladaní nastavení: {error}",
|
||||
"@repeater_errorSavingSettings": {
|
||||
"placeholders": {
|
||||
@@ -1070,11 +1162,9 @@
|
||||
"repeater_refreshBasicSettings": "Obnoviť základné nastavenia",
|
||||
"repeater_refreshRadioSettings": "Obnoviť Nastavenia Rádií",
|
||||
"repeater_refreshTxPower": "Obnoviť TX napájanie",
|
||||
"repeater_refreshLocationSettings": "Obnoviť Nastavenia Miesta",
|
||||
"repeater_refreshPacketForwarding": "Obnoviť smerovanie paketov",
|
||||
"repeater_refreshGuestAccess": "Obnoviť prístup hosťa",
|
||||
"repeater_refreshPrivacyMode": "Obnoviť Ochranný režim",
|
||||
"repeater_refreshAdvertisementSettings": "Obnoviť nastavenia reklamy",
|
||||
"repeater_refreshed": "{label} sa znova načítalo",
|
||||
"@repeater_refreshed": {
|
||||
"placeholders": {
|
||||
@@ -1192,6 +1282,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_digitalInputLabel": "Digitálny vstup",
|
||||
"telemetry_digitalOutputLabel": "Digitálny výstup",
|
||||
"telemetry_analogInputLabel": "Analógový vstup",
|
||||
"telemetry_analogOutputLabel": "Analógový výstup",
|
||||
"telemetry_genericLabel": "Všeobecný senzor",
|
||||
"telemetry_luminosityLabel": "Osvetlenie",
|
||||
"telemetry_presenceLabel": "Prítomnosť",
|
||||
"telemetry_humidityLabel": "Vlhkosť",
|
||||
"telemetry_accelerometerLabel": "Akcelerometer",
|
||||
"telemetry_pressureLabel": "Tlak",
|
||||
"telemetry_altitudeLabel": "Nadmorská výška",
|
||||
"telemetry_frequencyLabel": "Frekvencia",
|
||||
"telemetry_percentageLabel": "Percento",
|
||||
"telemetry_concentrationLabel": "Koncentrácia",
|
||||
"telemetry_powerLabel": "Výkon",
|
||||
"telemetry_distanceLabel": "Vzdialenosť",
|
||||
"telemetry_energyLabel": "Energia",
|
||||
"telemetry_directionLabel": "Smer",
|
||||
"telemetry_timeLabel": "Čas",
|
||||
"telemetry_gyrometerLabel": "Gyrometer",
|
||||
"telemetry_colourLabel": "Farba",
|
||||
"telemetry_gpsLabel": "GPS",
|
||||
"telemetry_switchLabel": "Prepínač",
|
||||
"telemetry_polylineLabel": "Lomená čiara",
|
||||
"telemetry_altitudeValue": "{meters} m",
|
||||
"telemetry_frequencyValue": "{hertz} Hz",
|
||||
"telemetry_pressureValue": "{hpa} hPa",
|
||||
"telemetry_luminosityValue": "{lux} lx",
|
||||
"telemetry_powerValue": "{watts} W",
|
||||
"telemetry_distanceValue": "{meters} m",
|
||||
"telemetry_energyValue": "{kilowattHours} kWh",
|
||||
"telemetry_directionValue": "{degrees}°",
|
||||
"telemetry_concentrationValue": "{ppm} ppm",
|
||||
"telemetry_percentageValue": "{percent}%",
|
||||
"telemetry_analogValue": "{value}",
|
||||
"telemetry_autoFetchQuantity": "Počet požiadaviek",
|
||||
"telemetry_error": "Nepodarilo sa získať údaje",
|
||||
"telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.",
|
||||
"telemetry_channelTitle": "Kanál {channel}",
|
||||
"@telemetry_channelTitle": {
|
||||
@@ -1347,7 +1474,7 @@
|
||||
"listFilter_sortBy": "Triediť podľa",
|
||||
"listFilter_latestMessages": "Posledné správy",
|
||||
"listFilter_heardRecently": "Nedávno počuli.",
|
||||
"listFilter_az": "A-Z",
|
||||
"listFilter_az": "Od A po Z",
|
||||
"listFilter_filters": "Filtre",
|
||||
"listFilter_all": "Všetko",
|
||||
"listFilter_users": "Používatelia",
|
||||
@@ -1619,8 +1746,8 @@
|
||||
"appSettings_unitsTitle": "Jednotky",
|
||||
"appSettings_unitsMetric": "Metrické (m / km)",
|
||||
"appSettings_unitsImperial": "Imperiálne (ft / mi)",
|
||||
"map_lineOfSight": "Line of Sight",
|
||||
"map_losScreenTitle": "Line of Sight",
|
||||
"map_lineOfSight": "Úroveň výhľadu",
|
||||
"map_losScreenTitle": "Úroveň výhľadu",
|
||||
"losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.",
|
||||
"losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}",
|
||||
"@losRunFailed": {
|
||||
@@ -1879,8 +2006,8 @@
|
||||
"tcpHostLabel": "IP adresa",
|
||||
"tcpScreenTitle": "Spojte sa pomocou protokolu TCP",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpPortLabel": "Port",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpPortLabel": "Prístav",
|
||||
"tcpPortHint": "5 000",
|
||||
"tcpStatus_notConnected": "Zadajte cieľovú adresu a pripojte sa.",
|
||||
"tcpStatus_connectingTo": "Pripojenie k {endpoint}...",
|
||||
"tcpErrorHostRequired": "Je potrebné zadať IP adresu.",
|
||||
@@ -1922,13 +2049,6 @@
|
||||
"contact_lastSeen": "Naposledy videný",
|
||||
"contact_teleBase": "Báza telemetrie",
|
||||
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty",
|
||||
"appSettings_initialRouteWeight": "Počiatočná váha trasy",
|
||||
@@ -1941,5 +2061,484 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
|
||||
"settings_multiAck": "Viaceré ACK: {value}"
|
||||
}
|
||||
"settings_multiAck": "Viaceré ACK",
|
||||
"map_showOverlaps": "Prekrývanie opakovača kľúča",
|
||||
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Prosím, počkajte chvíľu, než zašlete znova.",
|
||||
"appSettings_jumpToOldestUnread": "Presk oceň",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.",
|
||||
"appSettings_languageHu": "Maďarský",
|
||||
"appSettings_languageJa": "Japonský",
|
||||
"appSettings_languageKo": "Kórejský",
|
||||
"radioStats_tooltip": "Statistiky rádiových a sieťových kanálov",
|
||||
"radioStats_screenTitle": "Štatistiky rádiových vysielaní",
|
||||
"radioStats_notConnected": "Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.",
|
||||
"radioStats_firmwareTooOld": "Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.",
|
||||
"radioStats_waiting": "Čakám na údaje…",
|
||||
"radioStats_noiseFloor": "Úroveň hluku: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Posledný údaj RSSI: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Posledná hodnota SNR: {snr} dB",
|
||||
"radioStats_txAir": "Čas vysielania na TX (celkový): {seconds} s",
|
||||
"radioStats_rxAir": "Čas RX (celkový): {seconds} s",
|
||||
"radioStats_chartCaption": "Úroveň šumu (dBm) pre posledné vzorky.",
|
||||
"radioStats_stripNoise": "Úroveň hluku: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Získavanie údajov o rádiu…",
|
||||
"radioStats_settingsTile": "Štatistiky rádiových vysielaní",
|
||||
"radioStats_settingsSubtitle": "Úroveň hluku, RSSI, SNR a časové rozloženie",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.",
|
||||
"translation_enableTitle": "Aktivovať preklad",
|
||||
"translation_composerTitle": "Preložte pred odeslaním",
|
||||
"translation_title": "Preklad",
|
||||
"translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.",
|
||||
"translation_autoIncomingTitle": "Automaticky prekladať správy",
|
||||
"translation_autoIncomingSubtitle": "Automaticky prekladá správy pre upozornenia aj pre čet alebo kanál.",
|
||||
"translation_translateMessage": "Preložiť správu",
|
||||
"translation_targetLanguage": "Cieľový jazyk",
|
||||
"translation_useAppLanguage": "Použite jazyk aplikácie",
|
||||
"translation_downloadedModelLabel": "Stiahnutý model",
|
||||
"translation_presetModelLabel": "Prednastavený model od Hugging Face",
|
||||
"translation_manualUrlLabel": "Odkaz na manuál (v elektronickej forme)",
|
||||
"translation_downloadModel": "Stiahnuť model",
|
||||
"translation_downloading": "Stiahnutie...",
|
||||
"translation_working": "Práca...",
|
||||
"translation_stop": "Zastavte",
|
||||
"translation_mergingChunks": "Sliečenie stiahnutých častí do konečného súboru...",
|
||||
"translation_downloadedModels": "Stiahnuté modely",
|
||||
"translation_deleteModel": "Odstrániť model",
|
||||
"translation_modelDownloaded": "Model pre preklad bol stiahnutý.",
|
||||
"translation_downloadStopped": "Stiahnutie bolo prerušené.",
|
||||
"translation_downloadFailed": "Neúspešné stiahnutie: {error}",
|
||||
"translation_enterUrlFirst": "Najprv zadajte URL pre konkrétny model.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingHidePin": "Skryť PIN",
|
||||
"scanner_linuxPairingShowPin": "Zobraziť PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak neexistuje, nechajte prázdne).",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Posielajte správy v pôvodnej písanom jazyku.",
|
||||
"translation_composerEnabledHint": "Správy budú preložené, než budú odoslané.",
|
||||
"translation_translateBeforeSending": "Preložte pred odeslaním",
|
||||
"translation_messageTranslation": "Preklad textu",
|
||||
"translation_translateTo": "Preložte do {language}",
|
||||
"translation_translationOptions": "Možnosti prekladania",
|
||||
"translation_systemLanguage": "Jazyk systému",
|
||||
"repeater_cliQuickClockSync": "Synchronizácia hodin",
|
||||
"repeater_cliQuickDiscovery": "Objaviť susedov",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "Synchronizácia hodiniek po prihlávení",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.",
|
||||
"chat_sendMessage": "Odoslať správu",
|
||||
"repeater_guest": "Informácie o opakovači",
|
||||
"room_guest": "Informácie o serveri",
|
||||
"repeater_guestTools": "Nástroje pre hostí",
|
||||
"repeater_getCategory": "Zísť hodnoty",
|
||||
"repeater_powerMgmt": "Správa energie",
|
||||
"repeater_sensors": "Senzory",
|
||||
"repeater_cliHelpPowerOff": "Vypína zariadenie. (neočakáva sa žiadna odpoveď)",
|
||||
"repeater_cliHelpClkReboot": "Resetuje hodiny na známu epochu a reštartuje zariadenie.",
|
||||
"repeater_cliHelpAdvertZeroHop": "Rozosiela reklamu, ktorá sa prenáša len medzi susednými zariadeniami (bez prenosu cez iné siete).",
|
||||
"repeater_cliHelpStartOta": "Spustí aktualizáciu firmvéru prostredníctvom diaľkového prenosu na podporovaných doskách.",
|
||||
"repeater_cliHelpTime": "Nastavuje časovník zariadenia na zadané sekundy od Unix epochy. Časovník sa nedá otáčať dozadu.",
|
||||
"repeater_cliHelpBoard": "Zobrazuje informácie o výrobcom dosky / identifikátor hardvéru.",
|
||||
"repeater_cliHelpDiscoverNeighbors": "Odosiela požiadavku na nájdenie susedných uzlov. (Len pre opakovače)",
|
||||
"repeater_cliHelpPowersaving": "Ukazuje, či je režim úspory energie zapnutý alebo vypnutý.",
|
||||
"repeater_cliHelpPowersavingOnOff": "Umožňuje alebo vypína režim úspory energie (ak je podporovaný).",
|
||||
"repeater_cliHelpErase": "(Používa sa len pre sériové zariadenia) Formátuje systém súborov zariadenia. Vymaže všetky nastavenia a kontakty.",
|
||||
"repeater_cliHelpSetDutyCycle": "Nastavuje maximálnu povolenú frekvenciu prenosu ako percento (1-100). Internálne upravuje faktor času prenosu.",
|
||||
"repeater_cliHelpSetPrvKey": "(Používa sa len v sériovej verzii) Nahradí privátny kľúč, ktorý identifikuje zariadenie. Po aplikácii je potrebné zariadenie reštartovať. Generuje nový verejný kľúč.",
|
||||
"repeater_cliHelpSetRadioRxGain": "(iba pre SX126x) Zapína zvýšený zisk prijímania pre zlepšenie citlivosti pri vyššom príkonu.",
|
||||
"repeater_cliHelpSetOwnerInfo": "Definuje reťazec s informáciami o kontaktnom osobě, ktorý je zahrnutý v reklamách. Používajte '|' pre nové riadky.",
|
||||
"repeater_cliHelpSetPathHashMode": "Nastavuje režim hashovania cesty. 0 = starý režim, 1 = štandardný režim, 2 = striktný režim. Ovplyvňuje, ako sa prekladajú trasy.",
|
||||
"repeater_cliHelpSetLoopDetect": "Nastavuje citlivosť detekcie slučky routovania: vypnutá, minimálna, stredná alebo prísna.",
|
||||
"repeater_cliHelpSetFreq": "(Používa sa len v sériovej verzii) Rýchlo nastavuje len frekvenciu. Je potrebné reštartovať. Pre úplné nastavenie rádia preferujte funkciu \"nastavenie rádia\".",
|
||||
"repeater_cliHelpSetBridgeChannel": "(Používa sa len pre ESPNow most) Nastavuje WiFi kanál (1-14), ktorý používa most.",
|
||||
"repeater_cliHelpGetName": "Zobrazuje zadané meno uzla.",
|
||||
"repeater_cliHelpGetRole": "Ukazuje funkciu firmvéru (opakovač, server pre miestnosť atď.).",
|
||||
"repeater_cliHelpGetPublicKey": "Zobrazuje verejný kľúč zariadenia.",
|
||||
"repeater_cliHelpGetPrvKey": "(Používa sa len v sériových aplikáciách) Zobrazuje súkromný kľúč zariadenia. Zotriďte ho ako tajný údaj.",
|
||||
"repeater_cliHelpGetRepeat": "Ukazuje, či je funkcia preposielania paketov (funkcia opakéra) zapnutá alebo vypnutá.",
|
||||
"repeater_cliHelpGetTx": "Zobrazuje aktuálnu výkonovú hodnotu TX v dBm.",
|
||||
"repeater_cliHelpGetFreq": "Zobrazuje nakonfigurovanú frekvenciu v MHz.",
|
||||
"repeater_cliHelpGetRadio": "Zobrazuje všetky parametre rádiového signálu: frekvencia, šírka pásma, faktor rozširovania, rýchlosť kódovania.",
|
||||
"repeater_cliHelpGetRadioRxGain": "(iba pre SX126x) Zobrazuje stav zosilnenia prijímača RX.",
|
||||
"repeater_cliHelpGetAf": "Zobrazuje aktuálny koeficient času vysielania.",
|
||||
"repeater_cliHelpGetDutyCycle": "Zobrazuje aktuálnu povolenú frekvenciu ako percentáž.",
|
||||
"repeater_cliHelpGetIntThresh": "Zobrazuje hranicu pre prechodové signály v dB.",
|
||||
"repeater_cliHelpGetAgcResetInterval": "Zobrazuje interval reštartovania AGC v sekundách.",
|
||||
"repeater_cliHelpGetMultiAcks": "Ukazuje, či je režim dvojité potvrdenie zapnutý (1) alebo vypnutý (0).",
|
||||
"repeater_cliHelpGetAllowReadOnly": "Ukazuje, či je povolená len čítacia funkcia pre hostí.",
|
||||
"repeater_cliHelpGetAdvertInterval": "Zobrazuje čas trvania miestnej reklamnej pauzy v minútach.",
|
||||
"repeater_cliHelpGetFloodAdvertInterval": "Zobrazuje časový interval reklamy počas záplavy v hodinách.",
|
||||
"repeater_cliHelpGetGuestPassword": "Zobrazuje nastavené heslo pre hosta.",
|
||||
"repeater_cliHelpGetLat": "Zobrazuje nastavenú šírku.",
|
||||
"repeater_cliHelpGetLon": "Zobrazuje nastavenú dĺžku.",
|
||||
"repeater_cliHelpGetRxDelay": "Zobrazuje základnú hodnotu rxdelay.",
|
||||
"repeater_cliHelpGetTxDelay": "Ukazuje faktor zpoždenia pre režim povodňovej komunikácie.",
|
||||
"repeater_cliHelpGetDirectTxDelay": "Zobrazuje faktor zloženia pri priamej modulácii.",
|
||||
"repeater_cliHelpGetFloodMax": "Zobrazuje maximálny počet opakovaní povodňového stavu.",
|
||||
"repeater_cliHelpGetOwnerInfo": "Zobrazuje reťazec s kontaktnými údajmi vlastníka.",
|
||||
"repeater_cliHelpGetPathHashMode": "Zobrazuje režim hashovania cesty (0/1/2).",
|
||||
"repeater_cliHelpGetLoopDetect": "Ukazuje citlivosť na detekciu slučiek.",
|
||||
"repeater_cliHelpGetAcl": "(Používa sa len v sériovej konfigurácii) Zobrazuje prístupové pravidlá na opakovači.",
|
||||
"repeater_cliHelpGetBridgeEnabled": "Ukazuje, či je most povolený.",
|
||||
"repeater_cliHelpGetBridgeDelay": "Zobrazuje čas strávený prechodom mosta v milisekundách.",
|
||||
"repeater_cliHelpGetBridgeSource": "Ukazuje, či most prijíma alebo vysiela RX alebo TX balíky.",
|
||||
"repeater_cliHelpGetBridgeBaud": "(iba pre rozhranie RS232) Zobrazuje rýchlosť prenosu dát na rozhraní RS232.",
|
||||
"repeater_cliHelpGetBridgeChannel": "(Používa sa len pre ESPNow) Zobrazuje WiFi kanál mosta.",
|
||||
"repeater_cliHelpGetBridgeSecret": "(Používa sa len pre ESPNow most) Zobrazuje spoločný tajný kľúč mosta.",
|
||||
"repeater_cliHelpGetBootloaderVer": "(iba pre NRF52) Zobrazuje verziu bootloaderu.",
|
||||
"repeater_cliHelpGetAdcMultiplier": "Zobrazuje násobič ADC (škálovanie napätia batérie).",
|
||||
"repeater_cliHelpGetPwrMgtSupport": "Označuje, či riadiace orgány majú podporu pre správu energie.",
|
||||
"repeater_cliHelpGetPwrMgtSource": "Ukazuje aktuálny zdroj napájania: externý alebo batéria.",
|
||||
"repeater_cliHelpGetPwrMgtBootReason": "Zobrazuje najaktuálnejšie dôvody pre reštart a vypnutie.",
|
||||
"repeater_cliHelpGetPwrMgtBootMv": "Zobrazuje napätie batérie pri spustení systému v milivoltov (mV).",
|
||||
"repeater_cliHelpSensorGet": "Číta hodnotu nastavenia pre špecifický senzor pomocou klávesového vstupu.",
|
||||
"repeater_cliHelpSensorSet": "Vytvára vlastné nastavenie pre senzor.",
|
||||
"repeater_cliHelpSensorList": "Zobrazuje všetky nastavenia pre špecifické senzory, zoradené podľa voliteľného indexu začiatku.",
|
||||
"repeater_cliHelpRegionDefault": "Zobrazuje aktuálnu rozsiahku, ktorá je nastavená ako výchozí.",
|
||||
"repeater_cliHelpRegionDefaultSet": "Nastavuje výchoce rozsiahku regiónu. Použite \"<null>\", aby ju vymazal.",
|
||||
"repeater_cliHelpRegionListAllowed": "Zoznam oblastí, ktoré umožňujú premávku počas povodní.",
|
||||
"repeater_cliHelpRegionListDenied": "Zoznam oblastí, ktoré zakazujú premávku v dôsledku povodní.",
|
||||
"repeater_cliHelpStatsPackets": "(Len pre sériové záznamy) Zobrazuje štatistiky na úrovni paketov.",
|
||||
"repeater_cliHelpStatsRadio": "(Len pre sériu) Zobrazuje údaje o rádiových staniciach.",
|
||||
"repeater_cliHelpStatsCore": "(Len pre sériové modely) Zobrazuje základné štatistiky firmvéru.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "Zdieľané",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Zablokované miesta",
|
||||
"losBlockedSpotsHint": "Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.",
|
||||
"losSelectedObstructionTitle": "Vybraná prekážka",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||
"chat_markAsUnread": "Označenie ako neprečítané",
|
||||
"settings_companionDebugLogSubtitle": "Príkazy, odpovede a surové dáta pre protokoly BLE/TCP/USB",
|
||||
"settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)",
|
||||
"chat_newMessages": "Nové správy",
|
||||
"repeater_chanUtil": "Využitie kanálu",
|
||||
"@routing_lastWorked": {
|
||||
"placeholders": {
|
||||
"when": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@routing_deliveryCounts": {
|
||||
"placeholders": {
|
||||
"successes": {
|
||||
"type": "int"
|
||||
},
|
||||
"failures": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_hopCounter": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_invalidTokens": {
|
||||
"placeholders": {
|
||||
"tokens": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_communityShortId": {
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"messageStatus_sent": "Odoslané",
|
||||
"messageStatus_delivered": "Doručené",
|
||||
"messageStatus_pending": "Odoslanie",
|
||||
"common_undo": "Zrušiť",
|
||||
"messageStatus_failed": "Neúspešné odeslanie",
|
||||
"messageStatus_repeated": "Slyšal som to opakovane",
|
||||
"contacts_moreOptions": "Ďalšie možnosti",
|
||||
"contacts_searchOpen": "Vyhľadajte kontakty",
|
||||
"contacts_searchClose": "Zavrieť vyhľadávanie",
|
||||
"routing_title": "Navigácia",
|
||||
"routing_modeAuto": "Auto",
|
||||
"routing_modeFlood": "Povodňová vlna",
|
||||
"routing_modeManual": "Ručná príručka",
|
||||
"routing_modeAutoHint": "Automaticky vyberá najznámejší trasa, a ak žiadna nie je známa, použije náhodnú trasu.",
|
||||
"routing_modeFloodHint": "Prenos prostredníctvom všetkých opakovačov. Najspoľahlivejší spôsob, ale vyžaduje viac času vysielania.",
|
||||
"routing_modeManualHint": "Vždy dodáva presne podľa zadaného trasy.",
|
||||
"routing_currentRoute": "Aktuálna trasa",
|
||||
"routing_directNoHops": "Priamo – bez prechodných trás",
|
||||
"routing_noPathYet": "Zatiaľ neexistuje žiadna cesta. Nasledujúce správy budú pokračovať, kým sa nenájde trasa.",
|
||||
"routing_floodBroadcast": "Prenos prostredníctvom každého opakovača",
|
||||
"routing_editPath": "Upraviť trasu",
|
||||
"routing_forgetPath": "Zabudnite na trasu",
|
||||
"routing_knownPaths": "Známe cesty",
|
||||
"routing_knownPathsHint": "Kliknite na cestu, aby ste sa k nej presunuli.",
|
||||
"routing_inUse": "V prevádzke",
|
||||
"routing_qualityStrong": "Silný prvý krok",
|
||||
"routing_qualityGood": "Úspešný prvý krok",
|
||||
"routing_qualityFair": "Prvá, spravodlivá fáza",
|
||||
"routing_qualityWorked": "Dosiahnutý úspech",
|
||||
"routing_qualityFlood": "Zistil som to z informácií, ktoré som získal v dôsledku povodňovej situácie.",
|
||||
"routing_qualityUntested": "Neotestované",
|
||||
"routing_neverWorked": "nikedy nebolo potvrdené",
|
||||
"routing_floodDelivery": "Doručenie v prípade povodní",
|
||||
"pathEditor_title": "Vytvorenie cesty",
|
||||
"pathEditor_hopCounter": "{count} z 64 chmelových zŕš",
|
||||
"pathEditor_noHops": "Zatiaľ žiadne chmel. Kliknite na opakované, aby ste ich pridali postupne, alebo uložte bez chmelu, aby ste ho mohli poslať priamo.",
|
||||
"pathEditor_addHops": "Pridávajte chmel podľa zadaného poriadku.",
|
||||
"pathEditor_searchRepeaters": "Hľadať opakované",
|
||||
"pathEditor_advancedHex": "Pokročilé: pôvodná hexová cesta",
|
||||
"pathEditor_hexLabel": "Prefiksy pre hexadecimálne čísla",
|
||||
"pathEditor_hexHelper": "Dve hexové čísla na každý krok, oddelené čiarkami",
|
||||
"routing_lastWorked": "worked {when}",
|
||||
"pathEditor_invalidTokens": "Neplatné: {tokens}",
|
||||
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
|
||||
"pathEditor_tooManyHops": "Maximálne 64 krokov",
|
||||
"pathEditor_usePath": "Použite túto cestu",
|
||||
"pathEditor_removeHop": "Odstráňte chmel",
|
||||
"pathEditor_unknownHop": "Neznáme zariadenie na opakované vysielanie",
|
||||
"map_zoomIn": "Zväčšiť",
|
||||
"map_zoomOut": "Zmenť zamer zblízka",
|
||||
"map_centerMap": "Mapa centra",
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth vyžaduje prehliadač Chromium.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS potvrdilo",
|
||||
"pathTrace_legendInferred": "Odvodená poloha",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_online": "Online",
|
||||
"scanner_bluetoothWebUnsupported": "Funkcia Bluetooth nie je dostupná v prehliadači. Prepojte sa pomocou USB.",
|
||||
"map_searchHint": "Vyhľadajte podľa názvu alebo ID uzla",
|
||||
"map_activity": "Aktivita",
|
||||
"map_recent": "Nedávne",
|
||||
"map_stale": "Neaktuálne",
|
||||
"map_hidden": "Skrytý",
|
||||
"map_visible": "Viditeľný",
|
||||
"map_centerOnNode": "Nacentrovať na uzol",
|
||||
"map_details": "Podrobnosti",
|
||||
"map_noGps": "Bez GPS",
|
||||
"map_noResults": "Nenašli sa žiadne zodpovedajúce uzly.",
|
||||
"pathMap_viewSingle": "Jednotlivý",
|
||||
"pathMap_viewCombined": "Spojené",
|
||||
"pathMap_play": "Prehrať",
|
||||
"pathMap_pause": "Pozastaviť",
|
||||
"pathMap_replay": "Prehrať znova",
|
||||
"pathMap_stepBack": "Predchádzajúci skok",
|
||||
"pathMap_stepForward": "Nasledujúci skok",
|
||||
"pathMap_animationOn": "Zobraziť animáciu paketu",
|
||||
"pathMap_animationOff": "Skryť animáciu paketu",
|
||||
"pathMap_hopOf": "Skok {current} z {total}",
|
||||
"pathMap_observedPaths": "Pozorované cesty: {count}",
|
||||
"pathMap_primary": "Primárna",
|
||||
"pathMap_alternate": "Alternatívny {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 skok} few{{count} skoky} other{{count} skokov}}",
|
||||
"pathMap_legendShared": "Spoločný segment",
|
||||
"pathMap_legendEstimated": "Odhadovaný segment",
|
||||
"pathMap_sharedNodeCount": "Používané {count} cestami",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 skok nemá polohu — zobrazená trasa je neúplná} few{{count} skoky nemajú polohu — zobrazená trasa je neúplná} other{{count} skokov nemá polohu — zobrazená trasa je neúplná}}",
|
||||
"pathMap_showAllPaths": "Zobraziť všetky",
|
||||
"pathMap_hidePath": "Skryť cestu",
|
||||
"pathMap_showPath": "Zobraziť trasu",
|
||||
"pathMap_collapsePanel": "Zatvoriť panel",
|
||||
"pathMap_expandPanel": "Rozbaliť panel",
|
||||
"pathMap_noLocation": "Bez polohy",
|
||||
"pathMap_followPacket": "Uzamknúť pohľad na paket",
|
||||
"pathMap_unfollowPacket": "Odomknúť pohľad od paketu",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
}
|
||||
|
||||
+638
-39
@@ -33,6 +33,8 @@
|
||||
"common_remove": "Izbrisati",
|
||||
"common_enable": "Omogoči",
|
||||
"common_disable": "Izklopiti",
|
||||
"common_autoRefresh": "Samodejno osveževanje",
|
||||
"common_interval": "Časovni interval",
|
||||
"common_reboot": "Ponoviti",
|
||||
"common_loading": "Naložanje...",
|
||||
"common_notAvailable": "—",
|
||||
@@ -44,7 +46,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_percentValue": "{percent}%",
|
||||
"common_percentValue": "{percent} %",
|
||||
"@common_percentValue": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
@@ -52,7 +54,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_title": "MeshCore Open",
|
||||
"scanner_title": "MeshCore – Odprto",
|
||||
"scanner_scanning": "Skeniram za naprave...",
|
||||
"scanner_connecting": "Povezujem se...",
|
||||
"scanner_disconnecting": "Odklapljam se...",
|
||||
@@ -104,6 +106,8 @@
|
||||
"settings_privacyModeEnabled": "Privatni način je omogočen.",
|
||||
"settings_privacyModeDisabled": "Privatni način je onemogočen.",
|
||||
"settings_actions": "Akcije",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Pošlji Oglas",
|
||||
"settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah",
|
||||
"settings_advertisementSent": "Oglas poslan",
|
||||
@@ -115,13 +119,13 @@
|
||||
"settings_rebootDevice": "Ponovni zagon naprave",
|
||||
"settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo",
|
||||
"settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.",
|
||||
"settings_debug": "Debug",
|
||||
"settings_debug": "Odpravljanje napak",
|
||||
"settings_bleDebugLog": "BLE debug log (razhroščevanje)",
|
||||
"settings_bleDebugLogSubtitle": "BLE ukazi, odgovori in surovi podatki",
|
||||
"settings_appDebugLog": "Logi aplikacije",
|
||||
"settings_appDebugLogSubtitle": "Debug sporočila aplikacije",
|
||||
"settings_about": "Oglejte si",
|
||||
"settings_aboutVersion": "MeshCore Open v{version}",
|
||||
"settings_aboutVersion": "MeshCore, različ {version}",
|
||||
"@settings_aboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
@@ -133,7 +137,7 @@
|
||||
"settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.",
|
||||
"settings_infoName": "Ime",
|
||||
"settings_infoId": "ID",
|
||||
"settings_infoStatus": "Status",
|
||||
"settings_infoStatus": "Stanje",
|
||||
"settings_infoBattery": "Baterija",
|
||||
"settings_infoPublicKey": "Javni ključ",
|
||||
"settings_infoContactsCount": "Število stikov",
|
||||
@@ -146,7 +150,7 @@
|
||||
"settings_spreadingFactor": "Razširitveni faktor",
|
||||
"settings_codingRate": "Programska hitrost",
|
||||
"settings_txPower": "TX Moč (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerHelper": "0 – 22",
|
||||
"settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)",
|
||||
"settings_error": "Napaka: {message}",
|
||||
"@settings_error": {
|
||||
@@ -164,18 +168,18 @@
|
||||
"appSettings_themeDark": "Temno",
|
||||
"appSettings_language": "Jezik",
|
||||
"appSettings_languageSystem": "Sistemska privzeta vrednost",
|
||||
"appSettings_languageEn": "English",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageDe": "Deutsch",
|
||||
"appSettings_languagePl": "Polski",
|
||||
"appSettings_languageEn": "Angleščina",
|
||||
"appSettings_languageFr": "Francija",
|
||||
"appSettings_languageEs": "Španščina",
|
||||
"appSettings_languageDe": "Nemščina",
|
||||
"appSettings_languagePl": "Poljski",
|
||||
"appSettings_languageSl": "Slovenščina",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageIt": "Italiano",
|
||||
"appSettings_languagePt": "Portugalski",
|
||||
"appSettings_languageIt": "Italijanščina",
|
||||
"appSettings_languageZh": "中文",
|
||||
"appSettings_languageSv": "Svenska",
|
||||
"appSettings_languageNl": "Nederlands",
|
||||
"appSettings_languageSk": "Slovenčina",
|
||||
"appSettings_languageSv": "Švedska",
|
||||
"appSettings_languageNl": "Nizozemsko",
|
||||
"appSettings_languageSk": "Slovenščina",
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_notifications": "Obvestila",
|
||||
"appSettings_enableNotifications": "Omogoči obvestila",
|
||||
@@ -337,11 +341,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_hashtagChannel": "Hashtag kanal",
|
||||
"channels_public": "Javni",
|
||||
"channels_private": "Zasebni",
|
||||
"channels_publicChannel": "Javni kanal",
|
||||
"channels_privateChannel": "Zasebni kanal",
|
||||
"channels_editChannel": "Uredi kanal",
|
||||
"channels_muteChannel": "Utišaj kanal",
|
||||
"channels_unmuteChannel": "Vklopi obvestila kanala",
|
||||
@@ -388,6 +389,22 @@
|
||||
}
|
||||
},
|
||||
"channels_smazCompression": "Kompresija SMAZ",
|
||||
"channels_cyr2latCompression": "Kompresija Cyr2Lat",
|
||||
"channels_cyr2latCompressionDscr": "Pri pošiljanju nekatere cirilice nadomesti z latiničnimi.",
|
||||
"channels_cyr2latSettingsHeading": "Nastavitve Cyr2Lat",
|
||||
"channels_cyr2latSettingsSubheading": "Seznam zamenjav",
|
||||
"channels_cyr2latSettingsDscr": "Uredi JSON-konfiguracijo zamenjav znakov",
|
||||
"channels_cyr2latSettingsDialogHint": "JSON-tabela zamenjav",
|
||||
"channels_cyr2latSettingsDialogWrongJSON": "Nepravilen JSON: {error}",
|
||||
"settings_cyr2latProfileAdd": "Dodaj profil Cyr2Lat",
|
||||
"settings_cyr2latProfileName": "Ime profila",
|
||||
"settings_cyr2latProfileNameEmpty": "Ime profila ne sme biti prazno",
|
||||
"settings_cyr2latProfileAdded": "Profil je bil uspešno dodan",
|
||||
"settings_cyr2latProfileUpdated": "Profil je bil uspešno posodobljen",
|
||||
"settings_cyr2latProfileEdit": "Uredi profil Cyr2Lat",
|
||||
"settings_cyr2latProfileDelete": "Izbriši profil Cyr2Lat",
|
||||
"settings_cyr2latProfileDeleted": "Profil je bil uspešno izbrisan",
|
||||
"settings_cyr2latProfileDeleteDscr": "Ali res želite izbrisati profil \"{name}\"?",
|
||||
"channels_channelUpdated": "Kanal {name} je bil posodobljen",
|
||||
"@channels_channelUpdated": {
|
||||
"placeholders": {
|
||||
@@ -399,7 +416,7 @@
|
||||
"channels_publicChannelAdded": "javna skupnost dodana",
|
||||
"channels_sortBy": "Sortiraj po",
|
||||
"channels_sortManual": "Ročno",
|
||||
"channels_sortAZ": "A-Z",
|
||||
"channels_sortAZ": "A do Z",
|
||||
"channels_sortLatestMessages": "Najnovejše sporočilo",
|
||||
"channels_sortUnread": "Nerešeno",
|
||||
"chat_noMessages": "Še ni sporočil.",
|
||||
@@ -531,7 +548,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"debugFrame_textTypeCli": "CLI",
|
||||
"debugFrame_textTypeCli": "CLI (Command Line Interface)",
|
||||
"debugFrame_textTypePlain": "Preprosto",
|
||||
"debugFrame_text": "- Tekst: \"{text}\"",
|
||||
"@debugFrame_text": {
|
||||
@@ -541,7 +558,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"debugFrame_hexDump": "Hex Dump:",
|
||||
"debugFrame_hexDump": "Izpis heksadecimalnih vrednosti:",
|
||||
"chat_pathManagement": "Upravljanje poti",
|
||||
"chat_routingMode": "Navodilo za usmerjevalni način",
|
||||
"chat_autoUseSavedPath": "Avto (uporabi shranjeno pot)",
|
||||
@@ -550,7 +567,7 @@
|
||||
"chat_pathHistoryFull": "Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.",
|
||||
"chat_hopSingular": "skok",
|
||||
"chat_hopPlural": "skokov",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{skok} other{skoki}}",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -879,11 +896,11 @@
|
||||
"path_setPath": "Nastavi Pot",
|
||||
"repeater_management": "Upravljanje ponovitve",
|
||||
"repeater_managementTools": "Upravne orodje",
|
||||
"repeater_status": "Status",
|
||||
"repeater_status": "Stanje",
|
||||
"repeater_statusSubtitle": "Pogledati stanje, statistike in sosede repeatera",
|
||||
"repeater_telemetry": "Telemetrija",
|
||||
"repeater_telemetrySubtitle": "Pogledate telemetrijo senzorjev in sistemske statistike",
|
||||
"repeater_cli": "CLI",
|
||||
"repeater_cli": "CLI (Command Line Interface)",
|
||||
"repeater_cliSubtitle": "Pošlji ukazne povelje na ponovitveno enoto.",
|
||||
"repeater_settings": "Nastavitve",
|
||||
"repeater_settingsSubtitle": "Konfigurirajte parametre ponovitelja",
|
||||
@@ -992,7 +1009,7 @@
|
||||
"repeater_guestPasswordHelper": "Odpovedni dostopni geslo",
|
||||
"repeater_radioSettings": "Nastavitve Radija",
|
||||
"repeater_frequencyMhz": "Frekvenca (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
"repeater_frequencyHelper": "300–2500 MHz",
|
||||
"repeater_txPower": "TX Moč",
|
||||
"repeater_txPowerHelper": "1-30 dBm",
|
||||
"repeater_bandwidth": "Pasovna širina",
|
||||
@@ -1059,6 +1076,81 @@
|
||||
},
|
||||
"repeater_confirm": "Potrdit",
|
||||
"repeater_settingsSaved": "Nastavitve so shranjene uspešno.",
|
||||
"repeater_rxGain": "Povečana dobitka RX",
|
||||
"repeater_rxGainHelper": "Veća občutljivost, večji porabljeni tok (velja samo za SX1262/SX1268)",
|
||||
"repeater_refreshRxGain": "Povečana dobitka RX, posodobit",
|
||||
"repeater_multiAcks": "Več potrdil",
|
||||
"repeater_multiAcksSubtitle": "Potrdite sporočila po več poti za boljši dostop",
|
||||
"repeater_refreshMultiAcks": "Ponovite več potrdil",
|
||||
"repeater_networkHealth": "Zdravilo omrežja",
|
||||
"repeater_loopDetect": "Detekcija ciklov",
|
||||
"repeater_loopDetectHelper": "Izpišite pakete, ki izgledajo kot pete v omrežju.",
|
||||
"repeater_loopDetectOff": "Izklopljeno",
|
||||
"repeater_loopDetectMinimal": "Minimalen",
|
||||
"repeater_loopDetectModerate": "Umiren",
|
||||
"repeater_loopDetectStrict": "Strogi",
|
||||
"repeater_dutyCycle": "Ciklus delovanja",
|
||||
"repeater_dutyCycleHelper": "Najvišji odstotek časa, ki ga lahko posreduje.",
|
||||
"repeater_dutyCyclePercent": "{percent} %",
|
||||
"@repeater_dutyCyclePercent": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_ownerInfo": "Informacije o operaterju",
|
||||
"repeater_ownerInfoHelper": "javni podatki o tej napravi",
|
||||
"repeater_refreshOwnerInfo": "Prejmi informacije o operaterju",
|
||||
"repeater_floodMax": "Največji možni odmerek",
|
||||
"repeater_floodMaxHelper": "Največje število paketov, ki lahko potujejo v enem plovilnem paketu (0-64)",
|
||||
"repeater_advancedSettings": "Napredno",
|
||||
"repeater_advancedSettingsSubtitle": "Gumbi za nastavljanje za izkušene uporabnike",
|
||||
"repeater_pathHashMode": "Način ustvarjanja hash-a poti",
|
||||
"repeater_pathHashModeHelper": "Biti, ki so bila uporabljena za kodiranje ID-ja tega releja v oznakah za zaznavanje pot/kroga, imajo naslednje velikosti: 0=1 bit (256 ID-jev, do 64 skokov), 1=2 biti (65.000 ID-jev, do 32 skokov), 2=3 biti (16 milijonov ID-jev, do 21 skokov). V različicah 1.13 in starejših se ustvarjajo večbitne poti – vendar se to zgodi šele, ko je omrežje vklopljeno v različicah 1.14 in kasnejših.",
|
||||
"repeater_txDelay": "Zatemnitevanje zaradi poplav v Texasu",
|
||||
"repeater_txDelayHelper": "Uporaba intervalov za ponovno pošiljanje v primeru prometa zaradi poplav, kot pomnožnik časovne trajanje paketa (0-2, privzeto 0,5). Veje vrednost = manjše kolizije, vendar počasnejše dostavo.",
|
||||
"repeater_directTxDelay": "Neposredni časovno odlašanje",
|
||||
"repeater_directTxDelayHelper": "Razdalja za ponovno pošiljanje za neposredno (neobvezen) promet, kot pomnožnik časovne trajanja paketa (0-2, privzeto 0,3).",
|
||||
"repeater_intThresh": "Meja, pri kateri nastane motnja",
|
||||
"repeater_intThreshHelper": "Tretja stopnja se uporablja za kalibracijo šumnega nivoja radija, kar omogoča, da se izklaplja pri šumu, ki presega to raven. 0 izklopi – uporabite le, če zaznate napake v šumnem pasu.",
|
||||
"repeater_agcResetInterval": "Interval ponovne kalibracije AGC",
|
||||
"repeater_agcResetIntervalHelper": "Kako pogosto je treba ponovno nastaviti samodejno regulacijo občutljivosti, da se vrnete v normalno stanje? Interval je nastavljen na nekaj sekund, natančno na 4. 0 izklopi periodično ponovno nastavljanje.",
|
||||
"repeater_actionsTitle": "Dejanja",
|
||||
"repeater_sendAdvert": "Pošlji oglas o poplavah",
|
||||
"repeater_sendAdvertSubtitle": "Razpustite oglas o poplavah preko omrežja.",
|
||||
"repeater_sendAdvertZeroHop": "Pošlji oglas, ki ne potrebuje posrednika.",
|
||||
"repeater_sendAdvertZeroHopSubtitle": "Premejte oglas, ki uporablja eno povezavo (brez posrednikov).",
|
||||
"repeater_clockSync": "Sinerizirajte uro zdaj",
|
||||
"repeater_clockSyncSubtitle": "Nastavite čas na telefonu, da se sinhronizira s repeatrom.",
|
||||
"repeater_actionSucceeded": "{action} je uspel",
|
||||
"@repeater_actionSucceeded": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_actionFailed": "{action} ni bilo uspešno: {error}",
|
||||
"@repeater_actionFailed": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
},
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsSavedRebootNeeded": "Nastavitve shranjene – ponovni zagon repetitorja za uporabo",
|
||||
"repeater_settingsPartialFailure": "Nekatna nastavitva niso uspešna: {failures}",
|
||||
"@repeater_settingsPartialFailure": {
|
||||
"placeholders": {
|
||||
"failures": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_errorSavingSettings": "Napaka pri shranjevanju nastavitev: {error}",
|
||||
"@repeater_errorSavingSettings": {
|
||||
"placeholders": {
|
||||
@@ -1070,11 +1162,9 @@
|
||||
"repeater_refreshBasicSettings": "Ponovno nastavi osnovne nastavitve",
|
||||
"repeater_refreshRadioSettings": "Ponovno Nastavitve Radija",
|
||||
"repeater_refreshTxPower": "Ponovno nastavi TX moč",
|
||||
"repeater_refreshLocationSettings": "Ponovno Nastavi Nastavitve Lokacije",
|
||||
"repeater_refreshPacketForwarding": "Ponovno nastavitve usmerjevanja paketa",
|
||||
"repeater_refreshGuestAccess": "Ponovno nastavitve dostopa gostov",
|
||||
"repeater_refreshPrivacyMode": "Ponovno aktiviraj način zasebnosti",
|
||||
"repeater_refreshAdvertisementSettings": "Ponovno nastavi Oglede Oglasi",
|
||||
"repeater_refreshed": "{label} je bil/a posodobljen/a",
|
||||
"@repeater_refreshed": {
|
||||
"placeholders": {
|
||||
@@ -1192,6 +1282,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_digitalInputLabel": "Digitalni vhod",
|
||||
"telemetry_digitalOutputLabel": "Digitalni izhod",
|
||||
"telemetry_analogInputLabel": "Analogni vhod",
|
||||
"telemetry_analogOutputLabel": "Analogni izhod",
|
||||
"telemetry_genericLabel": "Splošni senzor",
|
||||
"telemetry_luminosityLabel": "Osvetljenost",
|
||||
"telemetry_presenceLabel": "Prisotnost",
|
||||
"telemetry_humidityLabel": "Vlažnost",
|
||||
"telemetry_accelerometerLabel": "Merilnik pospeška",
|
||||
"telemetry_pressureLabel": "Tlak",
|
||||
"telemetry_altitudeLabel": "Nadmorska višina",
|
||||
"telemetry_frequencyLabel": "Frekvenca",
|
||||
"telemetry_percentageLabel": "Odstotek",
|
||||
"telemetry_concentrationLabel": "Koncentracija",
|
||||
"telemetry_powerLabel": "Moč",
|
||||
"telemetry_distanceLabel": "Razdalja",
|
||||
"telemetry_energyLabel": "Energija",
|
||||
"telemetry_directionLabel": "Smer",
|
||||
"telemetry_timeLabel": "Čas",
|
||||
"telemetry_gyrometerLabel": "Žiroskop",
|
||||
"telemetry_colourLabel": "Barva",
|
||||
"telemetry_gpsLabel": "GPS",
|
||||
"telemetry_switchLabel": "Stikalo",
|
||||
"telemetry_polylineLabel": "Polilinija",
|
||||
"telemetry_altitudeValue": "{meters} m",
|
||||
"telemetry_frequencyValue": "{hertz} Hz",
|
||||
"telemetry_pressureValue": "{hpa} hPa",
|
||||
"telemetry_luminosityValue": "{lux} lx",
|
||||
"telemetry_powerValue": "{watts} W",
|
||||
"telemetry_distanceValue": "{meters} m",
|
||||
"telemetry_energyValue": "{kilowattHours} kWh",
|
||||
"telemetry_directionValue": "{degrees}°",
|
||||
"telemetry_concentrationValue": "{ppm} ppm",
|
||||
"telemetry_percentageValue": "{percent}%",
|
||||
"telemetry_analogValue": "{value}",
|
||||
"telemetry_autoFetchQuantity": "Število zahtev",
|
||||
"telemetry_error": "Podatkov ni bilo mogoče pridobiti",
|
||||
"telemetry_noData": "Niso na voljo podatki o telemetriji.",
|
||||
"telemetry_channelTitle": "Kanal {channel}",
|
||||
"@telemetry_channelTitle": {
|
||||
@@ -1347,7 +1474,7 @@
|
||||
"listFilter_sortBy": "Sortiraj po",
|
||||
"listFilter_latestMessages": "Najnovejše sporočilo",
|
||||
"listFilter_heardRecently": "Nedavno slišan",
|
||||
"listFilter_az": "A-Z",
|
||||
"listFilter_az": "A do Z",
|
||||
"listFilter_filters": "Filtri",
|
||||
"listFilter_all": "Vse",
|
||||
"listFilter_users": "Uporabniki",
|
||||
@@ -1922,13 +2049,6 @@
|
||||
"contact_teleEnv": "Okolje telemetrije",
|
||||
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
|
||||
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.",
|
||||
"appSettings_initialRouteWeight": "Izvirna teža poti",
|
||||
"appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti",
|
||||
@@ -1940,6 +2060,485 @@
|
||||
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Večkratni potrditvi: {value}",
|
||||
"settings_telemetryModeUpdated": "Način telemetrije posodobljen"
|
||||
}
|
||||
"settings_telemetryModeUpdated": "Način telemetrije posodobljen",
|
||||
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
|
||||
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_languageHu": "Madžarski",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.",
|
||||
"chat_sendCooldown": "Prosimo, počakajte trenutek, preden pošljete ponovno.",
|
||||
"appSettings_jumpToOldestUnread": "Pritisnite za najstarejše nepročitano sporočilo",
|
||||
"appSettings_languageJa": "Japonski",
|
||||
"appSettings_languageKo": "Korejski",
|
||||
"radioStats_tooltip": "Statistike za radio in mrežo",
|
||||
"radioStats_notConnected": "Povežite se z napravo, da si ogledate statistiko o radiju.",
|
||||
"radioStats_screenTitle": "Radijske statistike",
|
||||
"radioStats_firmwareTooOld": "Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.",
|
||||
"radioStats_waiting": "Čakam na podatke…",
|
||||
"radioStats_noiseFloor": "Število šuma: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Najkasnejše vrednost RSSI: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Najkasnejše vrednost SNR: {snr} dB",
|
||||
"radioStats_txAir": "Čas na TX (skupno): {seconds} s",
|
||||
"radioStats_rxAir": "Čas, namenjen RX-ju (skupno): {seconds} s",
|
||||
"radioStats_chartCaption": "Ravnovredna raven šuma (dBm) za nedavne vzorce.",
|
||||
"radioStats_stripNoise": "Število šuma: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Prejemanje statistike o radiju…",
|
||||
"radioStats_settingsTile": "Radijske statistike",
|
||||
"radioStats_settingsSubtitle": "Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Preprištejte, preden pošljete",
|
||||
"translation_title": "Prevod",
|
||||
"translation_enableSubtitle": "Prevedite vstopne sporočila in omogočite predhodno prevajanje.",
|
||||
"translation_enableTitle": "Omogočite prevod",
|
||||
"translation_composerSubtitle": "Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.",
|
||||
"translation_autoIncomingTitle": "Samodejno prevajaj sporočila",
|
||||
"translation_autoIncomingSubtitle": "Samodejno prevaja sporočila za obvestila ter za klepete ali kanale.",
|
||||
"translation_translateMessage": "Prevedi sporočilo",
|
||||
"translation_targetLanguage": "Ciljna jezika",
|
||||
"translation_useAppLanguage": "Uporabite jezik aplikacije",
|
||||
"translation_downloadedModelLabel": "Naložen model",
|
||||
"translation_presetModelLabel": "Prednastavljeni model Hugging Face",
|
||||
"translation_manualUrlLabel": "URL za ročni model",
|
||||
"translation_downloadModel": "Prenesite model",
|
||||
"translation_downloading": "Izvajanje...",
|
||||
"translation_working": "Delo...",
|
||||
"translation_stop": "Prekliji",
|
||||
"translation_mergingChunks": "Sklapljanje prenesenih delov v končni datoteko...",
|
||||
"translation_downloadedModels": "Naloženi modeli",
|
||||
"translation_deleteModel": "Izbrisati model",
|
||||
"translation_modelDownloaded": "Model za prevajanje je bil naložen.",
|
||||
"translation_downloadStopped": "Prenos je bil prekinjen.",
|
||||
"translation_downloadFailed": "Izgovoritev ni bila uspešna: {error}",
|
||||
"translation_enterUrlFirst": "Najprej vnesite URL model.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translateBeforeSending": "Preprištejte, preden pošljete",
|
||||
"translation_composerDisabledHint": "Pošljite sporočila v originalnem tipkanem jeziku.",
|
||||
"translation_composerEnabledHint": "Vsebina sporočil bo prevedena, preden jih pošljemo.",
|
||||
"translation_messageTranslation": "Prevod sporočila",
|
||||
"translation_translateTo": "Prevesti v {language}",
|
||||
"translation_translationOptions": "Možnosti prevoda",
|
||||
"translation_systemLanguage": "Jezik sistema",
|
||||
"scanner_linuxPairingShowPin": "Prikaži PIN",
|
||||
"scanner_linuxPairingHidePin": "Skrij PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje",
|
||||
"repeater_cliQuickDiscovery": "Odkrijte sosede",
|
||||
"repeater_cliQuickClockSync": "Usklajevanje ure",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.",
|
||||
"repeater_clockSyncAfterLogin": "Sinhronizacija ure po prijavi",
|
||||
"repeater_guest": "Informacije o ponovljalniku",
|
||||
"chat_sendMessage": "Pošlji sporočilo",
|
||||
"room_guest": "Informacije o strežniku",
|
||||
"repeater_guestTools": "Naložila za goste",
|
||||
"repeater_getCategory": "Dobite vrednosti",
|
||||
"repeater_powerMgmt": "Upravljanje z energijo",
|
||||
"repeater_sensors": "Senzori",
|
||||
"repeater_cliHelpPowerOff": "Izklopi naprave. (ne pričakujemo odziva)",
|
||||
"repeater_cliHelpClkReboot": "Ponovno nastavi uro na znano točko in ponovno vklopi naprave.",
|
||||
"repeater_cliHelpAdvertZeroHop": "Pošlje oglas, ki doseže samo neposredne sosede (brez posredovanja).",
|
||||
"repeater_cliHelpStartOta": "Začne nadstrekovno ažuriranje programne opreme na podprtih ploščah.",
|
||||
"repeater_cliHelpTime": "Nastavi časovni ukaz naprave na podano število sekund od Unixovega začetka. Časovni ukaz ne more iti nazaj.",
|
||||
"repeater_cliHelpBoard": "Prikaže proizvajalca plošče / identifikator strojne opreme.",
|
||||
"repeater_cliHelpDiscoverNeighbors": "Pošlje zahtevo za odkrivanje sosednjih naprav. (Samo za repeatere)",
|
||||
"repeater_cliHelpPowersaving": "Prikaže, ali je vklopljen način varčevanja z energijo.",
|
||||
"repeater_cliHelpPowersavingOnOff": "Omogoča ali onemogoča način varčevanja z energijo (če je podprt).",
|
||||
"repeater_cliHelpErase": "(Samo za serijske naprave) Formira datotapno sistemsko okolje. Izbriše vse nastavitve in kontakte.",
|
||||
"repeater_cliHelpSetDutyCycle": "Določi maksimalni dovoljeni čas, ki ga naprave lahko posredujejo, v odstotkih (1-100). Samodejno prilagodi faktor, ki odvisen je od časa, ki ga naprave lahko posredujejo.",
|
||||
"repeater_cliHelpSetPrvKey": "(Samo za serijske naprave) Nadomesti zasebni ključ za identifikacijo naprave. Za uporabo je potrebna ponovna aktivacija. Ustvari nov javni ključ.",
|
||||
"repeater_cliHelpSetRadioRxGain": "(Samo za SX126x) Vklopi povečano občutljivost RX za izboljšano delovanje pri večjih navorih.",
|
||||
"repeater_cliHelpSetOwnerInfo": "Določi niz z informacijami o kontaktni osebi, ki je v oglasih. Za uporabo novih vrstic uporabite '|'.",
|
||||
"repeater_cliHelpSetPathHashMode": "Nastavlja način \"hash poti\". 0 = za stare sisteme, 1 = za standard, 2 = za stroge. Vpliva na to, kako so poti uskladene.",
|
||||
"repeater_cliHelpSetLoopDetect": "Nastavlja občutljivost detekcije ponavljajočih se povezav: izklopljeno, minimalno, umeren, ali strogo.",
|
||||
"repeater_cliHelpSetFreq": "(Samo za serijske naprave) Hitro nastavi samo frekvenco. Potrebna je ponovna aktivacija. Za popolno nastavitev radio parametrov je priporočljivo uporabiti možnost \"nastavitev radia\".",
|
||||
"repeater_cliHelpSetBridgeChannel": "(Samo za most ESPNow) Nastavlja kanal WiFi-ja (1-14), ki ga uporablja most.",
|
||||
"repeater_cliHelpGetName": "Prikaže ime konfigurirane notranje.",
|
||||
"repeater_cliHelpGetRole": "Prikaže vlogo programskega oprema (repeater, strežnik za sobo itd.).",
|
||||
"repeater_cliHelpGetPublicKey": "Prikazuje javni ključ naprave.",
|
||||
"repeater_cliHelpGetPrvKey": "(Samo za serijske naprave) Prikazuje zasebni ključ naprave. Sprejemajte ga kot skrivno informacijo.",
|
||||
"repeater_cliHelpGetRepeat": "Pokaže, ali je omogočeno posredovanje paketov (delovanje kot repetitor).",
|
||||
"repeater_cliHelpGetTx": "Prikazuje trenutno moč TX v dBm.",
|
||||
"repeater_cliHelpGetFreq": "Prikaže nastavljeno frekvenco v MHz.",
|
||||
"repeater_cliHelpGetRadio": "Prikaže vse parametre radija: frekvenco, širino pasu, faktor razširjanja, raven kodiranja.",
|
||||
"repeater_cliHelpGetRadioRxGain": "(Samo za SX126x) Prikazuje stanje povečanega dobiča na RX.",
|
||||
"repeater_cliHelpGetAf": "Prikazuje trenutni faktor, ki določa časovno obdobje.",
|
||||
"repeater_cliHelpGetDutyCycle": "Prikazuje trenutno dovoljeno stopnjo delovanja kot odstotek.",
|
||||
"repeater_cliHelpGetIntThresh": "Prikazuje prag medsebojnega vpliva kanala v dB.",
|
||||
"repeater_cliHelpGetAgcResetInterval": "Prikazuje interval ponovne kalibracije AGC v sekundah.",
|
||||
"repeater_cliHelpGetMultiAcks": "Pokaže, ali je vklopljen način dvojnega potrdila (1) ali je izklopljen (0).",
|
||||
"repeater_cliHelpGetAllowReadOnly": "Pokaže, ali je omogočen le brani dostop za goste.",
|
||||
"repeater_cliHelpGetAdvertInterval": "Prikazuje časovno obdobje lokalne reklame v minutah.",
|
||||
"repeater_cliHelpGetFloodAdvertInterval": "Prikaže časovno obdobje, ko se prikazuje oglas o poplavah, v urah.",
|
||||
"repeater_cliHelpGetGuestPassword": "Prikaže nastavljeno geslo za gostitelja.",
|
||||
"repeater_cliHelpGetLat": "Prikaže določeno zemljepisno širino.",
|
||||
"repeater_cliHelpGetLon": "Prikaže določeno merilo dolžine.",
|
||||
"repeater_cliHelpGetRxDelay": "Prikazuje osnovno vrednost RX odlašanja.",
|
||||
"repeater_cliHelpGetTxDelay": "Prikazuje faktor zamude v načinu delovanja pri plavlju.",
|
||||
"repeater_cliHelpGetDirectTxDelay": "Prikazuje faktor odlašanja signala v režimu neposredne komunikacije.",
|
||||
"repeater_cliHelpGetFloodMax": "Prikazuje največjo število, kolikokrat lahko voda doseže najvišjo višino.",
|
||||
"repeater_cliHelpGetOwnerInfo": "Prikazuje niz z informacijami o lastniku.",
|
||||
"repeater_cliHelpGetPathHashMode": "Prikaže način delovanja z hashjem poti (0/1/2).",
|
||||
"repeater_cliHelpGetLoopDetect": "Prikazuje občutljivost na zaznavanje ciklov.",
|
||||
"repeater_cliHelpGetAcl": "(Samo za serije) Navaja vnos za nadzor dostopa na ponovljalniku.",
|
||||
"repeater_cliHelpGetBridgeEnabled": "Pokaže, ali je most omogočen.",
|
||||
"repeater_cliHelpGetBridgeDelay": "Prikazuje zamik mosta v milisekundah.",
|
||||
"repeater_cliHelpGetBridgeSource": "Pokaže, ali most prenaša pakete RX ali TX.",
|
||||
"repeater_cliHelpGetBridgeBaud": "(Samo za most RS232) Prikazuje hitrost prenosa podatkov na mostu.",
|
||||
"repeater_cliHelpGetBridgeChannel": "(Samo za most ESPNow) Prikazuje kanal WiFi mosta.",
|
||||
"repeater_cliHelpGetBridgeSecret": "(Samo za most ESPNow) Prikazuje tajno, ki jo deli most.",
|
||||
"repeater_cliHelpGetBootloaderVer": "(Samo za NRF52) Prikazuje različico programskega orodja.",
|
||||
"repeater_cliHelpGetAdcMultiplier": "Prikazuje pomnoževalnik ADC (skaliranje napetosti baterije).",
|
||||
"repeater_cliHelpGetPwrMgtSupport": "Navaja, ali ima uprava področje za upravljanje z energijo.",
|
||||
"repeater_cliHelpGetPwrMgtSource": "Prikaže trenutni vir napajanja: zunanji ali baterija.",
|
||||
"repeater_cliHelpGetPwrMgtBootReason": "Prikazuje najnovejšo razlog za ponovno nastavitve in izklop.",
|
||||
"repeater_cliHelpGetPwrMgtBootMv": "Prikazuje napetost baterije v mV ob zagonu.",
|
||||
"repeater_cliHelpSensorGet": "Prebere določeno vrednost senzorja preko tipke.",
|
||||
"repeater_cliHelpSensorSet": "Ustvari prilagojeno nastavitev za senzor.",
|
||||
"repeater_cliHelpSensorList": "Navaja vse nastavitve za uporabniške senzorje, razvrščene po želeni začetni indeksu.",
|
||||
"repeater_cliHelpRegionDefault": "Prikaže trenutno privzeto območje.",
|
||||
"repeater_cliHelpRegionDefaultSet": "Določi privzeto območje. Za izbris uporabite \"<null>\".",
|
||||
"repeater_cliHelpRegionListAllowed": "Navaja regije, ki dovoljujejo promet v času poplav.",
|
||||
"repeater_cliHelpRegionListDenied": "Navaja regije, ki preprečujejo promet zaradi poplav.",
|
||||
"repeater_cliHelpStatsPackets": "(Samo za serijske povezave) Prikazuje statistiko na nivoju paketov.",
|
||||
"repeater_cliHelpStatsRadio": "(Samo za serije) Prikazuje statistične podatke o radiju.",
|
||||
"repeater_cliHelpStatsCore": "(Samo za serijske naprave) Prikazuje osnovne statistične podatke.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Več potrdil",
|
||||
"map_sharedAt": "Deljeno",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "Dotaknite blokirano točko, da jo označite na zemljeplati.",
|
||||
"losSelectedObstructionTitle": "Izbrano ovire",
|
||||
"losBlockedSpotsTitle": "Zasedena parkirišča",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||
"settings_companionDebugLog": "Log zapis za odpravljanje napak",
|
||||
"chat_markAsUnread": "Označiti kot neneobdelano",
|
||||
"chat_newMessages": "Nove novice",
|
||||
"settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.",
|
||||
"repeater_chanUtil": "Uporaba kanala",
|
||||
"@routing_lastWorked": {
|
||||
"placeholders": {
|
||||
"when": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@routing_deliveryCounts": {
|
||||
"placeholders": {
|
||||
"successes": {
|
||||
"type": "int"
|
||||
},
|
||||
"failures": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_hopCounter": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_invalidTokens": {
|
||||
"placeholders": {
|
||||
"tokens": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_communityShortId": {
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_undo": "Preobrn",
|
||||
"messageStatus_delivered": "Dostavljeno",
|
||||
"messageStatus_sent": "Pošljeno",
|
||||
"messageStatus_pending": "Pošiljanje",
|
||||
"messageStatus_failed": "Uspešno ni bilo mogo, da se sporočilo pošlje",
|
||||
"messageStatus_repeated": "Slišal sem večkrat",
|
||||
"contacts_moreOptions": "Več možnosti",
|
||||
"contacts_searchOpen": "Iskanje kontaktov",
|
||||
"contacts_searchClose": "Izklopi iskanje",
|
||||
"routing_title": "Navigacija",
|
||||
"routing_modeAuto": "Avto",
|
||||
"routing_modeFlood": "Poplavo",
|
||||
"routing_modeManual": "Navodilo",
|
||||
"routing_modeAutoHint": "Samodejno izbere najbolj poznano pot, in sicer, ko ni na voljo nobena.",
|
||||
"routing_modeFloodHint": "Prenosi preko vseh repetitorjev. Najzanesljivejši način, vendar zahteva več časa.",
|
||||
"routing_modeManualHint": "Vedno sledi natančni poti, ki jo ste določili.",
|
||||
"routing_currentRoute": "Trenutna pot",
|
||||
"routing_directNoHops": "Neposredno – brez prehodov",
|
||||
"routing_noPathYet": "Žep trenutno ni mogoče najti. Naslednje sporočilo bo posredovano, dokler ne bo ugotovljeno, kje je pot.",
|
||||
"routing_floodBroadcast": "Prenos preko vseh repetitiv",
|
||||
"routing_editPath": "Uredi pot",
|
||||
"routing_forgetPath": "Pozabi na pot",
|
||||
"routing_knownPaths": "Poznati poti",
|
||||
"routing_knownPathsHint": "Kliknite na pot, da jo izberete.",
|
||||
"routing_inUse": "V uporabi",
|
||||
"routing_qualityStrong": "Močan prvi korak",
|
||||
"routing_qualityGood": "Prva uspešna faza",
|
||||
"routing_qualityFair": "Prva, uspešna faza",
|
||||
"routing_qualityWorked": "Izpolnil",
|
||||
"routing_qualityFlood": "Slišano preko poplave",
|
||||
"routing_qualityUntested": "Ne preizkušen",
|
||||
"routing_lastWorked": "delal/a {when}",
|
||||
"routing_neverWorked": "nikoli ni bilo potrjeno",
|
||||
"routing_floodDelivery": "Dostava zaradi poplave",
|
||||
"pathEditor_title": "Izgradnja poti",
|
||||
"pathEditor_hopCounter": "{count} od 64 različnih sort hropa",
|
||||
"pathEditor_noHops": "Še niso dodani hmelji. Za dodajanje hmelja v vrstnem redu kliknite na povezavo spodaj, ali pa shranite brez dodanega hmelja, da ga lahko posredujete neposredno.",
|
||||
"pathEditor_addHops": "Dodajte suho travo v skladu s postopkom.",
|
||||
"pathEditor_searchRepeaters": "Iskanje ponovitev",
|
||||
"pathEditor_advancedHex": "Napredno: surovi šestnajstni pot",
|
||||
"pathEditor_hexLabel": "Predfiks za heksadecimalno šifro",
|
||||
"pathEditor_hexHelper": "Dva šestbitna znaka na vsak skok, ločena z vejico",
|
||||
"pathEditor_invalidTokens": "Neveljaven: {tokens}",
|
||||
"pathEditor_tooManyHops": "Največ 64 hopov",
|
||||
"pathEditor_usePath": "Uporabite to poto",
|
||||
"pathEditor_removeHop": "Odstranite hmelj",
|
||||
"pathEditor_unknownHop": "Neznani ponovitelj",
|
||||
"map_zoomIn": "Povečaj",
|
||||
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
|
||||
"map_zoomOut": "Povečajte pogled",
|
||||
"map_centerMap": "Krajšarska karta",
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth zahteva brskalnik Chromium.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS potrdilo",
|
||||
"pathTrace_legendInferred": "Izpeljana lokacija",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_bluetoothWebUnsupported": "Funkcija Bluetooth v brskalniku ni na voljo. Povežite se preko USB-ja namesto tega.",
|
||||
"map_searchHint": "Iščite ime ali ID vozlišča",
|
||||
"map_online": "V omrežju",
|
||||
"map_activity": "Dejavnost",
|
||||
"map_recent": "Nedavni",
|
||||
"map_stale": "Zastarelo",
|
||||
"map_visible": "Vidno",
|
||||
"map_hidden": "Skrit",
|
||||
"map_centerOnNode": "Centriraj na vozlišče",
|
||||
"map_details": "Podrobnosti",
|
||||
"map_noGps": "Brez GPS",
|
||||
"map_noResults": "Ni ujemajočih se vozlišč",
|
||||
"pathMap_viewSingle": "Posamično",
|
||||
"pathMap_viewCombined": "Skupno",
|
||||
"pathMap_play": "Predvajaj",
|
||||
"pathMap_pause": "Premor",
|
||||
"pathMap_replay": "Ponovitev",
|
||||
"pathMap_stepBack": "Prejšnji skok",
|
||||
"pathMap_stepForward": "Naslednji skok",
|
||||
"pathMap_animationOn": "Prikaži animacijo paketa",
|
||||
"pathMap_animationOff": "Skrij animacijo paketa",
|
||||
"pathMap_hopOf": "Skok {current} od {total}",
|
||||
"pathMap_observedPaths": "Opazovane poti: {count}",
|
||||
"pathMap_primary": "Primarna",
|
||||
"pathMap_alternate": "Alternativa {index}",
|
||||
"pathMap_legendShared": "Deljen segment",
|
||||
"pathMap_legendEstimated": "Ocenjen segment",
|
||||
"pathMap_sharedNodeCount": "Uporablja {count} poti",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 skok nima lokacije — prikazana pot je delna} =2{2 skoka nimata lokacije — prikazana pot je delna} few{{count} skoki nimajo lokacije — prikazana pot je delna} other{{count} skokov nima lokacije — prikazana pot je delna}}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 skok} =2{2 skoka} few{{count} skoki} other{{count} skokov}}",
|
||||
"pathMap_showAllPaths": "Pokaži vse",
|
||||
"pathMap_hidePath": "Skrij pot",
|
||||
"pathMap_showPath": "Pokaži pot",
|
||||
"pathMap_collapsePanel": "Strni ploščo",
|
||||
"pathMap_expandPanel": "Razširi ploščo",
|
||||
"pathMap_noLocation": "Brez lokacije",
|
||||
"pathMap_followPacket": "Zakleni pogled na paket",
|
||||
"pathMap_unfollowPacket": "Odkleni pogled od paketa",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
}
|
||||
|
||||
+640
-41
@@ -33,6 +33,8 @@
|
||||
"common_remove": "Ta bort",
|
||||
"common_enable": "Aktivera",
|
||||
"common_disable": "Inaktivera",
|
||||
"common_autoRefresh": "Automatisk uppdatering",
|
||||
"common_interval": "Intervall",
|
||||
"common_reboot": "Start om",
|
||||
"common_loading": "Laddar...",
|
||||
"common_notAvailable": "—",
|
||||
@@ -52,7 +54,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_title": "MeshCore Open",
|
||||
"scanner_title": "MeshCore – Öppen version",
|
||||
"scanner_scanning": "Söker efter enheter...",
|
||||
"scanner_connecting": "Anslutning...",
|
||||
"scanner_disconnecting": "Anslutning bryts...",
|
||||
@@ -104,6 +106,8 @@
|
||||
"settings_privacyModeEnabled": "Privatläget är aktiverat",
|
||||
"settings_privacyModeDisabled": "Privatläge är avstängt",
|
||||
"settings_actions": "Åtgärder",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Skicka Annons",
|
||||
"settings_sendAdvertisementSubtitle": "Sändning finns nu",
|
||||
"settings_advertisementSent": "Annons skickad",
|
||||
@@ -121,7 +125,7 @@
|
||||
"settings_appDebugLog": "Appfelsökning",
|
||||
"settings_appDebugLogSubtitle": "Applikations felsökningsmeddelanden",
|
||||
"settings_about": "Om",
|
||||
"settings_aboutVersion": "MeshCore Open v{version}",
|
||||
"settings_aboutVersion": "MeshCore Open version {version}",
|
||||
"@settings_aboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
@@ -146,7 +150,7 @@
|
||||
"settings_spreadingFactor": "Spreadingfaktor",
|
||||
"settings_codingRate": "Kodningsgrad",
|
||||
"settings_txPower": "TX-effekt (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerHelper": "0 – 22",
|
||||
"settings_txPowerInvalid": "Ogiltig TX-effekt (0-22 dBm)",
|
||||
"settings_error": "Fel: {message}",
|
||||
"@settings_error": {
|
||||
@@ -164,19 +168,19 @@
|
||||
"appSettings_themeDark": "Mörk",
|
||||
"appSettings_language": "Språk",
|
||||
"appSettings_languageSystem": "Systemstandard",
|
||||
"appSettings_languageEn": "English",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageDe": "Deutsch",
|
||||
"appSettings_languagePl": "Polski",
|
||||
"appSettings_languageSl": "Slovenščina",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageIt": "Italiano",
|
||||
"appSettings_languageZh": "中文",
|
||||
"appSettings_languageEn": "Engelska",
|
||||
"appSettings_languageFr": "Franska",
|
||||
"appSettings_languageEs": "Spanska",
|
||||
"appSettings_languageDe": "Tyskt",
|
||||
"appSettings_languagePl": "Polsk",
|
||||
"appSettings_languageSl": "Sloveniska",
|
||||
"appSettings_languagePt": "Portugisiska",
|
||||
"appSettings_languageIt": "Italienska",
|
||||
"appSettings_languageZh": "Kinesiska",
|
||||
"appSettings_languageSv": "Svenska",
|
||||
"appSettings_languageNl": "Nederlands",
|
||||
"appSettings_languageSk": "Slovenčina",
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_languageNl": "Nederländska",
|
||||
"appSettings_languageSk": "Sloveniska",
|
||||
"appSettings_languageBg": "Bulgariska",
|
||||
"appSettings_notifications": "Meddelanden",
|
||||
"appSettings_enableNotifications": "Aktivera Notifikationer",
|
||||
"appSettings_enableNotificationsSubtitle": "Ta emot notiser för meddelanden och reklam",
|
||||
@@ -209,9 +213,9 @@
|
||||
}
|
||||
},
|
||||
"appSettings_batteryChemistryConnectFirst": "Anslut till en enhet för att välja",
|
||||
"appSettings_batteryNmc": "18650 NMC (3.0-4.2V)",
|
||||
"appSettings_batteryNmc": "18650 NMC (3,0-4,2V)",
|
||||
"appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65V)",
|
||||
"appSettings_batteryLipo": "LiPo (3.0-4.2V)",
|
||||
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
|
||||
"appSettings_mapDisplay": "Kartvisning",
|
||||
"appSettings_showRepeaters": "Visa återuppslag",
|
||||
"appSettings_showRepeatersSubtitle": "Visa återspelsnoder på kartan",
|
||||
@@ -337,11 +341,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_hashtagChannel": "Hashtagkanal",
|
||||
"channels_public": "Offentligt",
|
||||
"channels_private": "Privat",
|
||||
"channels_publicChannel": "Allmänt kanal",
|
||||
"channels_privateChannel": "Privat kanal",
|
||||
"channels_editChannel": "Redigera kanal",
|
||||
"channels_muteChannel": "Tysta kanal",
|
||||
"channels_unmuteChannel": "Slå på ljud för kanal",
|
||||
@@ -367,7 +368,7 @@
|
||||
"channels_channelName": "Kanalnamn",
|
||||
"channels_usePublicChannel": "Använd Publikkanal",
|
||||
"channels_standardPublicPsk": "Standard allmän PSK",
|
||||
"channels_pskHex": "PSK (Hex)",
|
||||
"channels_pskHex": "PSK (heks)",
|
||||
"channels_generateRandomPsk": "Generera slumpmässig PSK",
|
||||
"channels_enterChannelName": "Ange en kanalnamn",
|
||||
"channels_pskMustBe32Hex": "PSK måste vara 32 hexadecimala tecken",
|
||||
@@ -388,6 +389,22 @@
|
||||
}
|
||||
},
|
||||
"channels_smazCompression": "SMAZ-komprimering",
|
||||
"channels_cyr2latCompression": "Cyr2Lat-komprimering",
|
||||
"channels_cyr2latCompressionDscr": "Ersätter vissa kyrilliska tecken med latinska tecken när du skickar.",
|
||||
"channels_cyr2latSettingsHeading": "Inställningar för Cyr2Lat",
|
||||
"channels_cyr2latSettingsSubheading": "Ersättningslista",
|
||||
"channels_cyr2latSettingsDscr": "Redigera JSON-konfigurationen för teckenersättning",
|
||||
"channels_cyr2latSettingsDialogHint": "JSON-ersättningskarta",
|
||||
"channels_cyr2latSettingsDialogWrongJSON": "Felaktig JSON: {error}",
|
||||
"settings_cyr2latProfileAdd": "Lägg till Cyr2Lat-profil",
|
||||
"settings_cyr2latProfileName": "Profilnamn",
|
||||
"settings_cyr2latProfileNameEmpty": "Profilnamnet får inte vara tomt",
|
||||
"settings_cyr2latProfileAdded": "Profilen har lagts till",
|
||||
"settings_cyr2latProfileUpdated": "Profilen har uppdaterats",
|
||||
"settings_cyr2latProfileEdit": "Redigera Cyr2Lat-profil",
|
||||
"settings_cyr2latProfileDelete": "Ta bort Cyr2Lat-profil",
|
||||
"settings_cyr2latProfileDeleted": "Profilen har tagits bort",
|
||||
"settings_cyr2latProfileDeleteDscr": "Är du säker på att du vill ta bort profilen \"{name}\"?",
|
||||
"channels_channelUpdated": "Kanalen \"{name}\" har uppdaterats",
|
||||
"@channels_channelUpdated": {
|
||||
"placeholders": {
|
||||
@@ -531,7 +548,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"debugFrame_textTypeCli": "CLI",
|
||||
"debugFrame_textTypeCli": "Kommandorad",
|
||||
"debugFrame_textTypePlain": "Enkel",
|
||||
"debugFrame_text": "- Text: \"{text}\"",
|
||||
"@debugFrame_text": {
|
||||
@@ -550,7 +567,7 @@
|
||||
"chat_pathHistoryFull": "Historisk sökväg är full. Ta bort poster för att lägga till nya.",
|
||||
"chat_hopSingular": "hoppa",
|
||||
"chat_hopPlural": "hoppar",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{hopp} other{hopp} }",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -881,9 +898,9 @@
|
||||
"repeater_managementTools": "Administrationsverktyg",
|
||||
"repeater_status": "Status",
|
||||
"repeater_statusSubtitle": "Visa återspolningsstatus, statistik och grannar",
|
||||
"repeater_telemetry": "Telemetry",
|
||||
"repeater_telemetry": "Telemetri",
|
||||
"repeater_telemetrySubtitle": "Visa telemetri för sensorer och systemstatistik",
|
||||
"repeater_cli": "CLI",
|
||||
"repeater_cli": "Kommandoradgränssnitt",
|
||||
"repeater_cliSubtitle": "Skicka kommandon till repetitorn",
|
||||
"repeater_settings": "Inställningar",
|
||||
"repeater_settingsSubtitle": "Konfigurera återspolarparametrar",
|
||||
@@ -992,7 +1009,7 @@
|
||||
"repeater_guestPasswordHelper": "Läs-skyddspassord",
|
||||
"repeater_radioSettings": "Radioinställningar",
|
||||
"repeater_frequencyMhz": "Frekvens (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
"repeater_frequencyHelper": "300–2500 MHz",
|
||||
"repeater_txPower": "TX Effekt",
|
||||
"repeater_txPowerHelper": "1-30 dBm",
|
||||
"repeater_bandwidth": "Bandbredd",
|
||||
@@ -1059,6 +1076,81 @@
|
||||
},
|
||||
"repeater_confirm": "Bekräfta",
|
||||
"repeater_settingsSaved": "Inställningarna sparades framgångsrikt.",
|
||||
"repeater_rxGain": "Ökad RX-vinst",
|
||||
"repeater_rxGainHelper": "Ökad känslighet, högre strömförbrukning (endast för SX1262/SX1268)",
|
||||
"repeater_refreshRxGain": "Återställ förbättrad RX-signalstyrka",
|
||||
"repeater_multiAcks": "Flera bekräftelser",
|
||||
"repeater_multiAcksSubtitle": "Bekräfta meddelanden via flera olika kanaler för bättre leverans.",
|
||||
"repeater_refreshMultiAcks": "Återställ flera ACK-meddelanden",
|
||||
"repeater_networkHealth": "Nätverkets hälsa",
|
||||
"repeater_loopDetect": "Identifiering av loopar",
|
||||
"repeater_loopDetectHelper": "Skapa \"flödespaket\" som ser ut som att de bildar en loop (en återkommande krets).",
|
||||
"repeater_loopDetectOff": "Av",
|
||||
"repeater_loopDetectMinimal": "Minimal",
|
||||
"repeater_loopDetectModerate": "Måttlig",
|
||||
"repeater_loopDetectStrict": "Strikt",
|
||||
"repeater_dutyCycle": "Arbetscykel",
|
||||
"repeater_dutyCycleHelper": "Maximal procentandel av sändningstid",
|
||||
"repeater_dutyCyclePercent": "{percent}%",
|
||||
"@repeater_dutyCyclePercent": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_ownerInfo": "Information om operatören",
|
||||
"repeater_ownerInfoHelper": "Offentliga metadata för denna sändare",
|
||||
"repeater_refreshOwnerInfo": "Uppdatera information om personal",
|
||||
"repeater_floodMax": "Maximala mängden humle",
|
||||
"repeater_floodMaxHelper": "Maximalt antal hopp en paket kan färdas (0-64)",
|
||||
"repeater_advancedSettings": "Avancerad",
|
||||
"repeater_advancedSettingsSubtitle": "Ställjusteringsknappar för erfarna användare",
|
||||
"repeater_pathHashMode": "Hash-läge för sökväg",
|
||||
"repeater_pathHashModeHelper": "Byte används för att koda denna repeaters ID i taggar för att upptäcka loopar/flödesvägar. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Versioner 1.13 och äldre har stöd för multi-byte-vägar – endast en gång när nätverket är aktiverat (från och med version 1.14).",
|
||||
"repeater_txDelay": "Försening i Flood TX",
|
||||
"repeater_txDelayHelper": "Återöverföringsintervall för trafik under perioder med hög belastning, som en multiplikator av paketets överföringstid (0-2, standard 0,5). Högre värde = färre kollisioner, men långsammare leverans.",
|
||||
"repeater_directTxDelay": "Direkt TX-fördröjning",
|
||||
"repeater_directTxDelayHelper": "Återöverföringsintervall för direkt (icke-översvämmande) trafik, som en multiplikator av paketets överföringstid (0-2, standard 0,3).",
|
||||
"repeater_intThresh": "Tröskelvärde för störning",
|
||||
"repeater_intThreshHelper": "Tröskelvärdet har ställts in så att den filtrerar bort störningar som överstiger detta värde. 0 stänger av – aktivera endast om du ser RX-fel i ett störningsfyllt frekvensområde.",
|
||||
"repeater_agcResetInterval": "Återställningsintervall för AGC",
|
||||
"repeater_agcResetIntervalHelper": "Hur ofta ska man återställa radioens automatiska förstärkning för att återhämta sig från ett tillstånd där förstärkningen är fast? Sekunder, inställda till en multipel av 4. 0 stänger av periodiska återställningar.",
|
||||
"repeater_actionsTitle": "Åtgärder",
|
||||
"repeater_sendAdvert": "Skicka annons om översvämning",
|
||||
"repeater_sendAdvertSubtitle": "Sänd en reklamfilm om översvämningar via nätverket.",
|
||||
"repeater_sendAdvertZeroHop": "Skicka en annons som inte kräver någon mellanstopp",
|
||||
"repeater_sendAdvertZeroHopSubtitle": "Sänd en reklamkampanj med en enda sändare (utan mellanliggande sändare).",
|
||||
"repeater_clockSync": "Synkronisera klockan nu",
|
||||
"repeater_clockSyncSubtitle": "Ställ din telefons tid till repeatern.",
|
||||
"repeater_actionSucceeded": "{action} lyckades",
|
||||
"@repeater_actionSucceeded": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_actionFailed": "{action} misslyckades: {error}",
|
||||
"@repeater_actionFailed": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
},
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsSavedRebootNeeded": "Inställningar sparade – starta om repetern för att tillämpa dem",
|
||||
"repeater_settingsPartialFailure": "Vissa inställningar misslyckades: {failures}",
|
||||
"@repeater_settingsPartialFailure": {
|
||||
"placeholders": {
|
||||
"failures": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_errorSavingSettings": "Fel vid sparande av inställningar: {error}",
|
||||
"@repeater_errorSavingSettings": {
|
||||
"placeholders": {
|
||||
@@ -1070,11 +1162,9 @@
|
||||
"repeater_refreshBasicSettings": "Återställ Grundläggande Inställningar",
|
||||
"repeater_refreshRadioSettings": "Återställ Radiosinställningar",
|
||||
"repeater_refreshTxPower": "Återställ TX-effekt",
|
||||
"repeater_refreshLocationSettings": "Uppdatera Lokationsinställningar",
|
||||
"repeater_refreshPacketForwarding": "Återställ Paketväxling",
|
||||
"repeater_refreshGuestAccess": "Återställ Gäståtkomst",
|
||||
"repeater_refreshPrivacyMode": "Återställ Sekretessläge",
|
||||
"repeater_refreshAdvertisementSettings": "Återställ Annonsinställningar",
|
||||
"repeater_refreshed": "{label} har uppdaterats",
|
||||
"@repeater_refreshed": {
|
||||
"placeholders": {
|
||||
@@ -1192,6 +1282,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_digitalInputLabel": "Digital ingång",
|
||||
"telemetry_digitalOutputLabel": "Digital utgång",
|
||||
"telemetry_analogInputLabel": "Analog ingång",
|
||||
"telemetry_analogOutputLabel": "Analog utgång",
|
||||
"telemetry_genericLabel": "Allmän sensor",
|
||||
"telemetry_luminosityLabel": "Ljusstyrka",
|
||||
"telemetry_presenceLabel": "Närvaro",
|
||||
"telemetry_humidityLabel": "Luftfuktighet",
|
||||
"telemetry_accelerometerLabel": "Accelerometer",
|
||||
"telemetry_pressureLabel": "Tryck",
|
||||
"telemetry_altitudeLabel": "Höjd",
|
||||
"telemetry_frequencyLabel": "Frekvens",
|
||||
"telemetry_percentageLabel": "Procent",
|
||||
"telemetry_concentrationLabel": "Koncentration",
|
||||
"telemetry_powerLabel": "Effekt",
|
||||
"telemetry_distanceLabel": "Avstånd",
|
||||
"telemetry_energyLabel": "Energi",
|
||||
"telemetry_directionLabel": "Riktning",
|
||||
"telemetry_timeLabel": "Tid",
|
||||
"telemetry_gyrometerLabel": "Gyrometer",
|
||||
"telemetry_colourLabel": "Färg",
|
||||
"telemetry_gpsLabel": "GPS",
|
||||
"telemetry_switchLabel": "Brytare",
|
||||
"telemetry_polylineLabel": "Polylinje",
|
||||
"telemetry_altitudeValue": "{meters} m",
|
||||
"telemetry_frequencyValue": "{hertz} Hz",
|
||||
"telemetry_pressureValue": "{hpa} hPa",
|
||||
"telemetry_luminosityValue": "{lux} lx",
|
||||
"telemetry_powerValue": "{watts} W",
|
||||
"telemetry_distanceValue": "{meters} m",
|
||||
"telemetry_energyValue": "{kilowattHours} kWh",
|
||||
"telemetry_directionValue": "{degrees}°",
|
||||
"telemetry_concentrationValue": "{ppm} ppm",
|
||||
"telemetry_percentageValue": "{percent}%",
|
||||
"telemetry_analogValue": "{value}",
|
||||
"telemetry_autoFetchQuantity": "Antal förfrågningar",
|
||||
"telemetry_error": "Det gick inte att hämta data",
|
||||
"telemetry_noData": "Inga telemetridata tillgängliga.",
|
||||
"telemetry_channelTitle": "Kanal {channel}",
|
||||
"@telemetry_channelTitle": {
|
||||
@@ -1477,7 +1604,7 @@
|
||||
"community_qrTitle": "Dela Gemenskap",
|
||||
"community_qrInstructions": "Skanna denna QR-kod för att gå med i \"{name}\"",
|
||||
"community_hashtagPrivacyHint": "Community-hashtagkanaler kan endast nås av medlemmar i communityn",
|
||||
"community_hashtagChannel": "Community Hashtag",
|
||||
"community_hashtagChannel": "Hashtag för gemenskapen",
|
||||
"community_invalidQrCode": "Ogiltig community QR-kod",
|
||||
"community_alreadyMember": "Är redan medlem",
|
||||
"community_alreadyMemberMessage": "Du är redan medlem av \"{name}\".",
|
||||
@@ -1504,7 +1631,7 @@
|
||||
"community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)",
|
||||
"community_communityHashtagDesc": "Endast för medlemmar",
|
||||
"community_forCommunity": "För {name}",
|
||||
"community_communityHashtag": "Community Hashtag",
|
||||
"community_communityHashtag": "Gemenskaps-hashtag",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -1551,7 +1678,7 @@
|
||||
"pathTrace_failed": "Sökvägsföljning misslyckades.",
|
||||
"pathTrace_notAvailable": "Path trace ej tillgänglig.",
|
||||
"pathTrace_refreshTooltip": "Uppdatera Path Trace",
|
||||
"contacts_pathTrace": "Path Trace",
|
||||
"contacts_pathTrace": "Spårning",
|
||||
"contacts_ping": "Ping",
|
||||
"contacts_repeaterPathTrace": "Vägspårning till repeater",
|
||||
"contacts_repeaterPing": "Ping-repeater",
|
||||
@@ -1879,7 +2006,7 @@
|
||||
"tcpHostLabel": "IP-adress",
|
||||
"tcpScreenTitle": "Anslut via TCP",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpPortLabel": "Port",
|
||||
"tcpPortLabel": "Hamn",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Ange slutpunkt och anslut",
|
||||
"tcpStatus_connectingTo": "Anslutning till {endpoint}...",
|
||||
@@ -1922,13 +2049,6 @@
|
||||
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
|
||||
"contact_teleLoc": "Telemetridata plats",
|
||||
"contact_teleLocSubtitle": "Tillåt delning av platsdata",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar",
|
||||
"appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.",
|
||||
@@ -1941,5 +2061,484 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
}
|
||||
"settings_multiAck": "Flera bekräftelser",
|
||||
"map_showOverlaps": "Repeater-nyckelöverlappningar",
|
||||
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.",
|
||||
"chat_sendCooldown": "Vänligen vänta en stund innan du skickar igen.",
|
||||
"appSettings_jumpToOldestUnread": "Gå direkt till det äldsta, obesvarade meddelandet",
|
||||
"appSettings_languageHu": "Ungerskt",
|
||||
"appSettings_languageJa": "Japanska",
|
||||
"appSettings_languageKo": "Koreanska",
|
||||
"radioStats_tooltip": "Radio- och mesh-statistik",
|
||||
"radioStats_screenTitle": "Radiostation",
|
||||
"radioStats_notConnected": "Anslut till en enhet för att visa radiostatistik.",
|
||||
"radioStats_firmwareTooOld": "Radio statistik kräver kompatibel firmware version 8 eller senare.",
|
||||
"radioStats_waiting": "Väntar på data…",
|
||||
"radioStats_noiseFloor": "Bakgrundsnivå: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Senaste RSSI-värde: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Senaste SNR: {snr} dB",
|
||||
"radioStats_txAir": "TX-tid (total): {seconds} sekunder",
|
||||
"radioStats_rxAir": "RX-tid (total): {seconds} s",
|
||||
"radioStats_chartCaption": "Ljudnivå (dBm) baserat på de senaste mätningarna.",
|
||||
"radioStats_stripNoise": "Bakgrundsnivå: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Hämtar radiostatistik…",
|
||||
"radioStats_settingsTile": "Radiostation",
|
||||
"radioStats_settingsSubtitle": "Bakgrundsnivå, RSSI, SNR och tillgänglig tid",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Översätt inkommande meddelanden och möjliggör översättning före avsändning.",
|
||||
"translation_enableTitle": "Aktivera översättning",
|
||||
"translation_title": "Översättning",
|
||||
"translation_composerTitle": "Översätt innan du skickar",
|
||||
"translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.",
|
||||
"translation_autoIncomingTitle": "Översätt meddelanden automatiskt",
|
||||
"translation_autoIncomingSubtitle": "Översätter meddelanden automatiskt för aviseringar och för chattar eller kanaler.",
|
||||
"translation_translateMessage": "Översätt meddelande",
|
||||
"translation_targetLanguage": "Målmedvetet språk",
|
||||
"translation_useAppLanguage": "Använd appens språk",
|
||||
"translation_downloadedModelLabel": "Nedladdad modell",
|
||||
"translation_presetModelLabel": "Fördefinierat Hugging Face-modell",
|
||||
"translation_manualUrlLabel": "Manualens URL",
|
||||
"translation_downloadModel": "Ladda ner modellen",
|
||||
"translation_downloading": "Nedladdning...",
|
||||
"translation_working": "Arbeta...",
|
||||
"translation_stop": "Stopp",
|
||||
"translation_mergingChunks": "Slå samman de nedladdade delarna till en slutlig fil...",
|
||||
"translation_downloadedModels": "Nedladdade modeller",
|
||||
"translation_deleteModel": "Ta bort modell",
|
||||
"translation_modelDownloaded": "Översättningsmodellen har laddats ner.",
|
||||
"translation_downloadStopped": "Nedladdningen avbruten.",
|
||||
"translation_downloadFailed": "Nedladdning misslyckades: {error}",
|
||||
"translation_enterUrlFirst": "Ange först en URL för en specifik modell.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Skicka meddelanden på det ursprungliga, stavade språket.",
|
||||
"translation_translateBeforeSending": "Översätt innan du skickar",
|
||||
"translation_composerEnabledHint": "Meddelandena kommer att översättas innan de skickas.",
|
||||
"translation_messageTranslation": "Meddelandets översättning",
|
||||
"translation_translateTo": "Översätt till {language}",
|
||||
"translation_translationOptions": "Översättningsalternativ",
|
||||
"translation_systemLanguage": "Språk för systemet",
|
||||
"scanner_linuxPairingShowPin": "Visa PIN",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
|
||||
"scanner_linuxPairingHidePin": "Dölj PIN",
|
||||
"repeater_cliQuickDiscovery": "Upptäck grannar",
|
||||
"repeater_cliQuickClockSync": "Synkronisera klocka",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.",
|
||||
"repeater_clockSyncAfterLogin": "Synkronisera klockan efter inloggning",
|
||||
"repeater_guest": "Information om repetorer",
|
||||
"chat_sendMessage": "Skicka meddelande",
|
||||
"repeater_guestTools": "Gästverktyg",
|
||||
"room_guest": "Information om servern",
|
||||
"repeater_getCategory": "Hämta värden",
|
||||
"repeater_powerMgmt": "Effektstyrning",
|
||||
"repeater_sensors": "Sensorer",
|
||||
"repeater_cliHelpPowerOff": "Stänger av enheten. (ingen respons förväntas)",
|
||||
"repeater_cliHelpClkReboot": "Återställer klockan till en känd tidpunkt och startar om enheten.",
|
||||
"repeater_cliHelpAdvertZeroHop": "Skickar en annons som når endast direkt grannar (endast närmaste grannar).",
|
||||
"repeater_cliHelpStartOta": "Startar en firmware-uppdatering via luft, på kompatibla enheter.",
|
||||
"repeater_cliHelpTime": "Ställer enheten till den angivna Unix-epokens tid. Klockan kan inte gå bakåt.",
|
||||
"repeater_cliHelpBoard": "Visar tillverkaren av moderkortet / hårdvaru-identifieraren.",
|
||||
"repeater_cliHelpDiscoverNeighbors": "Skickar en förfrågan om att upptäcka närliggande noder. (Endast för repetrar)",
|
||||
"repeater_cliHelpPowersaving": "Visar om energisparläget är aktiverat eller avstängt.",
|
||||
"repeater_cliHelpPowersavingOnOff": "Aktiverar eller inaktiverar energisparläget (om det stöds).",
|
||||
"repeater_cliHelpErase": "(Endast för seriell kommunikation) Formaterar enhetens filsystem. Raderar alla inställningar och kontakter.",
|
||||
"repeater_cliHelpSetDutyCycle": "Anger den maximala tillåtna överföringsfrekvensen som en procent (1-100). Justerar automatiskt tidsfaktorn.",
|
||||
"repeater_cliHelpSetPrvKey": "(Endast för seriell användning) Ersätter enhetsens privata nyckel. Återstart krävs för att tillämpa. Genererar en ny publik nyckel.",
|
||||
"repeater_cliHelpSetRadioRxGain": "(Endast SX126x) Aktiverar förstärkt mottagargain för förbättrad känslighet vid högre strömförbrukning.",
|
||||
"repeater_cliHelpSetOwnerInfo": "Anger kontaktinformationen som ska inkluderas i annonserna. Använd '|' för att separera olika fält.",
|
||||
"repeater_cliHelpSetPathHashMode": "Definierar läget för hash-baserad ruttning. 0 = äldre läge, 1 = standard, 2 = strikt. Påverkar hur ruttvägar matchas.",
|
||||
"repeater_cliHelpSetLoopDetect": "Ställer in känsligheten för att detektera loopar i routningen: av, minimal, måttlig eller strikt.",
|
||||
"repeater_cliHelpSetFreq": "(Endast för seriell kommunikation) Ställer snabbt bara frekvensen. Kräver omstart. Föredrar \"ställ radio\" för att få full kontroll över radioinställningarna.",
|
||||
"repeater_cliHelpSetBridgeChannel": "(Endast ESPNow-brygga) Anger WiFi-kanalen (1-14) som används av bryggan.",
|
||||
"repeater_cliHelpGetName": "Visar det konfigurerade nodnamnet.",
|
||||
"repeater_cliHelpGetRole": "Visar firmware-funktionen (Repeater, Room Server, etc.).",
|
||||
"repeater_cliHelpGetPublicKey": "Visar enhetens publika nyckel.",
|
||||
"repeater_cliHelpGetPrvKey": "(Endast för seriell användning) Visar enheters privata nyckel. Behandla detta som en hemlighet.",
|
||||
"repeater_cliHelpGetRepeat": "Visar om funktionen för att vidarebefordra paket (som en repeater) är aktiverad eller inaktiverad.",
|
||||
"repeater_cliHelpGetTx": "Visar aktuell TX-effekt i dBm.",
|
||||
"repeater_cliHelpGetFreq": "Visar den konfigurerade radiovågen i MHz.",
|
||||
"repeater_cliHelpGetRadio": "Visar alla radioparametrar: frekvens, bandbredd, spridningsfaktor, kodningshastighet.",
|
||||
"repeater_cliHelpGetRadioRxGain": "(Endast för SX126x) Visar RX:s förstärkningstillstånd.",
|
||||
"repeater_cliHelpGetAf": "Visar aktuell tidssats.",
|
||||
"repeater_cliHelpGetDutyCycle": "Visar den aktuella tillåtna arbetscykeln i procent.",
|
||||
"repeater_cliHelpGetIntThresh": "Visar gränsen för kanalinterferens i dB.",
|
||||
"repeater_cliHelpGetAgcResetInterval": "Visar återställningsintervallet för AGC i sekunder.",
|
||||
"repeater_cliHelpGetMultiAcks": "Visar om dubbelbekräftelseläget är aktiverat (1) eller avstängt (0).",
|
||||
"repeater_cliHelpGetAllowReadOnly": "Visar om gäst har tillåtelse att endast läsa.",
|
||||
"repeater_cliHelpGetAdvertInterval": "Visar den lokala reklampausens längd i minuter.",
|
||||
"repeater_cliHelpGetFloodAdvertInterval": "Visar tidsintervallet för reklamsegmentet under översvämningen, i timmar.",
|
||||
"repeater_cliHelpGetGuestPassword": "Visar det angivna gästlösen.",
|
||||
"repeater_cliHelpGetLat": "Visar den angivna latituden.",
|
||||
"repeater_cliHelpGetLon": "Visar den angivna longituden.",
|
||||
"repeater_cliHelpGetRxDelay": "Visar grundvärdet för rxdelay.",
|
||||
"repeater_cliHelpGetTxDelay": "Visar faktor för fördröjning i flödesläge.",
|
||||
"repeater_cliHelpGetDirectTxDelay": "Visar faktorn för fördröjning i direktläge.",
|
||||
"repeater_cliHelpGetFloodMax": "Visar det maximala antalet gånger en översvämning har inträffat.",
|
||||
"repeater_cliHelpGetOwnerInfo": "Visar strängen med kontaktinformation för ägaren.",
|
||||
"repeater_cliHelpGetPathHashMode": "Visar hash-läge (0/1/2).",
|
||||
"repeater_cliHelpGetLoopDetect": "Visar känsligheten för att detektera loopar.",
|
||||
"repeater_cliHelpGetAcl": "(Endast för serier) Visar åtkomstkontrollinställningarna för en repeater.",
|
||||
"repeater_cliHelpGetBridgeEnabled": "Visar om bron är aktiverad.",
|
||||
"repeater_cliHelpGetBridgeDelay": "Visar fördröjningen i bron i millisekunder.",
|
||||
"repeater_cliHelpGetBridgeSource": "Visar om bron skickar RX- eller TX-paket.",
|
||||
"repeater_cliHelpGetBridgeBaud": "(Enbart RS232-brygga) Visar bryggans baud-hastighet.",
|
||||
"repeater_cliHelpGetBridgeChannel": "(Endast ESPNow-brygga) Visar WiFi-kanal för bryggan.",
|
||||
"repeater_cliHelpGetBridgeSecret": "(Endast ESPNow-brygga) Visar bryggans delade hemlighet.",
|
||||
"repeater_cliHelpGetBootloaderVer": "(Endast för NRF52) Visar versionen av bootloadern.",
|
||||
"repeater_cliHelpGetAdcMultiplier": "Visar ADC-multiplikatorn (skalning av batterispänning).",
|
||||
"repeater_cliHelpGetPwrMgtSupport": "Anger om styrelsen har stöd för energihantering.",
|
||||
"repeater_cliHelpGetPwrMgtSource": "Visar aktuell strömkälla: extern eller batteri.",
|
||||
"repeater_cliHelpGetPwrMgtBootReason": "Visar de senaste orsakerna till återställning och avstängning.",
|
||||
"repeater_cliHelpGetPwrMgtBootMv": "Visar batterispänningen vid start i millivolt (mV).",
|
||||
"repeater_cliHelpSensorGet": "Läser en anpassad sensorinställning via tangentbordet.",
|
||||
"repeater_cliHelpSensorSet": "Skapar en anpassad inställning för en sensor.",
|
||||
"repeater_cliHelpSensorList": "Visar alla anpassade sensorinställningar, sorterade från ett valfritt startindex.",
|
||||
"repeater_cliHelpRegionDefault": "Visar det aktuella standardområde.",
|
||||
"repeater_cliHelpRegionDefaultSet": "Definierar standardområde. Använd \"<null>\" för att återställa till standard.",
|
||||
"repeater_cliHelpRegionListAllowed": "Lista områden där det är tillåtet med trafik under översvämningsförhållanden.",
|
||||
"repeater_cliHelpRegionListDenied": "Listar områden där trafik på grund av översvämningar är förbjuden.",
|
||||
"repeater_cliHelpStatsPackets": "(Endast för seriell kommunikation) Visar statistik på paketnivå.",
|
||||
"repeater_cliHelpStatsRadio": "(Enbart för serier) Visar radiostatistik.",
|
||||
"repeater_cliHelpStatsCore": "(Enbart för seriell kommunikation) Visar grundläggande firmware-statistik.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "Delad",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losBlockedSpotsTitle": "Reserverade platser",
|
||||
"losSelectedObstructionTitle": "Vald hinder",
|
||||
"losBlockedSpotsHint": "Klicka på en markerad plats för att framhäva den på kartan.",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||
"chat_markAsUnread": "Markera som oläst",
|
||||
"settings_companionDebugLog": "Följande felsökningslogg",
|
||||
"chat_newMessages": "Nya meddelanden",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata",
|
||||
"repeater_chanUtil": "Användning av kanal",
|
||||
"@routing_lastWorked": {
|
||||
"placeholders": {
|
||||
"when": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@routing_deliveryCounts": {
|
||||
"placeholders": {
|
||||
"successes": {
|
||||
"type": "int"
|
||||
},
|
||||
"failures": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_hopCounter": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_invalidTokens": {
|
||||
"placeholders": {
|
||||
"tokens": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_communityShortId": {
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"messageStatus_sent": "Sen",
|
||||
"messageStatus_delivered": "Levererad",
|
||||
"common_undo": "Ångra",
|
||||
"messageStatus_pending": "Skicka",
|
||||
"messageStatus_failed": "Misslyckades med att skicka",
|
||||
"messageStatus_repeated": "Hördes upprepade gånger",
|
||||
"contacts_moreOptions": "Fler alternativ",
|
||||
"contacts_searchOpen": "Sök efter kontakter",
|
||||
"contacts_searchClose": "Avancerad sökning",
|
||||
"routing_title": "Ruttplanering",
|
||||
"routing_modeAuto": "Bil",
|
||||
"routing_modeFlood": "Översvämning",
|
||||
"routing_modeManual": "Instruktioner",
|
||||
"routing_modeAutoHint": "Väljer automatiskt den bästa kända vägen, och använder en \"flooding\"-strategi om ingen väg är känd.",
|
||||
"routing_modeFloodHint": "Sändningar via alla repetrar. Det mest pålitliga alternativet, men kräver mer sändtid.",
|
||||
"routing_modeManualHint": "Skickar alltid den exakta väg du har angivit.",
|
||||
"routing_currentRoute": "Nuvarande rutt",
|
||||
"routing_directNoHops": "Direkt – utan mellanliggande routrar",
|
||||
"routing_noPathYet": "Ingen väg hittad ännu. Nästa meddelande skickas tills en rutt har upptäckts.",
|
||||
"routing_floodBroadcast": "Sändas via alla repetrar",
|
||||
"routing_editPath": "Redigera sökväg",
|
||||
"routing_forgetPath": "Glöm vägen",
|
||||
"routing_knownPaths": "Kända vägar",
|
||||
"routing_knownPathsHint": "Välj en väg för att byta till den.",
|
||||
"routing_inUse": "I användning",
|
||||
"routing_qualityStrong": "En stark start",
|
||||
"routing_qualityGood": "Bra första steg",
|
||||
"routing_qualityFair": "Bra första hopp",
|
||||
"routing_qualityWorked": "Har levererat",
|
||||
"routing_qualityFlood": "Fått information via nyhetsflöde",
|
||||
"routing_qualityUntested": "Ej testat",
|
||||
"routing_lastWorked": "arbetade {when}",
|
||||
"routing_neverWorked": "aldrig bekräftat",
|
||||
"routing_floodDelivery": "Leverans vid översvämningsområde",
|
||||
"pathEditor_title": "Skapa väg",
|
||||
"pathEditor_hopCounter": "{count} av 64 humlor",
|
||||
"pathEditor_noHops": "Inga humle än. Använd knapparna nedan för att lägga till dem i rätt ordning, eller spara utan humle för att skicka direkt.",
|
||||
"pathEditor_addHops": "Tillsätt humlen i rätt ordning.",
|
||||
"pathEditor_searchRepeaters": "Sök efter återupptagna samtal",
|
||||
"pathEditor_advancedHex": "Avancerat: rå hex-sökväg",
|
||||
"pathEditor_hexLabel": "Hex-prefikser",
|
||||
"pathEditor_hexHelper": "Två hex-tecken per steg, separerade med kommatecken.",
|
||||
"pathEditor_invalidTokens": "Ogiltigt: {tokens}",
|
||||
"pathEditor_tooManyHops": "Maximalt 64 humlörter",
|
||||
"pathEditor_usePath": "Använd denna väg",
|
||||
"pathEditor_removeHop": "Ta bort humlen",
|
||||
"pathEditor_unknownHop": "Okänd förstärkare",
|
||||
"map_zoomIn": "Zooma in",
|
||||
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
|
||||
"map_zoomOut": "Zooma ut",
|
||||
"map_centerMap": "Kartöversikt",
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth kräver en Chromium-baserad webbläsare.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS-verifierat",
|
||||
"pathTrace_legendInferred": "Antagen position",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_online": "Online",
|
||||
"scanner_bluetoothWebUnsupported": "Bluetooth är inte tillgängligt i webbläsaren. Anslut istället via USB.",
|
||||
"map_activity": "Aktivitet",
|
||||
"map_searchHint": "Sök efter nodens namn eller ID",
|
||||
"map_recent": "Nyligen",
|
||||
"map_stale": "Inaktuell",
|
||||
"map_visible": "Synlig",
|
||||
"map_hidden": "Dold",
|
||||
"map_centerOnNode": "Centrera på nod",
|
||||
"map_details": "Detaljer",
|
||||
"map_noGps": "Ingen GPS",
|
||||
"map_noResults": "Inga matchande noder",
|
||||
"pathMap_viewSingle": "Enkel",
|
||||
"pathMap_viewCombined": "Kombinerat",
|
||||
"pathMap_play": "Spela",
|
||||
"pathMap_pause": "Pausa",
|
||||
"pathMap_replay": "Återspela",
|
||||
"pathMap_stepBack": "Föregående hopp",
|
||||
"pathMap_stepForward": "Nästa hopp",
|
||||
"pathMap_animationOn": "Visa paketanimering",
|
||||
"pathMap_animationOff": "Dölj paketanimering",
|
||||
"pathMap_hopOf": "Hopp {current} av {total}",
|
||||
"pathMap_observedPaths": "Observerade vägar: {count}",
|
||||
"pathMap_primary": "Primär",
|
||||
"pathMap_alternate": "Alternativ {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 hopp} other{{count} hopp}}",
|
||||
"pathMap_legendShared": "Delat segment",
|
||||
"pathMap_legendEstimated": "Uppskattat segment",
|
||||
"pathMap_sharedNodeCount": "Används av {count} vägar",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 hopp saknar position — den visade vägen är ofullständig} other{{count} hopp saknar position — den visade vägen är ofullständig}}",
|
||||
"pathMap_showAllPaths": "Visa allt",
|
||||
"pathMap_hidePath": "Dölj väg",
|
||||
"pathMap_showPath": "Visa väg",
|
||||
"pathMap_collapsePanel": "Fäll ihop panel",
|
||||
"pathMap_expandPanel": "Expandera panel",
|
||||
"pathMap_noLocation": "Ingen position",
|
||||
"pathMap_unfollowPacket": "Lås upp vy från paket",
|
||||
"pathMap_followPacket": "Lås vy till paket",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
}
|
||||
|
||||
+768
-189
File diff suppressed because it is too large
Load Diff
+620
-21
@@ -34,6 +34,8 @@
|
||||
"common_remove": "移除",
|
||||
"common_enable": "启用",
|
||||
"common_disable": "禁用",
|
||||
"common_autoRefresh": "自动刷新",
|
||||
"common_interval": "间隔",
|
||||
"common_reboot": "重启",
|
||||
"common_loading": "正在加载...",
|
||||
"common_notAvailable": "—",
|
||||
@@ -79,7 +81,7 @@
|
||||
"scanner_stop": "停止",
|
||||
"scanner_scan": "扫描",
|
||||
"device_quickSwitch": "快速切换",
|
||||
"device_meshcore": "MeshCore",
|
||||
"device_meshcore": "网格核心",
|
||||
"settings_title": "设置",
|
||||
"settings_deviceInfo": "设备信息",
|
||||
"settings_appSettings": "应用设置",
|
||||
@@ -109,6 +111,8 @@
|
||||
"settings_privacyModeEnabled": "隐私模式已启用",
|
||||
"settings_privacyModeDisabled": "隐私模式已关闭",
|
||||
"settings_actions": "操作",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "发送广播",
|
||||
"settings_sendAdvertisementSubtitle": "立即发送广播",
|
||||
"settings_advertisementSent": "已发送广播",
|
||||
@@ -352,11 +356,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_hashtagChannel": "标签频道",
|
||||
"channels_public": "公共",
|
||||
"channels_private": "私有",
|
||||
"channels_publicChannel": "公共频道",
|
||||
"channels_privateChannel": "私有频道",
|
||||
"channels_editChannel": "编辑频道",
|
||||
"channels_muteChannel": "静音频道",
|
||||
"channels_unmuteChannel": "取消静音频道",
|
||||
@@ -403,6 +404,22 @@
|
||||
}
|
||||
},
|
||||
"channels_smazCompression": "SMAZ 压缩",
|
||||
"channels_cyr2latCompression": "Cyr2Lat 压缩",
|
||||
"channels_cyr2latCompressionDscr": "发送时将一些西里尔字符替换为拉丁字符。",
|
||||
"channels_cyr2latSettingsHeading": "Cyr2Lat 設定",
|
||||
"channels_cyr2latSettingsSubheading": "替換清單",
|
||||
"channels_cyr2latSettingsDscr": "編輯 JSON 字元替換設定檔",
|
||||
"channels_cyr2latSettingsDialogHint": "JSON 替換映射表",
|
||||
"channels_cyr2latSettingsDialogWrongJSON": "JSON 格式錯誤:{error}",
|
||||
"settings_cyr2latProfileAdd": "新增 Cyr2Lat 設定檔",
|
||||
"settings_cyr2latProfileName": "設定檔名稱",
|
||||
"settings_cyr2latProfileNameEmpty": "設定檔名稱不能為空",
|
||||
"settings_cyr2latProfileAdded": "設定檔已成功新增",
|
||||
"settings_cyr2latProfileUpdated": "設定檔已成功更新",
|
||||
"settings_cyr2latProfileEdit": "編輯 Cyr2Lat 設定檔",
|
||||
"settings_cyr2latProfileDelete": "刪除 Cyr2Lat 設定檔",
|
||||
"settings_cyr2latProfileDeleted": "設定檔已成功刪除",
|
||||
"settings_cyr2latProfileDeleteDscr": "您確定要刪除設定檔 \"{name}\" 嗎?",
|
||||
"channels_channelUpdated": "频道 \"{name}\" 已更新",
|
||||
"@channels_channelUpdated": {
|
||||
"placeholders": {
|
||||
@@ -414,7 +431,7 @@
|
||||
"channels_publicChannelAdded": "已添加公共频道",
|
||||
"channels_sortBy": "排序方式",
|
||||
"channels_sortManual": "手动",
|
||||
"channels_sortAZ": "A-Z",
|
||||
"channels_sortAZ": "A到Z",
|
||||
"channels_sortLatestMessages": "最新消息",
|
||||
"channels_sortUnread": "未读",
|
||||
"channels_createPrivateChannel": "创建私有频道",
|
||||
@@ -1022,7 +1039,7 @@
|
||||
"repeater_guestPasswordHelper": "只读访问密码",
|
||||
"repeater_radioSettings": "无线电设置",
|
||||
"repeater_frequencyMhz": "频率 (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
"repeater_frequencyHelper": "300-2500 兆赫",
|
||||
"repeater_txPower": "TX 功率",
|
||||
"repeater_txPowerHelper": "1-30 dBm",
|
||||
"repeater_bandwidth": "带宽",
|
||||
@@ -1089,6 +1106,81 @@
|
||||
},
|
||||
"repeater_confirm": "确认",
|
||||
"repeater_settingsSaved": "设置保存成功",
|
||||
"repeater_rxGain": "增强的 RX 增益",
|
||||
"repeater_rxGainHelper": "更高的灵敏度,更大的电流消耗(仅适用于 SX1262/SX1268)",
|
||||
"repeater_refreshRxGain": "重新启动增强型 RX 功能",
|
||||
"repeater_multiAcks": "多重确认",
|
||||
"repeater_multiAcksSubtitle": "通过多个路径确认消息,以提高传递效率。",
|
||||
"repeater_refreshMultiAcks": "刷新多个确认",
|
||||
"repeater_networkHealth": "网络健康",
|
||||
"repeater_loopDetect": "循环检测",
|
||||
"repeater_loopDetectHelper": "创建看起来像路由环的“洪水包”",
|
||||
"repeater_loopDetectOff": "离开",
|
||||
"repeater_loopDetectMinimal": "最少",
|
||||
"repeater_loopDetectModerate": "适度的",
|
||||
"repeater_loopDetectStrict": "严格",
|
||||
"repeater_dutyCycle": "工作周期",
|
||||
"repeater_dutyCycleHelper": "最大可使用的空闲时间百分比",
|
||||
"repeater_dutyCyclePercent": "{percent}%",
|
||||
"@repeater_dutyCyclePercent": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_ownerInfo": "运营商信息",
|
||||
"repeater_ownerInfoHelper": "此复播器的公共元数据",
|
||||
"repeater_refreshOwnerInfo": "刷新操作员信息",
|
||||
"repeater_floodMax": "最大跳跃次数",
|
||||
"repeater_floodMaxHelper": "一个洪水包中,最大可以传输的跳数 (0-64)",
|
||||
"repeater_advancedSettings": "高级",
|
||||
"repeater_advancedSettingsSubtitle": "高级操作员使用的调节旋钮",
|
||||
"repeater_pathHashMode": "路径哈希模式",
|
||||
"repeater_pathHashModeHelper": "用于编码此复用器的 ID 的字节数,在“洪流路径/环检测”标签中使用。 0=1 字节(256 个 ID,最多 64 个跳跃),1=2 字节(65K 个 ID,最多 32 个跳跃),2=3 字节(16M 个 ID,最多 21 个跳跃)。 v1.13 及更早版本的固件会使用多字节路径——只有在您的网络升级到 v1.14 或更高版本后才会生效。",
|
||||
"repeater_txDelay": "洪水(德克萨斯州)延误",
|
||||
"repeater_txDelayHelper": "对于洪水流量,重新传输间隔应设置为包的传输时间(0-2,默认值为0.5)的倍数。 较高的值意味着更少的冲突,但传输速度会变慢。",
|
||||
"repeater_directTxDelay": "直接的 TX 延迟",
|
||||
"repeater_directTxDelayHelper": "对于直接(非广播)流量,重新传输间隔应设置为包的传输时间(0-2,默认值为0.3)的倍数。",
|
||||
"repeater_intThresh": "干扰阈值",
|
||||
"repeater_intThreshHelper": "将阈值传递给射频信号的噪声水平校准,使其能够拒绝高于该水平的干扰。 0 表示禁用——只有在您在嘈杂频段中看到 RX 错误时才启用。",
|
||||
"repeater_agcResetInterval": "AGC 恢复间隔",
|
||||
"repeater_agcResetIntervalHelper": "为了从失控的增益状态中恢复,应该多久重置收音机的自动增益控制?设置为“秒”,每次重置间隔为4秒。将此选项设置为“0”将禁用周期性重置。",
|
||||
"repeater_actionsTitle": "行动",
|
||||
"repeater_sendAdvert": "发布防洪广告",
|
||||
"repeater_sendAdvertSubtitle": "通过网络播放防洪广告",
|
||||
"repeater_sendAdvertZeroHop": "发送零跳广告",
|
||||
"repeater_sendAdvertZeroHopSubtitle": "进行单跳广告广播(不使用中继)",
|
||||
"repeater_clockSync": "现在同步时钟",
|
||||
"repeater_clockSyncSubtitle": "将手机的时间设置为与中继器同步",
|
||||
"repeater_actionSucceeded": "{action} 成功",
|
||||
"@repeater_actionSucceeded": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_actionFailed": "{action} 失败:{error}",
|
||||
"@repeater_actionFailed": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
},
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsSavedRebootNeeded": "设置已保存 — 重启发射器以应用",
|
||||
"repeater_settingsPartialFailure": "部分设置失败:{failures}",
|
||||
"@repeater_settingsPartialFailure": {
|
||||
"placeholders": {
|
||||
"failures": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_errorSavingSettings": "保存设置时出错:{error}",
|
||||
"@repeater_errorSavingSettings": {
|
||||
"placeholders": {
|
||||
@@ -1100,11 +1192,9 @@
|
||||
"repeater_refreshBasicSettings": "刷新基本设置",
|
||||
"repeater_refreshRadioSettings": "刷新无线电设置",
|
||||
"repeater_refreshTxPower": "刷新 TX 功率",
|
||||
"repeater_refreshLocationSettings": "刷新位置设置",
|
||||
"repeater_refreshPacketForwarding": "刷新包转发",
|
||||
"repeater_refreshGuestAccess": "刷新访客权限",
|
||||
"repeater_refreshPrivacyMode": "刷新隐私模式",
|
||||
"repeater_refreshAdvertisementSettings": "刷新广播设置",
|
||||
"repeater_refreshed": "{label} 已刷新",
|
||||
"@repeater_refreshed": {
|
||||
"placeholders": {
|
||||
@@ -1222,6 +1312,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_digitalInputLabel": "数字输入",
|
||||
"telemetry_digitalOutputLabel": "数字输出",
|
||||
"telemetry_analogInputLabel": "模拟输入",
|
||||
"telemetry_analogOutputLabel": "模拟输出",
|
||||
"telemetry_genericLabel": "通用传感器",
|
||||
"telemetry_luminosityLabel": "照度",
|
||||
"telemetry_presenceLabel": "存在检测",
|
||||
"telemetry_humidityLabel": "湿度",
|
||||
"telemetry_accelerometerLabel": "加速度计",
|
||||
"telemetry_pressureLabel": "气压",
|
||||
"telemetry_altitudeLabel": "高度",
|
||||
"telemetry_frequencyLabel": "频率",
|
||||
"telemetry_percentageLabel": "百分比",
|
||||
"telemetry_concentrationLabel": "浓度",
|
||||
"telemetry_powerLabel": "功率",
|
||||
"telemetry_distanceLabel": "距离",
|
||||
"telemetry_energyLabel": "能量",
|
||||
"telemetry_directionLabel": "方向",
|
||||
"telemetry_timeLabel": "时间",
|
||||
"telemetry_gyrometerLabel": "陀螺仪",
|
||||
"telemetry_colourLabel": "颜色",
|
||||
"telemetry_gpsLabel": "GPS",
|
||||
"telemetry_switchLabel": "开关",
|
||||
"telemetry_polylineLabel": "折线",
|
||||
"telemetry_altitudeValue": "{meters} m",
|
||||
"telemetry_frequencyValue": "{hertz} Hz",
|
||||
"telemetry_pressureValue": "{hpa} hPa",
|
||||
"telemetry_luminosityValue": "{lux} lx",
|
||||
"telemetry_powerValue": "{watts} W",
|
||||
"telemetry_distanceValue": "{meters} m",
|
||||
"telemetry_energyValue": "{kilowattHours} kWh",
|
||||
"telemetry_directionValue": "{degrees}°",
|
||||
"telemetry_concentrationValue": "{ppm} ppm",
|
||||
"telemetry_percentageValue": "{percent}%",
|
||||
"telemetry_analogValue": "{value}",
|
||||
"telemetry_autoFetchQuantity": "请求次数",
|
||||
"telemetry_error": "无法获取数据",
|
||||
"telemetry_noData": "暂无遥测数据",
|
||||
"telemetry_channelTitle": "频道 {channel}",
|
||||
"@telemetry_channelTitle": {
|
||||
@@ -1247,7 +1374,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_voltageValue": "{volts}V",
|
||||
"telemetry_voltageValue": "{volts}伏",
|
||||
"@telemetry_voltageValue": {
|
||||
"placeholders": {
|
||||
"volts": {
|
||||
@@ -1541,7 +1668,7 @@
|
||||
"listFilter_sortBy": "排序方式",
|
||||
"listFilter_latestMessages": "最新消息",
|
||||
"listFilter_heardRecently": "最近听到",
|
||||
"listFilter_az": "A-Z",
|
||||
"listFilter_az": "A到Z",
|
||||
"listFilter_filters": "筛选",
|
||||
"listFilter_all": "全部",
|
||||
"listFilter_users": "用户",
|
||||
@@ -1554,7 +1681,7 @@
|
||||
"pathTrace_notAvailable": "无法获取路径信息。",
|
||||
"pathTrace_refreshTooltip": "刷新路径追踪",
|
||||
"contacts_pathTrace": "路径追踪",
|
||||
"contacts_ping": "Ping",
|
||||
"contacts_ping": "乒",
|
||||
"contacts_repeaterPathTrace": "Trace 转发节点",
|
||||
"contacts_repeaterPing": "Ping 转发节点",
|
||||
"contacts_roomPathTrace": "Trace 房间服务器",
|
||||
@@ -1927,13 +2054,6 @@
|
||||
"contact_settings": "联系人设置",
|
||||
"contact_teleLocSubtitle": "允许共享位置数据",
|
||||
"contact_telemetry": "遥测数据",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "最大路径重量",
|
||||
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
|
||||
"appSettings_initialRouteWeight": "初始路线权重",
|
||||
@@ -1945,6 +2065,485 @@
|
||||
"appSettings_maxMessageRetries": "最大消息重试次数",
|
||||
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "多重ACK:{value}",
|
||||
"settings_telemetryModeUpdated": "遥测模式已更新"
|
||||
}
|
||||
"settings_multiAck": "多重ACK",
|
||||
"settings_telemetryModeUpdated": "遥测模式已更新",
|
||||
"map_showOverlaps": "重复键重叠",
|
||||
"map_runTraceWithReturnPath": "沿着相同的路径返回",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "请稍等片刻后再尝试发送。",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "在打开包含未读消息的聊天时,请滚动到第一个未读消息,而不是最新的消息。",
|
||||
"appSettings_jumpToOldestUnread": "跳转到最旧、未读的文章",
|
||||
"appSettings_languageHu": "匈牙利",
|
||||
"appSettings_languageJa": "日语",
|
||||
"appSettings_languageKo": "韩语",
|
||||
"radioStats_tooltip": "无线电和网状结构统计数据",
|
||||
"radioStats_screenTitle": "广播统计数据",
|
||||
"radioStats_notConnected": "连接到设备以查看收音机统计信息。",
|
||||
"radioStats_firmwareTooOld": "使用无线电统计功能需要配合使用 v8 或更高版本的固件。",
|
||||
"radioStats_waiting": "正在等待数据…",
|
||||
"radioStats_noiseFloor": "噪声水平:{noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "上次 RSSI 值:{rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "上次 SNR:{snr} dB",
|
||||
"radioStats_txAir": "TX 频道播出时间(总时长):{seconds} 秒",
|
||||
"radioStats_rxAir": "RX 使用时长(总时长):{seconds} 秒",
|
||||
"radioStats_chartCaption": "近期的噪声水平(dBm)。",
|
||||
"radioStats_stripNoise": "噪声水平:{noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "正在获取收音机数据…",
|
||||
"radioStats_settingsTile": "广播统计数据",
|
||||
"radioStats_settingsSubtitle": "噪声水平、RSSI、信噪比和空中时间",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_title": "翻译",
|
||||
"translation_enableSubtitle": "翻译收到的消息,并允许在发送前进行翻译。",
|
||||
"translation_composerTitle": "在发送之前进行翻译",
|
||||
"translation_enableTitle": "启用翻译功能",
|
||||
"translation_composerSubtitle": "控制作曲家翻译图标的默认状态。",
|
||||
"translation_autoIncomingTitle": "自动翻译消息",
|
||||
"translation_autoIncomingSubtitle": "自动为通知以及聊天或频道翻译消息。",
|
||||
"translation_translateMessage": "翻译消息",
|
||||
"translation_targetLanguage": "目标语言",
|
||||
"translation_useAppLanguage": "使用应用程序语言",
|
||||
"translation_downloadedModelLabel": "下载的模型",
|
||||
"translation_presetModelLabel": "预设的 Hugging Face 模型",
|
||||
"translation_downloadModel": "下载模型",
|
||||
"translation_manualUrlLabel": "手动模型网址",
|
||||
"translation_downloading": "正在下载...",
|
||||
"translation_working": "工作中...",
|
||||
"translation_stop": "停止",
|
||||
"translation_mergingChunks": "将下载的片段合并成最终文件...",
|
||||
"translation_downloadedModels": "下载的模型",
|
||||
"translation_deleteModel": "删除模型",
|
||||
"translation_modelDownloaded": "翻译模型已下载。",
|
||||
"translation_downloadStopped": "下载已停止。",
|
||||
"translation_downloadFailed": "下载失败:{error}",
|
||||
"translation_enterUrlFirst": "首先,请输入模型的 URL。",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingPinTitle": "蓝牙配对 PIN",
|
||||
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN 码(如果为空,则留空)。",
|
||||
"scanner_linuxPairingHidePin": "隐藏 PIN",
|
||||
"scanner_linuxPairingShowPin": "显示PIN码",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "使用原始的打字方式发送消息。",
|
||||
"translation_messageTranslation": "消息翻译",
|
||||
"translation_composerEnabledHint": "消息将在发送前进行翻译。",
|
||||
"translation_translateBeforeSending": "在发送前进行翻译",
|
||||
"translation_translateTo": "翻译成 {language}",
|
||||
"translation_translationOptions": "翻译选项",
|
||||
"translation_systemLanguage": "系统语言",
|
||||
"repeater_cliQuickDiscovery": "发现邻居",
|
||||
"repeater_cliQuickClockSync": "同步时钟",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "登录后,自动同步时钟",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "在成功登录后,自动发送“时钟同步”指令。",
|
||||
"repeater_guestTools": "访客工具",
|
||||
"repeater_guest": "重复器信息",
|
||||
"chat_sendMessage": "发送消息",
|
||||
"room_guest": "服务器信息",
|
||||
"repeater_getCategory": "获取值",
|
||||
"repeater_powerMgmt": "电源管理",
|
||||
"repeater_sensors": "传感器",
|
||||
"repeater_cliHelpPowerOff": "关闭设备。(不应有任何响应)",
|
||||
"repeater_cliHelpClkReboot": "将时钟重置为已知的时间点,并重启设备。",
|
||||
"repeater_cliHelpAdvertZeroHop": "发送无中继广告(仅限于邻居)。",
|
||||
"repeater_cliHelpStartOta": "在支持的板上启动通过空中进行固件更新。",
|
||||
"repeater_cliHelpTime": "将设备时钟设置为给定的 Unix 纪元秒。时钟不能倒退。",
|
||||
"repeater_cliHelpBoard": "显示制造商/硬件标识。",
|
||||
"repeater_cliHelpDiscoverNeighbors": "向附近的邻居发送节点发现请求。(仅限中继器)",
|
||||
"repeater_cliHelpPowersaving": "显示节能模式是否已开启或已关闭。",
|
||||
"repeater_cliHelpPowersavingOnOff": "启用或禁用节能模式(如果支持)。",
|
||||
"repeater_cliHelpErase": "(仅适用于序列模式)格式化设备的文件系统。清除所有设置和联系人。",
|
||||
"repeater_cliHelpSetDutyCycle": "设定允许的最大传输时段百分比(1-100)。内部调整空闲时间因子。",
|
||||
"repeater_cliHelpSetPrvKey": "(仅适用于序列号)替换设备身份私钥。需要重启才能应用。生成一个新的公钥。",
|
||||
"repeater_cliHelpSetRadioRxGain": "(仅适用于 SX126x 芯片) 启用增强型 RX 增益,以在较高电流下提高灵敏度。",
|
||||
"repeater_cliHelpSetOwnerInfo": "设置广告中包含的联系人信息字符串。使用 '|' 作为换行符。",
|
||||
"repeater_cliHelpSetPathHashMode": "设置路径哈希模式。 0 = 传统模式,1 = 标准模式,2 = 严格模式。 影响路由路径的匹配方式。",
|
||||
"repeater_cliHelpSetLoopDetect": "设置路由环检测的灵敏度:关闭、低、中、或高。",
|
||||
"repeater_cliHelpSetFreq": "(仅限串行模式)快速设置频率。需要重启。 建议使用“设置收音机参数”功能,以便设置完整的收音机参数。",
|
||||
"repeater_cliHelpSetBridgeChannel": "(仅适用于 ESPNow 桥)设置桥使用的 WiFi 频道(1-14)。",
|
||||
"repeater_cliHelpGetName": "显示配置的节点名称。",
|
||||
"repeater_cliHelpGetRole": "显示固件的功能(如:中继器、房间服务器等)。",
|
||||
"repeater_cliHelpGetPublicKey": "显示设备的公钥。",
|
||||
"repeater_cliHelpGetPrvKey": "(仅适用于序列号)显示设备的私钥。请将其视为机密信息。",
|
||||
"repeater_cliHelpGetRepeat": "显示数据包转发(作为中继器)是否已启用或已禁用。",
|
||||
"repeater_cliHelpGetTx": "显示当前的发射功率(以dBm为单位)。",
|
||||
"repeater_cliHelpGetFreq": "显示配置的射频频率(以兆赫兹为单位)。",
|
||||
"repeater_cliHelpGetRadio": "显示完整的无线电参数:频率、带宽、扩频因子、编码速率。",
|
||||
"repeater_cliHelpGetRadioRxGain": "(仅适用于 SX126x 模块)显示 RX 放大器的状态。",
|
||||
"repeater_cliHelpGetAf": "显示当前的空闲时间系数。",
|
||||
"repeater_cliHelpGetDutyCycle": "显示当前允许的占空比(以百分比表示)。",
|
||||
"repeater_cliHelpGetIntThresh": "显示信道干扰阈值(以dB为单位)。",
|
||||
"repeater_cliHelpGetAgcResetInterval": "显示 AGC 重置的间隔时间(以秒为单位)。",
|
||||
"repeater_cliHelpGetMultiAcks": "显示双重确认模式是否已启用(1)或已禁用(0)。",
|
||||
"repeater_cliHelpGetAllowReadOnly": "显示是否允许访客仅限查看权限。",
|
||||
"repeater_cliHelpGetAdvertInterval": "显示本地广告的时间间隔,单位为分钟。",
|
||||
"repeater_cliHelpGetFloodAdvertInterval": "显示洪水广告的播放时间间隔,以小时为单位。",
|
||||
"repeater_cliHelpGetGuestPassword": "显示已配置的访客密码。",
|
||||
"repeater_cliHelpGetLat": "显示已配置的纬度。",
|
||||
"repeater_cliHelpGetLon": "显示已配置的经度。",
|
||||
"repeater_cliHelpGetRxDelay": "显示 rxdelay 的基本值。",
|
||||
"repeater_cliHelpGetTxDelay": "显示洪水模式下的传输延迟系数。",
|
||||
"repeater_cliHelpGetDirectTxDelay": "显示直接模式下的时延系数。",
|
||||
"repeater_cliHelpGetFloodMax": "显示最大洪水传播次数。",
|
||||
"repeater_cliHelpGetOwnerInfo": "显示所有者的联系信息。",
|
||||
"repeater_cliHelpGetPathHashMode": "显示哈希模式(0/1/2)。",
|
||||
"repeater_cliHelpGetLoopDetect": "显示循环检测的灵敏度。",
|
||||
"repeater_cliHelpGetAcl": "(仅适用于序列号)列出复用器上的访问控制条目。",
|
||||
"repeater_cliHelpGetBridgeEnabled": "显示桥是否已启用。",
|
||||
"repeater_cliHelpGetBridgeDelay": "显示桥梁延迟的时间,单位为毫秒。",
|
||||
"repeater_cliHelpGetBridgeSource": "显示桥接设备是否接收或发送 RX 或 TX 类型的数据包。",
|
||||
"repeater_cliHelpGetBridgeBaud": "(仅限 RS232 桥)显示桥的波特率。",
|
||||
"repeater_cliHelpGetBridgeChannel": "(仅适用于 ESPNow 桥)显示桥的 WiFi 通道。",
|
||||
"repeater_cliHelpGetBridgeSecret": "(仅适用于 ESPNow 桥)显示桥的共享密钥。",
|
||||
"repeater_cliHelpGetBootloaderVer": "(仅适用于NRF52)显示引导程序版本。",
|
||||
"repeater_cliHelpGetAdcMultiplier": "显示 ADC 乘数(电池电压缩放)。",
|
||||
"repeater_cliHelpGetPwrMgtSupport": "报告董事会是否支持电源管理功能。",
|
||||
"repeater_cliHelpGetPwrMgtSource": "显示当前的电源:外部电源或电池。",
|
||||
"repeater_cliHelpGetPwrMgtBootReason": "显示最近的重置和关闭原因。",
|
||||
"repeater_cliHelpGetPwrMgtBootMv": "显示启动时的电池电压,单位为毫伏 (mV)。",
|
||||
"repeater_cliHelpSensorGet": "通过按键读取自定义传感器设置。",
|
||||
"repeater_cliHelpSensorSet": "编写自定义传感器设置。",
|
||||
"repeater_cliHelpSensorList": "列出所有自定义传感器设置,并按可选的起始索引进行分页显示。",
|
||||
"repeater_cliHelpRegionDefault": "显示当前默认的区域范围。",
|
||||
"repeater_cliHelpRegionDefaultSet": "设置默认的区域范围。使用 \"<null>\" 可以清除。",
|
||||
"repeater_cliHelpRegionListAllowed": "列出允许洪水交通的区域。",
|
||||
"repeater_cliHelpRegionListDenied": "列出禁止洪水交通的区域。",
|
||||
"repeater_cliHelpStatsPackets": "(仅显示序列信息)显示数据包级别的统计信息。",
|
||||
"repeater_cliHelpStatsRadio": "(仅显示序列信息)显示收音机相关统计数据。",
|
||||
"repeater_cliHelpStatsCore": "(仅显示序列号)显示核心固件统计信息。",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "已分享",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "被占用区域",
|
||||
"losBlockedSpotsHint": "点击地图上的某个被遮盖的区域,以突出显示该区域。",
|
||||
"losSelectedObstructionTitle": "选择性阻碍",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||
"chat_markAsUnread": "标记为未读",
|
||||
"settings_companionDebugLog": "调试日志",
|
||||
"chat_newMessages": "新的消息",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据",
|
||||
"repeater_chanUtil": "频道利用率",
|
||||
"@routing_lastWorked": {
|
||||
"placeholders": {
|
||||
"when": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@routing_deliveryCounts": {
|
||||
"placeholders": {
|
||||
"successes": {
|
||||
"type": "int"
|
||||
},
|
||||
"failures": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_hopCounter": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathEditor_invalidTokens": {
|
||||
"placeholders": {
|
||||
"tokens": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_communityShortId": {
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"messageStatus_sent": "发送",
|
||||
"common_undo": "撤销",
|
||||
"messageStatus_delivered": "已送达",
|
||||
"messageStatus_pending": "发送",
|
||||
"messageStatus_failed": "发送失败",
|
||||
"messageStatus_repeated": "多次听到",
|
||||
"contacts_moreOptions": "更多选择",
|
||||
"contacts_searchOpen": "搜索联系人",
|
||||
"contacts_searchClose": "高级搜索",
|
||||
"routing_title": "路由",
|
||||
"routing_modeAuto": "汽车",
|
||||
"routing_modeFlood": "洪水",
|
||||
"routing_modeManual": "手册",
|
||||
"routing_modeAutoHint": "自动选择已知最佳路径,当没有已知路径时,则进行“洪水”搜索。",
|
||||
"routing_modeFloodHint": "通过所有中继站进行广播。 这种方式最可靠,但占用更多的时间。",
|
||||
"routing_modeManualHint": "总是按照您设置的路径进行导航。",
|
||||
"routing_currentRoute": "当前路线",
|
||||
"routing_directNoHops": "直接连接— 无中继跳",
|
||||
"routing_noPathYet": "目前还没有找到路径。直到找到路径,才会收到后续消息。",
|
||||
"routing_floodBroadcast": "通过所有中继器进行广播",
|
||||
"routing_editPath": "编辑路径",
|
||||
"routing_forgetPath": "忘记原路",
|
||||
"routing_knownPaths": "已知的路径",
|
||||
"routing_knownPathsHint": "点击该路径以切换到它。",
|
||||
"routing_inUse": "使用中",
|
||||
"routing_qualityStrong": "强劲的初始阶段",
|
||||
"routing_qualityGood": "不错的开端",
|
||||
"routing_qualityFair": "第一次尝试,结果良好",
|
||||
"routing_qualityWorked": "已完成",
|
||||
"routing_qualityFlood": "通过新闻报道",
|
||||
"routing_qualityUntested": "未经测试",
|
||||
"routing_lastWorked": "工作于 {when}",
|
||||
"routing_neverWorked": "从未得到证实",
|
||||
"routing_floodDelivery": "洪水配送",
|
||||
"pathEditor_title": "构建路径",
|
||||
"pathEditor_noHops": "目前还没有添加任何啤酒花。点击下面的“添加”按钮,按顺序添加,或者直接保存,不添加任何啤酒花。",
|
||||
"pathEditor_addHops": "按照顺序添加啤酒花",
|
||||
"pathEditor_searchRepeaters": "重复搜索",
|
||||
"pathEditor_advancedHex": "高级:原始十六进制路径",
|
||||
"pathEditor_hexLabel": "十六进制前缀",
|
||||
"pathEditor_hexHelper": "每次跳跃,使用两个十六进制字符,用逗号分隔。",
|
||||
"pathEditor_invalidTokens": "无效:{tokens}",
|
||||
"pathEditor_tooManyHops": "最多 64 个跳跃",
|
||||
"pathEditor_usePath": "请使用此路径",
|
||||
"pathEditor_removeHop": "去除啤酒花",
|
||||
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
|
||||
"pathEditor_hopCounter": "{count} of 64 hops",
|
||||
"pathEditor_unknownHop": "未知的重复器",
|
||||
"map_zoomIn": "放大",
|
||||
"map_zoomOut": "放大",
|
||||
"map_centerMap": "中心地图",
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth 需要 Chromium 浏览器",
|
||||
"channels_communityShortId": "ID:{id}...",
|
||||
"pathTrace_legendGpsConfirmed": "通过GPS确认",
|
||||
"pathTrace_legendInferred": "推测的位置",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_online": "在线",
|
||||
"scanner_bluetoothWebUnsupported": "浏览器不支持蓝牙,请改用 USB 连接。",
|
||||
"map_activity": "活动",
|
||||
"map_searchHint": "搜索节点名称或ID",
|
||||
"map_recent": "最近",
|
||||
"map_visible": "可见",
|
||||
"map_stale": "过时",
|
||||
"map_hidden": "已隐藏",
|
||||
"map_centerOnNode": "以节点为中心",
|
||||
"map_details": "详细信息",
|
||||
"map_noGps": "无 GPS",
|
||||
"map_noResults": "未找到匹配的节点",
|
||||
"pathMap_viewSingle": "单条",
|
||||
"pathMap_viewCombined": "综合",
|
||||
"pathMap_play": "播放",
|
||||
"pathMap_pause": "暂停",
|
||||
"pathMap_replay": "重播",
|
||||
"pathMap_stepBack": "上一跳",
|
||||
"pathMap_stepForward": "下一跳",
|
||||
"pathMap_animationOn": "显示数据包动画",
|
||||
"pathMap_animationOff": "隐藏数据包动画",
|
||||
"pathMap_hopOf": "第 {current} 跳,共 {total} 跳",
|
||||
"pathMap_observedPaths": "观测到的路径:{count}",
|
||||
"pathMap_primary": "主路径",
|
||||
"pathMap_alternate": "备用 {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 跳} other{{count} 跳}}",
|
||||
"pathMap_legendShared": "共享路段",
|
||||
"pathMap_legendEstimated": "估算路段",
|
||||
"pathMap_sharedNodeCount": "已被 {count} 条路径使用",
|
||||
"pathMap_showAllPaths": "显示全部",
|
||||
"pathMap_hidePath": "隐藏路径",
|
||||
"pathMap_showPath": "显示路径",
|
||||
"pathMap_collapsePanel": "收起面板",
|
||||
"pathMap_expandPanel": "展开面板",
|
||||
"pathMap_noLocation": "无位置",
|
||||
"pathMap_followPacket": "锁定视图跟随数据包",
|
||||
"pathMap_unfollowPacket": "解锁视图跟随",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 跳无位置信息 — 显示的路径不完整} other{{count} 跳无位置信息 — 显示的路径不完整}}"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/contact.dart';
|
||||
import 'app_localizations.dart';
|
||||
|
||||
/// UI-level localization helpers for [Contact].
|
||||
///
|
||||
/// Kept out of the model layer so `Contact` does not depend on
|
||||
/// `AppLocalizations`. Use these from widgets/screens; for logs and
|
||||
/// non-UI export use `Contact.typeLabelRaw`.
|
||||
extension ContactLocalization on Contact {
|
||||
String typeLabel(AppLocalizations l10n) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return l10n.contact_typeChat;
|
||||
case advTypeRepeater:
|
||||
return l10n.contact_typeRepeater;
|
||||
case advTypeRoom:
|
||||
return l10n.contact_typeRoom;
|
||||
case advTypeSensor:
|
||||
return l10n.contact_typeSensor;
|
||||
default:
|
||||
return l10n.contact_typeUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
String pathLabel(AppLocalizations l10n) {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return l10n.chat_floodForced;
|
||||
if (pathOverride == 0) return l10n.chat_directForced;
|
||||
return l10n.chat_hopsForced(pathOverride!);
|
||||
}
|
||||
if (pathLength < 0) return l10n.channelPath_floodPath;
|
||||
if (pathLength == 0) return l10n.chat_direct;
|
||||
return l10n.chat_hopsCount(pathLength);
|
||||
}
|
||||
}
|
||||
+46
-18
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -19,14 +20,25 @@ import 'services/app_debug_log_service.dart';
|
||||
import 'services/background_service.dart';
|
||||
import 'services/map_tile_cache_service.dart';
|
||||
import 'services/chat_text_scale_service.dart';
|
||||
import 'services/translation_service.dart';
|
||||
import 'services/ui_view_state_service.dart';
|
||||
import 'services/timeout_prediction_service.dart';
|
||||
import 'storage/prefs_manager.dart';
|
||||
import 'theme/mesh_theme.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// On desktop, debugPrint is not suppressed in release builds and every
|
||||
// call is a synchronous stdout write. The connector logs heavily on hot
|
||||
// paths (frame handling, queue/channel sync), which shows up as syscall
|
||||
// overhead on low-end Linux machines (issue #202). The in-app debug log
|
||||
// screens are unaffected — they store entries themselves.
|
||||
if (kReleaseMode) {
|
||||
debugPrint = (String? message, {int? wrapWidth}) {};
|
||||
}
|
||||
|
||||
// Initialize SharedPreferences cache
|
||||
await PrefsManager.initialize();
|
||||
|
||||
@@ -41,6 +53,7 @@ void main() async {
|
||||
final backgroundService = BackgroundService();
|
||||
final mapTileCacheService = MapTileCacheService();
|
||||
final chatTextScaleService = ChatTextScaleService();
|
||||
final translationService = TranslationService(appSettingsService);
|
||||
final uiViewStateService = UiViewStateService();
|
||||
final timeoutPredictionService = TimeoutPredictionService(storage);
|
||||
|
||||
@@ -57,9 +70,13 @@ void main() async {
|
||||
final notificationService = NotificationService();
|
||||
await notificationService.initialize();
|
||||
await backgroundService.initialize();
|
||||
backgroundService.setLanguageOverrideProvider(
|
||||
() => appSettingsService.settings.languageOverride,
|
||||
);
|
||||
_registerThirdPartyLicenses();
|
||||
|
||||
await chatTextScaleService.initialize();
|
||||
await translationService.refreshDownloadedModels();
|
||||
await uiViewStateService.initialize();
|
||||
await timeoutPredictionService.initialize();
|
||||
|
||||
@@ -68,6 +85,7 @@ void main() async {
|
||||
retryService: retryService,
|
||||
pathHistoryService: pathHistoryService,
|
||||
appSettingsService: appSettingsService,
|
||||
translationService: translationService,
|
||||
bleDebugLogService: bleDebugLogService,
|
||||
appDebugLogService: appDebugLogService,
|
||||
backgroundService: backgroundService,
|
||||
@@ -93,6 +111,7 @@ void main() async {
|
||||
appDebugLogService: appDebugLogService,
|
||||
mapTileCacheService: mapTileCacheService,
|
||||
chatTextScaleService: chatTextScaleService,
|
||||
translationService: translationService,
|
||||
uiViewStateService: uiViewStateService,
|
||||
timeoutPredictionService: timeoutPredictionService,
|
||||
),
|
||||
@@ -130,6 +149,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
final AppDebugLogService appDebugLogService;
|
||||
final MapTileCacheService mapTileCacheService;
|
||||
final ChatTextScaleService chatTextScaleService;
|
||||
final TranslationService translationService;
|
||||
final UiViewStateService uiViewStateService;
|
||||
final TimeoutPredictionService timeoutPredictionService;
|
||||
|
||||
@@ -144,6 +164,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
required this.appDebugLogService,
|
||||
required this.mapTileCacheService,
|
||||
required this.chatTextScaleService,
|
||||
required this.translationService,
|
||||
required this.uiViewStateService,
|
||||
required this.timeoutPredictionService,
|
||||
});
|
||||
@@ -159,6 +180,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||
ChangeNotifierProvider.value(value: translationService),
|
||||
ChangeNotifierProvider.value(value: uiViewStateService),
|
||||
Provider.value(value: storage),
|
||||
Provider.value(value: mapTileCacheService),
|
||||
@@ -179,23 +201,8 @@ class MeshCoreApp extends StatelessWidget {
|
||||
locale: _localeFromSetting(
|
||||
settingsService.settings.languageOverride,
|
||||
),
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
useMaterial3: true,
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
),
|
||||
theme: MeshTheme.light(),
|
||||
darkTheme: MeshTheme.dark(),
|
||||
themeMode: _themeModeFromSetting(
|
||||
settingsService.settings.themeMode,
|
||||
),
|
||||
@@ -203,7 +210,10 @@ class MeshCoreApp extends StatelessWidget {
|
||||
// Update notification service with resolved locale
|
||||
final locale = Localizations.localeOf(context);
|
||||
NotificationService().setLocale(locale);
|
||||
return child ?? const SizedBox.shrink();
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: _systemUiOverlayStyle(context),
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||
? const ChromeRequiredScreen()
|
||||
@@ -225,6 +235,24 @@ class MeshCoreApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
SystemUiOverlayStyle _systemUiOverlayStyle(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final iconBrightness = isDark ? Brightness.light : Brightness.dark;
|
||||
|
||||
// Keep Android system bars aligned with the resolved Flutter theme.
|
||||
return SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: iconBrightness,
|
||||
statusBarBrightness: isDark ? Brightness.dark : Brightness.light,
|
||||
systemNavigationBarColor: colorScheme.surface,
|
||||
systemNavigationBarIconBrightness: iconBrightness,
|
||||
systemNavigationBarDividerColor: colorScheme.surface,
|
||||
systemNavigationBarContrastEnforced: false,
|
||||
);
|
||||
}
|
||||
|
||||
Locale? _localeFromSetting(String? languageCode) {
|
||||
if (languageCode == null) return null;
|
||||
return Locale(languageCode);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'translation_support.dart';
|
||||
|
||||
enum UnitSystem { metric, imperial }
|
||||
|
||||
extension UnitSystemValue on UnitSystem {
|
||||
@@ -11,6 +13,67 @@ extension UnitSystemValue on UnitSystem {
|
||||
}
|
||||
}
|
||||
|
||||
const Map<String, String> defaultCyr2LatCharMap = {
|
||||
'А': 'A',
|
||||
'В': 'B',
|
||||
'Е': 'E',
|
||||
'Ё': 'E',
|
||||
'З': '3',
|
||||
'К': 'K',
|
||||
'М': 'M',
|
||||
'Н': 'H',
|
||||
'О': 'O',
|
||||
'Р': 'P',
|
||||
'С': 'C',
|
||||
'Т': 'T',
|
||||
'Х': 'X',
|
||||
'Ь': 'b',
|
||||
'а': 'a',
|
||||
'е': 'e',
|
||||
'ё': 'e',
|
||||
'о': 'o',
|
||||
'р': 'p',
|
||||
'с': 'c',
|
||||
'у': 'y',
|
||||
'х': 'x',
|
||||
};
|
||||
|
||||
class Cyr2LatProfile {
|
||||
final String id;
|
||||
final String name;
|
||||
final Map<String, String> charMap;
|
||||
|
||||
Cyr2LatProfile({required this.id, required this.name, required this.charMap});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'id': id, 'name': name, 'char_map': charMap};
|
||||
}
|
||||
|
||||
factory Cyr2LatProfile.fromJson(Map<String, dynamic> json) {
|
||||
return Cyr2LatProfile(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
charMap:
|
||||
(json['char_map'] as Map?)?.map(
|
||||
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||
) ??
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
Cyr2LatProfile copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
Map<String, String>? charMap,
|
||||
}) {
|
||||
return Cyr2LatProfile(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
charMap: charMap ?? this.charMap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppSettings {
|
||||
static const Object _unset = Object();
|
||||
|
||||
@@ -18,6 +81,7 @@ class AppSettings {
|
||||
final bool mapShowRepeaters;
|
||||
final bool mapShowChatNodes;
|
||||
final bool mapShowOtherNodes;
|
||||
final bool mapShowOverlaps;
|
||||
final double mapTimeFilterHours; // 0 = all time
|
||||
final bool mapKeyPrefixEnabled;
|
||||
final String mapKeyPrefix;
|
||||
@@ -47,18 +111,37 @@ class AppSettings {
|
||||
final bool mapShowDiscoveryContacts;
|
||||
final String tcpServerAddress;
|
||||
final int tcpServerPort;
|
||||
final bool jumpToOldestUnread;
|
||||
final bool translationEnabled;
|
||||
final bool autoTranslateIncomingMessages;
|
||||
final String? translationTargetLanguageCode;
|
||||
final bool composerTranslationEnabled;
|
||||
final String? translationModelSourceUrl;
|
||||
final String? translationSelectedModelId;
|
||||
final List<TranslationModelRecord> translationDownloadedModels;
|
||||
final List<Cyr2LatProfile> cyr2latProfiles;
|
||||
final String selectedCyr2latProfileId;
|
||||
|
||||
Map<String, String> get cyr2latCharMap {
|
||||
final profile = cyr2latProfiles.firstWhere(
|
||||
(p) => p.id == selectedCyr2latProfileId,
|
||||
orElse: () => cyr2latProfiles.first,
|
||||
);
|
||||
return profile.charMap;
|
||||
}
|
||||
|
||||
AppSettings({
|
||||
this.clearPathOnMaxRetry = false,
|
||||
this.mapShowRepeaters = true,
|
||||
this.mapShowChatNodes = true,
|
||||
this.mapShowOtherNodes = true,
|
||||
this.mapShowOverlaps = false,
|
||||
this.mapTimeFilterHours = 0, // Default to all time
|
||||
this.mapKeyPrefixEnabled = false,
|
||||
this.mapKeyPrefix = '',
|
||||
this.mapShowMarkers = true,
|
||||
this.mapShowGuessedLocations = true,
|
||||
this.enableMessageTracing = false,
|
||||
this.enableMessageTracing = true,
|
||||
this.mapCacheBounds,
|
||||
this.mapCacheMinZoom = 10,
|
||||
this.mapCacheMaxZoom = 15,
|
||||
@@ -66,7 +149,7 @@ class AppSettings {
|
||||
this.notifyOnNewMessage = true,
|
||||
this.notifyOnNewChannelMessage = true,
|
||||
this.notifyOnNewAdvert = true,
|
||||
this.autoRouteRotationEnabled = false,
|
||||
this.autoRouteRotationEnabled = true,
|
||||
this.maxRouteWeight = 5.0,
|
||||
this.initialRouteWeight = 3.0,
|
||||
this.routeWeightSuccessIncrement = 0.5,
|
||||
@@ -82,9 +165,30 @@ class AppSettings {
|
||||
this.mapShowDiscoveryContacts = true,
|
||||
this.tcpServerAddress = '',
|
||||
this.tcpServerPort = 0,
|
||||
this.jumpToOldestUnread = false,
|
||||
this.translationEnabled = false,
|
||||
this.autoTranslateIncomingMessages = true,
|
||||
this.translationTargetLanguageCode,
|
||||
this.composerTranslationEnabled = false,
|
||||
this.translationModelSourceUrl,
|
||||
this.translationSelectedModelId,
|
||||
List<TranslationModelRecord>? translationDownloadedModels,
|
||||
List<Cyr2LatProfile>? cyr2latProfiles,
|
||||
String? selectedCyr2latProfileId,
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||
mutedChannels = mutedChannels ?? {};
|
||||
mutedChannels = mutedChannels ?? {},
|
||||
translationDownloadedModels = translationDownloadedModels ?? const [],
|
||||
cyr2latProfiles =
|
||||
cyr2latProfiles ??
|
||||
[
|
||||
Cyr2LatProfile(
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
charMap: defaultCyr2LatCharMap,
|
||||
),
|
||||
],
|
||||
selectedCyr2latProfileId = selectedCyr2latProfileId ?? 'default';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
@@ -92,6 +196,7 @@ class AppSettings {
|
||||
'map_show_repeaters': mapShowRepeaters,
|
||||
'map_show_chat_nodes': mapShowChatNodes,
|
||||
'map_show_other_nodes': mapShowOtherNodes,
|
||||
'map_show_overlaps': mapShowOverlaps,
|
||||
'map_time_filter_hours': mapTimeFilterHours,
|
||||
'map_key_prefix_enabled': mapKeyPrefixEnabled,
|
||||
'map_key_prefix': mapKeyPrefix,
|
||||
@@ -121,6 +226,20 @@ class AppSettings {
|
||||
'map_show_discovery_contacts': mapShowDiscoveryContacts,
|
||||
'tcp_server_address': tcpServerAddress,
|
||||
'tcp_server_port': tcpServerPort,
|
||||
'jump_to_oldest_unread': jumpToOldestUnread,
|
||||
'translation_enabled': translationEnabled,
|
||||
'auto_translate_incoming_messages': autoTranslateIncomingMessages,
|
||||
'translation_target_language_code': translationTargetLanguageCode,
|
||||
'composer_translation_enabled': composerTranslationEnabled,
|
||||
'translation_model_source_url': translationModelSourceUrl,
|
||||
'translation_selected_model_id': translationSelectedModelId,
|
||||
'translation_downloaded_models': translationDownloadedModels
|
||||
.map((model) => model.toJson())
|
||||
.toList(),
|
||||
'cyr2lat_profiles': cyr2latProfiles
|
||||
.map((profile) => profile.toJson())
|
||||
.toList(),
|
||||
'selected_cyr2lat_profile_id': selectedCyr2latProfileId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -137,6 +256,7 @@ class AppSettings {
|
||||
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
||||
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
|
||||
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
|
||||
mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false,
|
||||
mapTimeFilterHours:
|
||||
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
|
||||
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
|
||||
@@ -144,7 +264,7 @@ class AppSettings {
|
||||
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
|
||||
mapShowGuessedLocations:
|
||||
json['map_show_guessed_locations'] as bool? ?? true,
|
||||
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false,
|
||||
enableMessageTracing: json['enable_message_tracing'] as bool? ?? true,
|
||||
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
|
||||
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
|
||||
),
|
||||
@@ -156,7 +276,7 @@ class AppSettings {
|
||||
json['notify_on_new_channel_message'] as bool? ?? true,
|
||||
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
|
||||
autoRouteRotationEnabled:
|
||||
json['auto_route_rotation_enabled'] as bool? ?? false,
|
||||
json['auto_route_rotation_enabled'] as bool? ?? true,
|
||||
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
|
||||
initialRouteWeight:
|
||||
(json['initial_route_weight'] as num?)?.toDouble() ?? 3.0,
|
||||
@@ -188,6 +308,59 @@ class AppSettings {
|
||||
json['map_show_discovery_contacts'] as bool? ?? true,
|
||||
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
|
||||
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
|
||||
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
|
||||
translationEnabled: json['translation_enabled'] as bool? ?? false,
|
||||
autoTranslateIncomingMessages:
|
||||
json['auto_translate_incoming_messages'] as bool? ?? true,
|
||||
translationTargetLanguageCode:
|
||||
json['translation_target_language_code'] as String?,
|
||||
composerTranslationEnabled:
|
||||
json['composer_translation_enabled'] as bool? ?? false,
|
||||
translationModelSourceUrl:
|
||||
json['translation_model_source_url'] as String?,
|
||||
translationSelectedModelId:
|
||||
json['translation_selected_model_id'] as String?,
|
||||
translationDownloadedModels:
|
||||
(json['translation_downloaded_models'] as List<dynamic>?)
|
||||
?.map(
|
||||
(entry) => TranslationModelRecord.fromJson(
|
||||
Map<String, dynamic>.from(entry as Map),
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
cyr2latProfiles:
|
||||
(json['cyr2lat_profiles'] as List<dynamic>?)
|
||||
?.map(
|
||||
(entry) => Cyr2LatProfile.fromJson(
|
||||
Map<String, dynamic>.from(entry as Map),
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
// Backward compatibility: if old cyr2lat_char_map exists, create a profile from it
|
||||
(json['cyr2lat_char_map'] != null
|
||||
? [
|
||||
Cyr2LatProfile(
|
||||
id: 'migrated',
|
||||
name: 'Migrated Profile',
|
||||
charMap:
|
||||
(json['cyr2lat_char_map'] as Map?)?.map(
|
||||
(key, value) =>
|
||||
MapEntry(key.toString(), value.toString()),
|
||||
) ??
|
||||
defaultCyr2LatCharMap,
|
||||
),
|
||||
]
|
||||
: [
|
||||
Cyr2LatProfile(
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
charMap: defaultCyr2LatCharMap,
|
||||
),
|
||||
]),
|
||||
selectedCyr2latProfileId:
|
||||
json['selected_cyr2lat_profile_id'] as String? ??
|
||||
(json['cyr2lat_char_map'] != null ? 'migrated' : 'default'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,6 +369,7 @@ class AppSettings {
|
||||
bool? mapShowRepeaters,
|
||||
bool? mapShowChatNodes,
|
||||
bool? mapShowOtherNodes,
|
||||
bool? mapShowOverlaps,
|
||||
double? mapTimeFilterHours,
|
||||
bool? mapKeyPrefixEnabled,
|
||||
String? mapKeyPrefix,
|
||||
@@ -225,12 +399,23 @@ class AppSettings {
|
||||
bool? mapShowDiscoveryContacts,
|
||||
String? tcpServerAddress,
|
||||
int? tcpServerPort,
|
||||
bool? jumpToOldestUnread,
|
||||
bool? translationEnabled,
|
||||
bool? autoTranslateIncomingMessages,
|
||||
Object? translationTargetLanguageCode = _unset,
|
||||
bool? composerTranslationEnabled,
|
||||
Object? translationModelSourceUrl = _unset,
|
||||
Object? translationSelectedModelId = _unset,
|
||||
List<TranslationModelRecord>? translationDownloadedModels,
|
||||
List<Cyr2LatProfile>? cyr2latProfiles,
|
||||
String? selectedCyr2latProfileId,
|
||||
}) {
|
||||
return AppSettings(
|
||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
|
||||
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
|
||||
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
|
||||
mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps,
|
||||
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
|
||||
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
|
||||
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
|
||||
@@ -272,6 +457,26 @@ class AppSettings {
|
||||
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
|
||||
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
|
||||
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
||||
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
|
||||
translationEnabled: translationEnabled ?? this.translationEnabled,
|
||||
autoTranslateIncomingMessages:
|
||||
autoTranslateIncomingMessages ?? this.autoTranslateIncomingMessages,
|
||||
translationTargetLanguageCode: translationTargetLanguageCode == _unset
|
||||
? this.translationTargetLanguageCode
|
||||
: translationTargetLanguageCode as String?,
|
||||
composerTranslationEnabled:
|
||||
composerTranslationEnabled ?? this.composerTranslationEnabled,
|
||||
translationModelSourceUrl: translationModelSourceUrl == _unset
|
||||
? this.translationModelSourceUrl
|
||||
: translationModelSourceUrl as String?,
|
||||
translationSelectedModelId: translationSelectedModelId == _unset
|
||||
? this.translationSelectedModelId
|
||||
: translationSelectedModelId as String?,
|
||||
translationDownloadedModels:
|
||||
translationDownloadedModels ?? this.translationDownloadedModels,
|
||||
cyr2latProfiles: cyr2latProfiles ?? this.cyr2latProfiles,
|
||||
selectedCyr2latProfileId:
|
||||
selectedCyr2latProfileId ?? this.selectedCyr2latProfileId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import 'dart:typed_data';
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import 'community.dart';
|
||||
|
||||
enum ChannelType { public, private, hashtag, communityPublic, communityHashtag }
|
||||
|
||||
class Channel {
|
||||
final int index;
|
||||
@@ -111,5 +114,36 @@ class Channel {
|
||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
static bool isCommunityChannel(ChannelType channelType) {
|
||||
switch (channelType) {
|
||||
case ChannelType.communityPublic:
|
||||
case ChannelType.communityHashtag:
|
||||
return true;
|
||||
case ChannelType.public:
|
||||
case ChannelType.private:
|
||||
case ChannelType.hashtag:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static ChannelType getChannelType(
|
||||
Channel channel,
|
||||
CommunityPskIndex communityIndex,
|
||||
) {
|
||||
Community? community = communityIndex.getCommunityForChannel(channel);
|
||||
if (community != null) {
|
||||
if (Community.isCommunityPublicChannel(channel, community)) {
|
||||
return ChannelType.communityPublic;
|
||||
}
|
||||
return ChannelType.communityHashtag;
|
||||
}
|
||||
if (channel.isPublicChannel) {
|
||||
return ChannelType.public;
|
||||
} else if (channel.name.startsWith('#')) {
|
||||
return ChannelType.hashtag;
|
||||
}
|
||||
return ChannelType.private;
|
||||
}
|
||||
|
||||
static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:typed_data';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import 'translation_support.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
enum ChannelMessageStatus { pending, sent, failed }
|
||||
@@ -24,9 +25,16 @@ class Repeat {
|
||||
}
|
||||
|
||||
class ChannelMessage {
|
||||
static const Object _unset = Object();
|
||||
|
||||
final Uint8List? senderKey;
|
||||
final String senderName;
|
||||
final String text;
|
||||
final String? originalText;
|
||||
final String? translatedText;
|
||||
final String? translatedLanguageCode;
|
||||
final MessageTranslationStatus translationStatus;
|
||||
final String? translationModelId;
|
||||
final DateTime timestamp;
|
||||
final bool isOutgoing;
|
||||
final ChannelMessageStatus status;
|
||||
@@ -47,6 +55,11 @@ class ChannelMessage {
|
||||
this.senderKey,
|
||||
required this.senderName,
|
||||
required this.text,
|
||||
this.originalText,
|
||||
this.translatedText,
|
||||
this.translatedLanguageCode,
|
||||
this.translationStatus = MessageTranslationStatus.none,
|
||||
this.translationModelId,
|
||||
required this.timestamp,
|
||||
required this.isOutgoing,
|
||||
this.status = ChannelMessageStatus.pending,
|
||||
@@ -86,12 +99,30 @@ class ChannelMessage {
|
||||
String? replyToMessageId,
|
||||
String? replyToSenderName,
|
||||
String? replyToText,
|
||||
Object? originalText = _unset,
|
||||
Object? translatedText = _unset,
|
||||
Object? translatedLanguageCode = _unset,
|
||||
MessageTranslationStatus? translationStatus,
|
||||
Object? translationModelId = _unset,
|
||||
Map<String, int>? reactions,
|
||||
}) {
|
||||
return ChannelMessage(
|
||||
senderKey: senderKey,
|
||||
senderName: senderName,
|
||||
text: text,
|
||||
originalText: originalText == _unset
|
||||
? this.originalText
|
||||
: originalText as String?,
|
||||
translatedText: translatedText == _unset
|
||||
? this.translatedText
|
||||
: translatedText as String?,
|
||||
translatedLanguageCode: translatedLanguageCode == _unset
|
||||
? this.translatedLanguageCode
|
||||
: translatedLanguageCode as String?,
|
||||
translationStatus: translationStatus ?? this.translationStatus,
|
||||
translationModelId: translationModelId == _unset
|
||||
? this.translationModelId
|
||||
: translationModelId as String?,
|
||||
timestamp: timestamp,
|
||||
isOutgoing: isOutgoing,
|
||||
status: status ?? this.status,
|
||||
@@ -191,12 +222,18 @@ class ChannelMessage {
|
||||
static ChannelMessage outgoing(
|
||||
String text,
|
||||
String senderName,
|
||||
int channelIndex,
|
||||
) {
|
||||
int channelIndex, {
|
||||
String? originalText,
|
||||
String? translatedLanguageCode,
|
||||
String? translationModelId,
|
||||
}) {
|
||||
return ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: senderName,
|
||||
text: text,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
timestamp: DateTime.now(),
|
||||
isOutgoing: true,
|
||||
status: ChannelMessageStatus.pending,
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
|
||||
import 'channel.dart';
|
||||
|
||||
/// Represents a community with a shared secret for deriving channel PSKs.
|
||||
///
|
||||
/// A Community is a namespace with a shared secret K (32 random bytes),
|
||||
@@ -162,6 +164,12 @@ class Community {
|
||||
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
|
||||
}
|
||||
|
||||
/// Returns true if this is the community's public channel
|
||||
static bool isCommunityPublicChannel(Channel channel, Community community) {
|
||||
final publicPsk = community.deriveCommunityPublicPsk();
|
||||
return channel.pskHex == Channel.formatPskHex(publicPsk);
|
||||
}
|
||||
|
||||
/// Add a hashtag channel to this community's list
|
||||
Community addHashtagChannel(String hashtag) {
|
||||
final normalized = _normalizeCommunityHashtag(hashtag);
|
||||
@@ -237,3 +245,28 @@ class Community {
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
class CommunityPskIndex {
|
||||
// Cache of PSK hex -> Community for quick lookup
|
||||
final Map<String, Community> _pskToCommunity = {};
|
||||
|
||||
void initialize(List<Community> communities) {
|
||||
_pskToCommunity.clear();
|
||||
for (final community in communities) {
|
||||
// Map the community public channel PSK
|
||||
final publicPsk = community.deriveCommunityPublicPsk();
|
||||
_pskToCommunity[Channel.formatPskHex(publicPsk)] = community;
|
||||
|
||||
// Map all known hashtag channel PSKs
|
||||
for (final hashtag in community.hashtagChannels) {
|
||||
final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag);
|
||||
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the community this channel belongs to, or null if not a community channel
|
||||
Community? getCommunityForChannel(Channel channel) {
|
||||
return _pskToCommunity[channel.pskHex];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
/// Parsed `RESP_CODE_STATS` + `STATS_TYPE_RADIO` (14 bytes total).
|
||||
class CompanionRadioStats {
|
||||
final int noiseFloorDbm;
|
||||
final int lastRssiDbm;
|
||||
final double lastSnrDb;
|
||||
final int txAirSecs;
|
||||
final int rxAirSecs;
|
||||
final DateTime receivedAt;
|
||||
|
||||
const CompanionRadioStats({
|
||||
required this.noiseFloorDbm,
|
||||
required this.lastRssiDbm,
|
||||
required this.lastSnrDb,
|
||||
required this.txAirSecs,
|
||||
required this.rxAirSecs,
|
||||
required this.receivedAt,
|
||||
});
|
||||
|
||||
static CompanionRadioStats? tryParse(Uint8List frame) {
|
||||
if (frame.length < 14) return null;
|
||||
if (frame[0] != respCodeStats || frame[1] != statsTypeRadio) return null;
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(2);
|
||||
final noise = reader.readInt16LE();
|
||||
final rssi = reader.readInt8();
|
||||
final snrRaw = reader.readInt8();
|
||||
final txAir = reader.readUInt32LE();
|
||||
final rxAir = reader.readUInt32LE();
|
||||
return CompanionRadioStats(
|
||||
noiseFloorDbm: noise,
|
||||
lastRssiDbm: rssi,
|
||||
lastSnrDb: snrRaw / 4.0,
|
||||
txAirSecs: txAir,
|
||||
rxAirSecs: rxAir,
|
||||
receivedAt: DateTime.now(),
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.warn('CompanionRadioStats parse error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-26
@@ -17,6 +17,7 @@ class Contact {
|
||||
final double? longitude;
|
||||
final DateTime lastSeen;
|
||||
final DateTime lastMessageAt;
|
||||
final DateTime? lastModified;
|
||||
final bool isActive;
|
||||
final bool wasPulled;
|
||||
final Uint8List? rawPacket;
|
||||
@@ -33,6 +34,7 @@ class Contact {
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.lastSeen,
|
||||
this.lastModified,
|
||||
DateTime? lastMessageAt,
|
||||
this.isActive = true,
|
||||
this.wasPulled = false,
|
||||
@@ -41,7 +43,10 @@ class Contact {
|
||||
|
||||
String get publicKeyHex => pubKeyToHex(publicKey);
|
||||
|
||||
String get typeLabel {
|
||||
/// Non-localized type label, intended for logs and non-UI exports
|
||||
/// (e.g. GPX). For UI use the `typeLabel(l10n)` extension in
|
||||
/// `lib/l10n/contact_localization.dart`.
|
||||
String get typeLabelRaw {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return 'Chat';
|
||||
@@ -56,17 +61,6 @@ class Contact {
|
||||
}
|
||||
}
|
||||
|
||||
String get pathLabel {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return 'Flood (forced)';
|
||||
if (pathOverride == 0) return 'Direct (forced)';
|
||||
return '$pathOverride hops (forced)';
|
||||
}
|
||||
if (pathLength < 0) return 'Flood';
|
||||
if (pathLength == 0) return 'Direct';
|
||||
return '$pathLength hops';
|
||||
}
|
||||
|
||||
bool get hasLocation {
|
||||
const double epsilon = 1e-6;
|
||||
final lat = latitude ?? 0.0;
|
||||
@@ -94,6 +88,7 @@ class Contact {
|
||||
double? longitude,
|
||||
DateTime? lastSeen,
|
||||
DateTime? lastMessageAt,
|
||||
DateTime? lastModified,
|
||||
bool? isActive,
|
||||
Uint8List? rawPacket,
|
||||
}) {
|
||||
@@ -114,20 +109,20 @@ class Contact {
|
||||
longitude: longitude ?? this.longitude,
|
||||
lastSeen: lastSeen ?? this.lastSeen,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
isActive: isActive ?? this.isActive,
|
||||
rawPacket: rawPacket ?? this.rawPacket,
|
||||
);
|
||||
}
|
||||
|
||||
String get pathIdList {
|
||||
/// Formats path bytes into comma-separated hex groups of [hashByteWidth] bytes.
|
||||
String pathFormattedIdList(int hashByteWidth) {
|
||||
final pathBytes = pathBytesForDisplay;
|
||||
if (pathBytes.isEmpty) return '';
|
||||
final w = hashByteWidth.clamp(1, 8);
|
||||
final parts = <String>[];
|
||||
final groupSize = pathHashSize;
|
||||
for (int i = 0; i < pathBytes.length; i += groupSize) {
|
||||
final end = (i + groupSize) <= pathBytes.length
|
||||
? (i + groupSize)
|
||||
: pathBytes.length;
|
||||
for (int i = 0; i < pathBytes.length; i += w) {
|
||||
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
|
||||
final chunk = pathBytes.sublist(i, end);
|
||||
parts.add(
|
||||
chunk
|
||||
@@ -138,6 +133,9 @@ class Contact {
|
||||
return parts.join(',');
|
||||
}
|
||||
|
||||
/// Default grouping uses legacy single-byte hop hash width.
|
||||
String get pathIdList => pathFormattedIdList(pathHashSize);
|
||||
|
||||
String get shortPubKeyHex {
|
||||
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
||||
}
|
||||
@@ -180,15 +178,34 @@ class Contact {
|
||||
return null;
|
||||
}
|
||||
|
||||
final lastMod = reader.readUInt32LE();
|
||||
// mandatory last_advert_timestamp
|
||||
final lastAdvertTimestamp = reader.readUInt32LE();
|
||||
|
||||
double? lat, lon;
|
||||
final latRaw = reader.readInt32LE();
|
||||
final lonRaw = reader.readInt32LE();
|
||||
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
DateTime? lastModified;
|
||||
if (reader.remaining >= 12) {
|
||||
final latRaw = reader.readInt32LE();
|
||||
final lonRaw = reader.readInt32LE();
|
||||
final lastModRaw = reader.readUInt32LE();
|
||||
// TODO: should this be &&?
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
}
|
||||
if (lastModRaw != 0) {
|
||||
lastModified = DateTime.fromMillisecondsSinceEpoch(lastModRaw * 1000);
|
||||
}
|
||||
} else if (reader.remaining >= 8) {
|
||||
// Old layout: gps without lastmod
|
||||
final latRaw = reader.readInt32LE();
|
||||
final lonRaw = reader.readInt32LE();
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
}
|
||||
appLogger.info(
|
||||
'Contact ${pubKeyToHex(pubKey).substring(0, 8)} has gps but no lastmod (legacy firmware layout)',
|
||||
);
|
||||
}
|
||||
|
||||
return Contact(
|
||||
@@ -200,7 +217,10 @@ class Contact {
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(
|
||||
lastAdvertTimestamp * 1000,
|
||||
),
|
||||
lastModified: lastModified,
|
||||
isActive: true,
|
||||
rawPacket: null,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import 'path_history.dart';
|
||||
|
||||
/// One observed route rendered on the path map — the live traced path
|
||||
/// (primary) or an alternate from the contact's path history — resolved to
|
||||
/// map coordinates with per-hop confidence flags.
|
||||
class DisplayPath {
|
||||
final String id;
|
||||
final String label;
|
||||
final Color color;
|
||||
final bool isPrimary;
|
||||
|
||||
/// Outbound hop bytes, including hops that could not be placed on the map.
|
||||
final List<int> hopBytes;
|
||||
|
||||
/// Resolved map points: self, each locatable hop, then the target when its
|
||||
/// position is known. Hops with no position are skipped here but still
|
||||
/// counted in [unresolvedHops].
|
||||
final List<LatLng> points;
|
||||
|
||||
/// Display name for each entry of [points].
|
||||
final List<String> pointLabels;
|
||||
|
||||
/// Whether each entry of [points] is a GPS-grade position (vs inferred).
|
||||
final List<bool> pointConfirmed;
|
||||
|
||||
/// Per segment (length points-1): true when either endpoint is inferred or
|
||||
/// unlocatable hops were skipped in between — rendered dashed.
|
||||
final List<bool> segmentEstimated;
|
||||
|
||||
/// Per segment: the transmission ordinal of the segment's destination,
|
||||
/// used to highlight the matching hop-list row during animation.
|
||||
final List<int> rowForSegment;
|
||||
|
||||
/// Total transmissions on the full route (including unlocatable hops).
|
||||
final int totalTransmissions;
|
||||
|
||||
/// True when the route ends with a chat-target endpoint row.
|
||||
final bool hasTargetEndpoint;
|
||||
|
||||
final int gpsConfirmedHops;
|
||||
final int unresolvedHops;
|
||||
final double distanceMeters;
|
||||
|
||||
/// History metadata; null for the live traced (primary) path.
|
||||
final PathRecord? record;
|
||||
|
||||
const DisplayPath({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.color,
|
||||
required this.isPrimary,
|
||||
required this.hopBytes,
|
||||
required this.points,
|
||||
required this.pointLabels,
|
||||
required this.pointConfirmed,
|
||||
required this.segmentEstimated,
|
||||
required this.rowForSegment,
|
||||
required this.totalTransmissions,
|
||||
required this.hasTargetEndpoint,
|
||||
required this.gpsConfirmedHops,
|
||||
required this.unresolvedHops,
|
||||
required this.distanceMeters,
|
||||
this.record,
|
||||
});
|
||||
}
|
||||
+43
-3
@@ -1,19 +1,27 @@
|
||||
import 'dart:typed_data';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import 'translation_support.dart';
|
||||
|
||||
enum MessageStatus { pending, sent, delivered, failed }
|
||||
|
||||
class Message {
|
||||
static const Object _unset = Object();
|
||||
|
||||
final Uint8List senderKey;
|
||||
final String text;
|
||||
final DateTime timestamp;
|
||||
final bool isOutgoing;
|
||||
final bool isCli;
|
||||
final MessageStatus status;
|
||||
final String? originalText;
|
||||
final String? translatedText;
|
||||
final String? translatedLanguageCode;
|
||||
final MessageTranslationStatus translationStatus;
|
||||
final String? translationModelId;
|
||||
|
||||
// NEW: Retry logic fields
|
||||
final String? messageId;
|
||||
final String messageId;
|
||||
final int retryCount;
|
||||
final int? estimatedTimeoutMs;
|
||||
final int? expectedAckHash;
|
||||
@@ -33,7 +41,12 @@ class Message {
|
||||
required this.isOutgoing,
|
||||
this.isCli = false,
|
||||
this.status = MessageStatus.pending,
|
||||
this.messageId,
|
||||
String? messageId,
|
||||
this.originalText,
|
||||
this.translatedText,
|
||||
this.translatedLanguageCode,
|
||||
this.translationStatus = MessageTranslationStatus.none,
|
||||
this.translationModelId,
|
||||
this.retryCount = 0,
|
||||
this.estimatedTimeoutMs,
|
||||
this.expectedAckHash,
|
||||
@@ -45,7 +58,10 @@ class Message {
|
||||
Uint8List? fourByteRoomContactKey,
|
||||
Map<String, int>? reactions,
|
||||
Map<String, MessageStatus>? reactionStatuses,
|
||||
}) : pathBytes = pathBytes ?? Uint8List(0),
|
||||
}) : messageId =
|
||||
messageId ??
|
||||
'${timestamp.millisecondsSinceEpoch}_${pubKeyToHex(senderKey)}_${text.hashCode}',
|
||||
pathBytes = pathBytes ?? Uint8List(0),
|
||||
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
|
||||
reactions = reactions ?? {},
|
||||
reactionStatuses = reactionStatuses ?? {};
|
||||
@@ -63,6 +79,11 @@ class Message {
|
||||
int? pathLength,
|
||||
Uint8List? pathBytes,
|
||||
bool? isCli,
|
||||
Object? originalText = _unset,
|
||||
Object? translatedText = _unset,
|
||||
Object? translatedLanguageCode = _unset,
|
||||
MessageTranslationStatus? translationStatus,
|
||||
Object? translationModelId = _unset,
|
||||
Map<String, int>? reactions,
|
||||
Map<String, MessageStatus>? reactionStatuses,
|
||||
Uint8List? fourByteRoomContactKey,
|
||||
@@ -75,6 +96,19 @@ class Message {
|
||||
isCli: isCli ?? this.isCli,
|
||||
status: status ?? this.status,
|
||||
messageId: messageId,
|
||||
originalText: originalText == _unset
|
||||
? this.originalText
|
||||
: originalText as String?,
|
||||
translatedText: translatedText == _unset
|
||||
? this.translatedText
|
||||
: translatedText as String?,
|
||||
translatedLanguageCode: translatedLanguageCode == _unset
|
||||
? this.translatedLanguageCode
|
||||
: translatedLanguageCode as String?,
|
||||
translationStatus: translationStatus ?? this.translationStatus,
|
||||
translationModelId: translationModelId == _unset
|
||||
? this.translationModelId
|
||||
: translationModelId as String?,
|
||||
retryCount: retryCount ?? this.retryCount,
|
||||
estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs,
|
||||
expectedAckHash: expectedAckHash ?? this.expectedAckHash,
|
||||
@@ -124,12 +158,18 @@ class Message {
|
||||
static Message outgoing(
|
||||
Uint8List recipientKey,
|
||||
String text, {
|
||||
String? originalText,
|
||||
String? translatedLanguageCode,
|
||||
String? translationModelId,
|
||||
int? pathLength,
|
||||
Uint8List? pathBytes,
|
||||
}) {
|
||||
return Message(
|
||||
senderKey: recipientKey,
|
||||
text: text,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
timestamp: DateTime.now(),
|
||||
isOutgoing: true,
|
||||
isCli: false,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Timeline state for the packet-flow animation on the path map.
|
||||
///
|
||||
/// The packet travels each segment over [segmentMs] (scaled by [speed]),
|
||||
/// then dwells at the reached hop for [dwellMs] so the hop visibly lights up.
|
||||
/// Overlay layers listen to this controller directly; [activeSegment] only
|
||||
/// fires when the segment index changes so list highlights rebuild cheaply.
|
||||
class PathPlaybackController extends ChangeNotifier {
|
||||
static const double segmentMs = 1100;
|
||||
static const double dwellMs = 380;
|
||||
static const List<double> speedSteps = [0.5, 1.0, 2.0];
|
||||
|
||||
late final Ticker _ticker;
|
||||
List<LatLng> _points = const [];
|
||||
double _timelineMs = 0;
|
||||
Duration _lastTick = Duration.zero;
|
||||
bool _playing = false;
|
||||
bool _started = false;
|
||||
double _speed = 1.0;
|
||||
|
||||
/// Segment currently being traveled (clamped to the last segment), or -1
|
||||
/// while the animation has not been started — listeners use this for
|
||||
/// hop-list highlighting without rebuilding every tick.
|
||||
final ValueNotifier<int> activeSegment = ValueNotifier(-1);
|
||||
|
||||
PathPlaybackController(TickerProvider vsync) {
|
||||
_ticker = vsync.createTicker(_onTick);
|
||||
}
|
||||
|
||||
List<LatLng> get points => _points;
|
||||
bool get hasPath => _points.length >= 2;
|
||||
int get segmentCount => hasPath ? _points.length - 1 : 0;
|
||||
bool get playing => _playing;
|
||||
double get speed => _speed;
|
||||
|
||||
/// True once the user has started or stepped the animation; the packet
|
||||
/// overlay renders only in this state.
|
||||
bool get started => _started;
|
||||
|
||||
double get _slotMs => segmentMs + dwellMs;
|
||||
double get _totalMs => segmentCount * _slotMs;
|
||||
bool get isComplete => hasPath && _timelineMs >= _totalMs;
|
||||
|
||||
int get currentSegment {
|
||||
if (!hasPath) return 0;
|
||||
return (_timelineMs / _slotMs).floor().clamp(0, segmentCount - 1);
|
||||
}
|
||||
|
||||
/// Travel progress through [currentSegment]; 1.0 while dwelling at its end.
|
||||
double get segmentProgress {
|
||||
if (!hasPath) return 0;
|
||||
final within = _timelineMs - currentSegment * _slotMs;
|
||||
return (within / segmentMs).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
/// Dwell progress (0..1) at the reached hop, or null while traveling.
|
||||
double? get dwellProgress {
|
||||
if (!hasPath || isComplete) return null;
|
||||
final within = _timelineMs - currentSegment * _slotMs;
|
||||
if (within < segmentMs) return null;
|
||||
return ((within - segmentMs) / dwellMs).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
/// Index of the point the packet has most recently reached.
|
||||
int get reachedPointIndex {
|
||||
if (!hasPath) return 0;
|
||||
if (isComplete) return _points.length - 1;
|
||||
return segmentProgress >= 1.0 ? currentSegment + 1 : currentSegment;
|
||||
}
|
||||
|
||||
LatLng get position {
|
||||
if (!hasPath) return const LatLng(0, 0);
|
||||
final seg = currentSegment;
|
||||
final a = _points[seg];
|
||||
final b = _points[seg + 1];
|
||||
final t = segmentProgress;
|
||||
return LatLng(
|
||||
a.latitude + (b.latitude - a.latitude) * t,
|
||||
a.longitude + (b.longitude - a.longitude) * t,
|
||||
);
|
||||
}
|
||||
|
||||
/// Replaces the path and resets the animation to the start.
|
||||
void setPath(List<LatLng> points) {
|
||||
_ticker.stop();
|
||||
_points = List.unmodifiable(points);
|
||||
_timelineMs = 0;
|
||||
_playing = false;
|
||||
_started = false;
|
||||
activeSegment.value = -1;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void play() {
|
||||
if (!hasPath) return;
|
||||
if (isComplete) _timelineMs = 0;
|
||||
_started = true;
|
||||
_playing = true;
|
||||
activeSegment.value = currentSegment;
|
||||
if (!_ticker.isActive) {
|
||||
_lastTick = Duration.zero;
|
||||
_ticker.start();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void pause() {
|
||||
_ticker.stop();
|
||||
_playing = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void togglePlay() => _playing ? pause() : play();
|
||||
|
||||
void replay() {
|
||||
if (!hasPath) return;
|
||||
_timelineMs = 0;
|
||||
activeSegment.value = 0;
|
||||
play();
|
||||
}
|
||||
|
||||
/// Stops playback and hides the packet overlay.
|
||||
void stop() {
|
||||
_ticker.stop();
|
||||
_playing = false;
|
||||
_started = false;
|
||||
_timelineMs = 0;
|
||||
activeSegment.value = -1;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void stepForward() => _jumpToPoint(reachedPointIndex + 1);
|
||||
|
||||
void stepBack() => _jumpToPoint(reachedPointIndex - 1);
|
||||
|
||||
void cycleSpeed() {
|
||||
final index = speedSteps.indexOf(_speed);
|
||||
_speed = speedSteps[(index + 1) % speedSteps.length];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _jumpToPoint(int index) {
|
||||
if (!hasPath) return;
|
||||
_ticker.stop();
|
||||
_playing = false;
|
||||
_started = true;
|
||||
final clamped = index.clamp(0, _points.length - 1);
|
||||
// Land at the start of the dwell window so the hop pulse plays.
|
||||
_timelineMs = clamped == 0 ? 0 : (clamped - 1) * _slotMs + segmentMs;
|
||||
activeSegment.value = currentSegment;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _onTick(Duration elapsed) {
|
||||
final dtMs = (elapsed - _lastTick).inMicroseconds / 1000.0;
|
||||
_lastTick = elapsed;
|
||||
_timelineMs = (_timelineMs + dtMs * _speed).clamp(0.0, _totalMs);
|
||||
if (_timelineMs >= _totalMs) {
|
||||
_ticker.stop();
|
||||
_playing = false;
|
||||
}
|
||||
if (activeSegment.value != currentSegment) {
|
||||
activeSegment.value = currentSegment;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker.dispose();
|
||||
activeSegment.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,276 @@ class RadioSettings {
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Artyom (VVO)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 864.281,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_6,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Biysk (BSK)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.000,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Chelyabinsk (CEK)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_6,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Cherepovets (CEE)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.570,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Irkutsk (IKT)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Ivanovo (IWA)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Izhevsk (IJK)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.732,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Kaluga (KLF)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Kazan (KZN)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_6,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Khabarovsk (KHV)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 864.281,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_6,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Kirov (KVX)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Lipetsk (LPK)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.950,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Moscow (MOW)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Nizhny Novgorod (GOJ)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_6,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Novosibirsk (OVB)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.000,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Rostov-on-Don (ROV)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Ryazan (RZN)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.880,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Samara (KUF)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 864.281,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Saratov (GSV)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 864.281,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia St. Petersburg (LED)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.856,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Tambov (TBW)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.950,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Tula (TYA)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Tver (KLD)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.169,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Ufa (UFA)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.732,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Volgograd (VOG)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.525,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Voronezh (VOZ)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 868.731,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_6,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Russia Yekaterinburg (SVX)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.046,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_7,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Switzerland',
|
||||
RadioSettings(
|
||||
@@ -228,7 +498,7 @@ class RadioSettings {
|
||||
frequencyMHz: 433.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
@@ -238,7 +508,7 @@ class RadioSettings {
|
||||
frequencyMHz: 869.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
@@ -248,7 +518,7 @@ class RadioSettings {
|
||||
frequencyMHz: 918.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
enum MessageTranslationStatus { none, pending, completed, failed, skipped }
|
||||
|
||||
extension MessageTranslationStatusValue on MessageTranslationStatus {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case MessageTranslationStatus.pending:
|
||||
return 'pending';
|
||||
case MessageTranslationStatus.completed:
|
||||
return 'completed';
|
||||
case MessageTranslationStatus.failed:
|
||||
return 'failed';
|
||||
case MessageTranslationStatus.skipped:
|
||||
return 'skipped';
|
||||
case MessageTranslationStatus.none:
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageTranslationStatus parseMessageTranslationStatus(dynamic value) {
|
||||
if (value is! String) {
|
||||
return MessageTranslationStatus.none;
|
||||
}
|
||||
for (final status in MessageTranslationStatus.values) {
|
||||
if (status.value == value) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return MessageTranslationStatus.none;
|
||||
}
|
||||
|
||||
class TranslationModelRecord {
|
||||
final String id;
|
||||
final String name;
|
||||
final String sourceUrl;
|
||||
final String localPath;
|
||||
final DateTime downloadedAt;
|
||||
final int fileSizeBytes;
|
||||
|
||||
const TranslationModelRecord({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.sourceUrl,
|
||||
required this.localPath,
|
||||
required this.downloadedAt,
|
||||
required this.fileSizeBytes,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'source_url': sourceUrl,
|
||||
'local_path': localPath,
|
||||
'downloaded_at': downloadedAt.millisecondsSinceEpoch,
|
||||
'file_size_bytes': fileSizeBytes,
|
||||
};
|
||||
}
|
||||
|
||||
factory TranslationModelRecord.fromJson(Map<String, dynamic> json) {
|
||||
return TranslationModelRecord(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
sourceUrl: json['source_url'] as String? ?? '',
|
||||
localPath: json['local_path'] as String? ?? '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['downloaded_at'] as int? ?? 0,
|
||||
),
|
||||
fileSizeBytes: json['file_size_bytes'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String translationModelFriendlyName(TranslationModelRecord model) {
|
||||
switch (model.id) {
|
||||
case 'hy-mt1.5-1.8b-q4_k_m':
|
||||
return 'Tencent HY-MT 1.5 1.8B Q4_K_M';
|
||||
case 'hy-mt1.5-1.8b-q6_k':
|
||||
return 'Tencent HY-MT 1.5 1.8B Q6_K';
|
||||
default:
|
||||
final trimmed = model.name.trim();
|
||||
if (trimmed.endsWith('.gguf')) {
|
||||
return trimmed.substring(0, trimmed.length - 5);
|
||||
}
|
||||
return trimmed.isEmpty ? model.id : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
class TranslationLanguageOption {
|
||||
final String code;
|
||||
final String label;
|
||||
|
||||
const TranslationLanguageOption({required this.code, required this.label});
|
||||
}
|
||||
|
||||
const List<TranslationLanguageOption> supportedTranslationLanguages = [
|
||||
TranslationLanguageOption(code: 'bg', label: 'Bulgarian'),
|
||||
TranslationLanguageOption(code: 'de', label: 'German'),
|
||||
TranslationLanguageOption(code: 'en', label: 'English'),
|
||||
TranslationLanguageOption(code: 'es', label: 'Spanish'),
|
||||
TranslationLanguageOption(code: 'fr', label: 'French'),
|
||||
TranslationLanguageOption(code: 'hu', label: 'Hungarian'),
|
||||
TranslationLanguageOption(code: 'it', label: 'Italian'),
|
||||
TranslationLanguageOption(code: 'ja', label: 'Japanese'),
|
||||
TranslationLanguageOption(code: 'ko', label: 'Korean'),
|
||||
TranslationLanguageOption(code: 'nl', label: 'Dutch'),
|
||||
TranslationLanguageOption(code: 'pl', label: 'Polish'),
|
||||
TranslationLanguageOption(code: 'pt', label: 'Portuguese'),
|
||||
TranslationLanguageOption(code: 'ru', label: 'Russian'),
|
||||
TranslationLanguageOption(code: 'sk', label: 'Slovak'),
|
||||
TranslationLanguageOption(code: 'sl', label: 'Slovenian'),
|
||||
TranslationLanguageOption(code: 'sv', label: 'Swedish'),
|
||||
TranslationLanguageOption(code: 'uk', label: 'Ukrainian'),
|
||||
TranslationLanguageOption(code: 'zh', label: 'Chinese'),
|
||||
];
|
||||
|
||||
final List<TranslationModelRecord> translationPresetModels = [
|
||||
TranslationModelRecord(
|
||||
id: 'hy-mt1.5-1.8b-q4_k_m',
|
||||
name: 'HY-MT1.5-1.8B-Q4_K_M.gguf',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q4_K_M.gguf?download=true',
|
||||
localPath: '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
fileSizeBytes: 0,
|
||||
),
|
||||
TranslationModelRecord(
|
||||
id: 'hy-mt1.5-1.8b-q6_k',
|
||||
name: 'HY-MT1.5-1.8B-Q6_K.gguf',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q6_K.gguf?download=true',
|
||||
localPath: '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
fileSizeBytes: 0,
|
||||
),
|
||||
];
|
||||
@@ -4,7 +4,9 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class AppDebugLogScreen extends StatelessWidget {
|
||||
const AppDebugLogScreen({super.key});
|
||||
@@ -34,8 +36,9 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
.join('\n');
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.debugLog_copied)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.debugLog_copied),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
@@ -56,25 +59,57 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
child: hasEntries
|
||||
? ListView.separated(
|
||||
itemCount: entries.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
separatorBuilder: (_, _) =>
|
||||
const Divider(height: 1, color: MeshPalette.line),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: _buildLevelIcon(entry.level),
|
||||
title: Text(
|
||||
'[${entry.tag}] ${entry.message}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
return Container(
|
||||
color: MeshPalette.bg,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
subtitle: Text(
|
||||
entry.formattedTime,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildLevelIcon(context, entry.level),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '[${entry.tag}] ',
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: _levelColor(entry.level),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: entry.message,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: MeshPalette.ink2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
entry.formattedTime,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 9.5,
|
||||
color: MeshPalette.ink4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -83,25 +118,25 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
const Icon(
|
||||
Icons.bug_report_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey[400],
|
||||
color: MeshPalette.ink3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.l10n.debugLog_noEntries,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
color: MeshPalette.ink3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.debugLog_enableInSettings,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
color: MeshPalette.ink3,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -113,18 +148,37 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLevelIcon(AppDebugLogLevel level) {
|
||||
Color _levelColor(AppDebugLogLevel level) {
|
||||
switch (level) {
|
||||
case AppDebugLogLevel.info:
|
||||
return const Icon(Icons.info_outline, size: 18, color: Colors.blue);
|
||||
return MeshPalette.blue;
|
||||
case AppDebugLogLevel.warning:
|
||||
return MeshPalette.warn;
|
||||
case AppDebugLogLevel.error:
|
||||
return MeshPalette.alert;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLevelIcon(BuildContext context, AppDebugLogLevel level) {
|
||||
switch (level) {
|
||||
case AppDebugLogLevel.info:
|
||||
return const Icon(
|
||||
Icons.info_outline,
|
||||
size: 18,
|
||||
color: MeshPalette.blue,
|
||||
);
|
||||
case AppDebugLogLevel.warning:
|
||||
return const Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
size: 18,
|
||||
color: Colors.orange,
|
||||
color: MeshPalette.warn,
|
||||
);
|
||||
case AppDebugLogLevel.error:
|
||||
return const Icon(Icons.error_outline, size: 18, color: Colors.red);
|
||||
return const Icon(
|
||||
Icons.error_outline,
|
||||
size: 18,
|
||||
color: MeshPalette.alert,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2046
-784
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,9 @@ import 'package:flutter/services.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/ble_debug_log_service.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
enum _BleLogView { frames, rawLogRx }
|
||||
|
||||
@@ -31,6 +33,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: context.l10n.debugLog_copyLog,
|
||||
@@ -52,10 +55,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
.join('\n');
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.debugLog_bleCopied),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.debugLog_bleCopied),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
@@ -101,23 +103,14 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
itemCount: showingFrames
|
||||
? entries.length
|
||||
: rawEntries.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
separatorBuilder: (_, _) =>
|
||||
const Divider(height: 1, color: MeshPalette.line),
|
||||
itemBuilder: (context, index) {
|
||||
if (showingFrames) {
|
||||
final entry = entries[index];
|
||||
final time =
|
||||
'${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}';
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(entry.description),
|
||||
subtitle: Text('${entry.hexPreview}\n$time'),
|
||||
isThreeLine: true,
|
||||
leading: Icon(
|
||||
entry.outgoing
|
||||
? Icons.upload
|
||||
: Icons.download,
|
||||
size: 18,
|
||||
),
|
||||
return GestureDetector(
|
||||
onLongPress: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
@@ -131,6 +124,60 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
color: MeshPalette.bg,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
entry.outgoing
|
||||
? Icons.upload
|
||||
: Icons.download,
|
||||
size: 18,
|
||||
color: entry.outgoing
|
||||
? MeshPalette.blue
|
||||
: MeshPalette.signal,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
entry.description,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: MeshPalette.ink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
entry.hexPreview,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 10,
|
||||
color: MeshPalette.ink3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
time,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 9.5,
|
||||
color: MeshPalette.ink4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,18 +185,65 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
final info = _decodeRawPacket(entry.payload);
|
||||
final time =
|
||||
'${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}';
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(info.title),
|
||||
subtitle: Text('${info.summary}\n$time'),
|
||||
isThreeLine: true,
|
||||
leading: const Icon(Icons.download, size: 18),
|
||||
return GestureDetector(
|
||||
onTap: () => _showRawDialog(context, info),
|
||||
child: Container(
|
||||
color: MeshPalette.bg,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.download,
|
||||
size: 18,
|
||||
color: MeshPalette.signal,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
info.title,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: MeshPalette.ink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
info.summary,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 10,
|
||||
color: MeshPalette.ink3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
time,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 9.5,
|
||||
color: MeshPalette.ink4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Center(
|
||||
child: Text(context.l10n.debugLog_noBleActivity),
|
||||
child: Text(
|
||||
context.l10n.debugLog_noBleActivity,
|
||||
style: const TextStyle(color: MeshPalette.ink3),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+738
-570
File diff suppressed because it is too large
Load Diff
+844
-1076
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
|
||||
class ChromeRequiredScreen extends StatelessWidget {
|
||||
const ChromeRequiredScreen({super.key});
|
||||
@@ -7,81 +9,95 @@ class ChromeRequiredScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isDark
|
||||
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
|
||||
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.browser_not_supported_rounded,
|
||||
size: 80,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
l10n.scanner_chromeRequired,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.scanner_chromeRequiredMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
// We can't really "fix" it for them other than telling them to use Chrome
|
||||
// but we can provide a nice visual.
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20, color: Colors.blue),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"Web Bluetooth requires a Chromium browser",
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w500,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Icon in tinted circle
|
||||
Container(
|
||||
width: 88,
|
||||
height: 88,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: scheme.tertiary.withValues(alpha: 0.10),
|
||||
border: Border.all(
|
||||
color: scheme.tertiary.withValues(alpha: 0.25),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.browser_not_supported_rounded,
|
||||
size: 42,
|
||||
color: scheme.tertiary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
l10n.scanner_chromeRequired,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: scheme.onSurface,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Body text
|
||||
Text(
|
||||
l10n.scanner_chromeRequiredMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: scheme.onSurfaceVariant,
|
||||
height: 1.55,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Info chip
|
||||
MeshCard(
|
||||
margin: EdgeInsets.zero,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
color: scheme.secondaryContainer.withValues(alpha: 0.35),
|
||||
borderColor: scheme.outline.withValues(alpha: 0.3),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 18,
|
||||
color: scheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
l10n.chrome_bluetoothRequiresChromium,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 12,
|
||||
color: scheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import '../widgets/qr_scanner_widget.dart';
|
||||
|
||||
/// Screen for scanning community QR codes to join communities.
|
||||
@@ -34,16 +39,87 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
centerTitle: true,
|
||||
),
|
||||
body: _isProcessing
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
? Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: QrScannerWidget(
|
||||
onScanned: (data) => _handleScannedData(context, data),
|
||||
validator: Community.isValidQrData,
|
||||
onValidationFailed: (_) => _showInvalidQrError(context),
|
||||
instructions: context.l10n.community_scanInstructions,
|
||||
overlay: _buildThemedOverlay(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemedOverlay(BuildContext context) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Dark semi-transparent background with cutout
|
||||
ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.black.withValues(alpha: 0.5),
|
||||
BlendMode.srcOut,
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black,
|
||||
backgroundBlendMode: BlendMode.dstOut,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
height: 250,
|
||||
width: 250,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Corner brackets on top
|
||||
const ScannerCornerOverlay(
|
||||
scanWindowSize: 250,
|
||||
borderColor: MeshPalette.blue,
|
||||
borderWidth: 2,
|
||||
cornerLength: 24,
|
||||
),
|
||||
// Instructions pill below the scan window
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 250 + 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.72),
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.community_scanInstructions,
|
||||
style: const TextStyle(color: MeshPalette.ink2, fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleScannedData(BuildContext context, String data) async {
|
||||
if (_isProcessing) return;
|
||||
|
||||
@@ -76,11 +152,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: MeshPalette.alert,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -93,33 +168,77 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
}
|
||||
|
||||
void _showInvalidQrError(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: MeshPalette.warn,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAlreadyMemberDialog(BuildContext context, Community community) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.community_alreadyMember),
|
||||
content: Text(
|
||||
context.l10n.community_alreadyMemberMessage(community.name),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.common_ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
showMeshSheet(
|
||||
context,
|
||||
builder: (sheetContext) {
|
||||
final sheetScheme = Theme.of(sheetContext).colorScheme;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
BottomSheetHeader(title: context.l10n.community_alreadyMember),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 4),
|
||||
child: Text(
|
||||
context.l10n.community_alreadyMemberMessage(community.name),
|
||||
style: TextStyle(color: sheetScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
MeshCard(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.groups,
|
||||
color: MeshPalette.magenta,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
community.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'ID: ${community.shortCommunityId}...',
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: sheetScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(sheetContext);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.common_ok),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,77 +247,111 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
Community community,
|
||||
) async {
|
||||
bool addPublicChannel = true;
|
||||
final completer = Completer<bool>();
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (dialogContext, setDialogState) => AlertDialog(
|
||||
title: Text(context.l10n.community_joinTitle),
|
||||
content: Column(
|
||||
await showMeshSheet<void>(
|
||||
context,
|
||||
builder: (sheetContext) => StatefulBuilder(
|
||||
builder: (sheetContext, setSheetState) {
|
||||
final joinScheme = Theme.of(sheetContext).colorScheme;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(context.l10n.community_joinConfirmation(community.name)),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.groups,
|
||||
color: Theme.of(dialogContext).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
community.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'ID: ${community.shortCommunityId}...',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
BottomSheetHeader(title: context.l10n.community_joinTitle),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 4),
|
||||
child: Text(
|
||||
context.l10n.community_joinConfirmation(community.name),
|
||||
style: TextStyle(color: joinScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
MeshCard(
|
||||
child: Row(
|
||||
children: [
|
||||
AvatarCircle(
|
||||
name: community.name,
|
||||
icon: Icons.groups,
|
||||
color: MeshPalette.magenta,
|
||||
size: 44,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
community.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'ID: ${community.shortCommunityId}...',
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: joinScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
value: addPublicChannel,
|
||||
onChanged: (value) {
|
||||
setDialogState(() {
|
||||
setSheetState(() {
|
||||
addPublicChannel = value ?? true;
|
||||
});
|
||||
},
|
||||
title: Text(context.l10n.community_addPublicChannel),
|
||||
subtitle: Text(context.l10n.community_addPublicChannelHint),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
completer.complete(false);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
completer.complete(true);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
child: Text(context.l10n.community_join),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: Text(context.l10n.community_join),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true && context.mounted) {
|
||||
// If sheet was dismissed without a button press, treat as cancel
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(false);
|
||||
}
|
||||
|
||||
final result = await completer.future;
|
||||
|
||||
if (result && context.mounted) {
|
||||
await _joinCommunity(context, community, addPublicChannel);
|
||||
} else if (context.mounted) {
|
||||
// User cancelled - go back
|
||||
@@ -229,11 +382,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.community_joined(community.name)),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_joined(community.name)),
|
||||
backgroundColor: MeshPalette.signal,
|
||||
);
|
||||
|
||||
// Return to previous screen
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:meshcore_open/models/companion_radio_stats.dart';
|
||||
import 'package:meshcore_open/l10n/l10n.dart';
|
||||
import 'package:meshcore_open/theme/mesh_theme.dart';
|
||||
import 'package:meshcore_open/widgets/mesh_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CompanionRadioStatsScreen extends StatefulWidget {
|
||||
const CompanionRadioStatsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CompanionRadioStatsScreen> createState() =>
|
||||
_CompanionRadioStatsScreenState();
|
||||
}
|
||||
|
||||
class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
|
||||
final List<double> _noiseHistory = [];
|
||||
static const int _maxSamples = 120;
|
||||
MeshCoreConnector? _connector;
|
||||
DateTime? _lastChartSampleAt;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final c = context.read<MeshCoreConnector>();
|
||||
_connector = c;
|
||||
c.acquireRadioStatsPolling();
|
||||
c.setPollingInterval(1);
|
||||
c.radioStatsNotifier.addListener(_onStatsUpdate);
|
||||
}
|
||||
|
||||
void _onStatsUpdate() {
|
||||
final s = _connector?.radioStatsNotifier.value;
|
||||
if (s == null || !mounted) return;
|
||||
if (_lastChartSampleAt == s.receivedAt) return;
|
||||
_lastChartSampleAt = s.receivedAt;
|
||||
setState(() {
|
||||
_noiseHistory.add(s.noiseFloorDbm.toDouble());
|
||||
while (_noiseHistory.length > _maxSamples) {
|
||||
_noiseHistory.removeAt(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connector?.radioStatsNotifier.removeListener(_onStatsUpdate);
|
||||
_connector?.releaseRadioStatsPolling();
|
||||
_connector?.setPollingInterval(30);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _tile(String text, IconData icon, Color color) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: MeshTheme.mono(fontSize: 13, color: scheme.onSurface),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.radioStats_screenTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Selector<MeshCoreConnector, ({bool connected, bool supported})>(
|
||||
selector: (_, c) => (
|
||||
connected: c.isConnected,
|
||||
supported: c.supportsCompanionRadioStats,
|
||||
),
|
||||
builder: (context, state, _) {
|
||||
if (!state.connected) {
|
||||
return Center(child: Text(l10n.radioStats_notConnected));
|
||||
}
|
||||
if (!state.supported) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
l10n.radioStats_firmwareTooOld,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final tt = Theme.of(context).textTheme;
|
||||
|
||||
return ValueListenableBuilder<CompanionRadioStats?>(
|
||||
valueListenable: connector.radioStatsNotifier,
|
||||
builder: (context, stats, _) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: [
|
||||
if (stats != null) ...[
|
||||
const SectionHeader(
|
||||
'Signal',
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
),
|
||||
MeshCard(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_tile(
|
||||
l10n.radioStats_noiseFloor(stats.noiseFloorDbm),
|
||||
Icons.noise_aware,
|
||||
scheme.onSurfaceVariant,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_tile(
|
||||
l10n.radioStats_lastRssi(stats.lastRssiDbm),
|
||||
Icons.wifi_tethering,
|
||||
scheme.onSurfaceVariant,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_tile(
|
||||
l10n.radioStats_lastSnr(
|
||||
stats.lastSnrDb.toStringAsFixed(1),
|
||||
),
|
||||
Icons.signal_cellular_alt,
|
||||
MeshTheme.snrColor(stats.lastSnrDb, blocked: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SectionHeader(
|
||||
'Airtime',
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
),
|
||||
MeshCard(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_tile(
|
||||
l10n.radioStats_txAir(stats.txAirSecs),
|
||||
Icons.upload,
|
||||
MeshPalette.blue,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_tile(
|
||||
l10n.radioStats_rxAir(stats.rxAirSecs),
|
||||
Icons.download,
|
||||
MeshPalette.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 80),
|
||||
Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Text(
|
||||
l10n.radioStats_waiting,
|
||||
style: TextStyle(color: scheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
],
|
||||
SectionHeader(
|
||||
l10n.radioStats_chartCaption,
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: CustomPaint(
|
||||
painter: _NoiseChartPainter(
|
||||
samples: List<double>.from(_noiseHistory),
|
||||
colorScheme: scheme,
|
||||
textTheme: tt,
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoiseChartPainter extends CustomPainter {
|
||||
final List<double> samples;
|
||||
final ColorScheme colorScheme;
|
||||
final TextTheme textTheme;
|
||||
|
||||
_NoiseChartPainter({
|
||||
required this.samples,
|
||||
required this.colorScheme,
|
||||
required this.textTheme,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final bg = Paint()..color = colorScheme.surfaceContainerHighest;
|
||||
final border = Paint()
|
||||
..color = colorScheme.outlineVariant
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1;
|
||||
final grid = Paint()
|
||||
..color = colorScheme.outlineVariant.withValues(alpha: 0.5)
|
||||
..strokeWidth = 1;
|
||||
final line = Paint()
|
||||
..color = colorScheme.primary
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
|
||||
bg,
|
||||
);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
|
||||
border,
|
||||
);
|
||||
|
||||
const padL = 40.0;
|
||||
const padR = 8.0;
|
||||
const padT = 8.0;
|
||||
const padB = 24.0;
|
||||
final chart = Rect.fromLTRB(
|
||||
padL,
|
||||
padT,
|
||||
size.width - padR,
|
||||
size.height - padB,
|
||||
);
|
||||
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
final y = chart.top + (chart.height * i / 4);
|
||||
canvas.drawLine(Offset(chart.left, y), Offset(chart.right, y), grid);
|
||||
}
|
||||
|
||||
if (samples.length < 2) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '—',
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
tp.paint(
|
||||
canvas,
|
||||
Offset(chart.left + 4, chart.top + chart.height / 2 - tp.height / 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
double minV = samples.reduce((a, b) => a < b ? a : b);
|
||||
double maxV = samples.reduce((a, b) => a > b ? a : b);
|
||||
if ((maxV - minV).abs() < 1) {
|
||||
minV -= 2;
|
||||
maxV += 2;
|
||||
}
|
||||
final span = maxV - minV;
|
||||
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
final v = maxV - span * i / 4;
|
||||
final tp = _yAxisLabel(v);
|
||||
final y = chart.top + (chart.height * i / 4) - tp.height / 2;
|
||||
tp.paint(canvas, Offset(4, y));
|
||||
}
|
||||
|
||||
final path = Path();
|
||||
for (var i = 0; i < samples.length; i++) {
|
||||
final x = chart.left + (chart.width * i / (samples.length - 1));
|
||||
final t = (samples[i] - minV) / span;
|
||||
final y = chart.bottom - t * chart.height;
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, line);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _NoiseChartPainter oldDelegate) {
|
||||
return oldDelegate.samples.length != samples.length ||
|
||||
oldDelegate.colorScheme != colorScheme;
|
||||
}
|
||||
|
||||
TextPainter _yAxisLabel(double v) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(
|
||||
text: v.round().toString(),
|
||||
style: textTheme.labelSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
return tp;
|
||||
}
|
||||
}
|
||||
+520
-303
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user