Merge branch 'main' of github.com:meshtastic/Meshtastic-Apple

This commit is contained in:
Nikola Dašić 2025-05-12 15:37:49 +02:00
commit ba84a5d566
148 changed files with 22168 additions and 18309 deletions

File diff suppressed because it is too large Load diff

View file

@ -56,12 +56,12 @@
B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; };
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; };
BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */; };
BC5EBA3C2D002A2000C442FF /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */; };
BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; };
BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; };
BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; };
BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; };
BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; };
BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */; };
BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; };
BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; };
BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */; };
@ -145,7 +145,6 @@
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8EBF42285058FA00426DCA /* DisplayConfig.swift */; };
DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */; };
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */; };
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; };
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; };
DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */; };
DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */; };
@ -176,8 +175,6 @@
DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */; };
DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A0E2A05920E006ED576 /* FileManager.swift */; };
DDB75A112A059258006ED576 /* Url.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A102A059258006ED576 /* Url.swift */; };
DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */; };
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; };
DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; };
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; };
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */; };
@ -326,6 +323,7 @@
BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = "<group>"; };
BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = "<group>"; };
BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = "<group>"; };
BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = "<group>"; };
BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = "<group>"; };
BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = "<group>"; };
@ -437,7 +435,6 @@
DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConfig.swift; sourceTree = "<group>"; };
DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingError.swift; sourceTree = "<group>"; };
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = "<group>"; };
DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = "<group>"; };
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = "<group>"; };
DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapContent.swift; sourceTree = "<group>"; };
DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelForm.swift; sourceTree = "<group>"; };
@ -475,8 +472,6 @@
DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV12.xcdatamodel; sourceTree = "<group>"; };
DDB75A0E2A05920E006ED576 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
DDB75A102A059258006ED576 /* Url.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Url.swift; sourceTree = "<group>"; };
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTileManager.swift; sourceTree = "<group>"; };
DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = "<group>"; };
DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = "<group>"; };
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = "<group>"; };
DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = "<group>"; };
@ -733,7 +728,6 @@
DDDB263E2AABEE20003AFCB7 /* NodeList.swift */,
DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */,
DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */,
DD90860D26F69BAE00DC5189 /* NodeMap.swift */,
DD73FD1028750779000852D6 /* PositionLog.swift */,
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */,
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */,
@ -918,15 +912,6 @@
path = Map;
sourceTree = "<group>";
};
DDB75A122A0593CD006ED576 /* Map */ = {
isa = PBXGroup;
children = (
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */,
DDB75A152A0594AD006ED576 /* TileOverlay.swift */,
);
path = Map;
sourceTree = "<group>";
};
DDC2E14B26CE248E0042C5E4 = {
isa = PBXGroup;
children = (
@ -1064,7 +1049,6 @@
isa = PBXGroup;
children = (
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
DDB75A122A0593CD006ED576 /* Map */,
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */,
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */,
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
@ -1121,6 +1105,7 @@
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */,
251926882C3BAF2E00249DF5 /* Actions */,
BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1251,7 +1236,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1600;
LastUpgradeCheck = 1630;
TargetAttributes = {
25F5D5C62C4375A8008036E3 = {
CreatedOnToolsVersion = 15.4;
@ -1278,10 +1263,9 @@
pl,
he,
fr,
"zh-Hant-TW",
se,
"pt-PT",
sr,
it,
);
mainGroup = DDC2E14B26CE248E0042C5E4;
packageReferences = (
@ -1416,7 +1400,6 @@
DDC4D568275499A500A4208E /* Persistence.swift in Sources */,
DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */,
DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */,
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */,
DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */,
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
@ -1446,7 +1429,6 @@
DDDB444229F8A88700EE2349 /* Double.swift in Sources */,
DDF45C342BC1A48E005ED5F2 /* MQTTIcon.swift in Sources */,
DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */,
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */,
DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */,
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */,
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */,
@ -1499,7 +1481,6 @@
251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */,
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */,
BC5EBA3C2D002A2000C442FF /* MessageNodeIntent.swift in Sources */,
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */,
DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */,
@ -1521,7 +1502,6 @@
DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */,
DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */,
DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */,
DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */,
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */,
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */,
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */,
@ -1562,6 +1542,7 @@
DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */,
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */,
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */,
2344A2AF2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift in Sources */,
2344A2B02D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift in Sources */,
@ -1621,7 +1602,6 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@ -1645,7 +1625,6 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
@ -1695,6 +1674,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@ -1759,6 +1739,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = GCH7VS5Y9R;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -1795,7 +1776,6 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\"";
DEVELOPMENT_TEAM = GCH7VS5Y9R;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
@ -1805,7 +1785,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.5.22;
MARKETING_VERSION = 2.6.2;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1829,7 +1809,6 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\"";
DEVELOPMENT_TEAM = GCH7VS5Y9R;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
@ -1839,7 +1818,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.5.22;
MARKETING_VERSION = 2.6.2;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1860,7 +1839,6 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Widgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
@ -1871,7 +1849,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.22;
MARKETING_VERSION = 2.6.2;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1893,7 +1871,6 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Widgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
@ -1904,7 +1881,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.22;
MARKETING_VERSION = 2.6.2;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1600"
LastUpgradeVersion = "1630"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1600"
LastUpgradeVersion = "1630"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -16,7 +16,7 @@ class AppIntentErrors {
var localizedStringResource: LocalizedStringResource {
switch self {
case let .message(message):
Logger.services.error("App Intent: \(message,privacy: .public)")
Logger.services.error("App Intent: \(message, privacy: .public)")
return "Error: \(message)"
case .notConnected:
Logger.services.error("App Intent: No Connected Node")

View file

@ -13,38 +13,38 @@ import UIKit
@available(iOS 16.4, *)
struct NavigateToNodeIntent: ForegroundContinuableIntent {
static var title: LocalizedStringResource = "Navigate to Node Position"
static var openAppWhenRun: Bool = false
@Parameter(title: "Node Number")
var nodeNum: Int
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog {
if !BLEManager.shared.isConnected {
throw AppIntentErrors.AppIntentError.notConnected
}
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "NodeInfoEntity")
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity],
fetchedNode.count == 1 else {
throw $nodeNum.needsValueError("Could not find node")
}
let nodeInfo = fetchedNode[0]
if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
let longitude = nodeInfo.latestPosition?.coordinate.longitude {
let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)")
if let mapURL = url, UIApplication.shared.canOpenURL(mapURL) {
// Request to continue in foreground before opening the app
try await requestToContinueInForeground()
// Open Apple Maps for navigation
UIApplication.shared.open(mapURL, options: [:], completionHandler: nil)
return .result(dialog: "Navigating to node location.")

View file

@ -15,7 +15,7 @@ struct RestartNodeIntent: AppIntent {
func perform() async throws -> some IntentResult {
try await requestConfirmation(result: .result(dialog: "Reboot Node?"))
try await requestConfirmation(result: .result(dialog: "Reboot node?"))
if !BLEManager.shared.isConnected {
throw AppIntentErrors.AppIntentError.notConnected

View file

@ -29,12 +29,9 @@ struct SaveChannelSettingsIntent: AppIntent {
if channelUrl.absoluteString.lowercased().contains("meshtastic.org/e/#") {
// Split the URL to get the portion after "#"
let components = channelUrl.absoluteString.components(separatedBy: "#")
// Add channels flag based on the URL query parameter (if present)
let addChannels = Bool(channelUrl["add"] ?? "false") ?? false
var channelSettings: String?
// Extract the Base64 encoded channel settings (after "#")
if let lastComponent = components.last {
channelSettings = lastComponent.components(separatedBy: "?").first // Ignore any query parameters
@ -44,7 +41,6 @@ struct SaveChannelSettingsIntent: AppIntent {
if let channelSettings = channelSettings {
// Call the BLEManager to save the channel settings
let saveResult = BLEManager.shared.saveChannelSet(base64UrlString: channelSettings, addChannels: addChannels)
if !saveResult {
throw AppIntentErrors.AppIntentError.message("Failed to save the channel settings.")
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "thinknode_m1.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,109 @@
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg105" sodipodi:docname="thinknode_m1.svg" inkscape:version="1.4 (e7c3feb1, 2024-10-09)" viewBox="397.31 77.24 361 863.17">
<sodipodi:namedview id="namedview105" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="4.066786" inkscape:cx="564.94244" inkscape:cy="741.49463" inkscape:window-width="1472" inkscape:window-height="890" inkscape:window-x="0" inkscape:window-y="38" inkscape:window-maximized="1" inkscape:current-layer="Layer_3"/>
<defs id="defs1">
<style id="style1">.cls-1{fill:#353535;}.cls-2{fill:#262626;}.cls-3{fill:#cccccb;}.cls-4{fill:#2b2b2b;}.cls-5{fill:#f05043;}.cls-6{fill:#3d3d3d;}.cls-7{fill:#231f20;}.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}</style>
</defs>
<g id="Layer_3" data-name="Layer 3">
<path class="cls-1" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path1"/>
<rect class="cls-2" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect1"/>
<rect class="cls-1" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect2"/>
<rect class="cls-3" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect3"/>
<path class="cls-1" d="M507.38,77.74H472.16a7,7,0,0,0-7,7V359.93L452.2,396.26v39.91H561V396.26l-13.3-36V84.15a6.41,6.41,0,0,0-6.41-6.41Z" id="path3"/>
<rect class="cls-2" x="454.25" y="436.17" width="104.38" height="3.65" id="rect4"/>
<polygon class="cls-1" points="442.68 449.91 448.16 440.69 562.98 440.69 570.51 449.91 442.68 449.91" id="polygon4"/>
<rect class="cls-1" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.8" id="rect5"/>
<path class="cls-2" d="M611.2,356v-5.48a3.13,3.13,0,0,1,3.13-3.13h86.35a3.13,3.13,0,0,1,3.13,3.13V356Z" id="path5"/>
<rect class="cls-2" x="611.07" y="416.61" width="92.74" height="23.22" id="rect6"/>
<polygon class="cls-1" points="592.99 449.91 598.47 440.69 713.42 440.69 720.82 449.91 592.99 449.91" id="polygon6"/>
<rect class="cls-2" x="751.94" y="555.13" width="5.87" height="47.48" id="rect7"/>
<path class="cls-2" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path7"/>
<path class="cls-2" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path8"/>
<path class="cls-4" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path9"/>
<rect class="cls-2" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect9"/>
<circle class="cls-5" cx="518.72" cy="853.89" r="5.48" id="circle9"/>
<circle class="cls-1" cx="640.14" cy="853.89" r="5.48" id="circle10"/>
<circle class="cls-1" cx="541.83" cy="853.89" r="5.48" id="circle11"/>
<circle class="cls-1" cx="567.67" cy="853.89" r="5.48" id="circle12"/>
<circle class="cls-1" cx="593.51" cy="853.89" r="5.48" id="circle13"/>
<circle class="cls-1" cx="616.82" cy="853.89" r="5.48" id="circle14"/>
<path class="cls-4" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path14"/>
<path class="cls-4" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path15"/>
<path class="cls-6" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path16"/>
<path class="cls-6" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path17"/>
<path class="cls-6" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path18"/>
<path class="cls-6" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path19"/>
<path class="cls-6" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path20"/>
<path class="cls-6" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path21"/>
<path class="cls-1" d="M477,924.32h-3V903.09h-7.46v-2.65h17.9v2.65H477Z" id="path51"/>
<path class="cls-1" d="M490.59,906.36c0,.43,0,.86,0,1.31s-.07.85-.12,1.2h.2a5.17,5.17,0,0,1,1.44-1.54,7.08,7.08,0,0,1,1.94-.92,7.81,7.81,0,0,1,2.21-.31,8.61,8.61,0,0,1,3.63.68,4.62,4.62,0,0,1,2.19,2.13,8.22,8.22,0,0,1,.73,3.74v11.67h-2.9V912.85a4.72,4.72,0,0,0-1-3.24,3.92,3.92,0,0,0-3.05-1.07,5.65,5.65,0,0,0-3.14.75,4.07,4.07,0,0,0-1.62,2.21,11.17,11.17,0,0,0-.49,3.56v9.26h-2.94V898.91h2.94Z" id="path52"/>
<path class="cls-1" d="M509.82,899.67a1.73,1.73,0,0,1,1.19.46,2.17,2.17,0,0,1,0,2.82,1.74,1.74,0,0,1-1.19.47,1.77,1.77,0,0,1-1.24-.47,2.24,2.24,0,0,1,0-2.82A1.76,1.76,0,0,1,509.82,899.67Zm1.44,6.73v17.92h-2.94V906.4Z" id="path53"/>
<path class="cls-1" d="M525.58,906.06a6.8,6.8,0,0,1,4.85,1.56c1.09,1,1.63,2.71,1.63,5v11.67h-2.91V912.85a4.67,4.67,0,0,0-1-3.24,3.88,3.88,0,0,0-3-1.07c-2,0-3.36.56-4.11,1.67a8.54,8.54,0,0,0-1.14,4.82v9.29H517V906.4h2.37l.44,2.44h.16a5.68,5.68,0,0,1,1.49-1.56,6.41,6.41,0,0,1,2-.92A8.13,8.13,0,0,1,525.58,906.06Z" id="path54"/>
<path class="cls-1" d="M540.53,912.18c0,.36,0,.83,0,1.41s-.07,1.08-.09,1.5h.14l.6-.77.82-1c.28-.34.52-.63.72-.85l5.72-6.05h3.44l-7.26,7.66,7.76,10.26h-3.54L542.57,916l-2,1.78v6.58h-2.91V898.91h2.91Z" id="path55"/>
<path class="cls-1" d="M574.81,924.32H571.3l-12.78-19.83h-.13c0,.51.08,1.12.11,1.82s.07,1.45.1,2.24,0,1.61,0,2.43v13.34h-2.77V900.44h3.48l12.74,19.77h.13c0-.36-.05-.89-.08-1.6s-.07-1.5-.1-2.35,0-1.62,0-2.34V900.44h2.81Z" id="path56"/>
<path class="cls-1" d="M596.48,915.33a12.32,12.32,0,0,1-.58,4,8.32,8.32,0,0,1-1.67,2.93,7,7,0,0,1-2.65,1.82,9.31,9.31,0,0,1-3.46.62,8.63,8.63,0,0,1-3.28-.62,7.29,7.29,0,0,1-2.61-1.82,8.57,8.57,0,0,1-1.72-2.93,11.76,11.76,0,0,1-.62-4,11.43,11.43,0,0,1,1-5,7.12,7.12,0,0,1,2.87-3.14,8.76,8.76,0,0,1,4.45-1.09,8.4,8.4,0,0,1,4.3,1.09,7.45,7.45,0,0,1,2.91,3.14A11,11,0,0,1,596.48,915.33Zm-13.54,0a10.93,10.93,0,0,0,.55,3.66,4.82,4.82,0,0,0,1.72,2.39,5.69,5.69,0,0,0,5.95,0,4.78,4.78,0,0,0,1.73-2.39,10.93,10.93,0,0,0,.55-3.66,10.36,10.36,0,0,0-.57-3.65,4.84,4.84,0,0,0-1.72-2.32,5,5,0,0,0-3-.82,4.5,4.5,0,0,0-4,1.8A8.79,8.79,0,0,0,582.94,915.33Z" id="path57"/>
<path class="cls-1" d="M607.49,924.66a6.67,6.67,0,0,1-5.35-2.33c-1.34-1.54-2-3.86-2-6.94s.67-5.4,2-7a6.74,6.74,0,0,1,5.37-2.36,7.71,7.71,0,0,1,2.44.35,6.37,6.37,0,0,1,1.81,1,6.58,6.58,0,0,1,1.3,1.34h.2c0-.29-.06-.72-.11-1.29s-.09-1-.09-1.36v-7.15H616v25.41h-2.38l-.43-2.4h-.14a6.51,6.51,0,0,1-1.3,1.38,5.9,5.9,0,0,1-1.82,1A7.4,7.4,0,0,1,607.49,924.66Zm.46-2.44c1.9,0,3.23-.52,4-1.56a7.84,7.84,0,0,0,1.16-4.7v-.53a9.84,9.84,0,0,0-1.11-5.14c-.73-1.19-2.09-1.79-4.08-1.79a4,4,0,0,0-3.56,1.89,9.46,9.46,0,0,0-1.19,5.07,9,9,0,0,0,1.19,5A4,4,0,0,0,608,922.22Z" id="path58"/>
<path class="cls-1" d="M628.62,906.06a7.53,7.53,0,0,1,4,1,6.62,6.62,0,0,1,2.54,2.82,9.69,9.69,0,0,1,.89,4.27v1.77H623.74a6.75,6.75,0,0,0,1.56,4.63,5.42,5.42,0,0,0,4.16,1.59,12.58,12.58,0,0,0,3-.32,16.78,16.78,0,0,0,2.72-.92v2.58a13.84,13.84,0,0,1-2.71.89,16.15,16.15,0,0,1-3.17.28,9.46,9.46,0,0,1-4.5-1,7.15,7.15,0,0,1-3-3.09,10.61,10.61,0,0,1-1.09-5,12,12,0,0,1,1-5,7.33,7.33,0,0,1,6.94-4.38Zm0,2.41a4.26,4.26,0,0,0-3.33,1.36,6.34,6.34,0,0,0-1.45,3.76h9.13a7.05,7.05,0,0,0-.47-2.68,3.94,3.94,0,0,0-1.42-1.79A4.33,4.33,0,0,0,628.59,908.47Z" id="path59"/>
<path class="cls-1" d="M639.36,913h8.1v2.74h-8.1Z" id="path60"/>
<path class="cls-1" d="M662.87,924.32,655,903.39h-.13c0,.44.08,1,.12,1.7s.06,1.45.08,2.26,0,1.65,0,2.49v14.48h-2.77V900.44h4.45L664.15,920h.13l7.49-19.57h4.42v23.88h-3V909.64c0-.78,0-1.55,0-2.32s.06-1.5.1-2.18.08-1.25.1-1.72h-.13l-8,20.9Z" id="path61"/>
<path class="cls-1" d="M688.16,924.32V909.41c0-.63,0-1.3,0-2s0-1.42.07-2.1,0-1.27,0-1.76c-.35.38-.66.69-.92.92s-.6.54-1,.92l-2.41,2-1.57-2,6.26-4.89H691v23.88Z" id="path62"/>
<path class="cls-1" d="M502.55,894.19l-2.22-2.37a14.1,14.1,0,0,1,18.94-.33l-2.13,2.44a10.9,10.9,0,0,0-7.16-2.69A10.78,10.78,0,0,0,502.55,894.19Z" id="path63"/>
<path class="cls-1" d="M506.38,897.85l-2.4-2.18a8.08,8.08,0,0,1,11.61-.39l-2.24,2.33a4.85,4.85,0,0,0-7,.24Z" id="path64"/>
</g>
<g id="Layer_2" data-name="Layer 2">
<path class="cls-8" d="M472.55,77.74h68.09a7.43,7.43,0,0,1,7.43,7.43V360.26a0,0,0,0,1,0,0h-83a0,0,0,0,1,0,0V85.17A7.43,7.43,0,0,1,472.55,77.74Z" id="path65"/>
<line class="cls-8" x1="465.12" y1="123.91" x2="548.07" y2="123.91" id="line65"/>
<line class="cls-8" x1="465.12" y1="149.74" x2="548.07" y2="149.74" id="line66"/>
<polyline class="cls-8" points="465.12 360.26 452.2 396.26 452.2 436.17 560.99 436.17 560.99 396.26 548.07 360.26" id="polyline66"/>
<line class="cls-8" x1="452.2" y1="396.26" x2="560.98" y2="396.26" id="line67"/>
<path class="cls-8" d="M449.69,440.17H562a3.26,3.26,0,0,1,2.56,1.55l5.93,8.19H442.68l4-7.49A3.65,3.65,0,0,1,449.69,440.17Z" id="path67"/>
<path class="cls-8" d="M600,440.17H712.33a3.24,3.24,0,0,1,2.56,1.55l5.93,8.19H593l4-7.49A3.65,3.65,0,0,1,600,440.17Z" id="path68"/>
<line class="cls-8" x1="454.45" y1="436.17" x2="454.45" y2="439.83" id="line68"/>
<line class="cls-8" x1="558.64" y1="436.17" x2="558.64" y2="439.83" id="line69"/>
<rect class="cls-8" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.87" id="rect69"/>
<line class="cls-8" x1="611.07" y1="416.61" x2="611.07" y2="439.83" id="line70"/>
<line class="cls-8" x1="703.81" y1="416.61" x2="703.81" y2="439.83" id="line71"/>
<path class="cls-8" d="M614.2,347.35h86.48a3.13,3.13,0,0,1,3.13,3.13V356a0,0,0,0,1,0,0H611.07a0,0,0,0,1,0,0v-5.48A3.13,3.13,0,0,1,614.2,347.35Z" id="path71"/>
<line class="cls-8" x1="570.51" y1="449.91" x2="592.99" y2="449.91" id="line72"/>
<path class="cls-8" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path72"/>
<rect class="cls-8" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect72"/>
<rect class="cls-8" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect73"/>
<rect class="cls-8" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect74"/>
<path class="cls-8" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path74"/>
<path class="cls-8" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path75"/>
<path class="cls-8" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path76"/>
<path class="cls-8" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path77"/>
<path class="cls-8" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path78"/>
<path class="cls-8" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path79"/>
<rect class="cls-8" x="751.94" y="555.13" width="5.87" height="47.48" id="rect79"/>
<path class="cls-8" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path80"/>
<path class="cls-8" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path81"/>
<path class="cls-8" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path82"/>
<rect class="cls-8" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect82"/>
<circle class="cls-8" cx="518.72" cy="853.89" r="5.48" id="circle82"/>
<circle class="cls-8" cx="640.14" cy="853.89" r="5.48" id="circle83"/>
<circle class="cls-8" cx="541.83" cy="853.89" r="5.48" id="circle84"/>
<circle class="cls-8" cx="567.67" cy="853.89" r="5.48" id="circle85"/>
<circle class="cls-8" cx="593.51" cy="853.89" r="5.48" id="circle86"/>
<circle class="cls-8" cx="616.82" cy="853.89" r="5.48" id="circle87"/>
<line class="cls-8" x1="430.68" y1="572.74" x2="430.68" y2="602.61" id="line87"/>
<line class="cls-8" x1="424.42" y1="595.43" x2="424.42" y2="578.11" id="line88"/>
<line class="cls-8" x1="438.21" y1="595.43" x2="438.21" y2="578.11" id="line89"/>
<line class="cls-8" x1="430.68" y1="644.74" x2="430.68" y2="674.61" id="line90"/>
<line class="cls-8" x1="424.42" y1="667.43" x2="424.42" y2="650.11" id="line91"/>
<line class="cls-8" x1="438.21" y1="667.43" x2="438.21" y2="650.11" id="line92"/>
<line class="cls-8" x1="430.68" y1="716.74" x2="430.68" y2="746.61" id="line93"/>
<line class="cls-8" x1="424.42" y1="739.43" x2="424.42" y2="722.11" id="line94"/>
<line class="cls-8" x1="438.21" y1="739.43" x2="438.21" y2="722.11" id="line95"/>
<line class="cls-8" x1="730.03" y1="572.74" x2="730.03" y2="602.61" id="line96"/>
<line class="cls-8" x1="723.77" y1="595.43" x2="723.77" y2="578.11" id="line97"/>
<line class="cls-8" x1="737.56" y1="595.43" x2="737.56" y2="578.11" id="line98"/>
<line class="cls-8" x1="730.03" y1="644.74" x2="730.03" y2="674.61" id="line99"/>
<line class="cls-8" x1="723.77" y1="667.43" x2="723.77" y2="650.11" id="line100"/>
<line class="cls-8" x1="737.56" y1="667.43" x2="737.56" y2="650.11" id="line101"/>
<line class="cls-8" x1="730.03" y1="716.74" x2="730.03" y2="746.61" id="line102"/>
<line class="cls-8" x1="723.77" y1="739.43" x2="723.77" y2="722.11" id="line103"/>
<line class="cls-8" x1="737.56" y1="739.43" x2="737.56" y2="722.11" id="line104"/>
<path class="cls-8" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path104"/>
<path class="cls-8" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path105"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "thinknode_m2.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,391 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg75"
sodipodi:docname="thinknode_m2.svg"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
viewBox="388.5 121.73 413.05 787.86"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview75"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="3.7680002"
inkscape:cx="265.65816"
inkscape:cy="681.92672"
inkscape:window-width="1472"
inkscape:window-height="890"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_3" />
<defs
id="defs1">
<style
id="style1">.cls-1{fill:#262626;}.cls-2{fill:#353535;}.cls-3{fill:#303030;}.cls-4{fill:#f05043;}.cls-5,.cls-6,.cls-7,.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-6{stroke-width:0.88px;}.cls-7{stroke-width:0.95px;}.cls-8{stroke-width:1px;}.cls-9{fill:#acdee5;}</style>
</defs>
<g
id="Layer_3"
data-name="Layer 3">
<polygon
class="cls-1"
points="575.63 361.13 575.34 354.09 574.7 338.7 574.41 331.65 479.53 331.65 479.23 338.7 478.57 354.09 478.26 361.13 575.63 361.13"
id="polygon1" />
<polyline
class="cls-2"
points="458.33 403 473.46 384.87 579.68 384.87 595.03 403"
id="polyline1" />
<path
class="cls-2"
d="M579.68,384.87l-.87-21.35a2.51,2.51,0,0,0-2.5-2.39H477.56a2.5,2.5,0,0,0-2.49,2.39l-.87,21.35"
id="path1" />
<path
class="cls-2"
d="M578.32,351.57,577.88,341a2.41,2.41,0,0,0-2.41-2.32H478.41A2.42,2.42,0,0,0,476,341l-.43,10.55a2.42,2.42,0,0,0,2.42,2.52H575.9A2.43,2.43,0,0,0,578.32,351.57Z"
id="path2" />
<path
class="cls-2"
d="M476.46,329.56a2,2,0,0,0,2,2.09h96.94a2,2,0,0,0,2-2.09l-7.89-193.08a14.91,14.91,0,0,0-14.9-14.31H499.26a14.91,14.91,0,0,0-14.9,14.31Z"
id="path3" />
<polyline
class="cls-2"
points="491.72 331.65 499.03 137.3 555.9 137.3 561.64 331.65"
id="polyline3" />
<rect
class="cls-1"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect3" />
<rect
class="cls-2"
x="405.9"
y="417.86"
width="372.52"
height="471.54"
rx="38.16"
id="rect4" />
<path
class="cls-3"
d="M763.35,521.78v329A23.38,23.38,0,0,1,740,874.13H441.22a23.38,23.38,0,0,1-23.38-23.38V456.51a23.38,23.38,0,0,1,23.38-23.38H675.28A18,18,0,0,1,688,438.41L757.66,508A19.43,19.43,0,0,1,763.35,521.78Z"
id="path4" />
<path
class="cls-1"
d="M716.86,532.78H462.77a18,18,0,0,0-18,18V673.91a18,18,0,0,0,18,18H716.86a18,18,0,0,0,18-18V550.78A18,18,0,0,0,716.86,532.78Zm6.52,137.48a10.7,10.7,0,0,1-10.7,10.7H465.38a10.7,10.7,0,0,1-10.7-10.7V551.83a10.7,10.7,0,0,1,10.7-10.7h247.3a10.7,10.7,0,0,1,10.7,10.7Z"
id="path5" />
<rect
x="454.68"
y="541.13"
width="268.7"
height="139.83"
rx="10.7"
id="rect5" />
<path
class="cls-3"
d="M447,904.26v2.94a2,2,0,0,0,2,1.95h14.28a2,2,0,0,0,2-1.95v-2.94"
id="path6" />
<path
class="cls-3"
d="M718.74,904.26v2.94a2,2,0,0,0,2,1.95H735a2,2,0,0,0,2-1.95v-2.94"
id="path7" />
<path
class="cls-3"
d="M790.16,508.83h.39a10.56,10.56,0,0,1,10.56,10.56V656.57a10.56,10.56,0,0,1-10.56,10.56h-.39"
id="path8" />
<path
class="cls-3"
d="M394.16,518.17h-3.31a1.91,1.91,0,0,0-1.91,1.91V797a1.9,1.9,0,0,0,1.91,1.91h3.31"
id="path9" />
<rect
class="cls-1"
x="502.29"
y="782"
width="180.26"
height="24.65"
rx="12.33"
id="rect9" />
<circle
class="cls-4"
cx="515.99"
cy="794.33"
r="5.48"
id="circle9" />
<circle
class="cls-2"
cx="637.4"
cy="794.33"
r="5.48"
id="circle10" />
<circle
class="cls-2"
cx="660.08"
cy="794.33"
r="5.48"
id="circle11" />
<circle
class="cls-2"
cx="539.09"
cy="794.33"
r="5.48"
id="circle12" />
<circle
class="cls-2"
cx="564.93"
cy="794.33"
r="5.48"
id="circle13" />
<circle
class="cls-2"
cx="590.77"
cy="794.33"
r="5.48"
id="circle14" />
<circle
class="cls-2"
cx="614.09"
cy="794.33"
r="5.48"
id="circle15" />
<path
class="cls-1"
d="M475.77,856.71h-3.41V832.6h-8.47v-3H484.2v3h-8.43Z"
id="path15" />
<path
class="cls-1"
d="M491.19,836.32c0,.48,0,1-.06,1.48s-.08,1-.13,1.37h.23a5.69,5.69,0,0,1,1.63-1.75,7.82,7.82,0,0,1,2.2-1,8.81,8.81,0,0,1,2.51-.36,9.64,9.64,0,0,1,4.12.78,5.2,5.2,0,0,1,2.49,2.41,9.43,9.43,0,0,1,.83,4.25v13.25h-3.3v-13a5.37,5.37,0,0,0-1.1-3.69,4.46,4.46,0,0,0-3.46-1.21,6.41,6.41,0,0,0-3.57.85,4.68,4.68,0,0,0-1.84,2.51,12.81,12.81,0,0,0-.55,4v10.52h-3.34V827.85h3.34Z"
id="path16" />
<path
class="cls-1"
d="M513,828.73a2,2,0,0,1,1.35.51,2,2,0,0,1,.59,1.61,2.07,2.07,0,0,1-.59,1.6A2,2,0,0,1,513,833a2,2,0,0,1-1.4-.53,2.1,2.1,0,0,1-.57-1.6,2.06,2.06,0,0,1,.57-1.61A2,2,0,0,1,513,828.73Zm1.64,7.63v20.35h-3.35V836.36Z"
id="path17" />
<path
class="cls-1"
d="M530.91,836a7.69,7.69,0,0,1,5.5,1.77c1.24,1.17,1.86,3.08,1.86,5.71v13.25H535v-13a5.37,5.37,0,0,0-1.1-3.69,4.46,4.46,0,0,0-3.46-1.21q-3.37,0-4.67,1.9a9.7,9.7,0,0,0-1.29,5.47v10.55h-3.34V836.36h2.7l.49,2.77h.19a6.17,6.17,0,0,1,1.69-1.76,7.31,7.31,0,0,1,2.22-1A9.16,9.16,0,0,1,530.91,836Z"
id="path18" />
<path
class="cls-1"
d="M547.88,842.93q0,.6-.06,1.59t-.09,1.71h.15l.68-.87.93-1.16c.32-.39.59-.72.82-1l6.49-6.87h3.91l-8.24,8.69,8.81,11.66h-4l-7.06-9.49-2.32,2v7.48h-3.3V827.85h3.3Z"
id="path19" />
<path
class="cls-1"
d="M586.8,856.71h-4l-14.5-22.52h-.15c.05.59.09,1.28.13,2.07s.07,1.65.11,2.55.06,1.81.06,2.75v15.15h-3.15V829.6h4L583.72,852h.16c0-.4-.06-1-.1-1.82s-.08-1.7-.11-2.66-.06-1.85-.06-2.66V829.6h3.19Z"
id="path20" />
<path
class="cls-1"
d="M611.4,846.5a14.11,14.11,0,0,1-.66,4.5,9.41,9.41,0,0,1-1.9,3.32,7.92,7.92,0,0,1-3,2.07,10.53,10.53,0,0,1-3.93.7,9.66,9.66,0,0,1-3.72-.7,8.18,8.18,0,0,1-3-2.07,9.67,9.67,0,0,1-2-3.32,13.29,13.29,0,0,1-.7-4.5,13,13,0,0,1,1.14-5.72,8.25,8.25,0,0,1,3.26-3.57A10,10,0,0,1,602,836a9.47,9.47,0,0,1,4.87,1.23,8.61,8.61,0,0,1,3.31,3.57A12.41,12.41,0,0,1,611.4,846.5Zm-15.37,0a12.33,12.33,0,0,0,.62,4.15,5.45,5.45,0,0,0,2,2.72,6.49,6.49,0,0,0,6.76,0,5.5,5.5,0,0,0,2-2.72,12.32,12.32,0,0,0,.63-4.15,11.76,11.76,0,0,0-.65-4.14,5.53,5.53,0,0,0-1.95-2.64,5.76,5.76,0,0,0-3.4-.93,5.08,5.08,0,0,0-4.52,2.05A9.87,9.87,0,0,0,596,846.5Z"
id="path21" />
<path
class="cls-1"
d="M623.9,857.09a7.62,7.62,0,0,1-6.08-2.64c-1.52-1.76-2.28-4.38-2.28-7.88s.77-6.13,2.3-7.91a7.61,7.61,0,0,1,6.09-2.68,8.58,8.58,0,0,1,2.78.4,6.79,6.79,0,0,1,2,1.08,7.87,7.87,0,0,1,1.48,1.52h.22c0-.33-.07-.82-.13-1.46s-.09-1.16-.09-1.54v-8.13h3.34v28.86h-2.7l-.49-2.73h-.15a7.83,7.83,0,0,1-1.48,1.57,6.86,6.86,0,0,1-2.07,1.12A8.44,8.44,0,0,1,623.9,857.09Zm.53-2.77q3.23,0,4.53-1.77a8.85,8.85,0,0,0,1.31-5.33v-.61a11.15,11.15,0,0,0-1.25-5.83c-.83-1.35-2.38-2-4.63-2a4.45,4.45,0,0,0-4,2.15,10.68,10.68,0,0,0-1.35,5.75,10.12,10.12,0,0,0,1.35,5.66A4.56,4.56,0,0,0,624.43,854.32Z"
id="path22" />
<path
class="cls-1"
d="M647.89,836a8.5,8.5,0,0,1,4.5,1.14,7.45,7.45,0,0,1,2.89,3.21,11,11,0,0,1,1,4.84v2H642.35a7.71,7.71,0,0,0,1.76,5.26,6.19,6.19,0,0,0,4.73,1.8,14.83,14.83,0,0,0,3.44-.36,19.71,19.71,0,0,0,3.09-1v2.92a16,16,0,0,1-3.07,1,18.05,18.05,0,0,1-3.61.32,10.7,10.7,0,0,1-5.11-1.18,8.12,8.12,0,0,1-3.45-3.51,12,12,0,0,1-1.24-5.71A13.57,13.57,0,0,1,640,841a8.32,8.32,0,0,1,7.88-5Zm0,2.73a4.83,4.83,0,0,0-3.77,1.54,7.25,7.25,0,0,0-1.66,4.27h10.37a8,8,0,0,0-.53-3,4.49,4.49,0,0,0-1.61-2A4.92,4.92,0,0,0,647.85,838.71Z"
id="path23" />
<path
class="cls-1"
d="M660.08,843.88h9.19V847h-9.19Z"
id="path24" />
<path
class="cls-1"
d="M686.77,856.71l-8.92-23.77h-.15c0,.51.09,1.15.13,1.94s.07,1.64.1,2.56,0,1.87,0,2.83v16.44h-3.15V829.6h5.05l8.36,22.21h.15l8.5-22.21h5v27.11h-3.38V840q0-1.32,0-2.64t.12-2.46c.05-.78.09-1.43.11-2h-.15l-9,23.73Z"
id="path25" />
<path
class="cls-1"
d="M504.76,822.5l-2.52-2.69a16,16,0,0,1,21.51-.37l-2.42,2.77a12.35,12.35,0,0,0-16.57.29Z"
id="path27" />
<path
class="cls-1"
d="M509.12,826.65l-2.73-2.47a9.18,9.18,0,0,1,13.18-.45L517,826.38a5.51,5.51,0,0,0-7.9.27Z"
id="path28" />
<path
d="M 727.48499,857.1333 H 709.11 v -3.80989 q 1.91406,-1.64063 3.82812,-3.28125 1.93229,-1.64062 3.59115,-3.26302 3.49999,-3.39062 4.79426,-5.3776 1.29427,-2.0052 1.29427,-4.32031 0,-2.11458 -1.40364,-3.29947 -1.38542,-1.20313 -3.88281,-1.20313 -1.65885,0 -3.59114,0.58334 -1.93229,0.58333 -3.77344,1.78645 h -0.18229 v -3.82812 q 1.29427,-0.63802 3.44531,-1.16666 2.16927,-0.52865 4.19271,-0.52865 4.17447,0 6.54426,2.02344 2.36979,2.0052 2.36979,5.45051 0,1.54948 -0.40104,2.89844 -0.38281,1.33073 -1.14844,2.53385 -0.71093,1.13021 -1.67708,2.22396 -0.94791,1.09375 -2.3151,2.42447 -1.95052,1.91406 -4.02864,3.71875 -2.07813,1.78646 -3.88281,3.31771 h 14.60155 z"
id="text1"
style="font-size:37.3333px;fill:#262626"
aria-label="2" />
</g>
<g
id="Layer_2"
data-name="Layer 2">
<rect
class="cls-5"
x="502.29"
y="782"
width="180.26"
height="24.65"
rx="12.33"
id="rect28" />
<circle
class="cls-5"
cx="515.99"
cy="794.33"
r="5.48"
id="circle28" />
<circle
class="cls-5"
cx="637.4"
cy="794.33"
r="5.48"
id="circle29" />
<circle
class="cls-5"
cx="660.08"
cy="794.33"
r="5.48"
id="circle30" />
<circle
class="cls-5"
cx="539.09"
cy="794.33"
r="5.48"
id="circle31" />
<circle
class="cls-5"
cx="564.93"
cy="794.33"
r="5.48"
id="circle32" />
<circle
class="cls-5"
cx="590.77"
cy="794.33"
r="5.48"
id="circle33" />
<circle
class="cls-5"
cx="614.09"
cy="794.33"
r="5.48"
id="circle34" />
<path
class="cls-6"
d="M763.35,521.78v329A23.38,23.38,0,0,1,740,874.13H441.22a23.38,23.38,0,0,1-23.38-23.38V456.51a23.38,23.38,0,0,1,23.38-23.38H675.28A18,18,0,0,1,688,438.41L757.66,508A19.43,19.43,0,0,1,763.35,521.78Z"
id="path34" />
<rect
class="cls-7"
x="405.9"
y="417.86"
width="372.52"
height="471.54"
rx="38.16"
id="rect34" />
<rect
class="cls-8"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect35" />
<path
class="cls-6"
d="M763.35,462.15v30.34a5.64,5.64,0,0,1-9.62,4L700,442.75a5.63,5.63,0,0,1,4-9.62h30.34A29,29,0,0,1,763.35,462.15Z"
id="path35" />
<rect
class="cls-6"
x="444.77"
y="532.78"
width="290.09"
height="159.13"
rx="18"
id="rect36" />
<rect
class="cls-6"
x="454.68"
y="541.13"
width="268.7"
height="139.83"
rx="10.7"
id="rect37" />
<path
class="cls-6"
d="M447,904.26v2.94a2,2,0,0,0,2,1.95h14.28a2,2,0,0,0,2-1.95v-2.94"
id="path37" />
<path
class="cls-6"
d="M718.74,904.26v2.94a2,2,0,0,0,2,1.95H735a2,2,0,0,0,2-1.95v-2.94"
id="path38" />
<path
class="cls-6"
d="M790.16,508.83h.39a10.56,10.56,0,0,1,10.56,10.56V656.57a10.56,10.56,0,0,1-10.56,10.56h-.39"
id="path39" />
<path
class="cls-6"
d="M394.16,518.17h-3.31a1.91,1.91,0,0,0-1.91,1.91V797a1.9,1.9,0,0,0,1.91,1.91h3.31"
id="path40" />
<polyline
class="cls-6"
points="458.33 403 473.46 384.87 579.68 384.87 595.03 403"
id="polyline40" />
<path
class="cls-6"
d="M579.68,384.87l-.87-21.35a2.51,2.51,0,0,0-2.5-2.39H477.56a2.5,2.5,0,0,0-2.49,2.39l-.87,21.35"
id="path41" />
<path
class="cls-6"
d="M578.32,351.57,577.88,341a2.41,2.41,0,0,0-2.41-2.32H478.41A2.42,2.42,0,0,0,476,341l-.43,10.55a2.42,2.42,0,0,0,2.42,2.52H575.9A2.43,2.43,0,0,0,578.32,351.57Z"
id="path42" />
<path
class="cls-6"
d="M476.46,329.56a2,2,0,0,0,2,2.09h96.94a2,2,0,0,0,2-2.09l-7.89-193.08a14.91,14.91,0,0,0-14.9-14.31H499.26a14.91,14.91,0,0,0-14.9,14.31Z"
id="path43" />
<line
class="cls-6"
x1="479.53"
y1="331.65"
x2="479.23"
y2="338.7"
id="line43" />
<line
class="cls-6"
x1="478.57"
y1="354.09"
x2="478.26"
y2="361.13"
id="line44" />
<line
class="cls-6"
x1="574.7"
y1="338.7"
x2="574.41"
y2="331.65"
id="line45" />
<line
class="cls-6"
x1="575.63"
y1="361.13"
x2="575.34"
y2="354.09"
id="line46" />
<polyline
class="cls-6"
points="491.72 331.65 499.03 137.3 555.9 137.3 561.64 331.65"
id="polyline46" />
<rect
class="cls-8"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect46" />
<rect
class="cls-8"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect47" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "seeed_xiao_nrf52_kit.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -19,17 +19,17 @@ enum MeshMapTypes: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .standard:
return "standard".localized
return "Standard".localized
case .mutedStandard:
return "standard.muted".localized
return "Standard Muted".localized
case .hybrid:
return "hybrid".localized
return "Hybrid".localized
case .hybridFlyover:
return "hybrid.flyover".localized
return "Hybrid Flyover".localized
case .satellite:
return "satellite".localized
return "Satellite".localized
case .satelliteFlyover:
return "satellite.flyover".localized
return "Satellite Flyover".localized
}
}
func MKMapTypeValue() -> MKMapType {
@ -66,7 +66,7 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable {
var id: Double { self.rawValue }
var description: String {
let distanceFormatter = MKDistanceFormatter()
return String.localizedStringWithFormat("nodelist.filter.distance %@".localized, distanceFormatter.string(fromDistance: Double(self.rawValue)))
return String.localizedStringWithFormat("up to %@ away".localized, distanceFormatter.string(fromDistance: Double(self.rawValue)))
}
}
@ -78,11 +78,11 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .none:
return "map.usertrackingmode.none".localized
return "None".localized
case .follow:
return "map.usertrackingmode.follow".localized
return "Follow".localized
case .followWithHeading:
return "map.usertrackingmode.followwithheading".localized
return "Follow with heading".localized
}
}
var icon: String {
@ -117,21 +117,21 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .tenSeconds:
return "interval.ten.seconds".localized
return "Ten Seconds".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
return "Fifteen Seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
return "Thirty Seconds".localized
case .fortyFiveSeconds:
return "interval.fortyfive.seconds".localized
return "Forty Five Seconds".localized
case .oneMinute:
return "interval.one.minute".localized
return "One Minute".localized
case .fiveMinutes:
return "interval.five.minutes".localized
return "Five Minutes".localized
case .tenMinutes:
return "interval.ten.minutes".localized
return "Ten Minutes".localized
case .fifteenMinutes:
return "interval.fifteen.minutes".localized
return "Fifteen Minutes".localized
}
}
}

View file

@ -19,11 +19,11 @@ enum ConfigPresets: Int, CaseIterable, Identifiable {
switch self {
case .unset:
return "canned.messages.preset.manual".localized
return "Manual Configuration".localized
case .rakRotaryEncoder:
return "canned.messages.preset.rakrotary".localized
return "RAK Rotary Encoder".localized
case .cardKB:
return "canned.messages.preset.cardkb".localized
return "M5 Stack Card KB / RAK Keypad".localized
}
}
}
@ -45,21 +45,21 @@ enum InputEventChars: Int, CaseIterable, Identifiable {
switch self {
case .none:
return "inputevent.none".localized
return "None".localized
case .up:
return "inputevent.up".localized
return "Up".localized
case .down:
return "inputevent.down".localized
return "Down".localized
case .left:
return "inputevent.left".localized
return "Left".localized
case .right:
return "inputevent.right".localized
return "Right".localized
case .select:
return "inputevent.select".localized
return "Select".localized
case .back:
return "inputevent.back".localized
return "Back".localized
case .cancel:
return "inputevent.cancel".localized
return "Cancel".localized
}
}
func protoEnumValue() -> ModuleConfig.CannedMessageConfig.InputEventChar {

View file

@ -19,11 +19,11 @@ enum ChannelRoles: Int, CaseIterable, Identifiable {
switch self {
case .disabled:
return "channel.role.disabled".localized
return "Disabled".localized
case .primary:
return "channel.role.primary".localized
return "Primary".localized
case .secondary:
return "channel.role.secondary".localized
return "Secondary".localized
}
}
func protoEnumValue() -> Channel.Role {

View file

@ -21,65 +21,60 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
case takTracker = 10
case repeater = 4
case router = 2
case routerClient = 3
case routerLate = 11
var id: Int { self.rawValue }
var name: String {
switch self {
case .client:
return "device.role.name.client".localized
return "Client".localized
case .clientMute:
return "device.role.name.clientMute".localized
return "Client Mute".localized
case .router:
return "device.role.name.router".localized
case .routerClient:
return "device.role.name.routerClient".localized
return "Router".localized
case .repeater:
return "device.role.name.repeater".localized
return "Repeater".localized
case .tracker:
return "device.role.name.tracker".localized
return "Tracker".localized
case .sensor:
return "device.role.name.sensor".localized
return "Sensor".localized
case .tak:
return "device.role.name.tak".localized
return "TAK".localized
case .takTracker:
return "device.role.name.takTracker".localized
return "TAK Tracker".localized
case .clientHidden:
return "device.role.name.clientHidden".localized
return "Client Hidden".localized
case .lostAndFound:
return "device.role.name.lostAndFound".localized
return "Lost and Found".localized
case .routerLate:
return "device.role.name.routerlate".localized
return "Router Late".localized
}
}
var description: String {
switch self {
case .client:
return "device.role.client".localized
return "App connected or stand alone messaging device.".localized
case .clientMute:
return "device.role.clientmute".localized
return "Device that does not forward packets from other devices.".localized
case .router:
return "device.role.router".localized
case .routerClient:
return "device.role.routerclient".localized
return "Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Needs exceptional coverage. Visible in Nodes list.".localized
case .repeater:
return "device.role.repeater".localized
return "Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Relays messages with minimal overhead. Not visible in Nodes list.".localized
case .tracker:
return "device.role.tracker".localized
return "Broadcasts GPS position packets as priority.".localized
case .sensor:
return "device.role.sensor".localized
return "Broadcasts telemetry packets as priority.".localized
case .tak:
return "device.role.tak".localized
return "Optimized for ATAK system communication, reduces routine broadcasts.".localized
case .takTracker:
return "device.role.taktracker".localized
return "Enables automatic TAK PLI broadcasts and reduces routine broadcasts.".localized
case .clientHidden:
return "device.role.clienthidden".localized
return "Device that only broadcasts as needed for stealth or power savings.".localized
case .lostAndFound:
return "device.role.lostandfound".localized
return "Broadcasts location as message to default channel regularly for to assist with device recovery.".localized
case .routerLate:
return "device.role.routerlate".localized
return "Infrastructure node that always rebroadcasts packets once but only after all other modes, ensuring additional coverage for local clusters. Visible in Nodes list.".localized
}
}
@ -89,7 +84,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
return "apps.iphone"
case .clientMute:
return "speaker.slash"
case .router, .routerClient, .routerLate:
case .router, .routerLate:
return "wifi.router"
case .repeater:
return "repeat"
@ -116,8 +111,6 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
return Config.DeviceConfig.Role.clientMute
case .router:
return Config.DeviceConfig.Role.router
case .routerClient:
return Config.DeviceConfig.Role.routerClient
case .repeater:
return Config.DeviceConfig.Role.repeater
case .tracker:
@ -144,36 +137,41 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
case allSkipDecoding = 1
case localOnly = 2
case knownOnly = 3
case corePortnums = 4
case none = 4
case corePortnums = 5
var id: Int { self.rawValue }
var name: String {
switch self {
case .all:
return "All"
return "All".localized
case .allSkipDecoding:
return "All Skip Decoding"
return "All Skip Decoding".localized
case .localOnly:
return "Local Only"
return "Local Only".localized
case .knownOnly:
return "Known Only"
return "Known Only".localized
case .none:
return "None".localized
case .corePortnums:
return "Core Portnums Only"
return "Core Portnums Only".localized
}
}
var description: String {
switch self {
case .all:
return "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params."
return "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params.".localized
case .allSkipDecoding:
return "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior."
return "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior.".localized
case .localOnly:
return "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels."
return "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels.".localized
case .knownOnly:
return "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list."
return "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list.".localized
case .none:
return "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.".localized
case .corePortnums:
return "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing."
return "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing.".localized
}
}
func protoEnumValue() -> Config.DeviceConfig.RebroadcastMode {
@ -187,6 +185,8 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
return Config.DeviceConfig.RebroadcastMode.localOnly
case .knownOnly:
return Config.DeviceConfig.RebroadcastMode.knownOnly
case .none:
return Config.DeviceConfig.RebroadcastMode.none
case .corePortnums:
return Config.DeviceConfig.RebroadcastMode.corePortnumsOnly
}

View file

@ -49,21 +49,21 @@ enum ScreenOnIntervals: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
return "Fifteen Seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
return "Thirty Seconds".localized
case .oneMinute:
return "interval.one.minute".localized
return "One Minute".localized
case .fiveMinutes:
return "interval.five.minutes".localized
return "Five Minutes".localized
case .tenMinutes:
return "interval.ten.minutes".localized
return "Ten Minutes".localized
case .fifteenMinutes:
return "interval.fifteen.minutes".localized
return "Fifteen Minutes".localized
case .thirtyMinutes:
return "interval.thirty.minutes".localized
return "Thirty Minutes".localized
case .oneHour:
return "interval.one.hour".localized
return "One Hour".localized
case .max:
return "Always On".localized
}
@ -87,17 +87,17 @@ enum ScreenCarouselIntervals: Int, CaseIterable, Identifiable {
case .off:
return "off".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
return "Fifteen Seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
return "Thirty Seconds".localized
case .oneMinute:
return "interval.one.minute".localized
return "One Minute".localized
case .fiveMinutes:
return "interval.five.minutes".localized
return "Five Minutes".localized
case .tenMinutes:
return "interval.ten.minutes".localized
return "Ten Minutes".localized
case .fifteenMinutes:
return "interval.fifteen.minutes".localized
return "Fifteen Minutes".localized
}
}
}
@ -149,13 +149,13 @@ enum DisplayModes: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .defaultMode:
return "default.128x64.screen.layout".localized
return "Default 128x64 screen layout".localized
case .twoColor:
return "optimized.for.2.color.displays".localized
return "Optimized for 2 color displays".localized
case .inverted:
return "inverted.top.bar.for.2.color.display".localized
return "Inverted top bar for 2 Color display".localized
case .color:
return "tft.full.color.displays".localized
return "TFT Full Color Displays".localized
}
}
func protoEnumValue() -> Config.DisplayConfig.DisplayMode {

View file

@ -22,21 +22,21 @@ enum NagIntervals: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .unset:
return "unset".localized
return "Unset".localized
case .oneSecond:
return "interval.one.second".localized
return "One Second".localized
case .fiveSeconds:
return "interval.five.seconds".localized
return "Five Seconds".localized
case .tenSeconds:
return "interval.ten.seconds".localized
return "Ten Seconds".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
return "Fifteen Seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
return "Thirty Seconds".localized
case .oneMinute:
return "interval.one.minute".localized
return "One Minute".localized
case .fiveMinutes:
return "interval.five.minutes".localized
return "Five Minutes".localized
}
}
}
@ -59,25 +59,25 @@ enum OutputIntervals: Int, CaseIterable, Identifiable {
switch self {
case .unset:
return "unset".localized
return "Unset".localized
case .oneSecond:
return "interval.one.second".localized
return "One Second".localized
case .twoSeconds:
return "interval.two.seconds".localized
return "Two Seconds".localized
case .threeSeconds:
return "interval.three.seconds".localized
return "Three Seconds".localized
case .fourSeconds:
return "interval.four.seconds".localized
return "Four Seconds".localized
case .fiveSeconds:
return "interval.five.seconds".localized
return "Five Seconds".localized
case .tenSeconds:
return "interval.ten.seconds".localized
return "Ten Seconds".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
return "Fifteen Seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
return "Thirty Seconds".localized
case .oneMinute:
return "interval.one.minute".localized
return "One Minute".localized
}
}
}
@ -100,25 +100,25 @@ enum SenderIntervals: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .off:
return "off".localized
return "Off".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
return "Fifteen Seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
return "Thirty Seconds".localized
case .fortyFiveSeconds:
return "interval.fortyfive.seconds".localized
return "Forty Five Seconds".localized
case .oneMinute:
return "interval.one.minute".localized
return "One Minute".localized
case .fiveMinutes:
return "interval.five.minutes".localized
return "Five Minutes".localized
case .tenMinutes:
return "interval.ten.minutes".localized
return "Ten Minutes".localized
case .fifteenMinutes:
return "interval.fifteen.minutes".localized
return "Fifteen Minutes".localized
case .thirtyMinutes:
return "interval.thirty.minutes".localized
return "Thirty Minutes".localized
case .oneHour:
return "interval.one.hour".localized
return "One Hour".localized
}
}
}
@ -153,49 +153,49 @@ enum UpdateIntervals: Int, CaseIterable, Identifiable {
switch self {
case .tenSeconds:
return "interval.ten.seconds".localized
return "Ten Seconds".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
return "Fifteen Seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
return "Thirty Seconds".localized
case .fortyFiveSeconds:
return "interval.fortyfive.seconds".localized
return "Forty Five Seconds".localized
case .oneMinute:
return "interval.one.minute".localized
return "One Minute".localized
case .twoMinutes:
return "interval.two.minutes".localized
return "Two Minutes".localized
case .fiveMinutes:
return "interval.five.minutes".localized
return "Five Minutes".localized
case .tenMinutes:
return "interval.ten.minutes".localized
return "Ten Minutes".localized
case .fifteenMinutes:
return "interval.fifteen.minutes".localized
return "Fifteen Minutes".localized
case .thirtyMinutes:
return "interval.thirty.minutes".localized
return "Thirty Minutes".localized
case .oneHour:
return "interval.one.hour".localized
return "One Hour".localized
case .twoHours:
return "interval.two.hours".localized
return "Two Hours".localized
case .threeHours:
return "interval.three.hours".localized
return "Three Hours".localized
case .fourHours:
return "interval.four.hours".localized
return "Four Hours".localized
case .fiveHours:
return "interval.five.hours".localized
return "Five Hours".localized
case .sixHours:
return "interval.six.hours".localized
return "Six Hours".localized
case .twelveHours:
return "interval.twelve.hours".localized
return "Twelve Hours".localized
case .eighteenHours:
return "interval.eighteen.hours".localized
return "Eighteen Hours".localized
case .twentyFourHours:
return "interval.twentyfour.hours".localized
return "Twenty Four Hours".localized
case .thirtySixHours:
return "interval.thirtysix.hours".localized
return "Thirty Six Hours".localized
case .fortyeightHours:
return "interval.fortyeight.hours".localized
return "Forty Eight Hours".localized
case .seventyTwoHours:
return "interval.seventytwo.hours".localized
return "Seventy Two Hours".localized
}
}
}

View file

@ -105,27 +105,27 @@ enum RegionCodes: Int, CaseIterable, Identifiable {
case .in:
return "India".localized
case .nz865:
return "New Zealand 865mhz".localized
return "New Zealand 865MHz".localized
case .th:
return "Thailand".localized
case .ua433:
return "Ukraine 433mhz".localized
return "Ukraine 433MHz".localized
case .ua868:
return "Ukraine 868mhz".localized
return "Ukraine 868MHz".localized
case .lora24:
return "2.4 Ghz".localized
case .my433:
return "Malaysia 433mhz".localized
return "Malaysia 433MHz".localized
case .my919:
return "Malaysia 919mhz".localized
return "Malaysia 919MHz".localized
case .sg923:
return "Singapore 923mhz".localized
return "Singapore 923MHz".localized
case .ph433:
return "Philippines 433mhz".localized
return "Philippines 433MHz".localized
case .ph868:
return "Philippines 868mhz".localized
return "Philippines 868MHz".localized
case .ph915:
return "Philippines 915mhz".localized
return "Philippines 915MHz".localized
}
}
var dutyCycle: Int {
@ -280,7 +280,6 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
case longFast = 0
case longSlow = 1
case longModerate = 7
case vLongSlow = 2
case medSlow = 3
case medFast = 4
case shortSlow = 5
@ -291,23 +290,21 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .longFast:
return "long.range.fast".localized
return "Long Range - Fast".localized
case .longSlow:
return "long.range.slow".localized
return "Long Range - Slow".localized
case .longModerate:
return "long.range.moderate".localized
case .vLongSlow:
return "very.long.range.slow".localized
return "Long Range - Moderate".localized
case .medSlow:
return "medium.range.slow".localized
return "Medium Range - Slow".localized
case .medFast:
return "medium.range.fast".localized
return "Medium Range - Fast".localized
case .shortSlow:
return "short.range.slow".localized
return "Short Range - Slow".localized
case .shortFast:
return "short.range.fast".localized
return "Short Range - Fast".localized
case .shortTurbo:
return "short.range.turbo".localized
return "Short Range - Turbo".localized
}
}
var name: String {
@ -318,8 +315,6 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
return "LongSlow"
case .longModerate:
return "LongModerate"
case .vLongSlow:
return "VLongFast"
case .medSlow:
return "MediumSlow"
case .medFast:
@ -340,8 +335,6 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
return -7.5
case .longModerate:
return -17.5
case .vLongSlow:
return -20
case .medSlow:
return -15
case .medFast:
@ -362,8 +355,6 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
return Config.LoRaConfig.ModemPreset.longSlow
case .longModerate:
return Config.LoRaConfig.ModemPreset.longModerate
case .vLongSlow:
return Config.LoRaConfig.ModemPreset.veryLongSlow
case .medSlow:
return Config.LoRaConfig.ModemPreset.mediumSlow
case .medFast:

View file

@ -46,21 +46,21 @@ enum Tapbacks: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .wave:
return "tapback.wave".localized
return "Wave".localized
case .heart:
return "tapback.heart".localized
return "Heart".localized
case .thumbsUp:
return "tapback.thumbsup".localized
return "Thumbs Up".localized
case .thumbsDown:
return "tapback.thumbsdown".localized
return "Thumbs Down".localized
case .haHa:
return "tapback.haha".localized
return "HaHa".localized
case .exclamation:
return "tapback.exclamation".localized
return "Exclamation".localized
case .question:
return "tapback.question".localized
return "Question".localized
case .poop:
return "tapback.poop".localized
return "Poop".localized
}
}
}

