diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index f431452c..ea2501f9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1075,7 +1075,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.12; + MARKETING_VERSION = 2.0.14; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1108,7 +1108,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.12; + MARKETING_VERSION = 2.0.14; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 48a7fca4..e8b47d24 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1132,6 +1132,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return 0 } + public func saveLicensedUser(ham: HamParameters, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { + + var adminPacket = AdminMessage() + adminPacket.setHamMode = ham + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.channel = UInt32(adminIndex) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index fd68f7fe..70e8a9e0 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -114,7 +114,7 @@ struct MapViewSwiftUI: UIViewRepresentable { switch annotation { case _ as MKClusterAnnotation: - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup") + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "NodeGroup") annotationView.markerTintColor = .brown//.systemRed annotationView.displayPriority = .defaultLow annotationView.tag = -1 @@ -124,9 +124,21 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.tag = -1 annotationView.canShowCallout = true annotationView.glyphText = "📟" - annotationView.clusteringIdentifier = "nodeGroup" - annotationView.markerTintColor = UIColor(.indigo) - annotationView.displayPriority = .required + + let latest = parent.positions.last(where: { $0.nodePosition?.num ?? 0 == positionAnnotation.nodePosition?.num ?? -1 }) + + if latest == positionAnnotation { + annotationView.markerTintColor = .systemRed + annotationView.displayPriority = .required + annotationView.titleVisibility = .visible + } + else { + annotationView.markerTintColor = UIColor(.indigo) + annotationView.displayPriority = .defaultHigh + annotationView.titleVisibility = .adaptive + annotationView.clusteringIdentifier = "nodeGroup" + } + annotationView.titleVisibility = .adaptive let leftIcon = UIImageView(image: annotationView.glyphText?.image()) leftIcon.backgroundColor = UIColor(.indigo) @@ -172,7 +184,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } annotationView.clusteringIdentifier = "waypointGroup" annotationView.markerTintColor = UIColor(.accentColor) - annotationView.displayPriority = .required + annotationView.displayPriority = .defaultHigh annotationView.titleVisibility = .adaptive let leftIcon = UIImageView(image: annotationView.glyphText?.image()) leftIcon.backgroundColor = UIColor(.accentColor) diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index b5ca5b32..5d27efa3 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -20,7 +20,7 @@ struct DeviceMetricsLog: View { var body: some View { NavigationStack { let oneDayAgo = Calendar.current.date(byAdding: .day, value: -3, to: Date()) - let data = node.telemetries!.filtered(using: NSPredicate(format: "metricsType == 0 && time !=nil && time >= %@", oneDayAgo! as CVarArg)) + let data = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0 && time !=nil && time >= %@", oneDayAgo! as CVarArg)) ?? [] if data.count > 0 { GroupBox(label: Label("battery.level.trend", systemImage: "battery.100")) { Chart(data.array as! [TelemetryEntity], id: \.self) { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 89f94eda..b13caa0c 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -30,7 +30,7 @@ struct NodeMap: View { } } //&& nodePosition != nil - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none) private var positions: FetchedResults @@ -40,7 +40,7 @@ struct NodeMap: View { ), animation: .none) private var waypoints: FetchedResults - @State private var mapType: MKMapType? + @State private var mapType: MKMapType = .standard @State var waypointCoordinate: CLLocationCoordinate2D = LocationHelper.DefaultLocation @State var editingWaypoint: Int = 0 @State private var presentingWaypointForm = false diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index e645c370..dc5a208e 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -5,6 +5,7 @@ // Copyright (c) Garth Vander Houwen 6/27/22. // import SwiftUI +import CoreData struct UserConfig: View { @@ -14,40 +15,56 @@ struct UserConfig: View { var node: NodeInfoEntity? + enum Field: Hashable { + case frequencyOverride + } + @State private var isPresentingFactoryResetConfirm: Bool = false @State private var isPresentingSaveConfirm: Bool = false @State var hasChanges = false @State var shortName = "" @State var longName = "" @State var isLicensed = false + @State var overrideDutyCycle = false + @State var overrideFrequency: Float = 0.0 + @State var txPower = 0 + + @FocusState var focusedField: Field? + + let floatFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter + }() var body: some View { - + VStack { Form { Section(header: Text("User Details")) { - HStack { - Label("Long Name", systemImage: "person.crop.rectangle.fill") - TextField("Long Name", text: $longName) - .onChange(of: longName, perform: { value in - let totalBytes = longName.utf8.count - // Only mess with the value if it is too big - if totalBytes > 36 { - let firstNBytes = Data(longName.utf8.prefix(36)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the longName back to the last place where it was the right size - longName = maxBytesString + HStack { + Label(isLicensed ? "Call Sign" : "Long Name", systemImage: "person.crop.rectangle.fill") + TextField("Long Name", text: $longName) + .onChange(of: longName, perform: { value in + let totalBytes = longName.utf8.count + // Only mess with the value if it is too big + if totalBytes > 36 { + let firstNBytes = Data(longName.utf8.prefix(36)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the longName back to the last place where it was the right size + longName = maxBytesString + } } - } - }) - } - .keyboardType(.default) - .disableAutocorrection(true) - Text("Long name can be up to 36 bytes long.") - .font(.caption) + }) + } + .keyboardType(.default) + .disableAutocorrection(true) + Text("\(String(isLicensed ? "Call Sign" : "Long Name")) can be up to 36 bytes long.") + .font(.caption2) + HStack { Label("Short Name", systemImage: "circlebadge.fill") - TextField("Long Name", text: $shortName) + TextField("Short Name", text: $shortName) .foregroundColor(.gray) .onChange(of: shortName, perform: { value in let totalBytes = shortName.utf8.count @@ -64,15 +81,46 @@ struct UserConfig: View { } .keyboardType(.asciiCapable) .disableAutocorrection(true) - Text("The short name is used in maps and messaging and will be appended to the last 4 of the device MAC address to set the device's BLE Name. It can be up to 4 bytes long.") - .font(.caption) + Text("The last 4 of the device MAC address will be appended to the short name to set the device's BLE Name. Short name can be up to 4 bytes long.") + .font(.caption2) - Toggle(isOn: $isLicensed) { - Label("Licensed User", systemImage: "person.text.rectangle") + // Only manage ham mode for the locally connected node + if node?.num ?? 0 > 0 && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { + Toggle(isOn: $isLicensed) { + Label("Licensed Operator", systemImage: "person.text.rectangle") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if isLicensed { + + Text("Onboarding for licensed operators requires firmware 2.0.20 or greater. Make sure to refer to your local regulations and contact the local amateur frequency coordinators with questions.") + .font(.caption2) + Text("What licensed operator mode does:\n* Sets the node name to your call sign \n* Broadcasts node info every 10 minutes \n* Overrides frequency, dutycycle and tx power \n* Disables encryption") + .font(.caption2) + + HStack { + Label("Frequency", systemImage: "waveform.path.ecg") + Spacer() + TextField("Frequency Override", value: $overrideFrequency, formatter: floatFormatter) + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Button("dismiss.keyboard") { + focusedField = nil + } + .font(.subheadline) + } + } + .keyboardType(.decimalPad) + .scrollDismissesKeyboard(.immediately) + .focused($focusedField, equals: .frequencyOverride) + } + HStack { + Image(systemName: "antenna.radiowaves.left.and.right") + .foregroundColor(.accentColor) + Stepper("\(txPower)db Transmit Power", value: $txPower, in: 0...30, step: 1) + .padding(5) + } + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("Enable only if you are a licensed amateur radio user for your region.") - .font(.caption) } } .disabled(bleManager.connectedPeripheral == nil) @@ -97,13 +145,27 @@ struct UserConfig: View { let connectedUser = getUser(id: bleManager.connectedPeripheral.num, context: context) let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if connectedNode != nil { - var u = User() - u.shortName = shortName - u.longName = longName - let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) - if adminMessageId > 0 { - hasChanges = false - goBack() + + if !isLicensed { + var u = User() + u.shortName = shortName + u.longName = longName + let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + if adminMessageId > 0 { + hasChanges = false + goBack() + } + } else { + var ham = HamParameters() + //ham.shortName = shortName + ham.callSign = longName + ham.txPower = Int32(txPower) + ham.frequency = overrideFrequency + let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + if adminMessageId > 0 { + hasChanges = false + goBack() + } } } } @@ -115,13 +177,16 @@ struct UserConfig: View { } .navigationTitle("User Config") .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") }) .onAppear { self.bleManager.context = context - self.shortName = node?.user!.shortName ?? "" - self.longName = node?.user!.longName ?? "" + self.shortName = node?.user?.shortName ?? "" + self.longName = node?.user?.longName ?? "" + self.isLicensed = node?.user?.isLicensed ?? false + self.txPower = Int(node?.loRaConfig?.txPower ?? 0) + self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.00 self.hasChanges = false } .onChange(of: shortName) { newShort in @@ -134,5 +199,16 @@ struct UserConfig: View { if newLong != node?.user!.longName { hasChanges = true } } } + .onChange(of: isLicensed) { newIsLicensed in + if node != nil && node!.user != nil { + if newIsLicensed != node?.user!.isLicensed { hasChanges = true } + } + } + .onChange(of: overrideFrequency) { newOverrideFrequency in + //hasChanges = true + } + .onChange(of: txPower) { newTxPower in + //hasChanges = true + } } }