Merge pull request #490 from meshtastic/2.2.23_Working_Changes

2.2.23 working changes
This commit is contained in:
Garth Vander Houwen 2024-02-15 20:21:57 -08:00 committed by GitHub
commit f9b370aa1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 431 additions and 184 deletions

View file

@ -258,6 +258,7 @@
DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = "<group>"; };
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = "<group>"; };
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = "<group>"; };
DD31EC492B7F18B7006A3995 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = "<group>"; };
DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareApi.swift; sourceTree = "<group>"; };
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
@ -1071,6 +1072,7 @@
Base,
"zh-Hans",
pl,
he,
);
mainGroup = DDC2E14B26CE248E0042C5E4;
packageReferences = (
@ -1372,6 +1374,7 @@
DDCDC6CE294821AD004C1DDA /* de */,
A65FA974296876BF00A97686 /* zh-Hans */,
DDF6B24B2A9C2FC800BA6931 /* pl */,
DD31EC492B7F18B7006A3995 /* he */,
);
name = Localizable.strings;
sourceTree = "<group>";
@ -1522,7 +1525,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.22;
MARKETING_VERSION = 2.2.23;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1556,7 +1559,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.22;
MARKETING_VERSION = 2.2.23;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1678,7 +1681,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.22;
MARKETING_VERSION = 2.2.23;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1711,7 +1714,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.22;
MARKETING_VERSION = 2.2.23;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -9,156 +9,51 @@ import Foundation
import MapKit
class OfflineTileManager: ObservableObject {
enum DownloadStatus {
case download, downloading, downloaded
}
static let shared = OfflineTileManager()
init() {
print("Documents Directory = \(documentsDirectory)")
createDirectoriesIfNecessary()
}
// MARK: - Private properties
private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServer.openStreetMap.tileUrl) }
private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! }
private let fileManager = FileManager.default
// MARK: - Public property
var progress: Float = 0
var status: DownloadStatus = .download
// MARK: - Public methods
func getAllDownloadedSize() -> String {
fileManager.allocatedSizeOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"))
}
func hasBeenDownloaded(for boundingBox: MKMapRect) -> Bool {
getEstimatedDownloadSize(for: boundingBox) == 0
}
func getEstimatedDownloadSize(for boundingBox: MKMapRect) -> Double {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
let count = self.filterTilesAlreadyExisting(paths: paths).count
let size: Double = 30000 // Bytes (average size)
return Double(count) * size
}
func getDownloadedSize(for mapTileLink: MapTileServer) -> Double {
var accumulatedSize: UInt64 = 0
let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: [])
let matchingTiles = mapTiles.filter { fileName in
let fileNameLower = fileName.absoluteString
return fileNameLower.contains(mapTileLink.id)
}
print("Deleting \(matchingTiles.count) tiles for \(mapTileLink.id)")
for tile in matchingTiles {
let url = documentsDirectory.appendingPathComponent(tile.absoluteString)
accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0
}
return Double(accumulatedSize)
}
func getDownloadedSize(for boundingBox: MKMapRect) -> Double {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
var accumulatedSize: UInt64 = 0
for path in paths {
let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
let url = documentsDirectory.appendingPathComponent(file)
accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0
}
return Double(accumulatedSize)
}
func removeAll() {
try? fileManager.removeItem(at: documentsDirectory.appendingPathComponent("tiles"))
createDirectoriesIfNecessary()
}
func remove(for mapTileLink: MapTileServer) {
let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: [])
let matchingTiles = mapTiles.filter { fileName in
let fileNameLower = fileName.absoluteString
return fileNameLower.contains(mapTileLink.id)
func loadAndCacheTileOverlay(for path: MKTileOverlayPath) throws -> Data {
guard UserDefaults.enableOfflineMaps, UserDefaults.mapTileServer.zoomRange.contains(path.z) else {
return try Data(contentsOf: Bundle.main.url(forResource: "alpha", withExtension: "png")!)
}
print("Deleting \(matchingTiles.count) tiles for \(mapTileLink.id)")
for tile in matchingTiles {
try? fileManager.removeItem(at: tile.absoluteURL)
}
}
func remove(for boundingBox: MKMapRect) {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
for path in paths {
let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
let url = documentsDirectory.appendingPathComponent(file)
try? fileManager.removeItem(at: url)
}
self.status = .download
}
/// Download and persist all tiles within the boundingBox
func download(boundingBox: MKMapRect, name: String) {
NetworkManager.shared.runIfNetwork {
self.status = .downloading
self.progress = 0.01
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
let filteredPaths = self.filterTilesAlreadyExisting(paths: paths)
for i in 0..<filteredPaths.count {
self.persistLocally(path: filteredPaths[i])
self.progress = Float(i) / Float(filteredPaths.count)
}
DispatchQueue.main.async {
// NotificationManager.shared.sendNotification(title: "\("DownloadedTitle".localized) (\((self.getDownloadedSize(for: boundingBox)).toBytes))", message: "\("Downloaded".localized) (\(name))")
self.progress = 0
self.status = .downloaded
}
}
}
func getTileOverlay(for path: MKTileOverlayPath) -> URL {
let file = "\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
// Check is tile is already available
let tilesUrl = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file)
if fileManager.fileExists(atPath: tilesUrl.path) {
return tilesUrl
} else {
if UserDefaults.enableOfflineMaps, UserDefaults.mapTileServer.zoomRange.contains(path.z) { // Get and persist newTile
return persistLocally(path: path)
} else { // Else display empty tile (transparent over Maps tiles)
return Bundle.main.url(forResource: "alpha", withExtension: "png")!
}
}
}
// MARK: Private methods
private func computeTileOverlayPaths(boundingBox box: MKMapRect, maxZ: Int = 17) -> [MKTileOverlayPath] {
var paths = [MKTileOverlayPath]()
for z in 1...maxZ {
let topLeft = tranformCoordinate(coordinates: MKMapPoint(x: box.minX, y: box.minY).coordinate, zoom: z)
let topRight = tranformCoordinate(coordinates: MKMapPoint(x: box.maxX, y: box.minY).coordinate, zoom: z)
let bottomLeft = tranformCoordinate(coordinates: MKMapPoint(x: box.minX, y: box.maxY).coordinate, zoom: z)
for x in topLeft.x...topRight.x {
for y in topLeft.y...bottomLeft.y {
paths.append(MKTileOverlayPath(x: x, y: y, z: z, contentScaleFactor: 2))
}
}
}
return paths
}
private func tranformCoordinate(coordinates: CLLocationCoordinate2D, zoom: Int) -> TileCoordinates {
let lng = coordinates.longitude
let lat = coordinates.latitude
let tileX = Int(floor((lng + 180) / 360.0 * pow(2.0, Double(zoom))))
let tileY = Int(floor((1 - log( tan( lat * Double.pi / 180.0 ) + 1 / cos( lat * Double.pi / 180.0 )) / Double.pi ) / 2 * pow(2.0, Double(zoom))))
return (tileX, tileY, zoom)
}
@discardableResult private func persistLocally(path: MKTileOverlayPath) -> URL {
let url = overlay.url(forTilePath: path)
let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
let filename = documentsDirectory.appendingPathComponent(file)
let tilesUrl = documentsDirectory
.appendingPathComponent("tiles")
.appendingPathComponent("\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y)")
.appendingPathExtension("png")
do {
let data = try Data(contentsOf: url)
try data.write(to: filename)
} catch {
print("💀 Save Tile Error = \(error)")
return url
}
return filename
}
private func filterTilesAlreadyExisting(paths: [MKTileOverlayPath]) -> [MKTileOverlayPath] {
paths.filter {
let file = "\(UserDefaults.mapTileServer.id)-z\($0.z)x\($0.x)y\($0.y).png"
let tilesPath = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file).path
return !fileManager.fileExists(atPath: tilesPath)
return try Data(contentsOf: tilesUrl)
} catch let error as NSError where error.code == NSFileReadNoSuchFileError {
let data = try Data(contentsOf: overlay.url(forTilePath: path))
try data.write(to: tilesUrl)
return data
}
}
// MARK: Private methods
private func createDirectoriesIfNecessary() {
let tiles = documentsDirectory.appendingPathComponent("tiles")
try? fileManager.createDirectory(at: tiles, withIntermediateDirectories: true, attributes: [:])

View file

@ -8,8 +8,8 @@
import Foundation
import MapKit
typealias TileCoordinates = (x: Int, y: Int, z: Int)
class TileOverlay: MKTileOverlay {
override func url(forTilePath path: MKTileOverlayPath) -> URL { OfflineTileManager.shared.getTileOverlay(for: path) }
override func loadTile(at path: MKTileOverlayPath) async throws -> Data {
return try OfflineTileManager.shared.loadAndCacheTileOverlay(for: path)
}
}

View file

@ -203,7 +203,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
///
/// Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT
/// Newer V1.1, version is written on the PCB near the display.
case heltecWirelessTrackerV11 // = 48
case heltecWirelessTracker // = 48
///
/// Heltec Wireless Paper with ESP32-S3 CPU and E-Ink display
@ -307,7 +307,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case 45: self = .betafpv2400Tx
case 46: self = .betafpv900NanoTx
case 47: self = .rpiPico
case 48: self = .heltecWirelessTrackerV11
case 48: self = .heltecWirelessTracker
case 49: self = .heltecWirelessPaper
case 50: self = .tDeck
case 51: self = .tWatchS3
@ -367,7 +367,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case .betafpv2400Tx: return 45
case .betafpv900NanoTx: return 46
case .rpiPico: return 47
case .heltecWirelessTrackerV11: return 48
case .heltecWirelessTracker: return 48
case .heltecWirelessPaper: return 49
case .tDeck: return 50
case .tWatchS3: return 51
@ -432,7 +432,7 @@ extension HardwareModel: CaseIterable {
.betafpv2400Tx,
.betafpv900NanoTx,
.rpiPico,
.heltecWirelessTrackerV11,
.heltecWirelessTracker,
.heltecWirelessPaper,
.tDeck,
.tWatchS3,
@ -2622,7 +2622,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
45: .same(proto: "BETAFPV_2400_TX"),
46: .same(proto: "BETAFPV_900_NANO_TX"),
47: .same(proto: "RPI_PICO"),
48: .same(proto: "HELTEC_WIRELESS_TRACKER_V1_1"),
48: .same(proto: "HELTEC_WIRELESS_TRACKER"),
49: .same(proto: "HELTEC_WIRELESS_PAPER"),
50: .same(proto: "T_DECK"),
51: .same(proto: "T_WATCH_S3"),

View file

@ -209,17 +209,19 @@ struct Connect: View {
}.padding([.bottom, .top])
}
}
.confirmationDialog("Connecting to a new radio will clear all local app data on the phone. The app may close.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
.confirmationDialog("Connecting to a new radio will clear all local app data on the phone and will reset all app settings.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
Button("Connect to new radio?", role: .destructive) {
UserDefaults.preferredPeripheralId = selectedPeripherialId
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
bleManager.disconnectPeripheral()
}
context.reset()
do {
clearCoreDataDatabase(context: context)
PersistenceController.shared.clearDatabase()
context.reset()
UserDefaults.standard.reset()
} catch let error {
print("💣 Failed to re-create CoreData database: " + error.localizedDescription)
}

View file

@ -28,15 +28,16 @@ struct LoRaSignalStrengthMeter: View {
.foregroundColor(getRssiColor(rssi: rssi))
.font(.caption2)
}
.padding(.bottom, 2)
} else {
Gauge(value: Double(signalStrength.rawValue), in: 0...3) {
} currentValueLabel: {
Image(systemName: "dot.radiowaves.left.and.right")
.font(.caption)
.font(.callout)
.frame(width: 30)
Text("Signal \(signalStrength.description)")
.font(.caption)
.font(.callout)
.foregroundColor(.gray)
.fixedSize()
}
.gaugeStyle(.accessoryLinear)
.tint(gradient)

View file

@ -225,7 +225,7 @@ struct ChannelMessageList: View {
.padding(.bottom)
.id(channel.allPrivateMessages.firstIndex(of: message))
if currentUser && (message.ackError == 5 || message.ackError == 3) {
if currentUser && (message.ackError == 9 || message.ackError == 5 || message.ackError == 3) {
RetryButton(message: message)
}

View file

@ -203,7 +203,7 @@ struct UserMessageList: View {
.padding(.bottom)
.id(user.messageList.firstIndex(of: message))
if currentUser && (message.receivedACK && !message.realACK) {
if currentUser && (message.ackError == 9 || message.ackError == 5 || message.ackError == 3) || (message.receivedACK && !message.realACK) {
RetryButton(message: message)
}

View file

@ -21,7 +21,9 @@ struct NodeListItem: View {
LazyVStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65)
CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70)
.padding(.trailing, 5)
BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor)
.padding(.trailing, 5)
}
VStack(alignment: .leading) {
@ -41,7 +43,10 @@ struct NodeListItem: View {
.font(.callout)
.symbolRenderingMode(.hierarchical)
.foregroundColor(.green)
Text("connected").font(.callout)
.frame(width: 30, height: 15)
Text("connected")
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
}
HStack {
@ -49,8 +54,9 @@ struct NodeListItem: View {
.font(.callout)
.symbolRenderingMode(.hierarchical)
.foregroundColor(node.isOnline ? .green : .orange)
.frame(width: 30, height: 20)
LastHeardText(lastHeard: node.lastHeard)
.font(.caption)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
HStack {
@ -58,8 +64,9 @@ struct NodeListItem: View {
Image(systemName: role?.systemName ?? "figure")
.font(.callout)
.symbolRenderingMode(.hierarchical)
.frame(width: 30, height: 20)
Text("Role: \(role?.name ?? "unknown".localized)")
.font(.caption)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
if node.isStoreForwardRouter {
@ -67,8 +74,9 @@ struct NodeListItem: View {
Image(systemName: "envelope.arrow.triangle.branch")
.font(.callout)
.symbolRenderingMode(.hierarchical)
.frame(width: 30, height: 20)
Text("storeforward".localized)
.font(.caption)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
}
@ -85,7 +93,9 @@ struct NodeListItem: View {
Image(systemName: "lines.measurement.horizontal")
.font(.callout)
.symbolRenderingMode(.hierarchical)
DistanceText(meters: metersAway).font(.caption)
.frame(width: 30, height: 20)
DistanceText(meters: metersAway)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
}
@ -98,7 +108,9 @@ struct NodeListItem: View {
Image(systemName: "lines.measurement.horizontal")
.font(.callout)
.symbolRenderingMode(.hierarchical)
DistanceText(meters: metersAway).font(.caption)
.frame(width: 30, height: 20)
DistanceText(meters: metersAway)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
}
@ -109,6 +121,7 @@ struct NodeListItem: View {
Image(systemName: "fibrechannel")
.font(.callout)
.symbolRenderingMode(.hierarchical)
.frame(width: 30, height: 20)
Text("Channel: \(node.channel)")
.foregroundColor(.gray)
.font(.caption)
@ -117,44 +130,62 @@ struct NodeListItem: View {
Image(systemName: "network")
.symbolRenderingMode(.hierarchical)
.font(.callout)
.frame(width: 30, height: 20)
Text("Via MQTT")
.foregroundColor(.gray)
.font(.caption)
}
}
if node.hasPositions || node.hasEnvironmentMetrics || node.hasDetectionSensorMetrics || node.hasTraceRoutes {
HStack {
Image(systemName: "scroll")
.symbolRenderingMode(.hierarchical)
.font(.callout)
.frame(width: 30, height: 20)
Text("Logs:")
.foregroundColor(.gray)
.font(.callout)
if node.hasDeviceMetrics {
Image(systemName: "flipphone")
.symbolRenderingMode(.hierarchical)
.font(.callout)
.frame(width: 30, height: 20)
}
if node.hasPositions {
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.hierarchical)
.font(.callout)
.frame(width: 30, height: 20)
}
if node.hasEnvironmentMetrics {
Image(systemName: "cloud.sun.rain")
.symbolRenderingMode(.hierarchical)
.font(.callout)
.frame(width: 30, height: 20)
}
if node.hasDetectionSensorMetrics {
Image(systemName: "sensor")
.symbolRenderingMode(.hierarchical)
.font(.callout)
.frame(width: 30, height: 20)
}
if node.hasTraceRoutes {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.hierarchical)
.font(.callout)
.frame(width: 30, height: 20)
}
}
.padding(.top)
}
if !connected {
HStack {
let preset = ModemPresets(rawValue: Int(modemPreset))
LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true)
.padding(.top, 2)
}
.padding(.top)
}
HStack {
BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor)
if node.hasPositions {
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.hierarchical)
.font(.callout)
}
if node.hasEnvironmentMetrics {
Image(systemName: "cloud.sun.rain")
.symbolRenderingMode(.hierarchical)
.font(.callout)
}
if node.hasDetectionSensorMetrics {
Image(systemName: "sensor")
.symbolRenderingMode(.hierarchical)
.font(.callout)
}
if node.hasTraceRoutes {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.hierarchical)
.font(.callout)
}
}
.padding(.top, 3)
}
.frame(maxWidth: .infinity, alignment: .leading)
}

View file

@ -33,7 +33,7 @@ struct MQTTConfig: View {
if node != nil && node?.loRaConfig != nil {
let rc = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))
if rc?.dutyCycle ?? 0 <= 10 {
Text("Your region has a \(rc?.dutyCycle ?? 0)% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffice will quickly overwhelm your LoRa mesh.")
Text("Your region has a \(rc?.dutyCycle ?? 0)% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffic will quickly overwhelm your LoRa mesh.")
.font(.callout)
.foregroundColor(.red)
}

View file

@ -0,0 +1,315 @@
/*
Localizable.strings
Meshtastic
Copyright(c) Garth Vander Houwen on 12/12/22.
*/
"about"="אודות";
"about.meshtastic"="אודות משטסטיק";
"admin"="אדמין";
"admin.log"="היסטוריית הודעות אדמין";
"ago"="עברו";
"airtime"="זמן אוויר";
"always.on"="תמיד דלוק";
"ambient.lighting"="תאורת סביבה";
"ambient.lighting.config"="הגדרות תאורת סביבה";
"appsettings"="הגדרות אפליקציה";
"appsettings.provide.location"="שתף מיקום";
"appsettings.smartposition"="מיקום חכם";
"are.you.sure"="האם אתה בטוח?";
"ascii.capable"="בעל יכולת ASCII";
"available.radios"="מכשירים זמינים";
"automatic.detection"="זיהוי אוטומטי";
"battery.level"="רמת סוללה";
"ble.name"="שם בלוטוס";
"ble.connection.timeout %d %@"="התחברות נכשלה לאחר %d נסיונות להתחבר ל%@. יתכן ויש צורך 'לשכוח' את המכשיר בהגדרות מכשיר > בלוטוס.";
"ble.errorcode.6 %@"="%@ האפליקציה תנסה אוטומטית להתחבר מחדש למכשיר המועדף אם ייראה.";
"ble.errorcode.14 %@"="%@ שגיאה זו בדרך כלל אינה ניתנת לתיקון ללא שכחחת המכשיר בהגדרות מכשיר > בלוטוס ואז להתחבר מחדש למכשיר.";
"ble.errorcode.pin %@"="%@ בבקשה נסה שנית להתחבר למכשיר ובדוק את הקוד.";
"bluetooth"="בלוטוס";
"bluetooth.off"="בלוטוס כבוי";
"bluetooth.config"="הגדרות בלוטוס";
"bluetooth.mode.randompin"="קוד אקראי";
"bluetooth.mode.fixedpin"="קוד קבוע";
"bluetooth.mode.nopin"="ללא קוד (פשוט עובד)";
"bluetooth.pairingmode"="מצב הצמדה";
"bluetooth.pin.validation"="קוד בלוטוס חייבת להיות בת 6 ספרות.";
"bytes"="בייטים";
"cancel"="בטל";
"canned.messages"="הודעות קבועות";
"canned.messages.config"="הגדרות הודעות קבועות";
"canned.messages.preset.manual"="הגדרה ידנית";
"canned.messages.preset.rakrotary"="RAK Rotary Encoder Module"; /* left untranslated for clarity */
"canned.messages.preset.cardkb"="M5 Stack Card KB / RAK Keypad"; /* left untranslated for clarity */
"channel"="ערוץ";
"channel.role.disabled"="כבוי";
"channel.role.primary"="עיקרי";
"channel.role.secondary"="משני";
"channel.utilization"="שימוש ערוץ";
"channels"="ערוצים";
"clear.app.data"="אפס הגדרות אפליקציה";
"clear.log"="נקה";
"close"="סגור";
"config.save.confirm"="לאחר שמירת הגדרות המכשיר יתחיל מחדש.";
"communicating"="מתקשר עם מכשיר. .";
"connected.radio"="מכשיר מחובר";
"connected"="מחובר בבלוטוס";
"connecting"="מתחבר . .";
"contacts"="אנשי קשר";
"contacts %@"="אנשי קשר (%@)";
"copy"="העתק";
"current"="נוכחי";
"default"="ברירת מחדל";
"delete"="מחק";
"detection.sensor"="חיישן זיהוי";
"detection.sensor.config"="הגדרות חיישן זיהוי";
"detection.sensor.log"="יומן חיישן זיהוי";
"device"="מכשיר";
"device.config"="הגדרות מכשיר";
"device.metrics.delete"="נקה יומן מכשיר?";
"device.metrics.log"="יומן מכשיר";
"device.role.client"="אפליקציה מחוברת או מכשיר תקשורת עצמאי.";
"device.role.clientmute"="מכשיר שאינו מעביר הודעות ממכשירים אחרים הלאה.";
"device.role.clienthidden"="מכשיר שרק משדר לפי צורך בכדי לחסוך בחשמל או לשמור על חשאיות.";
"device.role.tracker"="משדר מיקום GPS בעדיפות גבוהה.";
"device.role.lostandfound"="משדר מיקום כהודעה לערוץ ברירת מחדל לעיתים קבועות בכדי לסייע במציאת המכשיר.";
"device.role.sensor"="משדר טלמטריה בעדיפות גבוהה.";
"device.role.tak"="מותאם למערכת ATAK, מקטין תקשורת קבועה.";
"device.role.repeater"="מכשיר תשתית להרחבת המש על ידי העברת הודעות עם דאטה נוסף מינימלי.";
"device.role.router"="מכשיר תשתית להרחבת המש על ידי העברת הודעות. מופיע ברשימת מכשירים.";
"device.role.routerclient"="קומבינציה של ROUTER וCLIENT. לא למכשירים ניידים.";
"direct.messages"="הודעה פרטית";
"dismiss.keyboard"="סגור מקלדת";
"display"="צג מכשיר";
"display.config"="הגדרות צג";
"distance"="מרחק";
"disconnect"="התנתק";
"echo"="הד";
"email.address"="כתובת דואר אלקטרוני";
"enabled"="מופעל";
"encrypted"="מוצפן";
"external.notification"="נוטיפיקציה חיצונית";
"external.notification.config"="הגדרות נוטיפיקציה חיצונית";
"finish"="סיים";
"firmware.version"="גרסת קושחה";
"firmware.version.unsupported"="גרסת קושחה אינה נתמכת, לא ניתן להתחבר למכשיר.";
"gas"="דלק";
"gas.resistance"="Gas Resistance"; /* left untranslated for clarity */
"generate.qr.code"="צור קוד QR";
"gpsformat.dec"="פורמט קואורדינטות";
"gpsformat.dms"="מעלות דקות שניות";
"gpsformat.utm"="Universal Transverse Mercator"; /* left untranslated for clarity. A translation exists but is frankly indecipherable */
"gpsformat.mgrs"="Military Grid Reference System"; /* left untranslated for clarity. A translation exists but is frankly indecipherable */
"gpsformat.olc"="Open Location Code (aka Plus Codes)"; /* left untranslated for clarity. A translation exists but is frankly indecipherable */
"gpsformat.osgr"="Ordnance Survey Grid Reference"; /* left untranslated for clarity. A translation exists but is frankly indecipherable */
"gpsmode.disabled"="כבוי";
"gpsmode.enabled"="מופעל";
"gpsmode.notPresent"="לא קיים";
"heard"="נשמע";
"heard.last"="נשמע לאחרונה";
"hybrid"="היברידי";
"hybrid.flyover"="היברידי מלמעלה";
"include"="כלול";
"inputevent.none"="ללא";
"inputevent.up"="למעלה";
"inputevent.down"="למטה";
"inputevent.left"="שמאלה";
"inputevent.right"="ימינה";
"inputevent.select"="בחר";
"inputevent.back"="אחרוה";
"inputevent.cancel"="בטל";
"interval.one.second"="שניה אחת";
"interval.two.seconds"="שתי שניות";
"interval.three.seconds"="שלוש שניות";
"interval.four.seconds"="ארבע שניות";
"interval.five.seconds"="חמש שניות";
"interval.ten.seconds"="עשר שניות";
"interval.fifteen.seconds"="חמש עשרה שניות";
"interval.twenty.seconds"="עשרים שניות";
"interval.twentyfive.seconds"="עשרים וחמש שניות";
"interval.thirty.seconds"="שלושים שניות";
"interval.fortyfive.seconds"="ארבעים וחמש שניות";
"interval.one.minute"="דקה אחת";
"interval.two.minutes"="שתי דקות";
"interval.five.minutes"="חמש דקות";
"interval.ten.minutes"="עשר דקות";
"interval.fifteen.minutes"="חמש עשרה דקות";
"interval.thirty.minutes"="שלושים דקות";
"interval.one.hour"="שעה אחת";
"interval.two.hours"="שעתיים";
"interval.three.hours"="שלוש שעות";
"interval.four.hours"="ארבע שעות";
"interval.five.hours"="חמש שעות";
"interval.six.hours"="שש שעות";
"interval.twelve.hours"="שניים עשר שעות";
"interval.eighteen.hours"="שמונה עשר שעות";
"interval.twentyfour.hours"="עשרים וארבע שעות";
"interval.thirtysix.hours"="שלושים ושש שעות";
"interval.fortyeight.hours"="ארבעים ושמונה שעות";
"interval.seventytwo.hours"="שבעים ושתיים שעות";
"keyboard.type"="סוג מקלדת";
"logging"="רישום";
"lora"="לורה";
"lora.config"="הגדרות לורה";
"map"="מפת מש";
"map.type"="סוג ברירת מחדל";
"map.centering"="מכשיר במרכז";
"map.tiles.delete"="מחק כל חלקי מפה שמורים";
"map.recentering"="מרכז מפה אוטומטית";
"map.use.legacy"="השתמש במפה מדור קודם";
"map.usertrackingmode"="מצב מעקב אחר משתמש";
"map.usertrackingmode.follow"="עקוב";
"map.usertrackingmode.followwithheading"="עקוב עם כיוון";
"map.usertrackingmode.none"="ללא";
"mesh.live.activity"="פעילות מש חיה";
"mesh.log"="יומן מש";
"mesh.log.ambientlighting.config %@"="הגדרות מודולת תאורת סביבה התקבלו: %@";
"mesh.log.bluetooth.config %@"="הגדרות בלוטוס התקבלו: %@";
"mesh.log.cannedmessage.config %@"="הגדרות מודולת תגובות שמורות התקבלו: %@";
"mesh.log.cannedmessages.messages.get %@"="התבקשו הודעות מודולת הודעות שמורות עבור מכשיר: %@";
"mesh.log.cannedmessages.messages.received %@"="הודעות עבור הודעות שמורות התקבלו מ-%@";
"mesh.log.channel.sent %@ %d"="נשלח ערוץ עבור: %@ אינדקס ערוצים %d";
"mesh.log.channel.received %d %@"="ערוץ %d התקבל מ-%@";
"mesh.log.device.config %@"="הגדרות מכשיר התקבלו: %@";
"mesh.log.display.config %@"="הגדרות תצוגה התקבלו: %@";
"mesh.log.devicemetadata %@"="מבקש מטא-דאטה עבור %@";
"mesh.log.device.metadata.received %@"="מטא-דאטה של מכשיר התקבל מ-%@";
"mesh.log.detectionsensor.config %@"="הגדרות מודולת חיישן זיהוי התקבלו: %@";
"mesh.log.externalnotification.config %@"="הגדרות מודולת נוטיפיקציה חיצונית התקבלו: %@";
"mesh.log.lora.config %@"="הגדרות לורה התקבלו: %@";
"mesh.log.lora.config.sent %@"="נשלחו הגדרות לורה עבור: %@";
"mesh.log.mqtt.config %@"="הגדרות מודולת MQTT התקבלו: %@";
"mesh.log.myinfo %@"="MyInfo התקבל: %@";
"mesh.log.network.config %@"="הגדרות רשת התקבלו: %@";
"mesh.log.nodeinfo.received %@"="מידע אודות מכשיר התקבל: %@";
"mesh.log.position.config %@"="הגדרות מיקום התקבלו: %@";
"mesh.log.position.received %@"="הודעת מיקום התקבלו מ-%@";
"mesh.log.rangetest.config %@"="הגדרות מודולת בדיקת טווח התקבלו: %@";
"mesh.log.ringtone.config %@"="הגדרות RTTTL רינגטון התקבלו: %@";
"mesh.log.routing.message %@ %@"="התקבל מסלול עבור בקשה: %@ מצב שליחה: %@";
"mesh.log.serial.config %@"="הגדרות מודולת תקשורת סיריאלית התקבלו: %@";
"mesh.log.sharelocation %@"="נשלח מיקום ממכשיר האפל למכשיר המשטסטיק: %@";
"mesh.log.storeforward.config %@"="הגדרות מודולת שמירה ושליחה התקבלו: %@";
"mesh.log.telemetry.config %@"="הגדרות מודולת טלמטריה התקבלו: %@";
"mesh.log.telemetry.received %@"="התקבל טלמטריה עבור: %@";
"mesh.log.textmessage.received"="הודעת טקסט התקבלה.";
"mesh.log.textmessage.send.failed %@"="שליחת הודעה נכשלה, אין חיבוריות ל-%@";
"mesh.log.textmessage.sent %@ %@ %@"="נשלחה הודעה %@ מ-%@ ל-%@";
"mesh.log.traceroute.received.direct %@"="בקשת בדיקת מסלול נשלחה למכשיר: %@ התקבל ישירות.";
"mesh.log.traceroute.received.route %@"="בקשת בדיקת מסלול הצליחה: %@";
"mesh.log.traceroute.sent %@"="נשלחה בקשת בדיקת מסלול למכשיר: %@";
"mesh.log.wantconfig %@"="שולח בקשת הגדרות ל-%@";
"mesh.log.waypoint.sent %@"="נשלחה נקודת ציון מ-%@";
"mesh.log.waypoint.received %@"="נקודת ציון התקבלה מ-%@";
"message"="הודעה";
"message.details"="פרטי הודעה";
"messages"="הודעות";
"mode"="מצב";
"module.configuration"="הגדרות מודולה";
"mqtt"="MQTT"; /*left untranslated for clarity */
"mqtt.connect"="התחבר ל-MQTT";
"mqtt.config"="הגדרות MQTT";
"mqtt.clientproxy"="MQTT Client Proxy"; /*left untranslated for clarity */
"mqtt.disconnect"="התנתק מ-MQTT";
"mqtt.username"="שם משתמש";
"name"="שם";
"network"="רשת";
"network.config"="הגדרות רשת";
"nodes"="מכשירים";
"nodes %@"="מכשירים (%@)";
"no.nodes"="לא נמצאו מכשירי משטסטיק";
"not.connected"="אין מכשיר מחובר";
"numbers.punctuation"="מספרים וסימני פיסוק ";
"off"="כבוי";
"offline"="מנותק";
"on.boot"="רק בעת הדלקה";
"options"="הגדרות";
"password"="סיסמא";
"pause"="הפסק";
"phone.gps"="GPS מהטלפון";
"phone.gps.interval.description"="כל כמה זמן מכשיר הטלפון ישלח את מיקומך למכשיר המשטסטיק. עדכוני מיקום למש מנוהלות על ידי המכשיר.";
"position"="מיקום";
"position.config"="הגדרות מיקום";
"preferred.radio"="רדיו מועדף";
"radio.configuration"="הגדרות רדיו";
"range.test"="בדיקת טווח";
"range.test.blocked"="חסום בדיקות טווח";
"range.test.config"="הגדרות בדיקת טווח";
"reply"="תגובה";
"reboot"="התחל מחדש";
"reboot.node"="התחל מכשיר מחדש??";
"received.ack"="התקבל אישור מסירה";
"received.ack.real"="התקבל אישור מסירה מהנמען";
"resume"="החל מחדש";
"ringtone"="רינגטון";
"ringtone.config"="הגדרות רינגטון";
"route.recorder"="מקליט מסלול";
"routes"="מסלולים";
"routing.acknowledged"="מאשר";
"routing.noroute"="אין מסלול";
"routing.gotnak"="התקבל אישור מסירה שלילי";
"routing.timeout"="נגמר הזמן";
"routing.nointerface"="אין ממשק";
"routing.maxretransmit"="הגיע למקסימום השליחות מדש";
"routing.nochannel"="אין ערוץ";
"routing.toolarge"="ההודעה ארוכה/גדולה מידי";
"routing.noresponse"="אין תגובה";
"routing.dutycyclelimit"="הגיע למקסימום שימוש אזורי לשעה זו";
"routing.badRequest"="בקשה לא תקינה";
"routing.notauthorized"="לא מאושר";
"satellite"="לווין";
"satellite.flyover"="לווין בשמיים";
"save"="שמור";
"save.config %@"="שמור הגדרות עבור %@";
"serial"="סיריאלי";
"serial.config"="'הגדרות מודולה 'סיריאלי";
"serial.mode.default"="ברירת מחדל";
"serial.mode.simple"="פשוט";
"serial.mode.proto"="Protobufs"; /*left untranslated for clarity */
"serial.mode.txtmsg"="הודעת טקסט";
"serial.mode.nmea"="מיקומי NMEA";
"settings"="הגדרות";
"share.channels"="שתף ערוצים באמצעות קוד QR";
"share.position"="שתף מיקום";
"subscribed"="מחובר למש";
"select.contact"="בחר איש קשר";
"select.node"="בחר מכשיר";
"select.menu.item"="בחר מהתפריט";
"set.region"="בחר אזור לורה";
"standard"="סטנדרטי";
"standard.muted"="סטנדרתי-השתק";
"start"="החל";
"storeforward"="שמירה ושליחה";
"storeforward.config"="הגדרות שמירה ושליחה";
"storeforward.heartbeat"="שלח דופק";
"ssid"="שם רשת וויפי";
"tapback"="תגובה מהירה";
"tapback.heart"="לב";
"tapback.thumbsup"="אגודל למעלה";
"tapback.thumbsdown"="אגודל למטה";
"tapback.haha"="חחח";
"tapback.exclamation"="סימן קריאה";
"tapback.question"="סימן שאלה";
"tapback.poop"="חרא";
"telemetry"="טלמטריה (חיישנים)";
"telemetry.config"="הגדרות טלמטריה";
"timeout"="זמן קצוב";
"timestamp"="שעה/תאריך";
"tip.bluetooth.connect.title"="מכשיר מחובר";
מראה מידע אודות מכשיר המשטסטיק המחובר כעת לבלוטוס. ניתן לגרור שמאלה להתנתקות או לחיצה ארוכה לראות סטטיסטיקה או להתחיל פעילות.
"tip.bluetooth.connect.message"="מראה מידע אודות מכשיר המשטסטיק המחובר כעת לבלוטוס. ניתן לגרור שמאלה להתנתקות או לחיצה ארוכה לראות סטטיסטיקה או להתחיל פעילות.";
"tip.channels.share.title"="משתף ערוצי משטסטיק";
"tip.channels.share.message"="במשטסטיק יש עד 8 ערוצים. הראשון הינו הראשי והינו היכן שרוב הפעילות מתבצעת והכרחי. אם לא תשתף את הערוץ הראשי שלך הערוץ הראשון שלך נהיה הערוץ הראשי ברשת השניה. הוא מדבר בערוץ הראשי שלו במשני שלך. ערוץ בעל השם 'admin' הינו לשליטה מרחוק. ערוצים נוספים הינם לקבוצות פרטיות, כל אחת עם מפתח הצפנה משלה.";
"tip.messages.title"="הודעות";
"tip.messages.message"="ניתן לשלוח הודעות ערוץ (קבוצות צ'אט) והודעות פרטיות. על הודעה ניתן לעשות לחיצה ארוכה בכדי לראות פעולות אפשריות כגון העתק, הגב, תגובה מהירה, מחק ובנוסף לראות מצב שליחה.";
"twitter"="טוויטר";
"unknown"="לא ידוע";
"unknown.age"="גיל לא ידוע";
"unset"="לא נקבע";
"update.firmware"="עדכן קושחה";
"update.interval"="זמן בין עדכונים";
"user"="משתמש";
"user.details"="פרטי משתמש";
"voltage"="וולטז'";
"waiting"="ממתין. . .";