View file

@ -21,17 +21,17 @@ enum GpsFormats: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .gpsFormatDec:
return "gpsformat.dec".localized
return "Decimal Degrees Format".localized
case .gpsFormatDms:
return "gpsformat.dms".localized
return "Degrees Minutes Seconds".localized
case .gpsFormatUtm:
return "gpsformat.utm".localized
return "Universal Transverse Mercator".localized
case .gpsFormatMgrs:
return "gpsformat.mgrs".localized
return "Military Grid Reference System".localized
case .gpsFormatOlc:
return "gpsformat.olc".localized
return "Open Location Code (aka Plus Codes)".localized
case .gpsFormatOsgr:
return "gpsformat.osgr".localized
return "Ordnance Survey Grid Reference".localized
}
}
func protoEnumValue() -> Config.DisplayConfig.GpsCoordinateFormat {
@ -73,29 +73,29 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .thirtySeconds:
return "interval.thirty.seconds".localized
return "Thirty Seconds".localized
case .oneMinute:
return "interval.one.minute".localized
return "One Minute".localized
case .twoMinutes:
return "interval.two.minutes".localized
return "Two Minutes".localized
case .fiveMinutes:
return "interval.five.minutes".localized
return "Five Minutes".localized
case .tenMinutes:
return "interval.ten.minutes".localized
return "Ten Minutes".localized
case .fifteenMinutes:
return "interval.fifteen.minutes".localized
return "Fifteen Minutes".localized
case .thirtyMinutes:
return "interval.thirty.minutes".localized
return "Thirty Minutes".localized
case .oneHour:
return "interval.one.hour".localized
return "One Hour".localized
case .sixHours:
return "interval.six.hours".localized
return "Six Hours".localized
case .twelveHours:
return "interval.twelve.hours".localized
return "Twelve Hours".localized
case .twentyFourHours:
return "interval.twentyfour.hours".localized
return "Twenty Four Hours".localized
case .maxInt32:
return "on.boot".localized
return "On Boot Only".localized
}
}
}
@ -110,11 +110,11 @@ enum GpsMode: Int, CaseIterable, Equatable {
var description: String {
switch self {
case .disabled:
return "gpsmode.disabled".localized
return "Disabled".localized
case .enabled:
return "gpsmode.enabled".localized
return "Enabled".localized
case .notPresent:
return "gpsmode.notPresent".localized
return "Not Present".localized
}
}
func protoEnumValue() -> Config.PositionConfig.GpsMode {

View file

@ -20,34 +20,34 @@ enum ActivityType: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .walking:
return "routes.activitytype.walking".localized
return "Walking".localized
case .hiking:
return "routes.activitytype.hiking".localized
return "Hiking".localized
case .biking:
return "routes.activitytype.biking".localized
return "Biking".localized
case .driving:
return "routes.activitytype.driving".localized
return "Driving".localized
case .overlanding:
return "routes.activitytype.overlanding".localized
return "Overlanding".localized
case .skiing:
return "routes.activitytype.skiing".localized
return "Skiing".localized
}
}
var fileNameString: String {
switch self {
case .walking:
return "routes.activitytype.filename.walking".localized
return "walk".localized
case .hiking:
return "routes.activitytype.filename.hiking".localized
return "hiking".localized
case .biking:
return "routes.activitytype.filename.biking".localized
return "biking".localized
case .driving:
return "routes.activitytype.filename.driving".localized
return "driving".localized
case .overlanding:
return "routes.activitytype.filename.overlanding".localized
return "overlanding".localized
case .skiing:
return "routes.activitytype.filename.skiing".localized
return "skiing".localized
}
}
}

