diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 49adb40d..298891c0 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -964,7 +964,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.41; + MARKETING_VERSION = 1.3.43; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -996,7 +996,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.41; + MARKETING_VERSION = 1.3.43; 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 ae52b23d..2b74a13a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -29,7 +29,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph @Published var connectedPeripheral: Peripheral! @Published var lastConnectionError: String - @Published var minimumVersion = "1.3.41" + @Published var minimumVersion = "1.3.43" @Published var connectedVersion: String @Published var invalidVersion = false @Published var preferredPeripheral = false @@ -459,7 +459,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph MeshLogger.log("ℹ️ Requesting Device Metadata for \(connectedPeripheral!.peripheral.name ?? "Unknown")") var adminPacket = AdminMessage() - adminPacket.getDeviceMetadataRequest = 0 + adminPacket.getDeviceMetadataRequest = true var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func getChannel(channelIndex: UInt32, fromUser: UserEntity, toUser: UserEntity, wantResponse: Bool) -> Bool { var adminPacket = AdminMessage() adminPacket.getChannelRequest = channelIndex var meshPacket: MeshPacket = MeshPacket() - meshPacket.to = UInt32(nodeNum) + meshPacket.to = UInt32(toUser.num) meshPacket.from = 0 //UInt32(cnodeNum) meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..'), those are ignored and only hex characters are processed. - /// - /// - returns: Data represented by this hexadecimal string. - - var hexadecimal: Data? { - var data = Data(capacity: count / 2) - - let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive) - regex.enumerateMatches(in: self, range: NSRange(startIndex..., in: self)) { match, _, _ in - let byteString = (self as NSString).substring(with: match!.range) - let num = UInt8(byteString, radix: 16)! - data.append(num) - } - - guard data.count > 0 else { return nil } - - return data - } + + func base64urlToBase64() -> String { + var base64 = self + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + if base64.count % 4 != 0 { + base64.append(String(repeating: "==", count: 4 - base64.count % 4)) + } + return base64 + } + + func base64ToBase64url() -> String { + let base64url = self + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + return base64url + } func image(fontSize:CGFloat = 40, bgColor:UIColor = UIColor.clear, imageSize:CGSize? = nil) -> UIImage? { let font = UIFont.systemFont(ofSize: fontSize) let attributes = [NSAttributedString.Key.font: font] let imageSize = imageSize ?? self.size(withAttributes: attributes) - UIGraphicsBeginImageContextWithOptions(imageSize, false, 0) bgColor.set() let rect = CGRect(origin: .zero, size: imageSize) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 29d27369..fac17e9d 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -194,32 +194,31 @@ func localConfig (config: Config, meshlogging: Bool, context:NSManagedObjectCont let newLoRaConfig = LoRaConfigEntity(context: context) newLoRaConfig.regionCode = Int32(config.lora.region.rawValue) - newLoRaConfig.modemPreset = Int32(config.lora.modemPreset.rawValue) - newLoRaConfig.hopLimit = Int32(config.lora.hopLimit) - newLoRaConfig.txPower = Int32(config.lora.txPower) - newLoRaConfig.txEnabled = config.lora.txEnabled newLoRaConfig.usePreset = config.lora.usePreset + newLoRaConfig.modemPreset = Int32(config.lora.modemPreset.rawValue) newLoRaConfig.bandwidth = Int32(config.lora.bandwidth) newLoRaConfig.spreadFactor = Int32(config.lora.spreadFactor) newLoRaConfig.codingRate = Int32(config.lora.codingRate) - newLoRaConfig.spreadFactor = Int32(config.lora.spreadFactor) - newLoRaConfig.frequencyOffset = Int32(config.lora.frequencyOffset) - + newLoRaConfig.frequencyOffset = config.lora.frequencyOffset + newLoRaConfig.hopLimit = Int32(config.lora.hopLimit) + newLoRaConfig.txPower = Int32(config.lora.txPower) + newLoRaConfig.txEnabled = config.lora.txEnabled + newLoRaConfig.channelNum = Int32(config.lora.channelNum) fetchedNode[0].loRaConfig = newLoRaConfig } else { fetchedNode[0].loRaConfig?.regionCode = Int32(config.lora.region.rawValue) - fetchedNode[0].loRaConfig?.modemPreset = Int32(config.lora.modemPreset.rawValue) - fetchedNode[0].loRaConfig?.hopLimit = Int32(config.lora.hopLimit) - fetchedNode[0].loRaConfig?.txPower = Int32(config.lora.txPower) - fetchedNode[0].loRaConfig?.txEnabled = config.lora.txEnabled fetchedNode[0].loRaConfig?.usePreset = config.lora.usePreset + fetchedNode[0].loRaConfig?.modemPreset = Int32(config.lora.modemPreset.rawValue) fetchedNode[0].loRaConfig?.bandwidth = Int32(config.lora.bandwidth) fetchedNode[0].loRaConfig?.spreadFactor = Int32(config.lora.spreadFactor) fetchedNode[0].loRaConfig?.codingRate = Int32(config.lora.codingRate) - fetchedNode[0].loRaConfig?.spreadFactor = Int32(config.lora.spreadFactor) - fetchedNode[0].loRaConfig?.frequencyOffset = Int32(config.lora.frequencyOffset) + fetchedNode[0].loRaConfig?.frequencyOffset = config.lora.frequencyOffset + fetchedNode[0].loRaConfig?.hopLimit = Int32(config.lora.hopLimit) + fetchedNode[0].loRaConfig?.txPower = Int32(config.lora.txPower) + fetchedNode[0].loRaConfig?.txEnabled = config.lora.txEnabled + fetchedNode[0].loRaConfig?.channelNum = Int32(config.lora.channelNum) } do { @@ -239,7 +238,6 @@ func localConfig (config: Config, meshlogging: Bool, context:NSManagedObjectCont print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Lora Config") } - } catch { let nsError = error as NSError @@ -820,6 +818,61 @@ func myInfoPacket (myInfo: MyNodeInfo, meshLogging: Bool, context: NSManagedObje return nil } +func channelPacket (channel: Channel, fromNum: Int64, meshLogging: Bool, context: NSManagedObjectContext) -> NodeInfoEntity? { + + if channel.isInitialized && channel.hasSettings { + + let fetchedMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") + fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", fromNum) + + do { + + let fetchedMyInfo = try context.fetch(fetchedMyInfoRequest) as! [MyInfoEntity] + + if fetchedMyInfo.count == 1 { + + let newChannel = ChannelEntity(context: context) + newChannel.index = Int32(channel.index) + newChannel.uplinkEnabled = channel.settings.uplinkEnabled + newChannel.downlinkEnabled = channel.settings.downlinkEnabled + newChannel.name = channel.settings.name + newChannel.role = Int32(channel.role.rawValue) + newChannel.psk = channel.settings.psk + + let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as! NSMutableOrderedSet + + mutableChannels.add(newChannel) + fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet + + } else { + print("💥 Trying to save a channel to a MyInfo that does not exist: \(fromNum)") + } + + try context.save() + + if meshLogging { + + MeshLogger.log("💾 Updated MyInfo channel \(channel.index) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum)") + + } + + } catch { + + context.rollback() + + let nsError = error as NSError + print("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError)") + } + } + //} + + + + + return nil + +} + func nodeInfoPacket (nodeInfo: NodeInfo, meshLogging: Bool, context: NSManagedObjectContext) -> NodeInfoEntity? { let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") @@ -1012,6 +1065,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, meshLogging: Bool, context: NSManagedOb return nil } + func nodeInfoAppPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObjectContext) { let fetchNodeInfoAppRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") @@ -1076,71 +1130,7 @@ func nodeInfoAppPacket (packet: MeshPacket, meshLogging: Bool, context: NSManage func adminAppPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObjectContext) { - if let channelMessage = try? Channel(serializedData: packet.decoded.payload) { - - if channelMessage.hasSettings { - - let fetchedMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(packet.from)) - - do { - - let fetchedMyInfo = try context.fetch(fetchedMyInfoRequest) as! [MyInfoEntity] - - if fetchedMyInfo.count == 1 { - - // Update - if fetchedMyInfo[0].channels?.count ?? 0 >= 1 { - - let newChannel = ChannelEntity(context: context) - newChannel.index = Int32(channelMessage.settings.channelNum) - newChannel.uplinkEnabled = channelMessage.settings.uplinkEnabled - newChannel.downlinkEnabled = channelMessage.settings.downlinkEnabled - newChannel.name = channelMessage.settings.name - newChannel.role = Int32(channelMessage.role.rawValue) - - let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as! NSMutableOrderedSet - - mutableChannels.add(newChannel) - fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet - - } else { - - let newChannel = ChannelEntity(context: context) - newChannel.index = Int32(channelMessage.settings.channelNum) - newChannel.uplinkEnabled = channelMessage.settings.uplinkEnabled - newChannel.downlinkEnabled = channelMessage.settings.downlinkEnabled - newChannel.name = channelMessage.settings.name - newChannel.role = Int32(channelMessage.role.rawValue) - - var newChannels = [ChannelEntity]() - newChannels.append(newChannel) - fetchedMyInfo[0].channels! = NSOrderedSet(array: newChannels) - } - - } else { - print("💥 Trying to save a channel to a MyInfo that does not exist: \(packet.from)") - } - - try context.save() - - if meshLogging { - - MeshLogger.log("💾 Updated MyInfo channel \(channelMessage.settings.channelNum + 1) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum)") - } - - } catch { - - context.rollback() - - let nsError = error as NSError - print("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError)") - } - } - } else { - print(try! packet.decoded.jsonString()) - } } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents index a34a3211..c79ca23e 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents @@ -24,7 +24,7 @@ - + @@ -60,7 +60,7 @@ - + diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 6dce5257..a4f99ec7 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -11,8 +11,9 @@ struct MeshtasticAppleApp: App { @ObservedObject private var bleManager: BLEManager = BLEManager.shared @ObservedObject private var userSettings: UserSettings = UserSettings() - @State var saveQR = false - @State var channelUrl: URL? + @State var saveChannels = false + @State var incomingUrl: URL? + @State var channelSettings: String? @Environment(\.scenePhase) var scenePhase @@ -25,32 +26,41 @@ struct MeshtasticAppleApp: App { .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in - print("QR Code URL received from the Camera \(userActivity)") - channelUrl = userActivity.webpageURL - if channelUrl!.absoluteString.lowercased().contains("https://meshtastic.org/e/#") { - saveQR = true + print("URL received \(userActivity)") + incomingUrl = userActivity.webpageURL + + if incomingUrl!.absoluteString.lowercased().contains("meshtastic.org/e/#") { + + if let components = incomingUrl?.absoluteString.components(separatedBy: "#") { + channelSettings = components.last! + } + saveChannels = true + print("User wants to open a Channel Settings URL: \(incomingUrl?.absoluteString ?? "No QR Code Link")") + } + if saveChannels { + print("User wants to open Channel Settings URL: \(String(describing: incomingUrl!.relativeString))") } - - print("User wants to open URL: \(String(describing: channelUrl?.relativeString))") - } - .sheet(isPresented: $saveQR) { - - SaveChannelQRCode(channelHash: channelUrl?.absoluteString ?? "Empty Channel URL") + .sheet(isPresented: $saveChannels) { + + SaveChannelQRCode(channelHash: channelSettings ?? "Empty Channel URL") .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) } .onOpenURL(perform: { (url) in print("Some sort of URL was received \(url)") - channelUrl = url + incomingUrl = url - - if url.absoluteString.lowercased().contains("https://meshtastic.org/e/#") { - saveQR = true - print("User wants to open a Channel Settings URL: \(channelUrl?.absoluteString ?? "No QR Code Link")") + if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { + if let components = incomingUrl?.absoluteString.components(separatedBy: "#") { + channelSettings = components.last! + } + saveChannels = true + print("User wants to open a Channel Settings URL: \(incomingUrl?.absoluteString ?? "No QR Code Link")") } else { - print("User wants to import a MBTILES offline map file: \(channelUrl?.absoluteString ?? "No Tiles link")") + saveChannels = false + print("User wants to import a MBTILES offline map file: \(incomingUrl?.absoluteString ?? "No Tiles link")") } //we are expecting a .mbtiles map file that contains raster data @@ -59,26 +69,28 @@ struct MeshtasticAppleApp: App { let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! let destination = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false) - //do we need to delete an old one? - if (fileManager.fileExists(atPath: destination.path)) { - print("ℹ️ Found an old map file. Deleting it") - try? fileManager.removeItem(atPath: destination.path) - } - - do { - try fileManager.copyItem(at: url, to: destination) - } catch { - print("Copy MB Tile file failed. Error: \(error)") - } - - if (fileManager.fileExists(atPath: destination.path)) { - print("ℹ️ Saved the map file") + if !saveChannels { + //do we need to delete an old one? + if (fileManager.fileExists(atPath: destination.path)) { + print("ℹ️ Found an old map file. Deleting it") + try? fileManager.removeItem(atPath: destination.path) + } - //need to tell the map view that it needs to update and try loading the new overlay - UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastUpdatedLocalMapFile") + do { + try fileManager.copyItem(at: url, to: destination) + } catch { + print("Copy MB Tile file failed. Error: \(error)") + } - } else { - print("💥 Didn't save the map file") + if (fileManager.fileExists(atPath: destination.path)) { + print("ℹ️ Saved the map file") + + //need to tell the map view that it needs to update and try loading the new overlay + UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastUpdatedLocalMapFile") + + } else { + print("💥 Didn't save the map file") + } } }) } diff --git a/Meshtastic/Protobufs/admin.pb.swift b/Meshtastic/Protobufs/admin.pb.swift index e43a6564..83169e01 100644 --- a/Meshtastic/Protobufs/admin.pb.swift +++ b/Meshtastic/Protobufs/admin.pb.swift @@ -114,16 +114,6 @@ struct AdminMessage { set {payloadVariant = .getModuleConfigResponse(newValue)} } - /// - /// Send all channels in the response to this message - var getAllChannelRequest: Bool { - get { - if case .getAllChannelRequest(let v)? = payloadVariant {return v} - return false - } - set {payloadVariant = .getAllChannelRequest(newValue)} - } - /// /// Get the Canned Message Module messages in the response to this message. var getCannedMessageModuleMessagesRequest: Bool { @@ -146,10 +136,10 @@ struct AdminMessage { /// /// Request the node to send device metadata (firmware, protobuf version, etc) - var getDeviceMetadataRequest: UInt32 { + var getDeviceMetadataRequest: Bool { get { if case .getDeviceMetadataRequest(let v)? = payloadVariant {return v} - return 0 + return false } set {payloadVariant = .getDeviceMetadataRequest(newValue)} } @@ -261,6 +251,17 @@ struct AdminMessage { set {payloadVariant = .confirmSetRadio(newValue)} } + /// + /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) + /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + var rebootOtaSeconds: Int32 { + get { + if case .rebootOtaSeconds(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .rebootOtaSeconds(newValue)} + } + /// /// This message is only supported for the simulator porduino build. /// If received the simulator will exit successfully. @@ -343,9 +344,6 @@ struct AdminMessage { /// Send the current Config in the response to this message. case getModuleConfigResponse(ModuleConfig) /// - /// Send all channels in the response to this message - case getAllChannelRequest(Bool) - /// /// Get the Canned Message Module messages in the response to this message. case getCannedMessageModuleMessagesRequest(Bool) /// @@ -353,7 +351,7 @@ struct AdminMessage { case getCannedMessageModuleMessagesResponse(String) /// /// Request the node to send device metadata (firmware, protobuf version, etc) - case getDeviceMetadataRequest(UInt32) + case getDeviceMetadataRequest(Bool) /// /// Device metadata response case getDeviceMetadataResponse(DeviceMetadata) @@ -392,6 +390,10 @@ struct AdminMessage { /// TODO: REPLACE case confirmSetRadio(Bool) /// + /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) + /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + case rebootOtaSeconds(Int32) + /// /// This message is only supported for the simulator porduino build. /// If received the simulator will exit successfully. case exitSimulator(Bool) @@ -446,10 +448,6 @@ struct AdminMessage { guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() } return l == r }() - case (.getAllChannelRequest, .getAllChannelRequest): return { - guard case .getAllChannelRequest(let l) = lhs, case .getAllChannelRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return { guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() } return l == r @@ -502,6 +500,10 @@ struct AdminMessage { guard case .confirmSetRadio(let l) = lhs, case .confirmSetRadio(let r) = rhs else { preconditionFailure() } return l == r }() + case (.rebootOtaSeconds, .rebootOtaSeconds): return { + guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() case (.exitSimulator, .exitSimulator): return { guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() } return l == r @@ -713,7 +715,6 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 6: .standard(proto: "get_config_response"), 7: .standard(proto: "get_module_config_request"), 8: .standard(proto: "get_module_config_response"), - 9: .standard(proto: "get_all_channel_request"), 10: .standard(proto: "get_canned_message_module_messages_request"), 11: .standard(proto: "get_canned_message_module_messages_response"), 12: .standard(proto: "get_device_metadata_request"), @@ -727,6 +728,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 65: .standard(proto: "confirm_set_module_config"), 66: .standard(proto: "confirm_set_channel"), 67: .standard(proto: "confirm_set_radio"), + 95: .standard(proto: "reboot_ota_seconds"), 96: .standard(proto: "exit_simulator"), 97: .standard(proto: "reboot_seconds"), 98: .standard(proto: "shutdown_seconds"), @@ -824,14 +826,6 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .getModuleConfigResponse(v) } }() - case 9: try { - var v: Bool? - try decoder.decodeSingularBoolField(value: &v) - if let v = v { - if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} - self.payloadVariant = .getAllChannelRequest(v) - } - }() case 10: try { var v: Bool? try decoder.decodeSingularBoolField(value: &v) @@ -849,8 +843,8 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } }() case 12: try { - var v: UInt32? - try decoder.decodeSingularUInt32Field(value: &v) + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) if let v = v { if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} self.payloadVariant = .getDeviceMetadataRequest(v) @@ -961,6 +955,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .confirmSetRadio(v) } }() + case 95: try { + var v: Int32? + try decoder.decodeSingularInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .rebootOtaSeconds(v) + } + }() case 96: try { var v: Bool? try decoder.decodeSingularBoolField(value: &v) @@ -1044,10 +1046,6 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .getModuleConfigResponse(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 8) }() - case .getAllChannelRequest?: try { - guard case .getAllChannelRequest(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularBoolField(value: v, fieldNumber: 9) - }() case .getCannedMessageModuleMessagesRequest?: try { guard case .getCannedMessageModuleMessagesRequest(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 10) @@ -1058,7 +1056,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat }() case .getDeviceMetadataRequest?: try { guard case .getDeviceMetadataRequest(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularUInt32Field(value: v, fieldNumber: 12) + try visitor.visitSingularBoolField(value: v, fieldNumber: 12) }() case .getDeviceMetadataResponse?: try { guard case .getDeviceMetadataResponse(let v)? = self.payloadVariant else { preconditionFailure() } @@ -1100,6 +1098,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .confirmSetRadio(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 67) }() + case .rebootOtaSeconds?: try { + guard case .rebootOtaSeconds(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularInt32Field(value: v, fieldNumber: 95) + }() case .exitSimulator?: try { guard case .exitSimulator(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 96) diff --git a/Meshtastic/Protobufs/config.pb.swift b/Meshtastic/Protobufs/config.pb.swift index b7215ece..97e28992 100644 --- a/Meshtastic/Protobufs/config.pb.swift +++ b/Meshtastic/Protobufs/config.pb.swift @@ -675,7 +675,7 @@ struct Config { /// /// The denominator of the coding rate. - /// ie for 4/8, the value is 8. 5/8 the value is 5. + /// ie for 4/5, the value is 5. 4/8 the value is 8. var codingRate: UInt32 = 0 /// diff --git a/Meshtastic/Protobufs/mesh.pb.swift b/Meshtastic/Protobufs/mesh.pb.swift index 1f674306..a548b47f 100644 --- a/Meshtastic/Protobufs/mesh.pb.swift +++ b/Meshtastic/Protobufs/mesh.pb.swift @@ -1850,6 +1850,16 @@ struct FromRadio { set {_uniqueStorage()._payloadVariant = .moduleConfig(newValue)} } + /// + /// One packet is sent for each channel + var channel: Channel { + get { + if case .channel(let v)? = _storage._payloadVariant {return v} + return Channel() + } + set {_uniqueStorage()._payloadVariant = .channel(newValue)} + } + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1887,6 +1897,9 @@ struct FromRadio { /// /// Include module config case moduleConfig(ModuleConfig) + /// + /// One packet is sent for each channel + case channel(Channel) #if !swift(>=4.1) static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { @@ -1926,6 +1939,10 @@ struct FromRadio { guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } return l == r }() + case (.channel, .channel): return { + guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -3244,6 +3261,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 7: .standard(proto: "config_complete_id"), 8: .same(proto: "rebooted"), 9: .same(proto: "moduleConfig"), + 10: .same(proto: "channel"), ] fileprivate class _StorageClass { @@ -3370,6 +3388,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation _storage._payloadVariant = .moduleConfig(v) } }() + case 10: try { + var v: Channel? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .channel(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .channel(v) + } + }() default: break } } @@ -3418,6 +3449,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .moduleConfig(let v)? = _storage._payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 9) }() + case .channel?: try { + guard case .channel(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() case nil: break } } diff --git a/Meshtastic/Protobufs/telemetry.pb.swift b/Meshtastic/Protobufs/telemetry.pb.swift index 73f89cc0..3e745000 100644 --- a/Meshtastic/Protobufs/telemetry.pb.swift +++ b/Meshtastic/Protobufs/telemetry.pb.swift @@ -60,6 +60,14 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// High accuracy pressure case lps22 // = 8 + + /// + /// 3-Axis magnetic sensor + case qmc6310 // = 9 + + /// + /// 6-Axis inertial measurement sensor + case qmi8658 // = 10 case UNRECOGNIZED(Int) init() { @@ -77,6 +85,8 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case 6: self = .bmp280 case 7: self = .shtc3 case 8: self = .lps22 + case 9: self = .qmc6310 + case 10: self = .qmi8658 default: self = .UNRECOGNIZED(rawValue) } } @@ -92,6 +102,8 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case .bmp280: return 6 case .shtc3: return 7 case .lps22: return 8 + case .qmc6310: return 9 + case .qmi8658: return 10 case .UNRECOGNIZED(let i): return i } } @@ -112,6 +124,8 @@ extension TelemetrySensorType: CaseIterable { .bmp280, .shtc3, .lps22, + .qmc6310, + .qmi8658, ] } @@ -272,6 +286,8 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 6: .same(proto: "BMP280"), 7: .same(proto: "SHTC3"), 8: .same(proto: "LPS22"), + 9: .same(proto: "QMC6310"), + 10: .same(proto: "QMI8658"), ] } diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 4024f861..5ff03068 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -9,7 +9,7 @@ import SwiftUI struct SaveChannelQRCode: View { var channelHash: String - + var body: some View { VStack { @@ -17,22 +17,37 @@ struct SaveChannelQRCode: View { Text("Save Channel Settings?") .font(.title) - Text("The settings embedded in this QR code will replace the current settings on your radio.") + Text("These settings will replace the current settings on your radio.") .foregroundColor(.gray) .font(.callout) .padding() Text(channelHash) - .font(.title2) + .font(.caption2) .padding() - Text("This does not work yet.") + Text("Error Message") .font(.callout) + .foregroundColor(.red) .padding() Text("Swipe down to dismiss.") .padding() } + .onChange(of: channelHash) { newSettings in + + var decodedString = newSettings.base64urlToBase64() + + if let decodedData = Data(base64Encoded: decodedString) { + decodedString = String(data: decodedData, encoding: .utf8)! + do { + var channelSet: ChannelSet = try ChannelSet(serializedData: decodedData) + print(channelSet) + } catch { + print("Invalid Meshtastic QR Code Link") + } + } + } } } diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 445903e0..cbc3a052 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -29,24 +29,31 @@ struct QrCodeImage { return qrImage } } + + struct ShareChannels: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings + @State var initialLoad: Bool = true + @State var channelSet: ChannelSet = ChannelSet() @State var includeChannel0 = true - @State var includeChannel1 = true - @State var includeChannel2 = true + @State var includeChannel1 = false + @State var includeChannel2 = false @State var includeChannel3 = false @State var includeChannel4 = false @State var includeChannel5 = false @State var includeChannel6 = false - @State var includeChannel7 = true + @State var includeChannel7 = false + + @State var isPresentingHelp = false var node: NodeInfoEntity? - @State private var channelsUrl = "https://meshtastic.org/e/#test" + @State private var channelsUrl = "https://www.meshtastic.org/e/#" + var qrCodeImage = QrCodeImage() var body: some View { @@ -69,14 +76,18 @@ struct ShareChannels: View { Text("Include") .font(.caption) .fontWeight(.bold) - Text("Name") + .padding(.trailing) + Text("Channel Name") + .font(.caption) + .fontWeight(.bold) + .padding(.trailing) + Text("Encrypted") .font(.caption) .fontWeight(.bold) Spacer() } - Divider() - ForEach(node!.myInfo!.channels?.array.sorted(by: { ($0 as! ChannelEntity).index < ($1 as! ChannelEntity).index }) as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in + ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in GridRow { Spacer() @@ -84,48 +95,57 @@ struct ShareChannels: View { Toggle("Channel 0 Included", isOn: $includeChannel0) .toggleStyle(.switch) .labelsHidden() - .disabled(true) - Text("Primary Channel") - + .disabled(channel.role == 1) + Text((channel.name!.isEmpty ? "primary" : channel.name) ?? "primary") } else if channel.index == 1 { Toggle("Channel 1 Included", isOn: $includeChannel1) .toggleStyle(.switch) .labelsHidden() - Text("Public Channel") + .disabled(channel.role == 0) + Text((channel.name!.isEmpty ? "channel \(channel.index)" : channel.name) ?? "Channel \(channel.index)") } else if channel.index == 2 { Toggle("Channel 2 Included", isOn: $includeChannel2) .toggleStyle(.switch) .labelsHidden() + .disabled(channel.role == 0) + Text((channel.name!.isEmpty ? "channel \(channel.index)" : channel.name) ?? "Channel \(channel.index)") } else if channel.index == 3 { Toggle("Channel 3 Included", isOn: $includeChannel3) .toggleStyle(.switch) .labelsHidden() + .disabled(channel.role == 0) + Text((channel.name!.isEmpty ? "channel \(channel.index)" : channel.name) ?? "Channel \(channel.index)") } else if channel.index == 4 { Toggle("Channel 4 Included", isOn: $includeChannel4) .toggleStyle(.switch) .labelsHidden() - .disabled(true) + .disabled(channel.role == 0) + Text((channel.name!.isEmpty ? "channel \(channel.index)" : channel.name) ?? "Channel \(channel.index)") } else if channel.index == 5 { Toggle("Channel 5 Included", isOn: $includeChannel5) .toggleStyle(.switch) .labelsHidden() - .disabled(true) + .disabled(channel.role == 0) + Text((channel.name!.isEmpty ? "channel \(channel.index)" : channel.name) ?? "Channel \(channel.index)") } else if channel.index == 6 { Toggle("Channel 6 Included", isOn: $includeChannel6) .toggleStyle(.switch) .labelsHidden() - .disabled(true) + .disabled(channel.role == 0) + Text((channel.name!.isEmpty ? "channel \(channel.index)" : channel.name) ?? "Channel \(channel.index)") } else if channel.index == 7 { Toggle("Channel 7 Included", isOn: $includeChannel7) .toggleStyle(.switch) .labelsHidden() - Text("Admin Channel") + .disabled(channel.role == 0) + Text((channel.name!.isEmpty ? "channel \(channel.index)" : channel.name) ?? "Channel \(channel.index)") } - if channel.index > 1 && channel.index < 4{ - Text("Private Chat - \(channel.index)") - } - if channel.index > 3 && channel.index < 7{ - Text("Channel - \(channel.index)") + if channel.role > 0 { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } else { + Image(systemName: "lock.slash") + .foregroundColor(.gray) } Spacer() } @@ -144,23 +164,62 @@ struct ShareChannels: View { preview: SharePreview("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you", image: Image(uiImage: qrImage)) ) - - - Divider() + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) Image(uiImage: qrImage) .resizable() .scaledToFit() .frame( - minWidth: smallest * 0.7, - maxWidth: smallest * 0.7, - minHeight: smallest * 0.7, - maxHeight: smallest * 0.7, + minWidth: smallest * 0.65, + maxWidth: smallest * 0.65, + minHeight: smallest * 0.65, + maxHeight: smallest * 0.65, alignment: .top ) + Button { + + isPresentingHelp = true + + } label: { + + Label("Help Me!", systemImage: "lifepreserver") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) } } + .sheet(isPresented: $isPresentingHelp) { + + VStack { + Text("Meshtastic Channels").font(.title) + Text("A Meshtastic LoRa Mesh network can have up to 8 distinct channels.") + .font(.headline) + .padding(.bottom) + Text("Primary Channel").font(.title2) + Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled.") + .font(.callout) + .padding([.leading,.trailing,.bottom]) + Text("Admin Channel").font(.title2) + Text("A channel with the name 'admin' is the Admin channel and can be used to remotely administer nodes on your mesh, text messages can not be sent over the admin channel.") + .font(.callout) + .padding([.leading,.trailing,.bottom]) + Text("Private Channels").font(.title2) + Text("The other six channels can be used for private group converations. Each of these groups has its own encryption key.") + .font(.callout) + .padding([.leading,.trailing,.bottom]) + Text("From this view your primary channel and mesh settings are always shared in the generated QR code and you can toggle to include your admin channel and any private groups you want the person you are sharing with to have access to.") + .font(.callout) + .padding([.leading,.trailing,.bottom]) + Divider() + } + .padding() + .presentationDetents([.large]) + .presentationDragIndicator(.automatic) + } .navigationTitle("Generate QR Code") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: @@ -171,10 +230,70 @@ struct ShareChannels: View { }) .onAppear { - self.bleManager.context = context + if self.initialLoad{ + + self.bleManager.context = context + + self.initialLoad = false + GenerateChannelSet() + } + } + .onChange(of: includeChannel1) { includeCh1 in + GenerateChannelSet() + } + .onChange(of: includeChannel2) { includeCh2 in + GenerateChannelSet() + } + .onChange(of: includeChannel3) { includeCh3 in + GenerateChannelSet() + } + .onChange(of: includeChannel4) { includeCh4 in + GenerateChannelSet() + } + .onChange(of: includeChannel5) { includeCh5 in + GenerateChannelSet() + } + .onChange(of: includeChannel6) { includeCh6 in + GenerateChannelSet() + } + .onChange(of: includeChannel7) { includeCh7 in + GenerateChannelSet() } } .navigationViewStyle(StackNavigationViewStyle()) } } + func GenerateChannelSet() { + channelSet = ChannelSet() + var loRaConfig = Config.LoRaConfig() + loRaConfig.region = RegionCodes(rawValue: Int(node!.loRaConfig!.regionCode))!.protoEnumValue() + loRaConfig.modemPreset = ModemPresets(rawValue: Int(node!.loRaConfig!.modemPreset))!.protoEnumValue() + loRaConfig.bandwidth = UInt32(node!.loRaConfig!.bandwidth) + loRaConfig.spreadFactor = UInt32(node!.loRaConfig!.spreadFactor) + loRaConfig.codingRate = UInt32(node!.loRaConfig!.codingRate) + loRaConfig.frequencyOffset = node!.loRaConfig!.frequencyOffset + loRaConfig.hopLimit = UInt32(node!.loRaConfig!.hopLimit) + loRaConfig.txEnabled = node!.loRaConfig!.txEnabled + loRaConfig.txPower = node!.loRaConfig!.txPower + loRaConfig.channelNum = UInt32(node!.loRaConfig!.channelNum) + channelSet.loraConfig = loRaConfig + for ch in node!.myInfo!.channels!.array as! [ChannelEntity] { + if ch.role > 0 { + + if ch.index == 0 && includeChannel0 || ch.index == 1 && includeChannel1 || ch.index == 2 && includeChannel2 || ch.index == 3 && includeChannel3 || + ch.index == 4 && includeChannel4 || ch.index == 5 && includeChannel5 || ch.index == 6 && includeChannel6 || ch.index == 7 && includeChannel7 { + + var channelSettings = ChannelSettings() + channelSettings.name = ch.name! + channelSettings.psk = ch.psk ?? Data() + channelSettings.id = UInt32(ch.id) + channelSettings.uplinkEnabled = ch.uplinkEnabled + channelSettings.downlinkEnabled = ch.downlinkEnabled + channelSet.settings.append(channelSettings) + } + } + } + let settingsString = try! channelSet.serializedData().base64EncodedString() + channelsUrl = ("https://www.meshtastic.org/e/#" + settingsString.base64ToBase64url()) + } }