View file

@ -32,31 +32,31 @@ enum RoutingError: Int, CaseIterable, Identifiable {
switch self {
case .none:
return "routing.acknowledged".localized
return "Acknowledged".localized
case .noRoute:
return "routing.noroute".localized
return "No Route".localized
case .gotNak:
return "routing.gotnak".localized
return "Received a negative acknowledgment".localized
case .timeout:
return "routing.timeout".localized
return "Timeout".localized
case .noInterface:
return "routing.nointerface".localized
return "No Interface".localized
case .maxRetransmit:
return "routing.maxretransmit".localized
return "Max Retransmission Reached".localized
case .noChannel:
return "routing.nochannel".localized
return "No Channel".localized
case .tooLarge:
return "routing.toolarge".localized
return "The packet is too large".localized
case .noResponse:
return "routing.noresponse".localized
return "No Response".localized
case .dutyCycleLimit:
return "routing.dutycyclelimit".localized
return "Regional Duty Cycle Limit Reached".localized
case .badRequest:
return "routing.badRequest".localized
return "Bad Request".localized
case .notAuthorized:
return "routing.notauthorized".localized
return "Not Authorized".localized
case .pkiFailed:
return "routing.pkifailed".localized
return "Encrypted Send Failed".localized
case .pkiUnknownPubkey:
return "Unknown public key".localized
case .adminBadSessionKey:

View file

@ -31,7 +31,7 @@ enum SerialBaudRates: Int, CaseIterable, Identifiable {
switch self {
case .baudDefault:
return "default".localized
return "Default".localized
case .baud110:
return "110 Baud"
case .baud300:
@ -118,17 +118,17 @@ enum SerialModeTypes: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .default:
return "serial.mode.default".localized
return "Default".localized
case .simple:
return "serial.mode.simple".localized
return "Simple".localized
case .proto:
return "serial.mode.proto".localized
return "Protobufs".localized
case .txtmsg:
return "serial.mode.txtmsg".localized
return "Text Message".localized
case .nmea:
return "serial.mode.nmea".localized
return "NMEA Positions".localized
case .caltopo:
return "serial.mode.caltopo".localized
return "CALTOPO".localized
}
}
func protoEnumValue() -> ModuleConfig.SerialConfig.Serial_Mode {
@ -166,21 +166,21 @@ enum SerialTimeoutIntervals: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .unset:
return "unset".localized
return "Unset".localized
case .oneSecond:
return "interval.one.second".localized
return "One Second".localized
case .fiveSeconds:
return "interval.five.seconds".localized
return "Five Seconds".localized
case .tenSeconds:
return "interval.ten.seconds".localized
return "Ten Seconds".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
return "Fifteen Seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
return "Thirty Seconds".localized
case .oneMinute:
return "interval.one.minute".localized
return "One Minute".localized
case .fiveMinutes:
return "interval.five.minutes".localized
return "Five Minutes".localized
}
}
}

View file

@ -20,17 +20,17 @@ enum Aqi: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .good:
return "telemetry.good".localized
return "Good".localized
case .moderate:
return "telemetry.moderate".localized
return "Moderate".localized
case .sensitive:
return "telemetry.sensitive".localized
return "Unhealthy for Sensitive Groups".localized
case .unhealthy:
return "telementry.unhealthy".localized
return "Unhealthy".localized
case .veryUnhealthy:
return "telementry.veryUnhealthy".localized
return "Very Unhealthy".localized
case .hazardous:
return "telementry.hazardous".localized
return "Hazardous".localized
}
}
var color: Color {

View file

@ -20,13 +20,10 @@ struct CsvDocument: FileDocument {
}
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
csvData = String(decoding: data, as: UTF8.self)
csvData = String(data: data, encoding: .utf8) ?? ""
} else {
throw CocoaError(.fileReadCorruptFile)
}
}

View file

@ -14,7 +14,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
if metricsType == 0 {
// Create Device Metrics Header
csvString = "\("battery.level".localized), \("Voltage".localized), \("channel.utilization".localized), \("airtime".localized), \("uptime".localized), \("Timestamp".localized)"
csvString = "\("battery.level".localized), \("Voltage".localized), \("Channel Utilization".localized), \("airtime".localized), \("Uptime".localized), \("Timestamp".localized)"
for dm in telemetry where dm.metricsType == 0 {
csvString += "\n"
csvString += dm.batteryLevel?.formatted(.number.grouping(.never)) ?? ""
@ -27,7 +27,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
csvString += ", "
csvString += dm.uptimeSeconds?.formatted(.number.grouping(.never)) ?? ""
csvString += ", "
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
}
} else if metricsType == 1 {
// Create Environment Telemetry Header
@ -44,7 +44,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
csvString += ", "
csvString += dm.gasResistance?.formatted(.number.grouping(.never)) ?? ""
csvString += ", "
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
}
} else if metricsType == 2 {
// Create Power Metrics Header
@ -63,7 +63,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
csvString += ", "
csvString += dm.powerCh3Current?.formatted(.number.grouping(.never)) ?? ""
csvString += ", "
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
}
}
return csvString
@ -121,7 +121,7 @@ func paxToCsvFile(pax: [PaxCounterEntity]) -> String {
csvString += ", "
csvString += String(p.uptime)
csvString += ", "
csvString += p.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
csvString += p.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
}
return csvString
}
@ -150,7 +150,7 @@ func positionToCsvFile(positions: [PositionEntity]) -> String {
csvString += ", "
csvString += String(pos.snr)
csvString += ", "
csvString += pos.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
csvString += pos.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
}
return csvString
}

View file

@ -89,6 +89,6 @@ extension PositionEntity {
extension PositionEntity: MKAnnotation {
public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? LocationsHandler.DefaultLocation }
public var title: String? { nodePosition?.user?.shortName ?? "unknown".localized }
public var title: String? { nodePosition?.user?.shortName ?? "Unknown".localized }
public var subtitle: String? { time?.formatted() }
}

View file

@ -13,7 +13,7 @@ extension Date {
if self.timeIntervalSince1970 > 0 && self < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
formatted()
} else {
"unknown.age".localized
"Unknown Age".localized
}
}
@ -23,18 +23,18 @@ extension Date {
if self.timeIntervalSince1970 > 0 && self < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
return dateformat.string(from: self)
} else {
return "unknown.age".localized
return "Unknown Age".localized
}
}
func relativeTimeOfDay() -> String {
let hour = Calendar.current.component(.hour, from: self)
switch hour {
case 6..<12: return "relativetimeofday.morning".localized
case 12: return "relativetimeofday.midday".localized
case 13..<17: return "relativetimeofday.afternoon".localized
case 17..<22: return "relativetimeofday.evening".localized
default: return "relativetimeofday.nighttime".localized
case 6..<12: return "Morning".localized
case 12: return "Midday".localized
case 13..<17: return "Afternoon".localized
case 17..<22: return "Evening".localized
default: return "Nighttime".localized
}
}
}

View file

@ -18,7 +18,7 @@ extension OSLogEntryLog.Level {
case .notice: "⚠️ Notice"
case .error: "🚨 Error"
case .fault: "💥 Fault"
@unknown default: "default"
@unknown default: "Default".localized
}
}
var color: Color {

View file

@ -19,11 +19,11 @@ struct UserDefault<T: Decodable> {
var wrappedValue: T {
get {
if defaultValue as? any RawRepresentable != nil {
if defaultValue is any RawRepresentable {
let storedValue = UserDefaults.standard.object(forKey: key.rawValue)
guard let storedValue,
let jsonString = (storedValue as? String != nil) ? "\"\(storedValue)\"" : "\(storedValue)",
let jsonString = (storedValue is String) ? "\"\(storedValue)\"" : "\(storedValue)",
let data = jsonString.data(using: .utf8),
let value = (try? JSONDecoder().decode(T.self, from: data)) else { return defaultValue }
@ -72,6 +72,7 @@ extension UserDefaults {
case firmwareVersion
case environmentEnableWeatherKit
case enableAdministration
case mapReportingOptIn
case testIntEnum
}
@ -97,7 +98,7 @@ extension UserDefaults {
@UserDefault(.meshMapDistance, defaultValue: 800000)
static var meshMapDistance: Double
@UserDefault(.enableMapWaypoints, defaultValue: false)
@UserDefault(.enableMapWaypoints, defaultValue: true)
static var enableMapWaypoints: Bool
@UserDefault(.enableMapRecentering, defaultValue: false)
@ -118,24 +119,6 @@ extension UserDefaults {
@UserDefault(.enableMapPointsOfInterest, defaultValue: false)
static var enableMapPointsOfInterest: Bool
@UserDefault(.enableOfflineMaps, defaultValue: false)
static var enableOfflineMaps: Bool
@UserDefault(.mapTileServer, defaultValue: .openStreetMap)
static var mapTileServer: MapTileServer
@UserDefault(.enableOverlayServer, defaultValue: false)
static var enableOverlayServer: Bool
@UserDefault(.mapOverlayServer, defaultValue: .baseReReflectivityCurrent)
static var mapOverlayServer: MapOverlayServer
@UserDefault(.mapTilesAboveLabels, defaultValue: false)
static var mapTilesAboveLabels: Bool
@UserDefault(.mapUseLegacy, defaultValue: false)
static var mapUseLegacy: Bool
@UserDefault(.enableDetectionNotifications, defaultValue: false)
static var enableDetectionNotifications: Bool
@ -166,6 +149,9 @@ extension UserDefaults {
@UserDefault(.enableAdministration, defaultValue: false)
static var enableAdministration: Bool
@UserDefault(.mapReportingOptIn, defaultValue: false)
static var mapReportingOptIn: Bool
@UserDefault(.testIntEnum, defaultValue: .one)
static var testIntEnum: TestIntEnum
}

View file

@ -231,7 +231,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
cancelPeripheralConnection()
if errorCode == 14 { // Peer removed pairing information
// Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that
lastConnectionError = "🚨 " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re pairing the radio.".localized, e.localizedDescription)
lastConnectionError = "🚨 " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device under Settings > Bluetooth and re pairing the radio.".localized, e.localizedDescription)
Logger.services.error("🚨 [BLE] Failed to connect: \(peripheral.name ?? "Unknown".localized) Error Code: \(errorCode, privacy: .public) Error: \(self.lastConnectionError, privacy: .public)")
} else {
lastConnectionError = "🚨 \(e.localizedDescription)"
@ -261,7 +261,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
Notification(
id: (peripheral.identifier.uuidString),
title: "Radio Disconnected".localized,
subtitle: "\(peripheral.name ?? "unknown".localized)",
subtitle: "\(peripheral.name ?? "Unknown".localized)",
content: e.localizedDescription,
target: "bluetooth",
path: "meshtastic:///bluetooth"
@ -273,7 +273,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)")
} else if errorCode == 14 { // Peer removed pairing information
// Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that
lastConnectionError = "🚨 " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio.".localized, e.localizedDescription)
lastConnectionError = "🚨 " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device under Settings > Bluetooth and re-connecting to the radio.".localized, e.localizedDescription)
Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized) Error Code: \(errorCode, privacy: .public) Error: \(self.lastConnectionError, privacy: .public)")
} else {
if UserDefaults.preferredPeripheralId == peripheral.identifier.uuidString {
@ -281,7 +281,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
Notification(
id: (peripheral.identifier.uuidString),
title: "Radio Disconnected".localized,
subtitle: "\(peripheral.name ?? "unknown".localized)",
subtitle: "\(peripheral.name ?? "Unknown".localized)",
content: e.localizedDescription,
target: "bluetooth",
path: "meshtastic:///bluetooth"
@ -428,7 +428,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return 0
}
let messageDescription = "🛎️ [Device Metadata] Requested for node \(toUser.longName ?? "unknown".localized) by \(fromUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ [Device Metadata] Requested for node \(toUser.longName ?? "Unknown".localized) by \(fromUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return Int64(meshPacket.id)
}
@ -477,14 +477,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
traceRoute.node = receivingNode
do {
try context.save()
Logger.data.info("💾 Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "unknown".localized), privacy: .public)")
Logger.data.info("💾 Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "Unknown".localized), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Updating Core Data BluetoothConfigEntity: \(nsError, privacy: .public)")
}
let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, destNum.toHex())
let logString = String.localizedStringWithFormat("Sent a Trace Route Request to node: %@".localized, destNum.toHex())
Logger.mesh.info("🪧 \(logString, privacy: .public)")
} catch {
@ -498,13 +498,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return }
if FROMRADIO_characteristic == nil {
Logger.mesh.error("🚨 \("firmware.version.unsupported".localized, privacy: .public)")
Logger.mesh.error("🚨 \("Unsupported Firmware Version Detected, unable to connect to device.".localized, privacy: .public)")
invalidVersion = true
return
} else {
let nodeName = connectedPeripheral?.peripheral.name ?? "unknown".localized
let logString = String.localizedStringWithFormat("mesh.log.wantconfig %@".localized, nodeName)
let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized
let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName)
Logger.mesh.info("🛎️ \(logString, privacy: .public)")
// BLE Characteristics discovered, issue wantConfig
var toRadio: ToRadio = ToRadio()
@ -579,7 +579,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error {
Logger.services.error("🚫 [BLE] didUpdateValueFor Characteristic error \(error.localizedDescription, privacy: .public)")
let errorCode = (error as NSError).code
if errorCode == 5 || errorCode == 15 {
@ -633,14 +632,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
} catch {
Logger.services.error("💥 \(error.localizedDescription, privacy: .public) \(characteristic.value!, privacy: .public)")
}
// Publish mqttClientProxyMessages received on the from radio
if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.mqttClientProxyMessage(decodedInfo.mqttClientProxyMessage) {
let message = CocoaMQTTMessage(
topic: decodedInfo.mqttClientProxyMessage.topic,
payload: [UInt8](decodedInfo.mqttClientProxyMessage.data),
retained: decodedInfo.mqttClientProxyMessage.retained
)
let message = CocoaMQTTMessage(topic: decodedInfo.mqttClientProxyMessage.topic, payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained)
mqttManager.mqttClientProxy?.publish(message)
} else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) {
if decodedInfo.clientNotification.hasReplyID {
@ -686,8 +680,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if myInfo != nil {
UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0)
connectedPeripheral.num = myInfo?.myNodeNum ?? 0
connectedPeripheral.name = myInfo?.bleName ?? "unknown".localized
connectedPeripheral.longName = myInfo?.bleName ?? "unknown".localized
connectedPeripheral.name = myInfo?.bleName ?? "Unknown".localized
connectedPeripheral.longName = myInfo?.bleName ?? "Unknown".localized
let newConnection = Int64(UserDefaults.preferredPeripheralNum) != Int64(decodedInfo.myInfo.myNodeNum)
if newConnection {
// Onboard a new device connection here
@ -702,7 +696,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo.num {
if nodeInfo.user != nil {
connectedPeripheral.shortName = nodeInfo.user?.shortName ?? "?"
connectedPeripheral.longName = nodeInfo.user?.longName ?? "unknown".localized
connectedPeripheral.longName = nodeInfo.user?.longName ?? "Unknown".localized
}
}
}
@ -745,7 +739,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame
if !supportedVersion {
invalidVersion = true
lastConnectionError = "🚨" + "update.firmware".localized
lastConnectionError = "🚨" + "Update Your Firmware".localized
return
}
}
@ -782,13 +776,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
adminAppPacket(packet: decodedInfo.packet, context: context)
case .replyApp:
Logger.mesh.info("🕸️ MESH PACKET received for Reply App handling as a text message")
textMessageAppPacket(
packet: decodedInfo.packet,
wantRangeTestPackets: wantRangeTestPackets,
connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0),
context: context,
appState: appState
)
textMessageAppPacket(packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context, appState: appState)
case .ipTunnelApp:
Logger.mesh.info("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED UNHANDLED")
case .serialApp:
@ -873,13 +861,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
hopNodes.append(traceRouteHop)
let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))
let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "Unknown".localized))
let mqttLabel = hopNode?.viaMqtt ?? false ? "MQTT " : ""
let snrLabel = (traceRouteHop.snr != -32) ? String(traceRouteHop.snr) : "unknown ".localized
routeString += "\(hopName) \(mqttLabel)(\(snrLabel)dB) --> "
}
let destinationHop = TraceRouteHopEntity(context: context)
destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized
destinationHop.name = traceRoute?.node?.user?.longName ?? "Unknown".localized
destinationHop.time = Date()
// If nil, set to unknown, INT8_MIN (-128) then divide by 4
destinationHop.snr = Float(routingMessage.snrTowards.last ?? -128) / 4
@ -892,14 +880,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
hopNodes.append(destinationHop)
/// Add the destination node to the end of the route towards string and the beginning of the route back string
routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(destinationHop.snr != -32 ? String(destinationHop.snr) : "unknown ".localized)dB)"
routeString += "\(traceRoute?.node?.user?.longName ?? "Unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(destinationHop.snr != -32 ? String(destinationHop.snr) : "unknown ".localized)dB)"
traceRoute?.routeText = routeString
// Default to -1 only fill in if routeBack is valid below
traceRoute?.hopsBack = -1
// Only if hopStart is set and there is an SNR entry
if decodedInfo.packet.hopStart > 0 && routingMessage.snrBack.count > 0 {
traceRoute?.hopsBack = Int32(routingMessage.routeBack.count)
var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) --> "
var routeBackString = "\(traceRoute?.node?.user?.longName ?? "Unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) --> "
for (index, node) in routingMessage.routeBack.enumerated() {
var hopNode = getNodeInfo(id: Int64(node), context: context)
if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 {
@ -930,7 +918,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
hopNodes.append(traceRouteHop)
let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))
let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "Unknown".localized))
let mqttLabel = hopNode?.viaMqtt ?? false ? "MQTT " : ""
let snrLabel = (traceRouteHop.snr != -32) ? String(traceRouteHop.snr) : "unknown ".localized
routeBackString += "\(hopName) \(mqttLabel)(\(snrLabel)dB) --> "
@ -950,7 +938,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
id: (UUID().uuidString),
title: "Traceroute Complete",
subtitle: "TR received back from \(destinationHop.name ?? "unknown")",
content: "Hops from: \(tr.hopsTowards), Hops back: \(tr.hopsBack)\n\(tr.routeText ?? "unknown".localized)\n\(tr.routeBackText ?? "unknown".localized)",
content: "Hops from: \(tr.hopsTowards), Hops back: \(tr.hopsBack)\n\(tr.routeText ?? "Unknown".localized)\n\(tr.routeBackText ?? "Unknown".localized)",
target: "nodes",
path: "meshtastic:///nodes?nodenum=\(connectedNode.user?.num ?? 0)"
)
@ -966,7 +954,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
let nsError = error as NSError
Logger.data.error("Error Updating Core Data TraceRouteHop: \(nsError, privacy: .public)")
}
let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString)
let logString = String.localizedStringWithFormat("Trace Route request returned: %@".localized, routeString)
Logger.mesh.info("🪧 \(logString, privacy: .public)")
}
case .neighborinfoApp:
@ -1069,8 +1057,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil {
connectTo(peripheral: preferredPeripheral!.peripheral)
}
let nodeName = connectedPeripheral?.peripheral.name ?? "unknown".localized
let logString = String.localizedStringWithFormat("mesh.log.textmessage.send.failed %@".localized, nodeName)
let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized
let logString = String.localizedStringWithFormat("Message Send Failed, not properly connected to %@".localized, nodeName)
Logger.mesh.info("🚫 \(logString, privacy: .public)")
success = false
@ -1156,7 +1144,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.textmessage.sent %@ %@ %@".localized, String(newMessage.messageId), fromUserNum.toHex(), toUserNum.toHex())
let logString = String.localizedStringWithFormat("Sent message %@ from %@ to %@".localized, String(newMessage.messageId), fromUserNum.toHex(), toUserNum.toHex())
Logger.mesh.info("💬 \(logString, privacy: .public)")
do {
@ -1204,7 +1192,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
guard let binaryData: Data = try? toRadio.serializedData() else {
return false
}
let logString = String.localizedStringWithFormat("mesh.log.waypoint.sent %@".localized, String(fromNodeNum))
let logString = String.localizedStringWithFormat("Sent a Waypoint Packet from: %@".localized, String(fromNodeNum))
Logger.mesh.info("📍 \(logString, privacy: .public)")
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
@ -1304,7 +1292,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
} else {
return false
}
let messageDescription = "🚀 Sent Set Fixed Postion Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
let messageDescription = "🚀 Sent Set Fixed Postion Admin Message to: \(fromUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -1329,7 +1317,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
} else {
return false
}
let messageDescription = "🚀 Sent Remove Fixed Position Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
let messageDescription = "🚀 Sent Remove Fixed Position Admin Message to: \(fromUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -1368,7 +1356,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum))
let logString = String.localizedStringWithFormat("Sent a Position Packet from the Apple device GPS to node: %@".localized, String(fromNodeNum))
Logger.services.debug("📍 \(logString, privacy: .public)")
return true
} else {
@ -1434,7 +1422,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
} else {
return false
}
let messageDescription = "🚀 Sent Shutdown Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
let messageDescription = "🚀 Sent Shutdown Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -1462,7 +1450,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
} else {
return false
}
let messageDescription = "🚀 Sent Reboot Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
let messageDescription = "🚀 Sent Reboot Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -1490,7 +1478,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
} else {
return false
}
let messageDescription = "🚀 Sent Reboot OTA Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
let messageDescription = "🚀 Sent Reboot OTA Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -1519,7 +1507,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return false
}
automaticallyReconnect = false
let messageDescription = "🚀 Sent enter DFU mode Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
let messageDescription = "🚀 Sent enter DFU mode Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -1547,7 +1535,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return false
}
let messageDescription = "🚀 Sent Factory Reset Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
let messageDescription = "🚀 Sent Factory Reset Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -1574,7 +1562,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🚀 Sent NodeDB Reset Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
let messageDescription = "🚀 Sent NodeDB Reset Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -1617,7 +1605,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "🎛️ Requested Channel \(channel.index) for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🎛️ Requested Channel \(channel.index) for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return Int64(meshPacket.id)
}
@ -1641,7 +1629,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Channel \(channel.index) for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Channel \(channel.index) for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return Int64(meshPacket.id)
}
@ -1723,7 +1711,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.channel.sent %@ %d".localized, String(connectedPeripheral.num), chan.index)
let logString = String.localizedStringWithFormat("Sent a Channel for: %@ Channel Index %d".localized, String(connectedPeripheral.num), chan.index)
Logger.mesh.info("🎛️ \(logString, privacy: .public)")
}
}
@ -1752,7 +1740,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.lora.config.sent %@".localized, String(connectedPeripheral.num))
let logString = String.localizedStringWithFormat("Sent a LoRa.Config for: %@".localized, String(connectedPeripheral.num))
Logger.mesh.info("📻 \(logString, privacy: .public)")
}
@ -1790,7 +1778,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
} else {
return 0
}
let messageDescription = "🛟 Saved User Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved User Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return Int64(meshPacket.id)
}
@ -1972,7 +1960,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Ham Parameters for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Ham Parameters for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return Int64(meshPacket.id)
}
@ -1998,7 +1986,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Bluetooth Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Bluetooth Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
@ -2029,7 +2017,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Device Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Device Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
return Int64(meshPacket.id)
@ -2059,7 +2047,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Display Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Display Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
return Int64(meshPacket.id)
@ -2088,7 +2076,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
@ -2120,7 +2108,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Position Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Position Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertPositionConfigPacket(config: config, nodeNum: toUser.num, context: context)
@ -2151,7 +2139,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Power Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Power Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertPowerConfigPacket(config: config, nodeNum: toUser.num, context: context)
@ -2184,7 +2172,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Network Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Network Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertNetworkConfigPacket(config: config, nodeNum: toUser.num, context: context)
@ -2217,7 +2205,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Security Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Security Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertSecurityConfigPacket(config: config, nodeNum: toUser.num, context: context)
@ -2249,7 +2237,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Ambient Lighting Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Ambient Lighting Module Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
@ -2281,7 +2269,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Canned Message Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Canned Message Module Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
@ -2314,7 +2302,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Canned Message Module Messages for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Canned Message Module Messages for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
@ -2347,7 +2335,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Detection Sensor Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Detection Sensor Module Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
@ -2378,7 +2366,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved External Notification Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved External Notification Module Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertExternalNotificationModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
@ -2409,7 +2397,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved PAX Counter Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved PAX Counter Module Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertPaxCounterModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
@ -2440,7 +2428,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved RTTTL Ringtone Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved RTTTL Ringtone Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: toUser.num, context: context)
@ -2474,7 +2462,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved MQTT Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved MQTT Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertMqttModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
@ -2504,7 +2492,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Range Test Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Range Test Module Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertRangeTestModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
@ -2537,7 +2525,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Serial Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Serial Module Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertSerialModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
@ -2568,7 +2556,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Store & Forward Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Store & Forward Module Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertStoreForwardModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
@ -2599,7 +2587,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertTelemetryModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
@ -2629,7 +2617,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Sent a Get Channel \(channelIndex) Request Admin Message for node: \(toUser.longName ?? "unknown".localized))"
let messageDescription = "🛎️ Sent a Get Channel \(channelIndex) Request Admin Message for node: \(toUser.longName ?? "Unknown".localized))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
@ -2672,7 +2660,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.cannedmessages.messages.get %@".localized, String(connectedPeripheral.num))
let logString = String.localizedStringWithFormat("Requested Canned Messages Module Messages for node: %@".localized, String(connectedPeripheral.num))
Logger.mesh.info("🥫 \(logString, privacy: .public)")
return true
}
@ -2735,7 +2723,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Device Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Device Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
@ -2765,7 +2753,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Display Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Display Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
@ -2795,7 +2783,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested LoRa Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested LoRa Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
@ -2826,7 +2814,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Network Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Network Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
@ -2856,7 +2844,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Position Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Position Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -2885,7 +2873,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Power Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Power Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -2914,7 +2902,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Security Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Security Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -2943,7 +2931,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Ambient Lighting Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Ambient Lighting Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -2972,7 +2960,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Canned Messages Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Canned Messages Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -3001,7 +2989,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested External Notificaiton Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested External Notificaiton Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -3030,7 +3018,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested PAX Counter Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested PAX Counter Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -3059,7 +3047,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested RTTTL Ringtone Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested RTTTL Ringtone Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -3088,7 +3076,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Range Test Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Range Test Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -3146,7 +3134,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Detection Sensor Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Detection Sensor Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -3175,7 +3163,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Serial Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Serial Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -3204,7 +3192,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Store and Forward Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Store and Forward Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -3234,7 +3222,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Telemetry Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛎️ Requested Telemetry Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
@ -3448,7 +3436,7 @@ extension BLEManager: CBCentralManagerDelegate {
case .unsupported:
status = "BLE is unsupported"
default:
status = "default"
status = "Default".localized
}
Logger.services.info("📜 [BLE] Bluetooth status: \(status, privacy: .public)")
}

View file

@ -1,74 +0,0 @@
//
// OfflineTileManager.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 4/23/23.
//
import Foundation
import MapKit
import OSLog
class OfflineTileManager: ObservableObject {
static let shared = OfflineTileManager()
// MARK: - Public properties
@Published var status: DownloadStatus = .downloaded
enum DownloadStatus {
case downloaded, downloading
}
init() {
Logger.services.info("🗂️ Documents Directory = \(self.documentsDirectory.absoluteString, privacy: .public)")
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 methods
func getAllDownloadedSize() -> String {
fileManager.allocatedSizeOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"))
}
func removeAll() {
try? fileManager.removeItem(at: documentsDirectory.appendingPathComponent("tiles"))
createDirectoriesIfNecessary()
}
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")!)
}
let tilesUrl = documentsDirectory
.appendingPathComponent("tiles")
.appendingPathComponent("\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y)")
.appendingPathExtension("png")
do {
return try Data(contentsOf: tilesUrl)
} catch let error as NSError where error.code == NSFileReadNoSuchFileError {
DispatchQueue.main.async { self.status = .downloading }
defer {
DispatchQueue.main.async { self.status = .downloaded }
}
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

@ -1,15 +0,0 @@
//
// TileOverlay.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 5/5/23.
//
import Foundation
import MapKit
class TileOverlay: MKTileOverlay {
override func loadTile(at path: MKTileOverlayPath) async throws -> Data {
return try OfflineTileManager.shared.loadAndCacheTileOverlay(for: path)
}
}

View file

@ -103,7 +103,7 @@ func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNu
func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedObjectContext) -> MyInfoEntity? {
let logString = String.localizedStringWithFormat("mesh.log.myinfo %@".localized, String(myInfo.myNodeNum))
let logString = String.localizedStringWithFormat("MyInfo received: %@".localized, String(myInfo.myNodeNum))
Logger.mesh.info(" \(logString, privacy: .public)")
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
@ -209,7 +209,7 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
if metadata.isInitialized {
let logString = String.localizedStringWithFormat("mesh.log.device.metadata.received %@".localized, fromNum.toHex())
let logString = String.localizedStringWithFormat("Device Metadata received from: %@".localized, fromNum.toHex())
Logger.mesh.info("🏷️ \(logString, privacy: .public)")
let fetchedNodeRequest = NodeInfoEntity.fetchRequest()
@ -261,7 +261,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPass
func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? {
let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, String(nodeInfo.num))
let logString = String.localizedStringWithFormat("Node info received for: %@".localized, String(nodeInfo.num))
Logger.mesh.info("📟 \(logString, privacy: .public)")
guard nodeInfo.num > 0 else { return nil }
@ -472,7 +472,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
if !cmmc.messages.isEmpty {
let logString = String.localizedStringWithFormat("mesh.log.cannedmessages.messages.received %@".localized, packet.from.toHex())
let logString = String.localizedStringWithFormat("Canned Messages Messages Received For: %@".localized, packet.from.toHex())
Logger.mesh.info("🥫 \(logString, privacy: .public)")
let fetchNodeRequest = NodeInfoEntity.fetchRequest()
@ -582,7 +582,7 @@ func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
}
func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.paxcounter %@".localized, String(packet.from))
let logString = String.localizedStringWithFormat("PAX Counter message received from: %@".localized, String(packet.from))
Logger.mesh.info("🧑‍🤝‍🧑 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -625,8 +625,8 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
let routingError = RoutingError(rawValue: routingMessage.errorReason.rawValue)
let routingErrorString = routingError?.display ?? "unknown".localized
let logString = String.localizedStringWithFormat("mesh.log.routing.message %@ %@".localized, String(packet.decoded.requestID), routingErrorString)
let routingErrorString = routingError?.display ?? "Unknown".localized
let logString = String.localizedStringWithFormat("Routing received for RequestID: %@ Ack Status: %@".localized, String(packet.decoded.requestID), routingErrorString)
Logger.mesh.info("🕸️ \(logString, privacy: .public)")
let fetchMessageRequest = MessageEntity.fetchRequest()
@ -686,20 +686,15 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) {
if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) {
let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from))
let logString = String.localizedStringWithFormat("Telemetry received for: %@".localized, String(packet.from))
Logger.mesh.info("📈 \(logString, privacy: .public)")
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
/// Other unhandled telemetry packets
return
}
let telemetry = TelemetryEntity(context: context)
let fetchNodeTelemetryRequest = NodeInfoEntity.fetchRequest()
fetchNodeTelemetryRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
do {
let fetchedNode = try context.fetch(fetchNodeTelemetryRequest)
if fetchedNode.count == 1 {
@ -756,7 +751,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)")
} else if telemetryMessage.variant == Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
Logger.data.info("📈 [Power Metrics] Received for Node: \(packet.from.toHex(), privacy: .public)")
telemetry.powerCh1Voltage = telemetryMessage.powerMetrics.hasCh1Voltage.then(telemetryMessage.powerMetrics.ch1Voltage)
telemetry.powerCh1Current = telemetryMessage.powerMetrics.hasCh1Current.then(telemetryMessage.powerMetrics.ch1Current)
telemetry.powerCh2Voltage = telemetryMessage.powerMetrics.hasCh2Voltage.then(telemetryMessage.powerMetrics.ch2Voltage)
@ -764,7 +758,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
telemetry.powerCh3Voltage = telemetryMessage.powerMetrics.hasCh3Voltage.then(telemetryMessage.powerMetrics.ch3Voltage)
telemetry.powerCh3Current = telemetryMessage.powerMetrics.hasCh3Current.then(telemetryMessage.powerMetrics.ch3Current)
telemetry.metricsType = 2
}
telemetry.snr = packet.rxSnr
telemetry.rssi = packet.rxRssi
@ -781,7 +774,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet
}
try context.save()
Logger.data.info("💾 [TelemetryEntity] of type \(MetricsTypes(rawValue: Int(telemetry.metricsType))?.name ?? "Unknown Metrics Type", privacy: .public) Saved for Node: \(packet.from.toHex(), privacy: .public)")
if telemetry.metricsType == 0 {
// Connected Device Metrics
@ -883,7 +875,7 @@ func textMessageAppPacket(
}
if messageText?.count ?? 0 > 0 {
Logger.mesh.info("💬 \("mesh.log.textmessage.received".localized, privacy: .public)")
Logger.mesh.info("💬 \("Message received from the text message app.".localized, privacy: .public)")
let messageUsers = UserEntity.fetchRequest()
messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from])
do {
@ -980,7 +972,7 @@ func textMessageAppPacket(
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
@ -992,7 +984,7 @@ func textMessageAppPacket(
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized, privacy: .public)")
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
}
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
@ -1011,7 +1003,7 @@ func textMessageAppPacket(
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
@ -1023,7 +1015,7 @@ func textMessageAppPacket(
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized, privacy: .public)")
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
}
}
}
@ -1045,7 +1037,7 @@ func textMessageAppPacket(
func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from))
let logString = String.localizedStringWithFormat("Waypoint Packet received from node: %@".localized, String(packet.from))
Logger.mesh.info("📍 \(logString, privacy: .public)")
let fetchWaypointRequest = WaypointEntity.fetchRequest()

View file

@ -8,6 +8,7 @@
import Foundation
import CocoaMQTT
import OSLog
import Security
protocol MqttClientProxyManagerDelegate: AnyObject {
func onMqttConnected()
@ -40,20 +41,20 @@ class MqttClientProxyManager {
if let host = host {
let port = defaultServerPort
var username = node.mqttConfig?.username
var password = node.mqttConfig?.password
// if host == defaultServerAddress {
//username = ProcessInfo.processInfo.environment["PUBLIC_MQTT_USERNAME"]
//password = ProcessInfo.processInfo.environment["PUBLIC_MQTT_PASSWORD"]
// }
let username = node.mqttConfig?.username
let password = node.mqttConfig?.password
let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh"
let prefix = root!
topic = prefix + "/2/e" + "/#"
let qos = CocoaMQTTQoS(rawValue: UInt8(1))!
connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic, qos: qos, cleanSession: true)
// Require opt in to map report terms to connect
if node.mqttConfig?.mapReportingEnabled ?? false && UserDefaults.mapReportingOptIn || !(node.mqttConfig?.mapReportingEnabled ?? false) {
connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic)
} else {
delegate?.onMqttError(message: "MQTT Map Reporting Terms need to be accepted.")
}
}
}
func connect(host: String, port: Int, useSsl: Bool, username: String?, password: String?, topic: String?, qos: CocoaMQTTQoS, cleanSession: Bool) {
func connect(host: String, port: Int, useSsl: Bool, username: String?, password: String?, topic: String?) {
guard !host.isEmpty else {
delegate?.onMqttDisconnected()
return
@ -66,7 +67,7 @@ class MqttClientProxyManager {
mqttClient.username = username
mqttClient.password = password
mqttClient.keepAlive = 60
mqttClient.cleanSession = cleanSession
mqttClient.cleanSession = true
if debugLog {
mqttClient.logLevel = .debug
}
@ -130,6 +131,16 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
self.disconnect()
}
}
func mqtt(_ mqtt: CocoaMQTT, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) {
let isValid = SecTrustEvaluateWithError(trust, nil)
if isValid {
Logger.mqtt.info("📲 [MQTT Client Proxy] TLS validation succeeded.")
completionHandler(true)
} else {
Logger.mqtt.warning("📲 [MQTT Client Proxy] TLS validation failed.")
completionHandler(true)
}
}
func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
Logger.mqtt.debug("📲 [MQTT Client Proxy] disconnected: \(err?.localizedDescription ?? "", privacy: .public)")
if let error = err {

View file

@ -41,7 +41,6 @@ class MetricsChartSeries: ObservableObject {
// Used for scaling the Y-axis
let initialYAxisRange: ClosedRange<Float>?
let minumumYAxisSpan: Float?
// Main initializer
init<Value, ChartBody: ChartContent, ForegroundStyle: ShapeStyle>(
id: String,

View file

@ -67,12 +67,12 @@ class MetricsSeriesList: ObservableObject, RandomAccessCollection, RangeReplacea
for aSeries in self.visible {
var seriesUpper = range[aSeries]?.upperBound ?? -.infinity
var seriesLower = range[aSeries]?.lowerBound ?? .infinity
if let value = aSeries.valueFor(te) {
// Update the global bounds
if value > globalUpper {globalUpper = value}
if value < globalLower {globalLower = value}
// Update the series bounds if necessary
if value > seriesUpper || value < seriesLower {
if value > seriesUpper {

View file

@ -45,6 +45,7 @@ class PersistenceController {
// Merge policy that favors in memory data over data in the db
self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
self.container.viewContext.automaticallyMergesChangesFromParent = true
self.container.viewContext.retainsRegisteredObjects = true
if let error = error as NSError? {

View file

@ -129,7 +129,7 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes
func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, packet.from.toHex())
let logString = String.localizedStringWithFormat("Node info received for: %@".localized, packet.from.toHex())
Logger.mesh.info("📟 \(logString, privacy: .public)")
guard packet.from > 0 else { return }
@ -200,7 +200,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
Notification(
id: (UUID().uuidString),
title: "New Node".localized,
subtitle: "\(newUser.longName ?? "unknown".localized)",
subtitle: "\(newUser.longName ?? "Unknown".localized)",
content: "New Node has been discovered".localized,
target: "nodes",
path: "meshtastic:///nodes?nodenum=\(newUser.num)"
@ -227,6 +227,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
let myInfoEntity = MyInfoEntity(context: context)
myInfoEntity.myNodeNum = Int64(packet.from)
myInfoEntity.rebootCount = 0
newNode.myInfo = myInfoEntity
do {
try context.save()
Logger.data.info("💾 [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)")
@ -236,7 +237,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
let nsError = error as NSError
Logger.data.error("💥 [MyInfoEntity] Error Inserting New Core Data: \(nsError, privacy: .public)")
}
newNode.myInfo = myInfoEntity
} else {
// Update an existing node
@ -312,7 +312,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.position.received %@".localized, String(packet.from))
let logString = String.localizedStringWithFormat("Position Packet received from node: %@".localized, String(packet.from))
Logger.mesh.info("📍 \(logString, privacy: .public)")
let fetchNodePositionRequest = NodeInfoEntity.fetchRequest()
@ -406,7 +406,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.bluetooth.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Bluetooth config received: %@".localized, String(nodeNum))
Logger.mesh.info("📶 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -450,7 +450,7 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64,
func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.device.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Device config received: %@".localized, String(nodeNum))
Logger.mesh.info("📟 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -505,7 +505,7 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi
func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.display.config %@".localized, nodeNum.toHex())
let logString = String.localizedStringWithFormat("Display config received: %@".localized, nodeNum.toHex())
Logger.data.info("🖥️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -567,7 +567,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses
func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.lora.config %@".localized, nodeNum.toHex())
let logString = String.localizedStringWithFormat("LoRa config received: %@".localized, nodeNum.toHex())
Logger.data.info("📻 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -638,7 +638,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPa
func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.network.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Network config received: %@".localized, String(nodeNum))
Logger.data.info("🌐 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -687,7 +687,7 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, ses
func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.position.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Position config received: %@".localized, String(nodeNum))
Logger.data.info("🗺️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -750,7 +750,7 @@ func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, s
}
func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.power.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Power config received: %@".localized, String(nodeNum))
Logger.data.info("🗺️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -863,7 +863,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s
func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.ambientlighting.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Ambient Lighting module config received: %@".localized, String(nodeNum))
Logger.data.info("🏮 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -916,7 +916,7 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin
func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.cannedmessage.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Canned Message module config received: %@".localized, String(nodeNum))
Logger.data.info("🥫 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -975,7 +975,7 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo
func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.detectionsensor.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Detection Sensor module config received: %@".localized, String(nodeNum))
Logger.data.info("🕵️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -1032,7 +1032,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso
func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.externalnotification.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("External Notification module config received: %@".localized, String(nodeNum))
Logger.data.info("📣 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -1101,7 +1101,7 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN
func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.paxcounter.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("PAX Counter config received: %@".localized, String(nodeNum))
Logger.data.info("🧑‍🤝‍🧑 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -1143,7 +1143,7 @@ func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, n
func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.ringtone.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("RTTTL Ringtone config received: %@".localized, String(nodeNum))
Logger.data.info("⛰️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -1183,7 +1183,7 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: D
func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.mqtt.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("MQTT module config received: %@".localized, String(nodeNum))
Logger.data.info("🌉 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -1245,7 +1245,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6
func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.rangetest.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Range Test module config received: %@".localized, String(nodeNum))
Logger.data.info("⛰️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -1289,7 +1289,7 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod
func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.serial.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Serial module config received: %@".localized, String(nodeNum))
Logger.data.info("🤖 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -1344,7 +1344,7 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum:
func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.storeforward.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Store & Forward module config received: %@".localized, String(nodeNum))
Logger.data.info("📬 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
@ -1393,7 +1393,7 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi
func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.telemetry.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("Telemetry module config received: %@".localized, String(nodeNum))
Logger.data.info("📈 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()

View file

@ -543,7 +543,7 @@
"images": [
"t-watch-s3.svg"
],
"partitionScheme": "16MB"
"partitionScheme": "8MB"
},
{
"hwModel": 52,
@ -845,25 +845,32 @@
"hwModelSlug": "THINKNODE_M1",
"platformioTarget": "thinknode_m1",
"architecture": "nrf52840",
"activelySupported": false,
"activelySupported": true,
"supportLevel": 1,
"displayName": "ThinkNode M1",
"tags": [
"Elecrow"
],
"requiresDfu": true
"requiresDfu": true,
"images": [
"thinknode_m1.svg"
],
"hasInkHud": true
},
{
"hwModel": 90,
"hwModelSlug": "THINKNODE_M2",
"platformioTarget": "thinknode_m2",
"architecture": "esp32-s3",
"activelySupported": false,
"activelySupported": true,
"supportLevel": 1,
"displayName": "ThinkNode M2",
"tags": [
"Elecrow"
],
"requiresDfu": false
"requiresDfu": false,
"images": [
"thinknode_m2.svg"
]
}
]

View file

@ -13,10 +13,10 @@
return "tip.channels.share"
}
var title: Text {
Text("tip.channels.share.title")
Text("Sharing Meshtastic Channels")
}
var message: Text? {
Text("tip.channels.share.message")
Text("A Meshtastic QR code contains the LoRa config and channel values needed for radios to communicate. You can share a complete channel configuration using the Replace Channels option, if you choose Add Channels your shared channels will be added to the channels on the receiving radio.")
}
var image: Image? {
Image(systemName: "qrcode")
@ -29,10 +29,10 @@ struct CreateChannelsTip: Tip {
return "tip.channels.create"
}
var title: Text {
Text("tip.channels.create.title")
Text("Manage Channels")
}
var message: Text? {
Text("tip.channels.create.message")
Text("Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)")
}
var image: Image? {
Image(systemName: "fibrechannel")
@ -45,10 +45,10 @@ struct AdminChannelTip: Tip {
return "tip.channel.admin"
}
var title: Text {
Text("tip.channel.admin.title")
Text("Administration Enabled")
}
var message: Text? {
Text("tip.channel.admin.message")
Text("Select a node from the drop down to manage connected or remote devices.")
}
var image: Image? {
Image(systemName: "fibrechannel")

View file

@ -13,10 +13,10 @@ struct MessagesTip: Tip {
return "tip.messages"
}
var title: Text {
Text("tip.messages.title")
Text("Messages")
}
var message: Text? {
Text("tip.messages.message")
Text("You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details.")
}
var image: Image? {
Image(systemName: "bubble.left.and.bubble.right")

View file

@ -46,7 +46,7 @@ struct Connect: View {
VStack {
List {
if bleManager.isSwitchedOn {
Section(header: Text("connected.radio").font(.title)) {
Section(header: Text("Connected Radio").font(.title)) {
if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected {
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
VStack(alignment: .leading) {
@ -64,10 +64,10 @@ struct Connect: View {
if node != nil {
Text(connectedPeripheral.longName.addingVariationSelectors).font(.title2)
}
Text("BLE Name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "unknown".localized)")
Text("BLE Name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "Unknown".localized)")
.font(.callout).foregroundColor(Color.gray)
if node != nil {
Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)")
Text("Firmware Version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "Unknown".localized)")
.font(.callout).foregroundColor(Color.gray)
}
if bleManager.isSubscribed {
@ -116,12 +116,12 @@ struct Connect: View {
#endif
}
} label: {
Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play")
Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play")
}
#endif
Text("Num: \(String(node!.num))")
Text("Short Name: \(node?.user?.shortName ?? "?")")
Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "unknown".localized)")
Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)")
Text("BLE RSSI: \(connectedPeripheral.rssi)")
Button {
@ -139,7 +139,7 @@ struct Connect: View {
NavigationLink {
LoRaConfig(node: node)
} label: {
Label("set.region", systemImage: "globe.americas.fill")
Label("Set LoRa Region", systemImage: "globe.americas.fill")
.foregroundColor(.red)
.font(.title)
}
@ -156,7 +156,7 @@ struct Connect: View {
.frame(width: 60, height: 60)
.padding(.trailing)
if bleManager.timeoutTimerCount == 0 {
Text("connecting")
Text("Connecting . .")
.font(.title2)
.foregroundColor(.orange)
} else {
@ -189,7 +189,7 @@ struct Connect: View {
.foregroundColor(.red)
.frame(width: 60, height: 60)
.padding(.trailing)
Text("not.connected").font(.title3)
Text("No device connected").font(.title3)
}
.padding()
}

View file

@ -17,7 +17,7 @@ struct InvalidVersion: View {
VStack {
Text("update.firmware")
Text("Update Your Firmware")
.font(.largeTitle)
.foregroundColor(.orange)
@ -49,7 +49,7 @@ struct InvalidVersion: View {
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)

View file

@ -11,6 +11,12 @@ struct ContentView: View {
@ObservedObject
var router: Router
init(appState: AppState, router: Router) {
self.appState = appState
self.router = router
UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance(idiom: .unspecified)
}
var body: some View {
TabView(selection: $appState.router.navigationState.selectedTab) {
Messages(
@ -19,7 +25,7 @@ struct ContentView: View {
unreadDirectMessages: $appState.unreadDirectMessages
)
.tabItem {
Label("messages", systemImage: "message")
Label("Messages", systemImage: "message")
}
.tag(NavigationState.Tab.messages)
.badge(appState.totalUnreadMessages)
@ -34,13 +40,13 @@ struct ContentView: View {
router: appState.router
)
.tabItem {
Label("nodes", systemImage: "flipphone")
Label("Nodes", systemImage: "flipphone")
}
.tag(NavigationState.Tab.nodes)
MeshMap(router: appState.router)
.tabItem {
Label("map", systemImage: "map")
Label("Mesh Map", systemImage: "map")
}
.tag(NavigationState.Tab.map)
@ -48,7 +54,7 @@ struct ContentView: View {
router: appState.router
)
.tabItem {
Label("settings", systemImage: "gear")
Label("Settings", systemImage: "gear")
.font(.title)
}
.tag(NavigationState.Tab.settings)

View file

@ -4,29 +4,43 @@ A view draws a circle in the background of the shortName text
*/
import SwiftUI
import CoreData
struct CircleText: View {
var text: String
var color: Color
var text: String
var color: Color
var circleSize: CGFloat = 45
var node: NodeInfoEntity?
var body: some View {
var body: some View {
if let node = node {
NavigationStack {
NavigationLink(destination: NodeDetail(node: node)) {
circleContent
}
}
ZStack {
Circle()
.fill(color)
.frame(width: circleSize, height: circleSize)
Text(text.addingVariationSelectors)
} else {
circleContent
}
}
var circleContent: some View {
ZStack {
Circle()
.fill(color)
.frame(width: circleSize, height: circleSize)
Text(text)
.frame(width: circleSize * 0.9, height: circleSize * 0.9, alignment: .center)
.foregroundColor(color.isLight() ? .black : .white)
.minimumScaleFactor(0.001)
.font(.system(size: 1300))
}
}
}
}
}
struct CircleText_Previews: PreviewProvider {
static var previews: some View {
static var previews: some View {
VStack {
HStack {
CircleText(text: "N1", color: Color.yellow, circleSize: 80)
@ -75,5 +89,5 @@ struct CircleText_Previews: PreviewProvider {
.previewLayout(.fixed(width: 300, height: 100))
}
}
}
}
}

View file

@ -31,7 +31,7 @@ import SwiftUI
WeightCompactWidget(weight: "123", unit: "kg")
SoilTemperatureCompactWidget(temperature: "23", unit: "°C")
SoilMoistureCompactWidget(moisture: "23", unit: "%")
let rain: Float = 10.1
let locale = NSLocale.current as NSLocale
let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches)

View file

@ -16,7 +16,7 @@ struct RadiationCompactWidget: View {
HStack(alignment: .firstTextBaseline) {
Text(verbatim: "")
.font(.system(size: 30, design: .monospaced))
.foregroundColor(.accentColor)
.tint(.accentColor)
Text("Radiation")
.textCase(.uppercase)
.font(.callout)

View file

@ -39,4 +39,3 @@ struct WeatherConditionsCompactWidget: View {
}
}
}

View file

@ -24,7 +24,7 @@ struct DateTimeText: View {
if dateTime != nil && dateTime! >= sixMonthsAgo! {
Text(" \(dateTime!.formattedDate(format: dateFormatString))")
} else {
Text("unknown.age")
Text("Unknown Age")
}
}
}

View file

@ -50,7 +50,7 @@ struct DirectMessagesHelp: View {
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)

View file

@ -47,13 +47,13 @@ enum LoRaSignalStrength: Int {
var description: String {
switch self {
case .none:
return "lora.signal.strength.none".localized
return "None".localized
case .bad:
return "lora.signal.strength.bad".localized
return "Bad".localized
case .fair:
return "lora.signal.strength.fair".localized
return "Fair".localized
case .good:
return "lora.signal.strength.good".localized
return "Good".localized
}
}
}

View file

@ -29,7 +29,7 @@ struct MQTTIcon: View {
VStack(spacing: 0.5) {
Text("Topic: \(topic)".localized)
.padding(20)
Button("close", action: { self.isPopoverOpen = false }).padding([.bottom], 20)
Button("Close", action: { self.isPopoverOpen = false }).padding([.bottom], 20)
}
.presentationCompactAdaptation(.popover)
})

View file

@ -69,8 +69,8 @@ struct PowerMetrics: View {
}
enum PowerMetricType: String {
case current = "current"
case voltage = "voltage"
case current = "Current"
case voltage = "Voltage"
}
struct PowerMetricCompactWidget: View {

View file

@ -24,7 +24,7 @@ struct ChannelList: View {
@State private var isPresentingTraceRouteSentAlert = false
var restrictedChannels = ["gpio", "mqtt", "serial"]
var restrictedChannels = ["gpio", "mqtt", "serial", "admin"]
@ViewBuilder
private func makeChannelRow(
@ -144,7 +144,7 @@ struct ChannelList: View {
context.refresh(myInfo, mergeChanges: true)
channelSelection = nil
} label: {
Text("delete")
Text("Delete")
}
}
}
@ -154,6 +154,6 @@ struct ChannelList: View {
.listStyle(.plain)
}
}
.navigationTitle("channels")
.navigationTitle("Channels")
}
}

View file

@ -14,136 +14,212 @@ struct ChannelMessageList: View {
@EnvironmentObject var appState: AppState
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
// Keyboard State
@FocusState var messageFieldFocused: Bool
@ObservedObject var myInfo: MyInfoEntity
@ObservedObject var channel: ChannelEntity
@State private var replyMessageId: Int64 = 0
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
// Scroll state
@State private var showScrollToBottomButton = false
@State private var hasReachedBottom = false
@State private var gotFirstUnreadMessage: Bool = false
@State private var messageToHighlight: Int64 = 0
var body: some View {
VStack {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach( channel.allPrivateMessages ) { (message: MessageEntity) in
let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false)
if message.replyID > 0 {
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
HStack {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
}
}
HStack(alignment: .bottom) {
if currentUser { Spacer(minLength: 50) }
if !currentUser {
CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44)
.padding(.all, 5)
.offset(y: -7)
}
VStack(alignment: currentUser ? .trailing : .leading) {
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
if !currentUser && message.fromUser != nil {
Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))")
.font(.caption)
.foregroundColor(.gray)
.offset(y: 8)
}
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach(channel.allPrivateMessages) { (message: MessageEntity) in
let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false)
if message.replyID > 0 {
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
HStack {
MessageText(
message: message,
tapBackDestination: .channel(channel),
isCurrentUser: currentUser
) {
self.replyMessageId = message.messageId
self.messageFieldFocused = true
}
Button {
if let messageNum = messageReply?.messageId {
withAnimation(.easeInOut(duration: 0.5)) {
messageToHighlight = messageNum
}
scrollView.scrollTo(messageNum, anchor: .center)
if currentUser && message.canRetry {
RetryButton(message: message, destination: .channel(channel))
}
}
TapbackResponses(message: message) {
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
}
HStack {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if currentUser && message.receivedACK {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("Waiting to be acknowledged. . .").font(
.caption2)
.foregroundColor(.orange)
} else if currentUser && !isDetectionSensorMessage {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
// Reset highlight after delay
Task {
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
withAnimation(.easeInOut(duration: 0.5)) {
messageToHighlight = -1
}
}
}
} label: {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
}
}
}
.padding(.bottom)
.id(channel.allPrivateMessages.firstIndex(of: message))
HStack(alignment: .bottom) {
if currentUser { Spacer(minLength: 50) }
if !currentUser {
CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44, node: getNodeInfo(id: Int64(message.fromUser?.num ?? 0), context: context))
.padding(.all, 5)
.offset(y: -7)
}
if !currentUser {
Spacer(minLength: 50)
}
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)
.onAppear {
if !message.read {
message.read = true
do {
for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) {
unreadMessage.read = true
VStack(alignment: currentUser ? .trailing : .leading) {
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
if !currentUser && message.fromUser != nil {
Text("\(message.fromUser?.longName ?? "Unknown".localized ) (\(message.fromUser?.userId ?? "?"))")
.font(.caption)
.foregroundColor(.gray)
.offset(y: 8)
}
HStack {
MessageText(
message: message,
tapBackDestination: .channel(channel),
isCurrentUser: currentUser
) {
self.replyMessageId = message.messageId
self.messageFieldFocused = true
}
if currentUser && message.canRetry {
RetryButton(message: message, destination: .channel(channel))
}
}
TapbackResponses(message: message) {
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
}
HStack {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if currentUser && message.receivedACK {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("Waiting to be acknowledged. . .").font(
.caption2)
.foregroundColor(.orange)
} else if currentUser && !isDetectionSensorMessage {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
}
}
}
.padding(.bottom)
.id(channel.allPrivateMessages.firstIndex(of: message))
if !currentUser {
Spacer(minLength: 50)
}
}
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 2)
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)
.onAppear {
if gotFirstUnreadMessage {
if !message.read {
message.read = true
do {
for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) {
unreadMessage.read = true
}
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
} catch {
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
// Check if we've reached the bottom message
if message.messageId == channel.allPrivateMessages.last?.messageId {
hasReachedBottom = true
showScrollToBottomButton = false
}
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
} catch {
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
}
// Invisible spacer to detect reaching bottom
Color.clear
.frame(height: 1)
.id("bottomAnchor")
.onAppear {
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
}
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
// Find first unread message
if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId {
withAnimation {
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
showScrollToBottomButton = true
}
} else {
// If no unread messages, scroll to bottom
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
}
}
gotFirstUnreadMessage = true
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
.onChange(of: channel.allPrivateMessages) {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
.onChange(of: channel.allPrivateMessages) {
if hasReachedBottom {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
}
} else {
showScrollToBottomButton = true
}
}
// Scroll to bottom button
if showScrollToBottomButton {
Button {
withAnimation {
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
} label: {
ScrollToBottomButtonView()
}
.padding(.bottom, 8)
.padding(.trailing, 16)
.transition(.opacity)
}
}
}
@ -161,7 +237,7 @@ struct ChannelMessageList: View {
ToolbarItem(placement: .principal) {
HStack {
CircleText(text: String(channel.index), color: .accentColor, circleSize: 44).fixedSize()
Text(String(channel.name ?? "unknown".localized).camelCaseToWords()).font(.headline)
Text(String(channel.name ?? "Unknown".localized).camelCaseToWords()).font(.headline)
}
}
ToolbarItem(placement: .navigationBarTrailing) {

View file

@ -51,7 +51,7 @@ struct MessageContextMenuItems: View {
Image(systemName: "doc.on.doc")
}
Menu("message.details") {
Menu("Message Details") {
VStack {
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))").foregroundColor(.gray)
@ -69,8 +69,8 @@ struct MessageContextMenuItems: View {
}
if isCurrentUser && message.receivedACK {
VStack {
Text("received.ack") + Text(": \(message.receivedACK ? "✔️" : "")")
Text("received.ack.real") + Text(": \(message.realACK ? "✔️" : "")")
Text("Received Ack") + Text(": \(message.receivedACK ? "✔️" : "")")
Text("Recipient Ack") + Text(": \(message.realACK ? "✔️" : "")")
}
} else if isCurrentUser && message.ackError == 0 {
// Empty Error
@ -104,7 +104,7 @@ struct MessageContextMenuItems: View {
Button(role: .destructive) {
isShowingDeleteConfirmation = true
} label: {
Text("delete")
Text("Delete")
Image(systemName: "trash")
}
}

View file

@ -73,7 +73,6 @@ struct MessageText: View {
} else {
EmptyView()
}
}
.contextMenu {
MessageContextMenuItems(

View file

@ -35,7 +35,7 @@ struct Messages: View {
List(selection: $router.navigationState.messages) {
NavigationLink(value: MessagesNavigationState.channels()) {
Label {
Text("channels")
Text("Channels")
.badge(unreadChannelMessages)
.font(.title2)
.padding()
@ -50,7 +50,7 @@ struct Messages: View {
}
NavigationLink(value: MessagesNavigationState.directMessages()) {
Label {
Text("direct.messages")
Text("Direct Messages")
.badge(unreadDirectMessages)
.font(.title2)
.padding()
@ -65,7 +65,7 @@ struct Messages: View {
TipView(MessagesTip(), arrowEdge: .top)
}
.navigationTitle("messages")
.navigationTitle("Messages")
.navigationBarTitleDisplayMode(.large)
.navigationBarItems(leading: MeshtasticLogo())
} content: {

View file

@ -15,78 +15,93 @@ struct TextMessageField: View {
@State private var sendPositionWithMessage = false
var body: some View {
#if targetEnvironment(macCatalyst)
HStack {
if destination.showAlertButton {
VStack {
#if targetEnvironment(macCatalyst)
HStack {
if destination.showAlertButton {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" }
}
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" }
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
}
Spacer()
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
}
#endif
#endif
HStack(alignment: .top) {
ZStack {
TextField("message", text: $typingMessage, axis: .vertical)
.onChange(of: typingMessage) { _, value in
totalBytes = value.utf8.count
// Only mess with the value if it is too big
while totalBytes > Self.maxbytes {
typingMessage = String(typingMessage.dropLast())
totalBytes = typingMessage.utf8.count
}
}
.keyboardType(.default)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Dismiss") {
isFocused = false
HStack(alignment: .top) {
if replyMessageId != 0 {
HStack {
Button {
withAnimation(.easeInOut(duration: 0.2)) {
replyMessageId = 0
}
.font(.subheadline)
isFocused = false
} label: {
Image(systemName: "x.circle.fill")
}
Text("Replying to a message")
}
}
ZStack {
TextField("Message", text: $typingMessage, axis: .vertical)
.onChange(of: typingMessage) { _, value in
totalBytes = value.utf8.count
while totalBytes > Self.maxbytes {
typingMessage = String(typingMessage.dropLast())
totalBytes = typingMessage.utf8.count
}
}
.keyboardType(.default)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Dismiss") {
isFocused = false
}
.font(.subheadline)
if destination.showAlertButton {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
}
if destination.showAlertButton {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
}
Spacer()
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
}
}
.padding(.horizontal, 8)
.focused($isFocused)
.multilineTextAlignment(.leading)
.frame(minHeight: 50)
.keyboardShortcut(.defaultAction)
.onSubmit {
#if targetEnvironment(macCatalyst)
sendMessage()
#endif
}
.padding(.horizontal, 8)
.focused($isFocused)
.multilineTextAlignment(.leading)
.frame(minHeight: 50)
.keyboardShortcut(.defaultAction)
.onSubmit {
#if targetEnvironment(macCatalyst)
sendMessage()
#endif
}
Text(typingMessage)
.opacity(0)
.padding(.all, 0)
}
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
.padding(.bottom, 15)
Text(typingMessage)
.opacity(0)
.padding(.all, 0)
}
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
.padding(.bottom, 15)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle.fill")
.font(.largeTitle)
.foregroundColor(.accentColor)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle.fill")
.font(.largeTitle)
.foregroundColor(.accentColor)
}
}
.padding(.all, 15)
}
.padding(.all, 15)
}
private func requestPosition() {
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
sendPositionWithMessage = true
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
}
private func sendMessage() {

View file

@ -93,7 +93,7 @@ struct UserList: View {
Image(systemName: "lock.open.fill")
.foregroundColor(.yellow)
}
Text(user.longName ?? "unknown".localized)
Text(user.longName ?? "Unknown".localized)
.font(.headline)
.allowsTightening(true)
Spacer()
@ -187,14 +187,14 @@ struct UserList: View {
deleteUserMessages(user: userSelection!, context: context)
context.refresh(node!.user!, mergeChanges: true)
} label: {
Text("delete")
Text("Delete")
}
}
}
}
}
.listStyle(.plain)
.navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count)))
.navigationTitle(String.localizedStringWithFormat("Contacts (%@)".localized, String(users.count == 0 ? 0 : users.count)))
.sheet(isPresented: $editingFilters) {
NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles)
}

View file

@ -20,115 +20,194 @@ struct UserMessageList: View {
// View State Items
@ObservedObject var user: UserEntity
@State private var replyMessageId: Int64 = 0
// Scroll state
@State private var showScrollToBottomButton = false
@State private var hasReachedBottom = false
@State private var gotFirstUnreadMessage: Bool = false
@State private var messageToHighlight: Int64 = 0
var body: some View {
VStack {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach( user.messageList ) { (message: MessageEntity) in
if user.num != bleManager.connectedPeripheral?.num ?? -1 {
let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false)
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach( user.messageList ) { (message: MessageEntity) in
if user.num != bleManager.connectedPeripheral?.num ?? -1 {
let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false)
if message.replyID > 0 {
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
HStack {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
}
}
HStack(alignment: .top) {
if currentUser { Spacer(minLength: 50) }
VStack(alignment: currentUser ? .trailing : .leading) {
if message.replyID > 0 {
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
HStack {
MessageText(
message: message,
tapBackDestination: .user(user),
isCurrentUser: currentUser
) {
self.replyMessageId = message.messageId
self.messageFieldFocused = true
}
Button {
if let messageNum = messageReply?.messageId {
withAnimation(.easeInOut(duration: 0.5)) {
messageToHighlight = messageNum
}
scrollView.scrollTo(messageNum, anchor: .center)
if currentUser && message.canRetry || (message.receivedACK && !message.realACK) {
RetryButton(message: message, destination: .user(user))
}
}
TapbackResponses(message: message) {
appState.unreadDirectMessages = user.unreadMessages
}
HStack {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if currentUser && message.receivedACK {
// Ack Received
if message.realACK {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")")
.font(.caption2)
.foregroundStyle(ackErrorVal?.color ?? Color.secondary)
} else {
Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange)
// Reset highlight after delay
Task {
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
withAnimation(.easeInOut(duration: 0.5)) {
messageToHighlight = -1
}
}
}
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow)
} else if currentUser && message.ackError > 0 {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
} label: {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
}
}
}
.padding(.bottom)
.id(user.messageList.firstIndex(of: message))
HStack(alignment: .top) {
if currentUser { Spacer(minLength: 50) }
VStack(alignment: currentUser ? .trailing : .leading) {
HStack {
MessageText(
message: message,
tapBackDestination: .user(user),
isCurrentUser: currentUser
) {
self.replyMessageId = message.messageId
self.messageFieldFocused = true
}
if !currentUser {
Spacer(minLength: 50)
if currentUser && message.canRetry || (message.receivedACK && !message.realACK) {
RetryButton(message: message, destination: .user(user))
}
}
TapbackResponses(message: message) {
appState.unreadDirectMessages = user.unreadMessages
}
HStack {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if currentUser && message.receivedACK {
// Ack Received
if message.realACK {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")")
.font(.caption2)
.foregroundStyle(ackErrorVal?.color ?? Color.secondary)
} else {
Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange)
}
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow)
} else if currentUser && message.ackError > 0 {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
}
}
}
.padding(.bottom)
.id(user.messageList.firstIndex(of: message))
if !currentUser {
Spacer(minLength: 50)
}
}
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)
.onAppear {
if !message.read {
message.read = true
do {
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadDirectMessages = user.unreadMessages
} catch {
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 2)
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)
.onAppear {
if gotFirstUnreadMessage {
if !message.read {
message.read = true
do {
for unreadMessage in user.messageList.filter({ !$0.read }) {
unreadMessage.read = true
}
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadDirectMessages = user.unreadMessages
} catch {
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
// Check if we've reached the bottom message
if message.messageId == user.messageList.last?.messageId {
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
}
}
// Invisible spacer to detect reaching bottom
Color.clear
.frame(height: 1)
.id("bottomAnchor")
.onAppear {
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
}
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
// Find first unread message
if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId {
withAnimation {
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
showScrollToBottomButton = true
}
} else {
// If no unread messages, scroll to bottom
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
}
}
gotFirstUnreadMessage = true
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
}
}
.onChange(of: user.messageList) {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
.onChange(of: user.messageList) {
if hasReachedBottom {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
}
} else {
showScrollToBottomButton = true
}
}
// Scroll to bottom button
if showScrollToBottomButton {
Button {
withAnimation {
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
} label: {
ScrollToBottomButtonView()
}
.padding(.bottom, 8)
.padding(.trailing, 16)
.transition(.opacity)
}
}
}

View file

@ -110,7 +110,7 @@ struct DetectionSensorLog: View {
exportString = detectionsToCsv(detections: chartData)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
@ -118,7 +118,7 @@ struct DetectionSensorLog: View {
.padding(.bottom)
.padding(.trailing)
}
.navigationTitle("detection.sensor.log")
.navigationTitle("Detection Sensor Log")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {
@ -128,7 +128,7 @@ struct DetectionSensorLog: View {
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") \("detection.sensor.log".localized)"),
defaultFilename: String("\(node.user?.longName ?? "Node") \("Detection Sensor Log".localized)"),
onCompletion: { result in
switch result {
case .success:

View file

@ -121,7 +121,7 @@ struct DeviceMetricsLog: View {
Table(deviceMetrics, selection: $selection, sortOrder: $sortOrder) {
TableColumn("Battery Level") { dm in
HStack {
Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
.font(.caption)
.fontWeight(.semibold)
Spacer()
@ -165,7 +165,7 @@ struct DeviceMetricsLog: View {
// dm.voltage.map { Text("\(String(format: "%.2f", $0))") } ?? Text("--")
Text("\(dm.voltage?.formatted(.number.precision(.fractionLength(2))) ?? Constants.nilValueIndicator)")
}
TableColumn("channel.utilization") { dm in
TableColumn("Channel Utilization") { dm in
dm.channelUtilization.map { channelUtilization in
// Text("\(String(format: "%.2f", channelUtilization))%")
Text("\(channelUtilization.formatted(.number.precision(.fractionLength(2))))%")
@ -188,7 +188,7 @@ struct DeviceMetricsLog: View {
}
.width(min: 100)
TableColumn("Timestamp") { dm in
Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
}
.width(min: 180)
}
@ -209,7 +209,7 @@ struct DeviceMetricsLog: View {
isPresented: $isPresentingClearLogConfirm,
titleVisibility: .visible
) {
Button("device.metrics.delete", role: .destructive) {
Button("Delete all device metrics?", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 0, context: context) {
Logger.data.notice("Cleared Device Metrics for \(node.num, privacy: .public)")
} else {
@ -222,7 +222,7 @@ struct DeviceMetricsLog: View {
exportString = telemetryToCsvFile(telemetry: deviceMetrics, metricsType: 0)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
@ -240,7 +240,7 @@ struct DeviceMetricsLog: View {
ContentUnavailableView("No Device Metrics", systemImage: "slash.circle")
}
}
.navigationTitle("device.metrics.log")
.navigationTitle("Device Metrics Log")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {
@ -250,7 +250,7 @@ struct DeviceMetricsLog: View {
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") \("device.metrics.log".localized)"),
defaultFilename: String("\(node.user?.longName ?? "Node") \("Device Metrics Log".localized)"),
onCompletion: { result in
switch result {
case .success:

View file

@ -145,7 +145,7 @@ struct EnvironmentMetricsLog: View {
exportString = telemetryToCsvFile(telemetry: environmentMetrics, metricsType: 1)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
.imageScale(imageScale)
}
.buttonStyle(.bordered)

View file

@ -49,7 +49,7 @@ struct DeleteNodeButton: View {
connectedNodeNum: connectedNode.num
)
if !success {
Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "unknown".localized, privacy: .public)")
Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "Unknown".localized, privacy: .public)")
} else {
dismiss()
}

View file

@ -14,45 +14,43 @@ struct NavigateToButton: View {
var node: NodeInfoEntity
var body: some View {
Button {
guard let userNum = node.user?.num else {
Logger.services.error("NavigateToAction: Selected node does not exist")
Button {
guard let userNum = node.user?.num else {
Logger.services.error("NavigateToAction: Selected node does not exist")
return
}
Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum, privacy: .public)")
let fetchRequest: NSFetchRequest<NodeInfoEntity> = NSFetchRequest(entityName: "NodeInfoEntity")
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
do {
let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest)
guard let nodeInfo = fetchedNodes.first else {
Logger.services.error("NavigateToAction: Node with userNum \(userNum, privacy: .public) not found in Core Data")
return
}
Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum, privacy: .public)")
let fetchRequest: NSFetchRequest<NodeInfoEntity> = NSFetchRequest(entityName: "NodeInfoEntity")
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
do {
let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest)
guard let nodeInfo = fetchedNodes.first else {
Logger.services.error("NavigateToAction: Node with userNum \(userNum, privacy: .public) not found in Core Data")
return
}
if let latitude = nodeInfo.latestPosition?.latitude,
let longitude = nodeInfo.latestPosition?.longitude {
if let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)") {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
Logger.services.error("Failed to create URL for navigation")
}
if let latitude = nodeInfo.latestPosition?.latitude,
let longitude = nodeInfo.latestPosition?.longitude {
if let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)") {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
Logger.services.warning("NavigateToAction: Node \(userNum, privacy: .public) has invalid or missing coordinates")
Logger.services.error("Failed to create URL for navigation")
}
} catch {
Logger.services.error("NavigateToAction: Failed to fetch node with userNum \(userNum, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
} label: {
Label {
Text("Navigate to node")
} icon: {
Image(systemName: "map")
.symbolRenderingMode(.hierarchical)
} else {
Logger.services.warning("NavigateToAction: Node \(userNum, privacy: .public) has invalid or missing coordinates")
}
} catch {
Logger.services.error("NavigateToAction: Failed to fetch node with userNum \(userNum, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
} label: {
Label {
Text("Navigate to node")
} icon: {
Image(systemName: "map")
.symbolRenderingMode(.hierarchical)
}
}
}
}

View file

@ -20,7 +20,7 @@ struct MeshMapContent: MapContent {
@Binding var selectedMapLayer: MapLayer
// Map Configuration
@Binding var selectedPosition: PositionEntity?
@AppStorage("enableMapWaypoints") private var showWaypoints = false
@AppStorage("enableMapWaypoints") private var showWaypoints = true
@Binding var selectedWaypoint: WaypointEntity?
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
@ -74,9 +74,9 @@ struct MeshMapContent: MapContent {
}
}
}
.onTapGesture { _ in
.highPriorityGesture(TapGesture().onEnded { _ in
selectedPosition = (selectedPosition == position ? nil : position)
}
})
}
/// Node History and Route Lines for favorites
if let nodePosition = position.nodePosition,
@ -186,7 +186,7 @@ struct MeshMapContent: MapContent {
LazyVStack {
ZStack {
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40)
.onTapGesture(perform: { _ in
.highPriorityGesture(TapGesture().onEnded { _ in
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
})
}

View file

@ -16,7 +16,7 @@ struct NodeMapContent: MapContent {
/// Map State User Defaults
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
@AppStorage("enableMapWaypoints") private var showWaypoints = false
@AppStorage("enableMapWaypoints") private var showWaypoints = true
@AppStorage("enableMapConvexHull") private var showConvexHull = false
@AppStorage("enableMapTraffic") private var showTraffic: Bool = false
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false

View file

@ -111,7 +111,7 @@ Spacer()
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)

View file

@ -189,7 +189,7 @@ struct NodeMapSwiftUI: View {
UIApplication.shared.isIdleTimerDisabled = false
}
}}
.navigationBarTitle(String((node.user?.shortName ?? "unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline)
.navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(

View file

@ -23,10 +23,10 @@ struct PositionPopover: View {
var body: some View {
// Node Color from node.num
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
NavigationStack {
VStack {
HStack {
ZStack {
if position.nodePosition?.isOnline ?? false {
Circle()
.fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5)))
@ -34,16 +34,15 @@ struct PositionPopover: View {
.scaleEffect(scale)
.animation(
Animation.easeInOut(duration: 0.6)
.repeatForever().delay(delay), value: scale
.repeatForever().delay(delay), value: scale
)
.onAppear {
self.scale = 1
}
.frame(width: 90, height: 90)
}
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65)
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65, node: getNodeInfo(id: Int64(position.nodePosition?.user?.num ?? 0), context: context))
}
Text(position.nodePosition?.user?.longName ?? "Unknown")
.font(.largeTitle)
}
@ -53,7 +52,7 @@ struct PositionPopover: View {
/// Time
Label {
if idiom != .phone {
Text("heard".localized + ":")
Text("Heard".localized + ":")
}
Text(position.time?.lastHeard ?? "unknown")
.foregroundColor(.primary)
@ -106,7 +105,6 @@ struct PositionPopover: View {
.foregroundColor(.primary)
.font(idiom == .phone ? .callout : .body)
}
} icon: {
Image(systemName: "mountain.2.fill")
.symbolRenderingMode(.hierarchical)
@ -147,9 +145,9 @@ struct PositionPopover: View {
Text("Heading: \(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))")
} icon: {
Image(systemName: "location.north")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
}
.padding(.bottom, 5)
/// Distance
@ -181,15 +179,14 @@ struct PositionPopover: View {
}
.padding(.bottom, 5)
if position.nodePosition?.viaMqtt ?? false {
Label {
Text("MQTT")
.font(idiom == .phone ? .callout : .body)
} icon: {
Image(systemName: "network")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
}
.padding(.bottom, 5)
}
@ -235,7 +232,7 @@ struct PositionPopover: View {
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
@ -244,6 +241,7 @@ struct PositionPopover: View {
#endif
}
}
}
.presentationDetents([.fraction(0.65), .large])
.presentationContentInteraction(.scrolls)
.presentationDragIndicator(.visible)

View file

@ -134,40 +134,44 @@ struct WaypointForm: View {
.scrollDismissesKeyboard(.immediately)
HStack {
Button {
/// Send a new or exiting waypoint
var newWaypoint = Waypoint()
if waypoint.id == 0 {
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
waypoint.id = Int64(newWaypoint.id)
} else {
newWaypoint.id = UInt32(waypoint.id)
}
newWaypoint.latitudeI = waypoint.latitudeI
newWaypoint.longitudeI = waypoint.longitudeI
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
newWaypoint.description_p = description
// Unicode scalar value for the icon emoji string
let unicodeScalers = icon.unicodeScalars
// First element as an UInt32
let unicode = unicodeScalers[unicodeScalers.startIndex].value
newWaypoint.icon = unicode
if locked {
if lockedTo == 0 {
newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num)
if bleManager.isConnected {
/// Send a new or exiting waypoint
var newWaypoint = Waypoint()
if waypoint.id == 0 {
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
waypoint.id = Int64(newWaypoint.id)
} else {
newWaypoint.lockedTo = UInt32(lockedTo)
newWaypoint.id = UInt32(waypoint.id)
}
newWaypoint.latitudeI = waypoint.latitudeI
newWaypoint.longitudeI = waypoint.longitudeI
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
newWaypoint.description_p = description
// Unicode scalar value for the icon emoji string
let unicodeScalers = icon.unicodeScalars
// First element as an UInt32
let unicode = unicodeScalers[unicodeScalers.startIndex].value
newWaypoint.icon = unicode
if locked {
if lockedTo == 0 {
newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num)
} else {
newWaypoint.lockedTo = UInt32(lockedTo)
}
}
if expires {
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
} else {
newWaypoint.expire = 0
}
if bleManager.sendWaypoint(waypoint: newWaypoint) {
dismiss()
} else {
dismiss()
Logger.mesh.warning("Send waypoint failed")
}
}
if expires {
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
} else {
newWaypoint.expire = 0
}
if bleManager.sendWaypoint(waypoint: newWaypoint) {
dismiss()
} else {
dismiss()
Logger.mesh.warning("Send waypoint failed")
Logger.mesh.warning("Send waypoint failed, node not connected")
}
} label: {
Label("Send", systemImage: "arrow.up")
@ -235,7 +239,7 @@ struct WaypointForm: View {
})
}
label: {
Label("delete", systemImage: "trash")
Label("Delete", systemImage: "trash")
.foregroundColor(.red)
}
.buttonStyle(.bordered)
@ -338,7 +342,7 @@ struct WaypointForm: View {
if LocationsHandler.currentLocation.distance(from: LocationsHandler.DefaultLocation) > 0.0 {
let metersAway = waypoint.coordinate.distance(from: LocationsHandler.currentLocation)
Label {
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
.foregroundColor(.primary)
} icon: {
Image(systemName: "lines.measurement.horizontal")
@ -354,7 +358,7 @@ struct WaypointForm: View {
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
@ -364,6 +368,18 @@ struct WaypointForm: View {
}
}
}
.onDisappear {
if waypoint.id == 0 {
// New, unsent waypoint created by the user: delete it
bleManager.context.delete(waypoint)
do {
try bleManager.context.save()
} catch {
bleManager.context.rollback()
Logger.mesh.error("Failed to save context on waypoint deletion: \(error)")
}
}
}
.onAppear {
if waypoint.id > 0 {
let waypoint = getWaypoint(id: Int64(waypoint.id), context: bleManager.context)

View file

@ -220,7 +220,6 @@ extension MetricsColumnList {
)
} ?? Text(Constants.nilValueIndicator)
}),
// Rainfall 24-hour
MetricsTableColumn(
id: "rainfall24H",
@ -334,7 +333,7 @@ extension MetricsColumnList {
.replacingOccurrences(of: ",", with: "")
Text(
time?.formattedDate(format: dateFormatString)
?? "unknown.age".localized
?? "Unknown Age".localized
)
})
])

View file

@ -80,7 +80,7 @@ extension MetricsSeriesList {
.alignsMarkStylesWithPlotArea()
}
}),
// Barometric Pressure Series Configuration
MetricsChartSeries(
id: "barometricPressure",
@ -106,7 +106,7 @@ extension MetricsSeriesList {
.alignsMarkStylesWithPlotArea()
}
}),
// Indoor Air Quality Series Configuration
MetricsChartSeries(
id: "iaq",
@ -134,7 +134,7 @@ extension MetricsSeriesList {
.alignsMarkStylesWithPlotArea()
}
}),
// Lux
MetricsChartSeries(
id: "lux",
@ -460,7 +460,7 @@ extension MetricsSeriesList {
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
})
])
}
}

View file

@ -63,7 +63,7 @@ struct MetricsColumnDetail: View {
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)

View file

@ -120,14 +120,14 @@ struct NodeDetail: View {
if let metadata = node.metadata {
HStack {
Label {
Text("firmware.version")
Text("Firmware Version")
} icon: {
Image(systemName: "memorychip")
.symbolRenderingMode(.multicolor)
}
Spacer()
Text(metadata.firmwareVersion ?? "unknown".localized)
Text(metadata.firmwareVersion ?? "Unknown".localized)
}
}
@ -147,7 +147,7 @@ struct NodeDetail: View {
if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds {
HStack {
Label {
Text("\("uptime".localized)")
Text("\("Uptime".localized)")
} icon: {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
@ -195,7 +195,7 @@ struct NodeDetail: View {
Spacer()
if dateFormatRelative, let text = Self.relativeFormatter.string(for: lastHeard) {
if lastHeard.formatted() != "unknown.age".localized {
if lastHeard.formatted() != "Unknown Age".localized {
Text(text)
.textSelection(.enabled)
}
@ -214,7 +214,7 @@ struct NodeDetail: View {
// to use with WeatherKit, or has actual data in the most recent EnvironmentMetrics entity
// that will be rendered in this section.
if node.hasPositions && UserDefaults.environmentEnableWeatherKit
|| node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "distance", "soilTemperature", "soilMoisture"]) {
|| node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) {
Section("Environment") {
if !node.hasEnvironmentMetrics {
LocalWeatherConditions(location: node.latestPosition?.nodeLocation)
@ -499,14 +499,14 @@ struct NodeDetail: View {
showingRebootConfirm = true
} label: {
Label(
"reboot",
"Reboot",
systemImage: "arrow.triangle.2.circlepath"
)
}.confirmationDialog(
"Are you sure?",
isPresented: $showingRebootConfirm
) {
Button("reboot.node", role: .destructive) {
Button("Reboot node?", role: .destructive) {
if !bleManager.sendReboot(
fromUser: connectedNode.user!,
toUser: node.user!,
@ -520,6 +520,7 @@ struct NodeDetail: View {
}
}
.listStyle(.insetGrouped)
.navigationBarTitle(String(node.user?.longName?.addingVariationSelectors ?? "Unknown".localized), displayMode: .inline)
}
}
}

View file

@ -74,7 +74,7 @@ struct NodeInfoItem: View {
}
Spacer()
if user.hwModel != "UNSET" {
Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "unset".localized)))
Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "Unset".localized)))
} else {
Text(String("incomplete".localized))
}

View file

@ -192,7 +192,7 @@ struct NodeListFilter: View {
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)

View file

@ -64,7 +64,7 @@ struct NodeListItem: View {
let (image, color) = userKeyStatus
IconAndText(systemName: image,
imageColor: color,
text: node.user?.longName?.addingVariationSelectors ?? "unknown".localized,
text: node.user?.longName?.addingVariationSelectors ?? "Unknown".localized,
textColor: .primary)
if node.favorite {
Spacer()
@ -75,16 +75,16 @@ struct NodeListItem: View {
if connected {
IconAndText(systemName: "antenna.radiowaves.left.and.right.circle.fill",
imageColor: .green,
text: "connected".localized)
text: "Connected".localized)
}
if node.lastHeard?.timeIntervalSince1970 ?? 0 > 0 && node.lastHeard! < Calendar.current.date(byAdding: .year, value: 1, to: Date())!{
if node.lastHeard?.timeIntervalSince1970 ?? 0 > 0 && node.lastHeard! < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
IconAndText(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill",
imageColor: node.isOnline ? .green : .orange,
text: node.lastHeard?.formatted() ?? "unknown.age".localized)
text: node.lastHeard?.formatted() ?? "Unknown Age".localized)
}
let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0))
IconAndText(systemName: role?.systemName ?? "figure",
text: "Role: \(role?.name ?? "unknown".localized)")
text: "Role: \(role?.name ?? "Unknown".localized)")
if node.isStoreForwardRouter {
IconAndText(systemName: "envelope.arrow.triangle.branch",
renderingMode: .multicolor,

View file

@ -0,0 +1,30 @@
//
// ScrollToBottomButtonView.swift
// Meshtastic
//
// Created by Benjamin Faershtein on 4/2/25.
//
import SwiftUI
struct ScrollToBottomButtonView: View {
var body: some View {
HStack(spacing: 4) {
Text("Jump to present")
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.cornerRadius(12)
Image(systemName: "arrow.down")
.font(.title2)
.symbolRenderingMode(.hierarchical)
}
.foregroundColor(.accentColor)
.shadow(radius: 2)
}
}
#Preview {
ScrollToBottomButtonView()
}

View file

@ -65,10 +65,7 @@ struct NodeList: View {
var nodes: FetchedResults<NodeInfoEntity>
var connectedNode: NodeInfoEntity? {
getNodeInfo(
id: bleManager.connectedPeripheral?.num ?? 0,
context: context
)
getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context)
}
@ViewBuilder
@ -78,19 +75,11 @@ struct NodeList: View {
) -> some View {
/// Allow users to mute notifications for a node even if they are not connected
if let user = node.user {
NodeAlertsButton(
context: context,
node: node,
user: user
)
NodeAlertsButton(context: context, node: node, user: user)
}
if let connectedNode {
/// Favoriting a node requires being connected
FavoriteNodeButton(
bleManager: bleManager,
context: context,
node: node
)
FavoriteNodeButton(bleManager: bleManager, context: context, node: node)
/// Don't show message, trace route, position exchange or delete context menu items for the connected node
if connectedNode.num != node.num {
if !node.viaMqtt || node.viaMqtt && node.hopsAway == 0 {
@ -203,7 +192,7 @@ struct NodeList: View {
.searchable(text: $searchText, placement: .automatic, prompt: "Find a node")
.disableAutocorrection(true)
.scrollDismissesKeyboard(.immediately)
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
.navigationTitle(String.localizedStringWithFormat("Nodes (%@)".localized, String(nodes.count)))
.listStyle(.plain)
.alert(
"Position Exchange Requested",
@ -237,7 +226,7 @@ struct NodeList: View {
if deleteNode != nil {
let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(bleManager.connectedPeripheral?.num ?? -1))
if !success {
Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized, privacy: .public)")
Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "Unknown".localized, privacy: .public)")
}
}
}
@ -264,7 +253,6 @@ struct NodeList: View {
columnVisibility: columnVisibility
)
.edgesIgnoringSafeArea([.leading, .trailing])
.navigationBarTitle(String(node.user?.longName?.addingVariationSelectors ?? "unknown".localized), displayMode: .inline)
.navigationBarItems(
trailing: ZStack {
if UIDevice.current.userInterfaceIdiom != .phone {
@ -284,7 +272,7 @@ struct NodeList: View {
)
}
} else {
ContentUnavailableView("select.node", systemImage: "flipphone")
ContentUnavailableView("Select Node", systemImage: "flipphone")
}
} detail: {
ContentUnavailableView("", systemImage: "line.3.horizontal")

View file

@ -1,233 +0,0 @@
////
//// NodeMap.swift
//// MeshtasticApple
////
//// Created by Garth Vander Houwen on 8/7/21.
////
//
//import SwiftUI
//import MapKit
//import CoreLocation
//import CoreData
//
//struct NodeMap: View {
// @Environment(\.managedObjectContext) var context
// @EnvironmentObject var bleManager: BLEManager
//
// @ObservedObject
// var router: Router
// @State var selectedMapLayer: MapLayer = UserDefaults.mapLayer
// @State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering
// @State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines
// @State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins
// @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps
// @State var selectedTileServer: MapTileServer = UserDefaults.mapTileServer
// @State var enableOverlayServer: Bool = UserDefaults.enableOverlayServer
// @State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer
// @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels
// let fromDate: NSDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! as NSDate
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
// predicate: NSPredicate(format: "nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none)
// private var positions: FetchedResults<PositionEntity>
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
// predicate: NSPredicate(
// format: "expire == nil || expire >= %@", Date() as NSDate
// ), animation: .none)
// private var waypoints: FetchedResults<WaypointEntity>
// @State var waypointCoordinate: WaypointCoordinate?
// @State var selectedTracking: UserTrackingModes = .none
// @State var isPresentingInfoSheet: Bool = false
// @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
// mapName: "offlinemap",
// tileType: "png",
// canReplaceMapContent: true
// )
// var body: some View {
// NavigationStack {
// ZStack {
// MapViewSwiftUI(
// onLongPress: { coord in
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
// }, onWaypointEdit: { wpId in
// if wpId > 0 {
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
// }
// },
// selectedMapLayer: selectedMapLayer,
// positions: Array(positions),
// waypoints: Array(waypoints),
// userTrackingMode: selectedTracking.MKUserTrackingModeValue(),
// showNodeHistory: enableMapNodeHistoryPins,
// showRouteLines: enableMapRouteLines,
// customMapOverlay: self.customMapOverlay
// )
// VStack(alignment: .trailing) {
// HStack(alignment: .top) {
// Spacer()
// MapButtons(tracking: $selectedTracking, isPresentingInfoSheet: $isPresentingInfoSheet)
// .padding(.trailing, 8)
// .padding(.top, 16)
// }
// Spacer()
// TileDownloadStatus()
// .padding(.trailing, 16)
// .padding(.bottom, 20)
// }
// }
// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
// .frame(maxHeight: .infinity)
// .sheet(item: $waypointCoordinate, content: { wpc in
// WaypointFormMapKit(coordinate: wpc)
// .presentationDetents([.medium, .large])
// .presentationDragIndicator(.automatic)
// })
// .sheet(isPresented: $isPresentingInfoSheet) {
// VStack {
// Form {
// Section(header: Text("Map Options")) {
// Picker(selection: $selectedMapLayer, label: Text("")) {
// ForEach(MapLayer.allCases, id: \.self) { layer in
// if layer == MapLayer.offline && enableOfflineMaps {
// Text(layer.localized)
// } else if layer != MapLayer.offline {
// Text(layer.localized)
// }
// }
// }
// .pickerStyle(SegmentedPickerStyle())
// .onChange(of: selectedMapLayer) { _, newMapLayer in
// UserDefaults.mapLayer = newMapLayer
// }
// .padding(.top, 5)
// .padding(.bottom, 5)
// Toggle(isOn: $enableMapRecentering) {
// Label("map.recentering", systemImage: "camera.metering.center.weighted")
// }
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// .onTapGesture {
// self.enableMapRecentering.toggle()
// UserDefaults.enableMapRecentering = self.enableMapRecentering
// }
// Toggle(isOn: $enableMapNodeHistoryPins) {
// Label("Show Node History", systemImage: "building.columns.fill")
// }
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// .onTapGesture {
// self.enableMapNodeHistoryPins.toggle()
// UserDefaults.enableMapNodeHistoryPins = self.enableMapNodeHistoryPins
// }
// Toggle(isOn: $enableMapRouteLines) {
// Label("Show Route Lines", systemImage: "road.lanes")
// }
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// .onTapGesture {
// self.enableMapRouteLines.toggle()
// UserDefaults.enableMapRouteLines = self.enableMapRouteLines
// }
// let locale = Locale.current
// if locale.region?.identifier ?? "no locale" == "US" {
// Toggle(isOn: $enableOverlayServer) {
// Label("Show Weather", systemImage: "cloud.fill")
// }
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// .onTapGesture {
// self.enableOverlayServer.toggle()
// UserDefaults.enableOverlayServer = self.enableOverlayServer
// }
// if enableOverlayServer {
// Picker(selection: $selectedOverlayServer,
// label: Text("Radar")) {
// ForEach(MapOverlayServer.allCases, id: \.self) { mos in
// Text(mos.description)
// .font(.footnote)
// }
// }
// .pickerStyle(DefaultPickerStyle())
// .onChange(of: (selectedOverlayServer)) { _, newSelectedOverlayServer in
// UserDefaults.mapOverlayServer = newSelectedOverlayServer
// }
// Text(LocalizedStringKey(selectedOverlayServer.attribution))
// .font(.footnote)
// .foregroundColor(.gray)
// .padding(0)
// }
// }
// }
// Section(header: Text("Offline Maps")) {
// Toggle(isOn: $enableOfflineMaps) {
// Text("Enable Offline Maps")
// }
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// .onChange(of: enableOfflineMaps) { _, newEnableOfflineMaps in
// UserDefaults.enableOfflineMaps = newEnableOfflineMaps
// if !enableOfflineMaps {
// if self.selectedMapLayer == .offline {
// self.selectedMapLayer = .standard
// }
// }
// }
// if enableOfflineMaps {
// VStack(alignment: .leading) {
// Picker(selection: $selectedTileServer,
// label: Text("Tile Server")) {
// ForEach(MapTileServer.allCases, id: \.self) { tsl in
// Text(tsl.description)
// }
// }
// .pickerStyle(DefaultPickerStyle())
// .onChange(of: (selectedTileServer)) { _, newSelectedTileServer in
// UserDefaults.mapTileServer = newSelectedTileServer
// }
// Text("Attribution:")
// .fontWeight(.semibold)
// .font(.footnote)
// Text(LocalizedStringKey(selectedTileServer.attribution))
// .font(.footnote)
// .foregroundColor(.gray)
// .padding(0)
// Divider()
// Toggle(isOn: $mapTilesAboveLabels) {
// Text("Tiles above Labels")
// }
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// .onTapGesture {
// self.mapTilesAboveLabels.toggle()
// UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels
// }
// }
// }
// }
// }
// #if targetEnvironment(macCatalyst)
// Button {
// isPresentingInfoSheet = false
// } label: {
// Label("close", systemImage: "xmark")
// }
// .buttonStyle(.bordered)
// .buttonBorderShape(.capsule)
// .controlSize(.large)
// .padding(.bottom)
// #endif
// }
// .presentationDetents([enableOfflineMaps || enableOverlayServer ? .large : .medium])
// .presentationDragIndicator(.visible)
// }
// }
// .navigationBarItems(leading:
// MeshtasticLogo(), trailing:
// ZStack {
// ConnectedDevice(
// bluetoothOn: bleManager.isSwitchedOn,
// deviceConnected: bleManager.connectedPeripheral != nil,
// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName :
// "?")
// })
// .onAppear(perform: {
// UIApplication.shared.isIdleTimerDisabled = true
// })
// .onDisappear(perform: {
// UIApplication.shared.isIdleTimerDisabled = false
// })
// }
//}

View file

@ -44,7 +44,7 @@ struct PaxCounterLog: View {
y: .value("y", (point.wifi + point.ble))
)
}
.accessibilityLabel("paxcounter.total")
.accessibilityLabel("Total PAX")
.accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)")
.foregroundStyle(paxChartColor)
.interpolationMethod(.cardinal)
@ -55,7 +55,7 @@ struct PaxCounterLog: View {
y: .value("y", point.wifi)
)
}
.accessibilityLabel("paxcounter.wifi")
.accessibilityLabel("WiFi")
.accessibilityValue("X: \(point.time!), Y: \(point.wifi)")
.foregroundStyle(wifiChartColor)
@ -65,7 +65,7 @@ struct PaxCounterLog: View {
y: .value("y", point.ble)
)
}
.accessibilityLabel("paxcounter.ble")
.accessibilityLabel("BLE")
.accessibilityValue("X: \(point.time!), Y: \(point.ble)")
.foregroundStyle(bleChartColor)
}
@ -76,9 +76,9 @@ struct PaxCounterLog: View {
.chartXAxis(.automatic)
.chartYScale(domain: 0...maxValue)
.chartForegroundStyleScale([
"paxcounter.ble".localized: .blue,
"paxcounter.wifi".localized: .orange,
"paxcounter.total".localized: .green
"BLE".localized: .blue,
"WiFi".localized: .orange,
"Total PAX".localized: .green
])
.chartLegend(position: .automatic, alignment: .bottom)
}
@ -89,23 +89,23 @@ struct PaxCounterLog: View {
if UIScreen.main.bounds.size.width > 768 && (UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac) {
// Add a table for mac and ipad
Table(pax) {
TableColumn("paxcounter.ble") { pc in
TableColumn("BLE") { pc in
Text("\(pc.ble)")
}
TableColumn("paxcounter.wifi") { pc in
TableColumn("WiFi") { pc in
Text("\(pc.wifi)")
}
TableColumn("paxcounter.total") { pc in
TableColumn("Total PAX") { pc in
Text("\(pc.wifi + pc.ble)")
}
TableColumn("uptime") { pc in
TableColumn("Uptime") { pc in
let now = Date.now
let later = now + TimeInterval(pc.uptime)
let components = (now..<later).formatted(.components(style: .condensedAbbreviated))
Text(components)
}
TableColumn("Timestamp") { pc in
Text(pc.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
Text(pc.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
}
.width(min: 180)
}
@ -120,10 +120,10 @@ struct PaxCounterLog: View {
]
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
GridRow {
Text("paxcounter.ble")
Text("BLE")
.font(.caption)
.fontWeight(.bold)
Text("paxcounter.wifi")
Text("WiFi")
.font(.caption)
.fontWeight(.bold)
Text("Total")
@ -149,7 +149,7 @@ struct PaxCounterLog: View {
let components = (now..<later).formatted(.components(style: .condensedAbbreviated))
Text(components)
.font(.caption)
Text(pc.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
Text(pc.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
.font(.caption)
}
}
@ -174,7 +174,7 @@ struct PaxCounterLog: View {
isPresented: $isPresentingClearLogConfirm,
titleVisibility: .visible
) {
Button("paxcounter.delete", role: .destructive) {
Button("Delete all pax data?", role: .destructive) {
if clearPax(destNum: node.num, context: context) {
Logger.services.info("Cleared Pax Counter for \(node.num, privacy: .public)")
} else {
@ -187,7 +187,7 @@ struct PaxCounterLog: View {
exportString = paxToCsvFile(pax: pax)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
@ -196,10 +196,10 @@ struct PaxCounterLog: View {
.padding(.trailing)
}
} else {
ContentUnavailableView("paxcounter.content.unavailable", systemImage: "slash.circle")
ContentUnavailableView("No PAX Counter Logs", systemImage: "slash.circle")
}
}
.navigationTitle("paxcounter.log")
.navigationTitle("PAX Counter Log")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {

View file

@ -60,13 +60,11 @@ struct PositionLog: View {
Text("\(String(format: "%.2f", position.snr)) dB")
}
TableColumn("Time Stamp") { position in
Text(position.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
Text(position.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
}
.width(min: 180)
}
.textSelection(.enabled)
} else {
ScrollView {
// Use a grid on iOS as a table only shows a single column
@ -107,7 +105,7 @@ struct PositionLog: View {
.font(.caption2)
Text(altitude.formatted())
.font(.caption2)
Text(mappin.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
Text(mappin.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
.font(.caption2)
}
}

View file

@ -122,7 +122,7 @@ struct PowerMetricsLog: View {
Table(powerMetrics, selection: $selection, sortOrder: $sortOrder) {
TableColumn("Timestamp") { m in
HStack {
Text(m.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
Text(m.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
Spacer()
HStack {
VStack {
@ -213,7 +213,7 @@ struct PowerMetricsLog: View {
}
.width(min: 75)
TableColumn("Timestamp") { dm in
Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
}
.width(min: 180)
@ -282,7 +282,7 @@ struct PowerMetricsLog: View {
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") \("power.metrics.log".localized)"),
defaultFilename: String("\(node.user?.longName ?? "Node") \("Power Metrics Log".localized)"),
onCompletion: { result in
switch result {
case .success:

View file

@ -37,14 +37,14 @@ struct TraceRouteLog: View {
VStack {
List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in
Label {
let routeTime = route.time?.formatted() ?? "unknown".localized
let routeTime = route.time?.formatted() ?? "Unknown".localized
if route.response && route.hopsTowards == route.hopsBack {
let hopString = String(localized: "\(route.hopsTowards) Hops")
Text("\(routeTime) - \(hopString)")
.font(.caption)
} else if route.response {
let hopTowardsString = String(localized: "\(route.hopsTowards) Hops")
let hopBackString = route.hopsBack >= 0 ? String(localized: "\(route.hopsBack) Hops") : String(localized: "unknown")
let hopBackString = route.hopsBack >= 0 ? String(localized: "\(route.hopsBack) Hops") : String(localized: "Unknown")
Text("\(routeTime) - \(hopTowardsString) Towards \(hopBackString) Back")
.font(.caption)
} else if route.sent {
@ -67,7 +67,7 @@ struct TraceRouteLog: View {
Logger.data.error("\(error.localizedDescription, privacy: .public)")
}
} label: {
Label("delete", systemImage: "trash")
Label("Delete", systemImage: "trash")
}
}
}
@ -78,14 +78,14 @@ struct TraceRouteLog: View {
if selectedRoute != nil {
if selectedRoute?.response ?? false && selectedRoute?.hopsTowards ?? 0 >= 0 {
Label {
Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)")
Text("Route: \(selectedRoute?.routeText ?? "Unknown".localized)")
} icon: {
Image(systemName: "signpost.right")
.symbolRenderingMode(.hierarchical)
}
.font(.title3)
Label {
Text("Route Back: \(selectedRoute?.routeBackText ?? "unknown".localized)")
Text("Route Back: \(selectedRoute?.routeBackText ?? "Unknown".localized)")
} icon: {
Image(systemName: "signpost.left")
.symbolRenderingMode(.hierarchical)
@ -94,7 +94,7 @@ struct TraceRouteLog: View {
} else if !(selectedRoute?.sent ?? true) {
Label {
VStack {
Text("Trace route to \(selectedRoute?.node?.user?.longName ?? "unknown".localized) was not sent.")
Text("Trace route to \(selectedRoute?.node?.user?.longName ?? "Unknown".localized) was not sent.")
.font(idiom == .phone ? .body : .largeTitle)
.fontWeight(.semibold)
Text("Trace Route was rate limited. You can send a trace route a maximum of once every thirty seconds.")
@ -109,7 +109,7 @@ struct TraceRouteLog: View {
} else {
Label {
VStack {
Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)")
Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "Unknown".localized)")
.font(idiom == .phone ? .body : .largeTitle)
.fontWeight(.semibold)
Text("A Trace Route was sent, no response has been received.")

View file

@ -14,7 +14,6 @@ struct AboutMeshtastic: View {
var body: some View {
VStack {
List {
Section(header: Text("What is Meshtastic?")) {
Text("An open source, off-grid, decentralized, mesh network that runs on affordable, low-power radios.")
@ -44,12 +43,12 @@ struct AboutMeshtastic: View {
Button("Review the app") {
if let scene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene)
AppStore.requestReview(in: scene)
}
}
.font(.title2)
Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild)) ")
Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild))")
}
Section(header: Text("Project information")) {

View file

@ -21,7 +21,7 @@ struct AppData: View {
VStack {
Section(header: Text("phone.gps")) {
Section(header: Text("Phone GPS")) {
GPSStatus()
}
Divider()
@ -41,7 +41,7 @@ struct AppData: View {
Logger.services.error("🗑️ Delete file error: \(error, privacy: .public)")
}
} label: {
Label("delete", systemImage: "trash")
Label("Delete", systemImage: "trash")
}
}
} icon: {
@ -61,7 +61,7 @@ struct AppData: View {
Logger.services.error("🗑️ Delete file error: \(error, privacy: .public)")
}
} label: {
Label("delete", systemImage: "trash")
Label("Delete", systemImage: "trash")
}
}
} icon: {

View file

@ -35,7 +35,7 @@ struct AppLog: View {
if idiom == .phone {
Table(logs, selection: $selection, sortOrder: $sortOrder) {
TableColumn("log.message", value: \.composedMessage) { value in
TableColumn("Message", value: \.composedMessage) { value in
Text(value.composedMessage)
.foregroundStyle(value.level.color)
.font(.caption)
@ -75,18 +75,18 @@ struct AppLog: View {
}
} else {
Table(logs, selection: $selection, sortOrder: $sortOrder) {
TableColumn("log.time") { value in
TableColumn("Time") { value in
Text(value.date.formatted(dateFormatStyle))
}
.width(min: 125, max: 150)
TableColumn("log.level") { value in
TableColumn("Level") { value in
Text(value.level.description)
.foregroundStyle(value.level.color)
}
.width(min: 85, max: 110)
TableColumn("log.category", value: \.category)
TableColumn("Category", value: \.category)
.width(min: 80, max: 130)
TableColumn("log.message", value: \.composedMessage) { value in
TableColumn("Message", value: \.composedMessage) { value in
Text(value.composedMessage)
.foregroundStyle(value.level.color)
.font(.body)
@ -270,4 +270,4 @@ extension AppLog {
}
}
extension OSLogEntry: Identifiable { }
extension OSLogEntry: @retroactive Identifiable { }

View file

@ -8,7 +8,6 @@ import OSLog
struct AppSettings: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ObservedObject var tileManager = OfflineTileManager.shared
@State var totalDownloadedTileSize = ""
@State private var isPresentingCoreDataResetConfirm = false
@State private var isPresentingDeleteMapTilesConfirm = false
@ -44,7 +43,7 @@ struct AppSettings: View {
Button {
isPresentingCoreDataResetConfirm = true
} label: {
Label("clear.app.data", systemImage: "trash")
Label("Clear App Data", systemImage: "trash")
.foregroundColor(.red)
}
.confirmationDialog(
@ -85,31 +84,7 @@ struct AppSettings: View {
.foregroundColor(.red)
}
}
if totalDownloadedTileSize != "0MB" {
Section(header: Text("Map Tile Data")) {
Button {
isPresentingDeleteMapTilesConfirm = true
} label: {
Label("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))", systemImage: "trash")
.foregroundColor(.red)
}
.confirmationDialog(
"Are you sure?",
isPresented: $isPresentingDeleteMapTilesConfirm,
titleVisibility: .visible
) {
Button("Delete all map tiles?", role: .destructive) {
tileManager.removeAll()
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
Logger.services.debug("delete all tiles")
}
}
}
}
}
.onAppear(perform: {
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
})
}
.navigationTitle("App Settings")
.navigationBarItems(trailing:

View file

@ -219,7 +219,7 @@ struct Channels: View {
hasChanges = false
}
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil)// || !hasChanges)// !hasValidKey)
.buttonStyle(.bordered)
@ -230,7 +230,7 @@ struct Channels: View {
Button {
goBack()
} label: {
Label("close", systemImage: "xmark")
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
@ -279,7 +279,7 @@ struct Channels: View {
.padding()
}
}
.navigationTitle("channels")
.navigationTitle("Channels")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
@ -379,6 +379,6 @@ enum PositionPrecision: Int, CaseIterable, Identifiable {
var description: String {
let distanceFormatter = MKDistanceFormatter()
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
return String.localizedStringWithFormat("Within %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
}
}

View file

@ -30,7 +30,7 @@ struct ChannelForm: View {
Form {
Section(header: Text("channel details")) {
HStack {
Text("name")
Text("Name")
Spacer()
TextField(
"Channel Name",
@ -128,7 +128,7 @@ struct ChannelForm: View {
}
}
Section(header: Text("position")) {
Section(header: Text("Position")) {
VStack(alignment: .leading) {
Toggle(isOn: $positionsEnabled) {
Label(channelRole == 1 ? "Positions Enabled" : "Allow Position Requests", systemImage: positionsEnabled ? "mappin" : "mappin.slash")
@ -170,7 +170,7 @@ struct ChannelForm: View {
}
}
}
Section(header: Text("mqtt")) {
Section(header: Text("MQTT")) {
Toggle(isOn: $uplink) {
Label("Uplink Enabled", systemImage: "arrowshape.up")
}

View file

@ -29,9 +29,9 @@ struct BluetoothConfig: View {
Form {
ConfigHeader(title: "Bluetooth", config: \.bluetoothConfig, node: node, onAppear: setBluetoothValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "antenna.radiowaves.left.and.right")
Label("Enabled", systemImage: "antenna.radiowaves.left.and.right")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("Pairing Mode", selection: $mode ) {

View file

@ -30,17 +30,15 @@ struct DeviceConfig: View {
@State var ledHeartbeatEnabled = true
@State var tripleClickAsAdHocPing = true
@State var tzdef = ""
@State private var showRouterWarning = false
@State private var confirmWarning = false
var body: some View {
VStack {
Form {
ConfigHeader(title: "Device", config: \.deviceConfig, node: node, onAppear: setDeviceValues)
Section(header: Text("options")) {
Section(header: Text("Options")) {
VStack(alignment: .leading) {
Picker("Device Role", selection: $deviceRole ) {
ForEach(DeviceRoles.allCases) { dr in
@ -149,7 +147,7 @@ struct DeviceConfig: View {
Picker("Button GPIO", selection: $buttonGPIO) {
ForEach(0..<49) {
if $0 == 0 {
Text("unset")
Text("Unset")
} else {
Text("Pin \($0)")
}
@ -159,7 +157,7 @@ struct DeviceConfig: View {
Picker("Buzzer GPIO", selection: $buzzerGPIO) {
ForEach(0..<49) {
if $0 == 0 {
Text("unset")
Text("Unset")
} else {
Text("Pin \($0)")
}
@ -249,7 +247,7 @@ struct DeviceConfig: View {
}
Spacer()
}
.navigationTitle("device.config")
.navigationTitle("Device Config")
.navigationBarItems(
trailing: ZStack {
ConnectedDevice(
@ -312,6 +310,9 @@ struct DeviceConfig: View {
}
}
func setDeviceValues() {
if node?.deviceConfig?.role ?? 0 == 3 {
node?.deviceConfig?.role = 1
}
self.deviceRole = Int(node?.deviceConfig?.role ?? 0)
self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0)
self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0)

View file

@ -153,7 +153,7 @@ struct DisplayConfig: View {
}
}
.navigationTitle("display.config")
.navigationTitle("Display Config")
.navigationBarItems(
trailing: ZStack {
ConnectedDevice(

View file

@ -228,7 +228,7 @@ struct LoRaConfig: View {
}
}
}
.navigationTitle("lora.config")
.navigationTitle("LoRa Config")
.navigationBarItems(
trailing: ZStack {
ConnectedDevice(
@ -304,6 +304,9 @@ struct LoRaConfig: View {
}
}
func setLoRaValues() {
if node?.loRaConfig?.modemPreset ?? 0 == 2 {
node?.loRaConfig?.modemPreset = 0
}
self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3)
self.region = Int(node?.loRaConfig?.regionCode ?? 0)
self.usePreset = node?.loRaConfig?.usePreset ?? true

Some files were not shown because too many files have changed in this diff Show more