From 10568b9df941e2502b7550f36ae0ddb84c033f18 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 21 Dec 2022 23:18:26 -0800 Subject: [PATCH 01/17] Re-enable channel editor, bump version --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Views/Settings/Channels.swift | 564 +++++++++++------------ Meshtastic/Views/Settings/Settings.swift | 20 +- 3 files changed, 294 insertions(+), 294 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 0d6c3898..c5f1370d 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1000,7 +1000,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.8; + MARKETING_VERSION = 2.0.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1033,7 +1033,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.8; + MARKETING_VERSION = 2.0.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 7f73dc12..427b1dd3 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -4,285 +4,285 @@ // // Copyright(c) Garth Vander Houwen 4/8/22. // -//import SwiftUI -//import CoreData -// -//func generateChannelKey(size: Int) -> String { -// var keyData = Data(count: size) -// _ = keyData.withUnsafeMutableBytes { -// SecRandomCopyBytes(kSecRandomDefault, size, $0.baseAddress!) -// } -// return keyData.base64EncodedString() -//} -// -//struct Channels: View { -// -// @Environment(\.managedObjectContext) var context -// @EnvironmentObject var bleManager: BLEManager -// @Environment(\.dismiss) private var goBack -// @Environment(\.sizeCategory) var sizeCategory -// -// -// var node: NodeInfoEntity? -// -// @State var hasChanges = false -// @State private var isPresentingEditView = false -// @State private var isPresentingSaveConfirm: Bool = false -// @State private var channelIndex: Int32 = 0 -// @State private var channelName = "" -// @State private var channelKeySize = 32 -// @State private var channelKey = "AQ==" -// @State private var channelRole = 0 -// @State private var uplink = false -// @State private var downlink = false -// -// var body: some View { -// -// NavigationStack { -// List { -// if node != nil && node?.myInfo != nil { -// ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in -// Button(action: { -// channelIndex = channel.index -// channelRole = Int(channel.role) -// channelKey = channel.psk?.base64EncodedString() ?? "" -// if channelKey.count == 0 { -// channelKeySize = 0 -// } else if channelKey == "AQ==" { -// channelKeySize = -1 -// } else if channelKey.count == 24 { -// channelKeySize = 16 -// } else if channelKey.count == 32 { -// channelKeySize = 24 -// } else if channelKey.count == 44 { -// channelKeySize = 32 -// } -// channelName = channel.name ?? "" -// uplink = channel.uplinkEnabled -// downlink = channel.downlinkEnabled -// isPresentingEditView = true -// hasChanges = false -// }) { -// VStack(alignment: .leading) { -// HStack { -// CircleText(text: String(channel.index), color: .accentColor, circleSize: 45, fontSize: 36, brightness: 0.1) -// .padding(.trailing, 5) -// VStack { -// HStack { -// if channel.name?.isEmpty ?? false { -// if channel.role == 1 { -// Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) -// } else { -// Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline) -// } -// } else { -// Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline) -// } -// } -// } -// } -// } -// } -// } -// } -// } -// if node?.myInfo?.channels?.array.count ?? 0 < 8 { -// -// Button { -// let key = generateChannelKey(size: 32) -// channelName = "" -// channelIndex = Int32(node!.myInfo!.channels!.array.count) -// channelRole = 2 -// channelKey = key -// uplink = false -// downlink = false -// hasChanges = false -// isPresentingEditView = true -// -// } label: { -// Label("Add Channel", systemImage: "plus.square") -// } -// .buttonStyle(.bordered) -// .buttonBorderShape(.capsule) -// .controlSize(.large) -// .padding() -// .sheet(isPresented: $isPresentingEditView) { -// -// #if targetEnvironment(macCatalyst) -// Text("channel") -// .font(.largeTitle) -// .padding() -// #endif -// Form { -// HStack { -// Text("name") -// Spacer() -// TextField( -// "Channel Name", -// text: $channelName -// ) -// .disableAutocorrection(true) -// .keyboardType(.alphabet) -// .foregroundColor(Color.gray) -// .disabled(channelRole == 1 && channelName.count > 0) -// .onChange(of: channelName, perform: { value in -// channelName = channelName.replacing(" ", with: "") -// let totalBytes = channelName.utf8.count -// // Only mess with the value if it is too big -// if totalBytes > 11 { -// let firstNBytes = Data(channelName.utf8.prefix(11)) -// if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { -// // Set the channelName back to the last place where it was the right size -// channelName = maxBytesString -// } -// } -// hasChanges = true -// }) -// } -// HStack { -// Picker("Key Size", selection: $channelKeySize) { -// Text("Empty").tag(0) -// Text("Default").tag(-1) -// Text("1 bit").tag(1) -// Text("128 bit").tag(16) -// Text("192 bit").tag(24) -// Text("256 bit").tag(32) -// } -// .pickerStyle(DefaultPickerStyle()) -// Spacer() -// Button { -// if channelKeySize == -1 { -// channelKey = "AQ==" -// } else { -// let key = generateChannelKey(size: channelKeySize) -// channelKey = key -// } -// } label: { -// Image(systemName: "lock.rotation") -// .font(.title) -// } -// .buttonStyle(.bordered) -// .buttonBorderShape(.capsule) -// .controlSize(.small) -// } -// HStack (alignment: .top) { -// Text("Key") -// Spacer() -// TextField ( -// "", -// text: $channelKey, -// axis: .vertical -// ) -// .foregroundColor(Color.gray) -// .disabled(true) -// -// } -// .textSelection(.enabled) -// Picker("Channel Role", selection: $channelRole) { -// if channelRole == 1 { -// Text("Primary").tag(1) -// } else{ -// Text("Disabled").tag(0) -// Text("Secondary").tag(2) -// } -// } -// .pickerStyle(DefaultPickerStyle()) -// .disabled(channelRole == 1) -// Toggle("Uplink Enabled", isOn: $uplink) -// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) -// Toggle("Downlink Enabled", isOn: $downlink) -// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) -// } -// .onSubmit { -// //validate(name: channelName) -// } -// .onChange(of: channelName) { newName in -// hasChanges = true -// } -// .onChange(of: channelKeySize) { newKeySize in -// if channelKeySize == -1 { -// channelKey = "AQ==" -// } else { -// let key = generateChannelKey(size: channelKeySize) -// channelKey = key -// } -// hasChanges = true -// } -// .onChange(of: channelKey) { newKey in -// hasChanges = true -// } -// .onChange(of: channelRole) { newRole in -// hasChanges = true -// } -// .onChange(of: uplink) { newUplink in -// hasChanges = true -// } -// .onChange(of: downlink) { newDownlink in -// hasChanges = true -// } -// HStack { -// Button { -// isPresentingSaveConfirm = true -// } label: { -// Label("save", systemImage: "square.and.arrow.down") -// } -// .disabled(bleManager.connectedPeripheral == nil || !hasChanges) -// .buttonStyle(.bordered) -// .buttonBorderShape(.capsule) -// .controlSize(.large) -// .padding(.bottom) -// .confirmationDialog( -// "are.you.sure", -// isPresented: $isPresentingSaveConfirm, -// titleVisibility: .visible -// ) { -// Button("Save Channel \(channelIndex) to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { -// -// var channel = Channel() -// channel.index = channelIndex -// channel.settings.id = UInt32(channelIndex) -// channel.settings.name = channelName -// channel.settings.psk = Data(base64Encoded: channelKey) ?? Data() -// channel.role = ChannelRoles(rawValue: channelRole)?.protoEnumValue() ?? .secondary -// channel.settings.uplinkEnabled = uplink -// channel.settings.downlinkEnabled = downlink -// -// let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) -// -// if adminMessageId > 0 { -// // Should show a saved successfully alert once I know that to be true -// // for now just disable the button after a successful save -// channelName = "" -// hasChanges = false -// isPresentingEditView = false -// bleManager.disconnectPeripheral() -// } -// } -// } -// #if targetEnvironment(macCatalyst) -// Button { -// isPresentingEditView = false -// } label: { -// Label("Close", systemImage: "xmark") -// } -// .buttonStyle(.bordered) -// .buttonBorderShape(.capsule) -// .controlSize(.large) -// .padding(.bottom) -// #endif -// } -// .presentationDetents([.medium, .large]) -// } -// } -// } -// .navigationTitle("channels") -// .navigationSplitViewStyle(.automatic) -// .navigationBarItems(trailing: -// ZStack { -// ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") -// }) -// .onAppear { -// bleManager.context = context -// } -// } -//} +import SwiftUI +import CoreData + +func generateChannelKey(size: Int) -> String { + var keyData = Data(count: size) + _ = keyData.withUnsafeMutableBytes { + SecRandomCopyBytes(kSecRandomDefault, size, $0.baseAddress!) + } + return keyData.base64EncodedString() +} + +struct Channels: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @Environment(\.dismiss) private var goBack + @Environment(\.sizeCategory) var sizeCategory + + + var node: NodeInfoEntity? + + @State var hasChanges = false + @State private var isPresentingEditView = false + @State private var isPresentingSaveConfirm: Bool = false + @State private var channelIndex: Int32 = 0 + @State private var channelName = "" + @State private var channelKeySize = 32 + @State private var channelKey = "AQ==" + @State private var channelRole = 0 + @State private var uplink = false + @State private var downlink = false + + var body: some View { + + NavigationStack { + List { + if node != nil && node?.myInfo != nil { + ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in + Button(action: { + channelIndex = channel.index + channelRole = Int(channel.role) + channelKey = channel.psk?.base64EncodedString() ?? "" + if channelKey.count == 0 { + channelKeySize = 0 + } else if channelKey == "AQ==" { + channelKeySize = -1 + } else if channelKey.count == 24 { + channelKeySize = 16 + } else if channelKey.count == 32 { + channelKeySize = 24 + } else if channelKey.count == 44 { + channelKeySize = 32 + } + channelName = channel.name ?? "" + uplink = channel.uplinkEnabled + downlink = channel.downlinkEnabled + isPresentingEditView = true + hasChanges = false + }) { + VStack(alignment: .leading) { + HStack { + CircleText(text: String(channel.index), color: .accentColor, circleSize: 45, fontSize: 36, brightness: 0.1) + .padding(.trailing, 5) + VStack { + HStack { + if channel.name?.isEmpty ?? false { + if channel.role == 1 { + Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) + } else { + Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline) + } + } else { + Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline) + } + } + } + } + } + } + } + } + } + if node?.myInfo?.channels?.array.count ?? 0 < 8 { + + Button { + let key = generateChannelKey(size: 32) + channelName = "" + channelIndex = Int32(node!.myInfo!.channels!.array.count) + channelRole = 2 + channelKey = key + uplink = false + downlink = false + hasChanges = false + isPresentingEditView = true + + } label: { + Label("Add Channel", systemImage: "plus.square") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .sheet(isPresented: $isPresentingEditView) { + + #if targetEnvironment(macCatalyst) + Text("channel") + .font(.largeTitle) + .padding() + #endif + Form { + HStack { + Text("name") + Spacer() + TextField( + "Channel Name", + text: $channelName + ) + .disableAutocorrection(true) + .keyboardType(.alphabet) + .foregroundColor(Color.gray) + .disabled(channelRole == 1 && channelName.count > 0) + .onChange(of: channelName, perform: { value in + channelName = channelName.replacing(" ", with: "") + let totalBytes = channelName.utf8.count + // Only mess with the value if it is too big + if totalBytes > 11 { + let firstNBytes = Data(channelName.utf8.prefix(11)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the channelName back to the last place where it was the right size + channelName = maxBytesString + } + } + hasChanges = true + }) + } + HStack { + Picker("Key Size", selection: $channelKeySize) { + Text("Empty").tag(0) + Text("Default").tag(-1) + Text("1 bit").tag(1) + Text("128 bit").tag(16) + Text("192 bit").tag(24) + Text("256 bit").tag(32) + } + .pickerStyle(DefaultPickerStyle()) + Spacer() + Button { + if channelKeySize == -1 { + channelKey = "AQ==" + } else { + let key = generateChannelKey(size: channelKeySize) + channelKey = key + } + } label: { + Image(systemName: "lock.rotation") + .font(.title) + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + } + HStack (alignment: .top) { + Text("Key") + Spacer() + TextField ( + "", + text: $channelKey, + axis: .vertical + ) + .foregroundColor(Color.gray) + .disabled(true) + + } + .textSelection(.enabled) + Picker("Channel Role", selection: $channelRole) { + if channelRole == 1 { + Text("Primary").tag(1) + } else{ + Text("Disabled").tag(0) + Text("Secondary").tag(2) + } + } + .pickerStyle(DefaultPickerStyle()) + .disabled(channelRole == 1) + Toggle("Uplink Enabled", isOn: $uplink) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle("Downlink Enabled", isOn: $downlink) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + .onSubmit { + //validate(name: channelName) + } + .onChange(of: channelName) { newName in + hasChanges = true + } + .onChange(of: channelKeySize) { newKeySize in + if channelKeySize == -1 { + channelKey = "AQ==" + } else { + let key = generateChannelKey(size: channelKeySize) + channelKey = key + } + hasChanges = true + } + .onChange(of: channelKey) { newKey in + hasChanges = true + } + .onChange(of: channelRole) { newRole in + hasChanges = true + } + .onChange(of: uplink) { newUplink in + hasChanges = true + } + .onChange(of: downlink) { newDownlink in + hasChanges = true + } + HStack { + Button { + isPresentingSaveConfirm = true + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .disabled(bleManager.connectedPeripheral == nil || !hasChanges) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingSaveConfirm, + titleVisibility: .visible + ) { + Button("Save Channel \(channelIndex) to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { + + var channel = Channel() + channel.index = channelIndex + channel.settings.id = UInt32(channelIndex) + channel.settings.name = channelName + channel.settings.psk = Data(base64Encoded: channelKey) ?? Data() + channel.role = ChannelRoles(rawValue: channelRole)?.protoEnumValue() ?? .secondary + channel.settings.uplinkEnabled = uplink + channel.settings.downlinkEnabled = downlink + + let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) + + if adminMessageId > 0 { + // Should show a saved successfully alert once I know that to be true + // for now just disable the button after a successful save + channelName = "" + hasChanges = false + isPresentingEditView = false + bleManager.disconnectPeripheral() + } + } + } + #if targetEnvironment(macCatalyst) + Button { + isPresentingEditView = false + } label: { + Label("Close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + #endif + } + .presentationDetents([.medium, .large]) + } + } + } + .navigationTitle("channels") + .navigationSplitViewStyle(.automatic) + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + }) + .onAppear { + bleManager.context = context + } + } +} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 9bb805f7..1ec48400 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -59,16 +59,16 @@ struct Settings: View { Text("lora") } -// NavigationLink() { -// -// Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) -// } label: { -// -// Image(systemName: "fibrechannel") -// .symbolRenderingMode(.hierarchical) -// -// Text("channels") -// } + NavigationLink() { + + Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) + } label: { + + Image(systemName: "fibrechannel") + .symbolRenderingMode(.hierarchical) + + Text("channels") + } NavigationLink() { From e21659336ee0928851bbea7aa52a6bb1f3fc2d14 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Dec 2022 10:03:43 -0800 Subject: [PATCH 02/17] Fix for channel messages message view not updating when new messages arrive Update protobufs Add 0 to external notification output intervals --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Helpers/MeshPackets.swift | 4 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 276 ++++++++++++++++++ Meshtastic/Protobufs/module_config.pb.swift | 94 +++++- .../Views/Messages/ChannelMessageList.swift | 2 +- .../Views/Messages/UserMessageList.swift | 2 +- .../Module/ExternalNotificationConfig.swift | 6 +- 8 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index c5f1370d..50e4621a 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -232,6 +232,7 @@ DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = ""; }; DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = ""; }; DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = ""; }; + DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1209,11 +1210,12 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */, DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */, DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */; + currentVersion = DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f947ecbf..469a5073 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1320,6 +1320,10 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel { + context.refresh(channel, mergeChanges: true) + } + if channel.index == newMessage.channel && !channel.mute { // Create an iOS Notification for the received private channel message and schedule it immediately let manager = LocalNotificationManager() diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index d57dfe2f..a8bccd83 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV3.xcdatamodel + MeshtasticDataModelV4.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents new file mode 100644 index 00000000..7bb559e9 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Protobufs/module_config.pb.swift b/Meshtastic/Protobufs/module_config.pb.swift index 3f0709a2..19fa66f7 100644 --- a/Meshtastic/Protobufs/module_config.pb.swift +++ b/Meshtastic/Protobufs/module_config.pb.swift @@ -481,33 +481,71 @@ struct ModuleConfig { // methods supported on all messages. /// - /// Preferences for the ExternalNotificationModule + /// Enable the ExternalNotificationModule var enabled: Bool = false /// - /// TODO: REPLACE + /// When using in On/Off mode, keep the output on for this many + /// milliseconds. Default 1000ms (1 second). var outputMs: UInt32 = 0 /// - /// TODO: REPLACE + /// Define the output pin GPIO setting Defaults to + /// EXT_NOTIFY_OUT if set for the board. + /// In standalone devices this pin should drive the LED to match the UI. var output: UInt32 = 0 /// - /// TODO: REPLACE + /// Optional: Define a secondary output pin for a vibra motor + /// This is used in standalone devices to match the UI. + var outputVibra: UInt32 = 0 + + /// + /// Optional: Define a tertiary output pin for an active buzzer + /// This is used in standalone devices to to match the UI. + var outputBuzzer: UInt32 = 0 + + /// + /// IF this is true, the 'output' Pin will be pulled active high, false + /// means active low. var active: Bool = false /// - /// TODO: REPLACE + /// True: Alert when a text message arrives (output) var alertMessage: Bool = false /// - /// TODO: REPLACE + /// True: Alert when a text message arrives (output_vibra) + var alertMessageVibra: Bool = false + + /// + /// True: Alert when a text message arrives (output_buzzer) + var alertMessageBuzzer: Bool = false + + /// + /// True: Alert when the bell character is received (output) var alertBell: Bool = false /// - /// TODO: REPLACE + /// True: Alert when the bell character is received (output_vibra) + var alertBellVibra: Bool = false + + /// + /// True: Alert when the bell character is received (output_buzzer) + var alertBellBuzzer: Bool = false + + /// + /// use a PWM output instead of a simple on/off output. This will ignore + /// the 'output', 'output_ms' and 'active' settings and use the + /// device.buzzer_gpio instead. var usePwm: Bool = false + /// + /// The notification will toggle with 'output_ms' for this time of seconds. + /// Default is 0 which means don't repeat at all. 60 would mean blink + /// and/or beep for 60 seconds + var nagTimeout: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -1248,10 +1286,17 @@ extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftP 1: .same(proto: "enabled"), 2: .standard(proto: "output_ms"), 3: .same(proto: "output"), + 8: .standard(proto: "output_vibra"), + 9: .standard(proto: "output_buzzer"), 4: .same(proto: "active"), 5: .standard(proto: "alert_message"), + 10: .standard(proto: "alert_message_vibra"), + 11: .standard(proto: "alert_message_buzzer"), 6: .standard(proto: "alert_bell"), + 12: .standard(proto: "alert_bell_vibra"), + 13: .standard(proto: "alert_bell_buzzer"), 7: .standard(proto: "use_pwm"), + 14: .standard(proto: "nag_timeout"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1267,6 +1312,13 @@ extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftP case 5: try { try decoder.decodeSingularBoolField(value: &self.alertMessage) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.alertBell) }() case 7: try { try decoder.decodeSingularBoolField(value: &self.usePwm) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self.outputVibra) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &self.outputBuzzer) }() + case 10: try { try decoder.decodeSingularBoolField(value: &self.alertMessageVibra) }() + case 11: try { try decoder.decodeSingularBoolField(value: &self.alertMessageBuzzer) }() + case 12: try { try decoder.decodeSingularBoolField(value: &self.alertBellVibra) }() + case 13: try { try decoder.decodeSingularBoolField(value: &self.alertBellBuzzer) }() + case 14: try { try decoder.decodeSingularUInt32Field(value: &self.nagTimeout) }() default: break } } @@ -1294,6 +1346,27 @@ extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftP if self.usePwm != false { try visitor.visitSingularBoolField(value: self.usePwm, fieldNumber: 7) } + if self.outputVibra != 0 { + try visitor.visitSingularUInt32Field(value: self.outputVibra, fieldNumber: 8) + } + if self.outputBuzzer != 0 { + try visitor.visitSingularUInt32Field(value: self.outputBuzzer, fieldNumber: 9) + } + if self.alertMessageVibra != false { + try visitor.visitSingularBoolField(value: self.alertMessageVibra, fieldNumber: 10) + } + if self.alertMessageBuzzer != false { + try visitor.visitSingularBoolField(value: self.alertMessageBuzzer, fieldNumber: 11) + } + if self.alertBellVibra != false { + try visitor.visitSingularBoolField(value: self.alertBellVibra, fieldNumber: 12) + } + if self.alertBellBuzzer != false { + try visitor.visitSingularBoolField(value: self.alertBellBuzzer, fieldNumber: 13) + } + if self.nagTimeout != 0 { + try visitor.visitSingularUInt32Field(value: self.nagTimeout, fieldNumber: 14) + } try unknownFields.traverse(visitor: &visitor) } @@ -1301,10 +1374,17 @@ extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftP if lhs.enabled != rhs.enabled {return false} if lhs.outputMs != rhs.outputMs {return false} if lhs.output != rhs.output {return false} + if lhs.outputVibra != rhs.outputVibra {return false} + if lhs.outputBuzzer != rhs.outputBuzzer {return false} if lhs.active != rhs.active {return false} if lhs.alertMessage != rhs.alertMessage {return false} + if lhs.alertMessageVibra != rhs.alertMessageVibra {return false} + if lhs.alertMessageBuzzer != rhs.alertMessageBuzzer {return false} if lhs.alertBell != rhs.alertBell {return false} + if lhs.alertBellVibra != rhs.alertBellVibra {return false} + if lhs.alertBellBuzzer != rhs.alertBellBuzzer {return false} if lhs.usePwm != rhs.usePwm {return false} + if lhs.nagTimeout != rhs.nagTimeout {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 68f407e3..72a81c15 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -319,7 +319,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantAck: true) { + if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { print("Location Sent") } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 6dda3220..391fab03 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -316,7 +316,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantAck: true) { + if bleManager.sendPosition(destNum: user.num, wantResponse: true) { print("Location Sent") } } diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 69896e77..5d6260cc 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -8,6 +8,7 @@ import SwiftUI enum OutputIntervals: Int, CaseIterable, Identifiable { + case unset = 0 case oneSecond = 1000 case twoSeconds = 2000 case threeSeconds = 3000 @@ -23,6 +24,8 @@ enum OutputIntervals: Int, CaseIterable, Identifiable { get { switch self { + case .unset: + return "Unset" case .oneSecond: return "One Second" case .twoSeconds: @@ -85,6 +88,8 @@ struct ExternalNotificationConfig: View { Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Use a PWM Buzzer (RAK buzzer module), disable PWM buzzer to set manual GPIO options.") + .font(.caption) } if !usePWM { Section(header: Text("GPIO")) { @@ -94,7 +99,6 @@ struct ExternalNotificationConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Text("Specifies whether the external circuit is triggered when the device's GPIO is low or high.") .font(.caption) - .listRowSeparator(.visible) Picker("GPIO to monitor", selection: $output) { ForEach(0..<40) { if $0 == 0 { From 8a31ffaf19e2cf332c96a5fdd61c4495c34a0395 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Dec 2022 11:30:55 -0800 Subject: [PATCH 03/17] Add new fields to external notification config --- .../Module/ExternalNotificationConfig.swift | 348 +++++++++++------- 1 file changed, 223 insertions(+), 125 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 5d6260cc..99c57412 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -7,7 +7,7 @@ import SwiftUI enum OutputIntervals: Int, CaseIterable, Identifiable { - + case unset = 0 case oneSecond = 1000 case twoSeconds = 2000 @@ -18,12 +18,12 @@ enum OutputIntervals: Int, CaseIterable, Identifiable { case fifteenSeconds = 15000 case thirtySeconds = 30000 case oneMinute = 60000 - + var id: Int { self.rawValue } var description: String { get { switch self { - + case .unset: return "Unset" case .oneSecond: @@ -61,152 +61,250 @@ struct ExternalNotificationConfig: View { @State var hasChanges = false @State var enabled = false @State var alertBell = false + @State var alertBellBuzzer = false + @State var alertBellVibra = false @State var alertMessage = false + @State var alertMessageBuzzer = false + @State var alertMessageVibra = false @State var active = false @State var usePWM = true @State var output = 0 + @State var outputBuzzer = 0 + @State var outputVibra = 0 @State var outputMilliseconds = 0 + @State var nagTimeout = 0 var body: some View { - - VStack { - Form { - Section(header: Text("options")) { - Toggle(isOn: $enabled) { - Label("enabled", systemImage: "megaphone") + + Form { + Section(header: Text("options")) { + Toggle(isOn: $enabled) { + Label("enabled", systemImage: "megaphone") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertBell) { + Label("Alert when receiving a bell", systemImage: "bell") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertMessage) { + Label("Alert when receiving a message", systemImage: "message") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $usePWM) { + Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Use a PWM output (like the RAK Buzzer) instead of a on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.") + .font(.caption) + } + if !usePWM { + Section(header: Text("Primary GPIO")) { + Toggle(isOn: $active) { + Label("Active", systemImage: "togglepower") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $alertBell) { - Label("Alert when receiving a bell", systemImage: "bell") + Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.") + .font(.caption) + Picker("Output pin GPIO", selection: $output) { + ForEach(0..<40) { + if $0 == 0 { + Text("Unset") + } else { + Text("Pin \($0)") + } + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $alertMessage) { - Label("Alert when receiving a message", systemImage: "message") + .pickerStyle(DefaultPickerStyle()) + Picker("GPIO Output Duration", selection: $outputMilliseconds ) { + ForEach(OutputIntervals.allCases) { oi in + Text(oi.description) + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $usePWM) { - Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("Use a PWM Buzzer (RAK buzzer module), disable PWM buzzer to set manual GPIO options.") + .pickerStyle(DefaultPickerStyle()) + Text("When using in GPIO mode, keep the output on for this long. ") .font(.caption) } - if !usePWM { - Section(header: Text("GPIO")) { - Toggle(isOn: $active) { - Label("Active", systemImage: "togglepower") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("Specifies whether the external circuit is triggered when the device's GPIO is low or high.") - .font(.caption) - Picker("GPIO to monitor", selection: $output) { - ForEach(0..<40) { - if $0 == 0 { - Text("Unset") - } else { - Text("Pin \($0)") - } - } - } - .pickerStyle(DefaultPickerStyle()) - Text("Specifies the GPIO that your external circuit is attached to on the device.") - .font(.caption) - Picker("GPIO Output Duration", selection: $outputMilliseconds ) { - ForEach(OutputIntervals.allCases) { oi in - Text(oi.description) - } - } - .pickerStyle(DefaultPickerStyle()) - Text("Specifies how long the monitored GPIO should output.") - .font(.caption) - } - } - } - .disabled(bleManager.connectedPeripheral == nil) - Button { - isPresentingSaveConfirm = true - } label: { - Label("save", systemImage: "square.and.arrow.down") - } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges) - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .confirmationDialog( - "are.you.sure", - isPresented: $isPresentingSaveConfirm, - titleVisibility: .visible - ) { - Button("Save External Notification Module Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { - var enc = ModuleConfig.ExternalNotificationConfig() - enc.enabled = enabled - enc.alertBell = alertBell - enc.alertMessage = alertMessage - enc.active = active - enc.output = UInt32(output) - enc.outputMs = UInt32(outputMilliseconds) - enc.usePwm = usePWM - let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: node!.user!, toUser: node!.user!) - if adminMessageId > 0{ - // Should show a saved successfully alert once I know that to be true - // for now just disable the button after a successful save - hasChanges = false - goBack() + Section(header: Text("Optional GPIO")) { + Toggle(isOn: $alertBellBuzzer) { + Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell") } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertBellVibra) { + Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertMessageBuzzer) { + Label("Alert GPIO Buzzer when receiving a message", systemImage: "message") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertMessageBuzzer) { + Label("Alert GPIO vibra motor when receiving a message", systemImage: "message") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) { + ForEach(0..<40) { + if $0 == 0 { + Text("Unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + Picker("Output pin vibra GPIO", selection: $outputVibra) { + ForEach(0..<40) { + if $0 == 0 { + Text("Unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + Picker("Nag timeout", selection: $nagTimeout ) { + ForEach(OutputIntervals.allCases) { oi in + Text(oi.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("Specifies how long the monitored GPIO should output.") + .font(.caption) } } - .navigationTitle("external.notification.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") - }) - .onAppear { - self.bleManager.context = context - self.enabled = node?.externalNotificationConfig?.enabled ?? false - self.alertBell = node?.externalNotificationConfig?.alertBell ?? false - self.alertMessage = node?.externalNotificationConfig?.alertMessage ?? false - self.active = node?.externalNotificationConfig?.active ?? false - self.output = Int(node?.externalNotificationConfig?.output ?? 0) - self.outputMilliseconds = Int(node?.externalNotificationConfig?.outputMilliseconds ?? 0) - self.usePWM = node?.externalNotificationConfig?.usePWM ?? true - self.hasChanges = false - } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.externalNotificationConfig != nil { - if newEnabled != node!.externalNotificationConfig!.enabled { hasChanges = true } + + } + .disabled(bleManager.connectedPeripheral == nil) + Button { + isPresentingSaveConfirm = true + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .disabled(bleManager.connectedPeripheral == nil || !hasChanges) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingSaveConfirm, + titleVisibility: .visible + ) { + Button("Save External Notification Module Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { + var enc = ModuleConfig.ExternalNotificationConfig() + enc.enabled = enabled + enc.alertBell = alertBell + enc.alertBellBuzzer = alertBellBuzzer + enc.alertBellVibra = alertBellVibra + enc.alertMessage = alertMessage + enc.alertMessageBuzzer = alertMessageBuzzer + enc.alertMessageVibra = alertMessageVibra + enc.active = active + enc.output = UInt32(output) + enc.outputBuzzer = UInt32(outputBuzzer) + enc.outputVibra = UInt32(outputVibra) + enc.outputMs = UInt32(outputMilliseconds) + enc.usePwm = usePWM + let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: node!.user!, toUser: node!.user!) + if adminMessageId > 0{ + // Should show a saved successfully alert once I know that to be true + // for now just disable the button after a successful save + hasChanges = false + goBack() } } - .onChange(of: alertBell) { newAlertBell in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBell != node!.externalNotificationConfig!.alertBell { hasChanges = true } - } + } + .navigationTitle("external.notification.config") + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + }) + .onAppear { + self.bleManager.context = context + self.enabled = node?.externalNotificationConfig?.enabled ?? false + self.alertBell = node?.externalNotificationConfig?.alertBell ?? false + self.alertBellBuzzer = node?.externalNotificationConfig?.alertBellBuzzer ?? false + self.alertBellVibra = node?.externalNotificationConfig?.alertBellVibra ?? false + self.alertMessage = node?.externalNotificationConfig?.alertMessage ?? false + self.alertMessageBuzzer = node?.externalNotificationConfig?.alertMessageBuzzer ?? false + self.alertMessageVibra = node?.externalNotificationConfig?.alertMessageVibra ?? false + self.active = node?.externalNotificationConfig?.active ?? false + self.output = Int(node?.externalNotificationConfig?.output ?? 0) + self.outputBuzzer = Int(node?.externalNotificationConfig?.outputBuzzer ?? 0) + self.outputVibra = Int(node?.externalNotificationConfig?.outputVibra ?? 0) + self.outputMilliseconds = Int(node?.externalNotificationConfig?.outputMilliseconds ?? 0) + self.nagTimeout = Int(node?.externalNotificationConfig?.nagTimeout ?? 0) + self.usePWM = node?.externalNotificationConfig?.usePWM ?? true + self.hasChanges = false + } + .onChange(of: enabled) { newEnabled in + if node != nil && node!.externalNotificationConfig != nil { + if newEnabled != node!.externalNotificationConfig!.enabled { hasChanges = true } } - .onChange(of: alertMessage) { newAlertMessage in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessage != node!.externalNotificationConfig!.alertMessage { hasChanges = true } - } + } + .onChange(of: alertBell) { newAlertBell in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertBell != node!.externalNotificationConfig!.alertBell { hasChanges = true } } - .onChange(of: active) { newActuve in - if node != nil && node!.externalNotificationConfig != nil { - if newActuve != node!.externalNotificationConfig!.active { hasChanges = true } - } + } + .onChange(of: alertBellBuzzer) { newAlertBellBuzzer in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertBellBuzzer != node!.externalNotificationConfig!.alertBellBuzzer { hasChanges = true } } - .onChange(of: output) { newOutput in - if node != nil && node!.externalNotificationConfig != nil { - if newOutput != node!.externalNotificationConfig!.output { hasChanges = true } - } + } + .onChange(of: alertBellVibra) { newAlertBellVibra in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertBellVibra != node!.externalNotificationConfig!.alertBellVibra { hasChanges = true } } - .onChange(of: outputMilliseconds) { newOutputMs in - if node != nil && node!.externalNotificationConfig != nil { - if newOutputMs != node!.externalNotificationConfig!.outputMilliseconds { hasChanges = true } - } + } + .onChange(of: alertMessage) { newAlertMessage in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertMessage != node!.externalNotificationConfig!.alertMessage { hasChanges = true } } - .onChange(of: usePWM) { newUsePWM in - if node != nil && node!.externalNotificationConfig != nil { - if newUsePWM != node!.externalNotificationConfig!.usePWM { hasChanges = true } - } + } + .onChange(of: alertMessageBuzzer) { newAlertMessageBuzzer in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertMessageBuzzer != node!.externalNotificationConfig!.alertMessageBuzzer { hasChanges = true } + } + } + .onChange(of: alertMessageVibra) { newAlertMessageVibra in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertMessageVibra != node!.externalNotificationConfig!.alertMessageVibra { hasChanges = true } + } + } + .onChange(of: active) { newActive in + if node != nil && node!.externalNotificationConfig != nil { + if newActive != node!.externalNotificationConfig!.active { hasChanges = true } + } + } + .onChange(of: output) { newOutput in + if node != nil && node!.externalNotificationConfig != nil { + if newOutput != node!.externalNotificationConfig!.output { hasChanges = true } + } + } + .onChange(of: output) { newOutputBuzzer in + if node != nil && node!.externalNotificationConfig != nil { + if newOutputBuzzer != node!.externalNotificationConfig!.outputBuzzer { hasChanges = true } + } + } + .onChange(of: output) { newOutputVibra in + if node != nil && node!.externalNotificationConfig != nil { + if newOutputVibra != node!.externalNotificationConfig!.outputVibra { hasChanges = true } + } + } + .onChange(of: outputMilliseconds) { newOutputMs in + if node != nil && node!.externalNotificationConfig != nil { + if newOutputMs != node!.externalNotificationConfig!.outputMilliseconds { hasChanges = true } + } + } + .onChange(of: usePWM) { newUsePWM in + if node != nil && node!.externalNotificationConfig != nil { + if newUsePWM != node!.externalNotificationConfig!.usePWM { hasChanges = true } + } + } + .onChange(of: nagTimeout) { newNagTimeout in + if node != nil && node!.externalNotificationConfig != nil { + if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true } } } } From 7501c4bbe3df113ada43f79eb523e99df9bc5337 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Dec 2022 14:00:48 -0800 Subject: [PATCH 04/17] Clean up external notifications config --- .../Module/ExternalNotificationConfig.swift | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 99c57412..c3a9b9fe 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -67,7 +67,7 @@ struct ExternalNotificationConfig: View { @State var alertMessageBuzzer = false @State var alertMessageVibra = false @State var active = false - @State var usePWM = true + @State var usePWM = false @State var output = 0 @State var outputBuzzer = 0 @State var outputVibra = 0 @@ -75,7 +75,7 @@ struct ExternalNotificationConfig: View { @State var nagTimeout = 0 var body: some View { - + Form { Section(header: Text("options")) { Toggle(isOn: $enabled) { @@ -94,7 +94,7 @@ struct ExternalNotificationConfig: View { Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("Use a PWM output (like the RAK Buzzer) instead of a on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.") + Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.") .font(.caption) } if !usePWM { @@ -123,6 +123,14 @@ struct ExternalNotificationConfig: View { .pickerStyle(DefaultPickerStyle()) Text("When using in GPIO mode, keep the output on for this long. ") .font(.caption) + Picker("Nag timeout", selection: $nagTimeout ) { + ForEach(OutputIntervals.allCases) { oi in + Text(oi.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("Specifies how long the monitored GPIO should output.") + .font(.caption) } Section(header: Text("Optional GPIO")) { @@ -135,7 +143,7 @@ struct ExternalNotificationConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $alertMessageBuzzer) { - Label("Alert GPIO Buzzer when receiving a message", systemImage: "message") + Label("Alert GPIO buzzer when receiving a message", systemImage: "message") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $alertMessageBuzzer) { @@ -162,17 +170,8 @@ struct ExternalNotificationConfig: View { } } .pickerStyle(DefaultPickerStyle()) - Picker("Nag timeout", selection: $nagTimeout ) { - ForEach(OutputIntervals.allCases) { oi in - Text(oi.description) - } - } - .pickerStyle(DefaultPickerStyle()) - Text("Specifies how long the monitored GPIO should output.") - .font(.caption) } } - } .disabled(bleManager.connectedPeripheral == nil) Button { @@ -234,7 +233,7 @@ struct ExternalNotificationConfig: View { self.outputVibra = Int(node?.externalNotificationConfig?.outputVibra ?? 0) self.outputMilliseconds = Int(node?.externalNotificationConfig?.outputMilliseconds ?? 0) self.nagTimeout = Int(node?.externalNotificationConfig?.nagTimeout ?? 0) - self.usePWM = node?.externalNotificationConfig?.usePWM ?? true + self.usePWM = node?.externalNotificationConfig?.usePWM ?? false self.hasChanges = false } .onChange(of: enabled) { newEnabled in From 82c1d20a211011e0cf6261dc0b9819fd7626f604 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Dec 2022 14:07:55 -0800 Subject: [PATCH 05/17] Save all the external notification values --- Meshtastic/Helpers/MeshPackets.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 469a5073..03ad3af2 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -409,20 +409,36 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum if fetchedNode[0].externalNotificationConfig == nil { let newExternalNotificationConfig = ExternalNotificationConfigEntity(context: context) newExternalNotificationConfig.enabled = config.externalNotification.enabled + newExternalNotificationConfig.usePWM = config.externalNotification.usePwm newExternalNotificationConfig.alertBell = config.externalNotification.alertBell + newExternalNotificationConfig.alertBellBuzzer = config.externalNotification.alertBellBuzzer + newExternalNotificationConfig.alertBellVibra = config.externalNotification.alertBellVibra newExternalNotificationConfig.alertMessage = config.externalNotification.alertMessage + newExternalNotificationConfig.alertMessageBuzzer = config.externalNotification.alertMessageBuzzer + newExternalNotificationConfig.alertMessageVibra = config.externalNotification.alertMessageVibra newExternalNotificationConfig.active = config.externalNotification.active newExternalNotificationConfig.output = Int32(config.externalNotification.output) + newExternalNotificationConfig.outputBuzzer = Int32(config.externalNotification.outputBuzzer) + newExternalNotificationConfig.outputVibra = Int32(config.externalNotification.outputVibra) newExternalNotificationConfig.outputMilliseconds = Int32(config.externalNotification.outputMs) + newExternalNotificationConfig.nagTimeout = Int32(config.externalNotification.nagTimeout) fetchedNode[0].externalNotificationConfig = newExternalNotificationConfig } else { fetchedNode[0].externalNotificationConfig?.enabled = config.externalNotification.enabled + fetchedNode[0].externalNotificationConfig?.usePWM = config.externalNotification.usePwm fetchedNode[0].externalNotificationConfig?.alertBell = config.externalNotification.alertBell + fetchedNode[0].externalNotificationConfig?.alertBellBuzzer = config.externalNotification.alertBellBuzzer + fetchedNode[0].externalNotificationConfig?.alertBellVibra = config.externalNotification.alertBellVibra fetchedNode[0].externalNotificationConfig?.alertMessage = config.externalNotification.alertMessage + fetchedNode[0].externalNotificationConfig?.alertMessageBuzzer = config.externalNotification.alertMessageBuzzer + fetchedNode[0].externalNotificationConfig?.alertMessageVibra = config.externalNotification.alertMessageVibra fetchedNode[0].externalNotificationConfig?.active = config.externalNotification.active fetchedNode[0].externalNotificationConfig?.output = Int32(config.externalNotification.output) + fetchedNode[0].externalNotificationConfig?.outputBuzzer = Int32(config.externalNotification.outputBuzzer) + fetchedNode[0].externalNotificationConfig?.outputVibra = Int32(config.externalNotification.outputVibra) fetchedNode[0].externalNotificationConfig?.outputMilliseconds = Int32(config.externalNotification.outputMs) + fetchedNode[0].externalNotificationConfig?.nagTimeout = Int32(config.externalNotification.nagTimeout) } do { From 41f4eb510d2157ab2703b3f5daf0cefdcbea8908 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Dec 2022 20:48:47 -0800 Subject: [PATCH 06/17] parse links in messages so they can be clicked by recipients --- Meshtastic/Helpers/MeshPackets.swift | 16 ++++++++++++++-- .../Views/Messages/ChannelMessageList.swift | 4 +++- Meshtastic/Views/Messages/UserMessageList.swift | 4 +++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 03ad3af2..619b56fa 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1301,8 +1301,20 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) } - - newMessage.messagePayload = messageText + let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + let matches = detector.matches(in: messageText, options: [], range: NSRange(location: 0, length: messageText.utf16.count)) + if matches.count > 0 { + var messageWithLink = "" + for match in matches { + guard let range = Range(match.range, in: messageText) else { continue } + let url = messageText[range] + print(match.url?.baseURL) + messageWithLink = messageText.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))](\(url))") + } + newMessage.messagePayload = messageWithLink + } else { + newMessage.messagePayload = messageText + } newMessage.fromUser?.objectWillChange.send() newMessage.toUser?.objectWillChange.send() diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 72a81c15..0df569c6 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -60,7 +60,9 @@ struct ChannelMessageList: View { .offset(y: -5) } VStack(alignment: currentUser ? .trailing : .leading) { - Text(message.messagePayload ?? "EMPTY MESSAGE") + let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayload ?? "EMPTY MESSAGE") + + Text(markdownText) .padding(10) .foregroundColor(.white) .background(currentUser ? .accentColor : Color(.gray)) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 391fab03..3bd5ca57 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -61,7 +61,9 @@ struct UserMessageList: View { .offset(y: -5) } VStack(alignment: currentUser ? .trailing : .leading) { - Text(message.messagePayload ?? "EMPTY MESSAGE") + let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayload ?? "EMPTY MESSAGE") + + Text(markdownText) .padding(10) .foregroundColor(.white) .background(currentUser ? .accentColor : Color(.gray)) From 81e1b61443bd72adff3ea8886118ad514f4bca9b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Dec 2022 21:21:08 -0800 Subject: [PATCH 07/17] links in messages --- Meshtastic/Helpers/MeshPackets.swift | 1 - Meshtastic/Views/Messages/ChannelMessageList.swift | 3 ++- Meshtastic/Views/Messages/UserMessageList.swift | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 619b56fa..3c9f51d6 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1308,7 +1308,6 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM for match in matches { guard let range = Range(match.range, in: messageText) else { continue } let url = messageText[range] - print(match.url?.baseURL) messageWithLink = messageText.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))](\(url))") } newMessage.messagePayload = messageWithLink diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 0df569c6..0773f431 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -61,8 +61,9 @@ struct ChannelMessageList: View { } VStack(alignment: currentUser ? .trailing : .leading) { let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayload ?? "EMPTY MESSAGE") - + let skyBlue = Color(red: 0.4627, green: 0.8392, blue: 1.0) Text(markdownText) + .tint(skyBlue) .padding(10) .foregroundColor(.white) .background(currentUser ? .accentColor : Color(.gray)) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 3bd5ca57..f80f61b6 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -62,8 +62,9 @@ struct UserMessageList: View { } VStack(alignment: currentUser ? .trailing : .leading) { let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayload ?? "EMPTY MESSAGE") - + let skyBlue = Color(red: 0.4627, green: 0.8392, blue: 1.0) Text(markdownText) + .tint(skyBlue) .padding(10) .foregroundColor(.white) .background(currentUser ? .accentColor : Color(.gray)) From 4d8a8543510919e61613cf27d2cfa735e8b4ae89 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Dec 2022 23:05:16 -0800 Subject: [PATCH 08/17] Make addresses, links and phone numbers clickable in messages --- Meshtastic/Helpers/MeshPackets.swift | 22 ++++++++++++++----- .../contents | 1 + .../Views/Messages/ChannelMessageList.swift | 2 +- .../Views/Messages/UserMessageList.swift | 4 ++-- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 3c9f51d6..784fb2f3 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1301,18 +1301,30 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) } - let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber] + let detector = try! NSDataDetector(types: types.rawValue) let matches = detector.matches(in: messageText, options: [], range: NSRange(location: 0, length: messageText.utf16.count)) if matches.count > 0 { - var messageWithLink = "" + var messageWithLink = messageText for match in matches { guard let range = Range(match.range, in: messageText) else { continue } - let url = messageText[range] - messageWithLink = messageText.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))](\(url))") + if match.resultType == .address { + let address = messageText[range] + let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) + messageWithLink = messageWithLink.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))") + } else if match.resultType == .phoneNumber { + let phone = messageText[range] + messageWithLink = messageWithLink.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") + } else if match.resultType == .link { + let url = messageText[range] + messageWithLink = messageWithLink.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))](\(url))") + } } - newMessage.messagePayload = messageWithLink + newMessage.messagePayload = messageText + newMessage.messagePayloadMarkdown = messageWithLink } else { newMessage.messagePayload = messageText + newMessage.messagePayloadMarkdown = messageText } newMessage.fromUser?.objectWillChange.send() newMessage.toUser?.objectWillChange.send() diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents index 7bb559e9..11a72c34 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents @@ -96,6 +96,7 @@ + diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 0773f431..dba468b2 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -60,7 +60,7 @@ struct ChannelMessageList: View { .offset(y: -5) } VStack(alignment: currentUser ? .trailing : .leading) { - let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayload ?? "EMPTY MESSAGE") + let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) let skyBlue = Color(red: 0.4627, green: 0.8392, blue: 1.0) Text(markdownText) .tint(skyBlue) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index f80f61b6..deca1d4a 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -61,8 +61,8 @@ struct UserMessageList: View { .offset(y: -5) } VStack(alignment: currentUser ? .trailing : .leading) { - let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayload ?? "EMPTY MESSAGE") - let skyBlue = Color(red: 0.4627, green: 0.8392, blue: 1.0) + let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) + let skyBlue = Color(red: 0.4627, green: 0.8392, blue: 1.0) Text(markdownText) .tint(skyBlue) .padding(10) From 1f54338ab95b3fd39d56450a082c7c049fb9c7a6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Dec 2022 23:48:35 -0800 Subject: [PATCH 09/17] Generate message markdown for incoming and outgoing messages --- Meshtastic/Helpers/BLEManager.swift | 1 + Meshtastic/Helpers/MeshPackets.swift | 54 +++++++++++++++------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index acd11c67..85eb1717 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -620,6 +620,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { newMessage.replyID = replyID } newMessage.messagePayload = message + newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: message) let dataType = PortNum.textMessageApp let payloadData: Data = message.data(using: String.Encoding.utf8)! diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 784fb2f3..ed76338a 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -9,6 +9,32 @@ import Foundation import CoreData import SwiftUI +func generateMessageMarkdown (message: String) -> String { + + let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber] + let detector = try! NSDataDetector(types: types.rawValue) + let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count)) + var messageWithMarkdown = message + if matches.count > 0 { + + for match in matches { + guard let range = Range(match.range, in: message) else { continue } + if match.resultType == .address { + let address = message[range] + let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))") + } else if match.resultType == .phoneNumber { + let phone = messageWithMarkdown[range] + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") + } else if match.resultType == .link { + let url = messageWithMarkdown[range] + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))\(String(match.url?.path ?? ""))](\(url))") + } + } + } + return messageWithMarkdown +} + func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { // We don't care about any of the Power settings, config is available for everyting else @@ -1301,31 +1327,9 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) } - let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber] - let detector = try! NSDataDetector(types: types.rawValue) - let matches = detector.matches(in: messageText, options: [], range: NSRange(location: 0, length: messageText.utf16.count)) - if matches.count > 0 { - var messageWithLink = messageText - for match in matches { - guard let range = Range(match.range, in: messageText) else { continue } - if match.resultType == .address { - let address = messageText[range] - let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) - messageWithLink = messageWithLink.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))") - } else if match.resultType == .phoneNumber { - let phone = messageText[range] - messageWithLink = messageWithLink.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") - } else if match.resultType == .link { - let url = messageText[range] - messageWithLink = messageWithLink.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))](\(url))") - } - } - newMessage.messagePayload = messageText - newMessage.messagePayloadMarkdown = messageWithLink - } else { - newMessage.messagePayload = messageText - newMessage.messagePayloadMarkdown = messageText - } + newMessage.messagePayload = messageText + newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText) + newMessage.fromUser?.objectWillChange.send() newMessage.toUser?.objectWillChange.send() From 33e6193543b60fbb28d503c29ba8be6dc389fae9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 Dec 2022 17:03:53 -0800 Subject: [PATCH 10/17] Handle urls sent in messages without a scheme (ie apple.com) --- Meshtastic/Helpers/MeshPackets.swift | 7 +++++-- Meshtastic/Views/Messages/ChannelMessageList.swift | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index ed76338a..6e82970d 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -19,6 +19,7 @@ func generateMessageMarkdown (message: String) -> String { for match in matches { guard let range = Range(match.range, in: message) else { continue } + print(match.url ?? "No URL") if match.resultType == .address { let address = message[range] let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) @@ -27,8 +28,10 @@ func generateMessageMarkdown (message: String) -> String { let phone = messageWithMarkdown[range] messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") } else if match.resultType == .link { - let url = messageWithMarkdown[range] - messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))\(String(match.url?.path ?? ""))](\(url))") + let url = match.url?.absoluteString ?? "" + if url.count > 0 { + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))\(String(match.url?.path ?? ""))](\(url))") + } } } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index dba468b2..196c3fb1 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -61,9 +61,9 @@ struct ChannelMessageList: View { } VStack(alignment: currentUser ? .trailing : .leading) { let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) - let skyBlue = Color(red: 0.4627, green: 0.8392, blue: 1.0) + let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */ Text(markdownText) - .tint(skyBlue) + .tint(linkBlue) .padding(10) .foregroundColor(.white) .background(currentUser ? .accentColor : Color(.gray)) From be625b693c0b0c7879afbfac11773d5e61d33aaa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 Dec 2022 18:02:25 -0800 Subject: [PATCH 11/17] Remove extra print statement --- Meshtastic/Helpers/MeshPackets.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 6e82970d..e6e43598 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -19,7 +19,6 @@ func generateMessageMarkdown (message: String) -> String { for match in matches { guard let range = Range(match.range, in: message) else { continue } - print(match.url ?? "No URL") if match.resultType == .address { let address = message[range] let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) @@ -726,7 +725,6 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO myInfoEntity.hasGps = myInfo.hasGps_p myInfoEntity.hasWifi = myInfo.hasWifi_p myInfoEntity.bitrate = myInfo.bitrate - // Swift does strings weird, this does work to get the version without the github hash let lastDotIndex = myInfo.firmwareVersion.lastIndex(of: ".") var version = myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: myInfo.firmwareVersion))] @@ -737,15 +735,12 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO myInfoEntity.maxChannels = Int32(bitPattern: myInfo.maxChannels) do { - try context.save() MeshLogger.log("💾 Saved a new myInfo for node number: \(String(myInfo.myNodeNum))") return myInfoEntity } catch { - context.rollback() - let nsError = error as NSError print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)") } @@ -756,7 +751,6 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum) fetchedMyInfo[0].hasGps = myInfo.hasGps_p fetchedMyInfo[0].bitrate = myInfo.bitrate - let lastDotIndex = myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1) var version = myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset:6, in: myInfo.firmwareVersion))] version = version.dropLast() @@ -766,20 +760,16 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO fetchedMyInfo[0].maxChannels = Int32(bitPattern: myInfo.maxChannels) do { - try context.save() MeshLogger.log("💾 Updated myInfo for node number: \(String(myInfo.myNodeNum))") return fetchedMyInfo[0] } catch { - context.rollback() - let nsError = error as NSError print("💥 Error Updating Core Data MyInfoEntity: \(nsError)") } } - } catch { print("💥 Fetch MyInfo Error") @@ -1154,7 +1144,6 @@ func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) { } func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { - print("Routing packet", packet) if let routingMessage = try? Routing(serializedData: packet.decoded.payload) { From b7ef87031d04a515c5037d3a52156cae87b851fc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 Dec 2022 21:35:50 -0800 Subject: [PATCH 12/17] Fix url replacement for links with no scheme --- Meshtastic/Helpers/MeshPackets.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index e6e43598..f6336b9a 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -27,10 +27,9 @@ func generateMessageMarkdown (message: String) -> String { let phone = messageWithMarkdown[range] messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") } else if match.resultType == .link { - let url = match.url?.absoluteString ?? "" - if url.count > 0 { - messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))\(String(match.url?.path ?? ""))](\(url))") - } + let url = messageWithMarkdown[range] + let absoluteUrl = match.url?.absoluteString ?? "" + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))\(String(match.url?.path ?? ""))](\(absoluteUrl))") } } } From ebe77459ee94441772b71c8dc7ef69f70bd34f00 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 Dec 2022 22:06:28 -0800 Subject: [PATCH 13/17] Make link colors consistent --- .../Views/Messages/ChannelMessageList.swift | 16 ++++++------- .../Views/Messages/UserMessageList.swift | 23 ++++++++++--------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 196c3fb1..ba3c638a 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -9,7 +9,7 @@ import SwiftUI import CoreData struct ChannelMessageList: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings @@ -29,7 +29,7 @@ struct ChannelMessageList: View { @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 @State private var sendPositionWithMessage: Bool = false - + var body: some View { NavigationStack { ScrollViewReader { scrollView in @@ -244,7 +244,7 @@ struct ChannelMessageList: View { typingMessage = "📍 " + userLongName + " has shared their position with you." } - + } label: { Text("share.position") Image(systemName: "mappin.and.ellipse") @@ -260,7 +260,7 @@ struct ChannelMessageList: View { } #endif HStack(alignment: .top) { - + ZStack { let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) TextField("message", text: $typingMessage, axis: .vertical) @@ -296,13 +296,13 @@ struct ChannelMessageList: View { typingMessage = "📍 " + userLongName + " has shared their position with you." } - + } label: { Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .imageScale(.large).foregroundColor(.accentColor) } - + ProgressView("\(NSLocalizedString("bytes", comment: "")): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) .frame(width: 130) .padding(5) @@ -316,7 +316,7 @@ struct ChannelMessageList: View { .frame(minHeight: 50) .keyboardShortcut(.defaultAction) .onSubmit { - #if targetEnvironment(macCatalyst) + #if targetEnvironment(macCatalyst) if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) { typingMessage = "" focusedField = nil @@ -327,7 +327,7 @@ struct ChannelMessageList: View { } } } - #endif + #endif } Text(typingMessage).opacity(0).padding(.all, 0) } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index deca1d4a..83fd25f3 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -9,7 +9,7 @@ import SwiftUI import CoreData struct UserMessageList: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings @@ -28,8 +28,8 @@ struct UserMessageList: View { @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 @State private var sendPositionWithMessage: Bool = false - - var body: some View { + + var body: some View { NavigationStack { ScrollViewReader { scrollView in ScrollView { @@ -37,7 +37,7 @@ struct UserMessageList: View { ForEach( user.messageList ) { (message: MessageEntity) in if user.num != userSettings.preferredNodeNum { let currentUser: Bool = (userSettings.preferredNodeNum == message.fromUser?.num ? true : false) - + if message.replyID > 0 { let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) HStack { @@ -61,10 +61,11 @@ struct UserMessageList: View { .offset(y: -5) } VStack(alignment: currentUser ? .trailing : .leading) { - let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) - let skyBlue = Color(red: 0.4627, green: 0.8392, blue: 1.0) + let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) + + let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */ Text(markdownText) - .tint(skyBlue) + .tint(linkBlue) .padding(10) .foregroundColor(.white) .background(currentUser ? .accentColor : Color(.gray)) @@ -259,7 +260,7 @@ struct UserMessageList: View { .padding(.trailing) } #endif - + HStack(alignment: .top) { ZStack { let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) @@ -313,7 +314,7 @@ struct UserMessageList: View { .frame(minHeight: 50) .keyboardShortcut(.defaultAction) .onSubmit { - #if targetEnvironment(macCatalyst) + #if targetEnvironment(macCatalyst) if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) { typingMessage = "" focusedField = nil @@ -324,7 +325,7 @@ struct UserMessageList: View { } } } - #endif + #endif } Text(typingMessage).opacity(0).padding(.all, 0) } @@ -364,5 +365,5 @@ struct UserMessageList: View { } } } - } + } } From 31005c1a4fd42c70440cc95443a76d901f0bc54e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 27 Dec 2022 17:44:27 -0800 Subject: [PATCH 14/17] Trace route context menu item Lazy grid for all 3 node logs (position, device metrics, environment metrics) --- Meshtastic/Helpers/BLEManager.swift | 43 ++++++++++++++++++- Meshtastic/Views/Messages/Contacts.swift | 20 ++++++++- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 33 ++++++++------ .../Views/Nodes/EnvironmentMetricsLog.swift | 21 ++++----- Meshtastic/Views/Nodes/PositionLog.swift | 10 ++++- 5 files changed, 97 insertions(+), 30 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 85eb1717..618ed01a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -313,6 +313,35 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) } + func sendTraceRouteRequest(destNum: Int64, wantResponse: Bool) -> Bool { + + var success = false + let fromNodeNum = connectedPeripheral.num + + let routePacket = RouteDiscovery() + + var meshPacket = MeshPacket() + meshPacket.to = UInt32(destNum) + meshPacket.from = UInt32(fromNodeNum)//0 // Send 0 as from from phone to device to avoid warning about client trying to set node num + var dataMessage = DataMessage() + dataMessage.payload = try! routePacket.serializedData() + dataMessage.portnum = PortNum.tracerouteApp + dataMessage.wantResponse = wantResponse + meshPacket.decoded = dataMessage + + var toRadio: ToRadio! + toRadio = ToRadio() + toRadio.packet = meshPacket + let binaryData: Data = try! toRadio.serializedData() + + if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { + connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) + success = true + MeshLogger.log("🪧 Sent a Trace Route Packet to node: \(destNum).") + } + return success + } + func sendWantConfig() { guard (connectedPeripheral!.peripheral.state == CBPeripheralState.connected) else { return } @@ -504,7 +533,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .audioApp: MeshLogger.log("ℹ️ MESH PACKET received for Audio App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .tracerouteApp: - MeshLogger.log("ℹ️ MESH PACKET received for Trace Route App UNHANDLED \(try! decodedInfo.packet.jsonString())") + if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { + + if routingMessage.route.count == 0 { + MeshLogger.log("🪧 Trace Route request sent to \(decodedInfo.packet.from) was recieived directly.") + } else { + + var routeString = "🪧 Trace Route request returned: \(decodedInfo.packet.to) --> " + for node in routingMessage.route { + routeString += "\(node) --> " + } + routeString += "\(decodedInfo.packet.from)" + } + } case .UNRECOGNIZED(_): MeshLogger.log("ℹ️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .max: diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 308e4188..d009d5f6 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -23,6 +23,7 @@ struct Contacts: View { @State private var selection: UserEntity? = nil // Nothing selected by default. @State private var isPresentingDeleteChannelMessagesConfirm: Bool = false @State private var isPresentingDeleteUserMessagesConfirm: Bool = false + @State private var isPresentingTraceRouteSentAlert = false var body: some View { @@ -194,6 +195,14 @@ struct Contacts: View { } label: { Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") } + Button { + let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true) + if success { + isPresentingTraceRouteSentAlert = true + } + } label: { + Label("Trace Route", systemImage: "signpost.right.and.left") + } if user.messageList.count > 0 { Button(role: .destructive) { isPresentingDeleteUserMessagesConfirm = true @@ -202,12 +211,21 @@ struct Contacts: View { } } } + .alert( + "Trace Route Sent", + isPresented: $isPresentingTraceRouteSentAlert + ) + { + Button("OK", role: .cancel) { } + } + message: { + Text("This could take a while, response will appear in the mesh log.") + } .confirmationDialog( "This conversation will be deleted.", isPresented: $isPresentingDeleteUserMessagesConfirm, titleVisibility: .visible ) { - Button(role: .destructive) { deleteUserMessages(user: user, context: context) context.refresh(node!.user!, mergeChanges: true) diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 198f1f72..903d94b3 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -75,43 +75,50 @@ struct DeviceMetricsLog: View { } else { ScrollView { - Grid(alignment: .topLeading, horizontalSpacing: 2) { + + let columns = [ + GridItem(), + GridItem(), + GridItem(), + GridItem(), + GridItem(.fixed(120)) + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { GridRow { Text("Batt") - .font(.callout) + .font(.caption) .fontWeight(.bold) Text("Voltage") - .font(.callout) + .font(.caption) .fontWeight(.bold) Text("ChUtil") - .font(.callout) + .font(.caption) .fontWeight(.bold) Text("AirTm") - .font(.callout) + .font(.caption) .fontWeight(.bold) Text("Timestamp") - .font(.callout) + .font(.caption) .fontWeight(.bold) } - Divider() ForEach(node.telemetries!.reversed() as! [TelemetryEntity], id: \.self) { (dm: TelemetryEntity) in if dm.metricsType == 0 { GridRow { if dm.batteryLevel == 0 { Text("USB") - .font(.callout) + .font(.caption) } else { Text("\(String(dm.batteryLevel))%") - .font(.callout) + .font(.caption) } Text(String(dm.voltage)) - .font(.callout) + .font(.caption) Text("\(String(format: "%.2f", dm.channelUtilization))%") - .font(.callout) + .font(.caption) Text("\(String(format: "%.2f", dm.airUtilTx))%") - .font(.callout) + .font(.caption) Text(dm.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time") - .font(.callout) + .font(.caption) } } } diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 9b8c803a..18f72249 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -63,8 +63,14 @@ struct EnvironmentMetricsLog: View { } } else { ScrollView { - - Grid(alignment: .topLeading, horizontalSpacing: 2) { + let columns = [ + GridItem(), + GridItem(), + GridItem(), + GridItem(), + GridItem(.fixed(115)) + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { GridRow { @@ -80,17 +86,10 @@ struct EnvironmentMetricsLog: View { Text("Gas") .font(.caption) .fontWeight(.bold) - Text("DC") - .font(.caption) - .fontWeight(.bold) - Text("Volt") - .font(.caption) - .fontWeight(.bold) Text("Timestamp") .font(.caption) .fontWeight(.bold) } - Divider() ForEach(node.telemetries!.reversed() as! [TelemetryEntity], id: \.self) { (em: TelemetryEntity) in if em.metricsType == 1 { @@ -105,10 +104,6 @@ struct EnvironmentMetricsLog: View { .font(.caption) Text("\(String(format: "%.2f", em.gasResistance))") .font(.caption) - Text("\(String(format: "%.2f", em.current))") - .font(.caption) - Text("\(String(format: "%.2f", em.voltage))") - .font(.caption) Text(em.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time") .font(.caption) } diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 46333fe1..4554c7b9 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -58,7 +58,14 @@ struct PositionLog: View { ScrollView { // Use a grid on iOS as a table only shows a single column - Grid(alignment: .topLeading, horizontalSpacing: 2) { + let columns = [ + GridItem(.fixed(95)), + GridItem(.fixed(95)), + GridItem(), + GridItem(), + GridItem(.fixed(115)) + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { GridRow { @@ -78,7 +85,6 @@ struct PositionLog: View { .font(.caption2) .fontWeight(.bold) } - Divider() ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in GridRow { Text(String(format: "%.6f", mappin.latitude ?? 0)) From 9deda3d6a575dba0b5095c52701f1cbe99ef5309 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 28 Dec 2022 06:52:12 -0800 Subject: [PATCH 15/17] Actually log the trace route if there are nodes in the array --- Meshtastic/Helpers/BLEManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 618ed01a..57bd1f01 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -544,6 +544,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { routeString += "\(node) --> " } routeString += "\(decodedInfo.packet.from)" + MeshLogger.log(routeString) } } case .UNRECOGNIZED(_): From 4c83a718d4e9761ec9ab898e14e9920e9beb3db5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 28 Dec 2022 09:35:59 -0800 Subject: [PATCH 16/17] Comment out channel editor --- Meshtastic/Views/Settings/Channels.swift | 570 +++++++++++------------ Meshtastic/Views/Settings/Settings.swift | 20 +- 2 files changed, 295 insertions(+), 295 deletions(-) diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 427b1dd3..486c3693 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -1,288 +1,288 @@ +//// +//// ShareChannel.swift +//// MeshtasticApple +//// +//// Copyright(c) Garth Vander Houwen 4/8/22. +//// +//import SwiftUI +//import CoreData // -// ShareChannel.swift -// MeshtasticApple +//func generateChannelKey(size: Int) -> String { +// var keyData = Data(count: size) +// _ = keyData.withUnsafeMutableBytes { +// SecRandomCopyBytes(kSecRandomDefault, size, $0.baseAddress!) +// } +// return keyData.base64EncodedString() +//} // -// Copyright(c) Garth Vander Houwen 4/8/22. +//struct Channels: View { +// +// @Environment(\.managedObjectContext) var context +// @EnvironmentObject var bleManager: BLEManager +// @Environment(\.dismiss) private var goBack +// @Environment(\.sizeCategory) var sizeCategory // -import SwiftUI -import CoreData - -func generateChannelKey(size: Int) -> String { - var keyData = Data(count: size) - _ = keyData.withUnsafeMutableBytes { - SecRandomCopyBytes(kSecRandomDefault, size, $0.baseAddress!) - } - return keyData.base64EncodedString() -} - -struct Channels: View { - - @Environment(\.managedObjectContext) var context - @EnvironmentObject var bleManager: BLEManager - @Environment(\.dismiss) private var goBack - @Environment(\.sizeCategory) var sizeCategory - - - var node: NodeInfoEntity? - - @State var hasChanges = false - @State private var isPresentingEditView = false - @State private var isPresentingSaveConfirm: Bool = false - @State private var channelIndex: Int32 = 0 - @State private var channelName = "" - @State private var channelKeySize = 32 - @State private var channelKey = "AQ==" - @State private var channelRole = 0 - @State private var uplink = false - @State private var downlink = false - - var body: some View { - - NavigationStack { - List { - if node != nil && node?.myInfo != nil { - ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in - Button(action: { - channelIndex = channel.index - channelRole = Int(channel.role) - channelKey = channel.psk?.base64EncodedString() ?? "" - if channelKey.count == 0 { - channelKeySize = 0 - } else if channelKey == "AQ==" { - channelKeySize = -1 - } else if channelKey.count == 24 { - channelKeySize = 16 - } else if channelKey.count == 32 { - channelKeySize = 24 - } else if channelKey.count == 44 { - channelKeySize = 32 - } - channelName = channel.name ?? "" - uplink = channel.uplinkEnabled - downlink = channel.downlinkEnabled - isPresentingEditView = true - hasChanges = false - }) { - VStack(alignment: .leading) { - HStack { - CircleText(text: String(channel.index), color: .accentColor, circleSize: 45, fontSize: 36, brightness: 0.1) - .padding(.trailing, 5) - VStack { - HStack { - if channel.name?.isEmpty ?? false { - if channel.role == 1 { - Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) - } else { - Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline) - } - } else { - Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline) - } - } - } - } - } - } - } - } - } - if node?.myInfo?.channels?.array.count ?? 0 < 8 { - - Button { - let key = generateChannelKey(size: 32) - channelName = "" - channelIndex = Int32(node!.myInfo!.channels!.array.count) - channelRole = 2 - channelKey = key - uplink = false - downlink = false - hasChanges = false - isPresentingEditView = true - - } label: { - Label("Add Channel", systemImage: "plus.square") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .sheet(isPresented: $isPresentingEditView) { - - #if targetEnvironment(macCatalyst) - Text("channel") - .font(.largeTitle) - .padding() - #endif - Form { - HStack { - Text("name") - Spacer() - TextField( - "Channel Name", - text: $channelName - ) - .disableAutocorrection(true) - .keyboardType(.alphabet) - .foregroundColor(Color.gray) - .disabled(channelRole == 1 && channelName.count > 0) - .onChange(of: channelName, perform: { value in - channelName = channelName.replacing(" ", with: "") - let totalBytes = channelName.utf8.count - // Only mess with the value if it is too big - if totalBytes > 11 { - let firstNBytes = Data(channelName.utf8.prefix(11)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the channelName back to the last place where it was the right size - channelName = maxBytesString - } - } - hasChanges = true - }) - } - HStack { - Picker("Key Size", selection: $channelKeySize) { - Text("Empty").tag(0) - Text("Default").tag(-1) - Text("1 bit").tag(1) - Text("128 bit").tag(16) - Text("192 bit").tag(24) - Text("256 bit").tag(32) - } - .pickerStyle(DefaultPickerStyle()) - Spacer() - Button { - if channelKeySize == -1 { - channelKey = "AQ==" - } else { - let key = generateChannelKey(size: channelKeySize) - channelKey = key - } - } label: { - Image(systemName: "lock.rotation") - .font(.title) - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.small) - } - HStack (alignment: .top) { - Text("Key") - Spacer() - TextField ( - "", - text: $channelKey, - axis: .vertical - ) - .foregroundColor(Color.gray) - .disabled(true) - - } - .textSelection(.enabled) - Picker("Channel Role", selection: $channelRole) { - if channelRole == 1 { - Text("Primary").tag(1) - } else{ - Text("Disabled").tag(0) - Text("Secondary").tag(2) - } - } - .pickerStyle(DefaultPickerStyle()) - .disabled(channelRole == 1) - Toggle("Uplink Enabled", isOn: $uplink) - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle("Downlink Enabled", isOn: $downlink) - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - } - .onSubmit { - //validate(name: channelName) - } - .onChange(of: channelName) { newName in - hasChanges = true - } - .onChange(of: channelKeySize) { newKeySize in - if channelKeySize == -1 { - channelKey = "AQ==" - } else { - let key = generateChannelKey(size: channelKeySize) - channelKey = key - } - hasChanges = true - } - .onChange(of: channelKey) { newKey in - hasChanges = true - } - .onChange(of: channelRole) { newRole in - hasChanges = true - } - .onChange(of: uplink) { newUplink in - hasChanges = true - } - .onChange(of: downlink) { newDownlink in - hasChanges = true - } - HStack { - Button { - isPresentingSaveConfirm = true - } label: { - Label("save", systemImage: "square.and.arrow.down") - } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges) - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - .confirmationDialog( - "are.you.sure", - isPresented: $isPresentingSaveConfirm, - titleVisibility: .visible - ) { - Button("Save Channel \(channelIndex) to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { - - var channel = Channel() - channel.index = channelIndex - channel.settings.id = UInt32(channelIndex) - channel.settings.name = channelName - channel.settings.psk = Data(base64Encoded: channelKey) ?? Data() - channel.role = ChannelRoles(rawValue: channelRole)?.protoEnumValue() ?? .secondary - channel.settings.uplinkEnabled = uplink - channel.settings.downlinkEnabled = downlink - - let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) - - if adminMessageId > 0 { - // Should show a saved successfully alert once I know that to be true - // for now just disable the button after a successful save - channelName = "" - hasChanges = false - isPresentingEditView = false - bleManager.disconnectPeripheral() - } - } - } - #if targetEnvironment(macCatalyst) - Button { - isPresentingEditView = false - } label: { - Label("Close", systemImage: "xmark") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - #endif - } - .presentationDetents([.medium, .large]) - } - } - } - .navigationTitle("channels") - .navigationSplitViewStyle(.automatic) - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") - }) - .onAppear { - bleManager.context = context - } - } -} +// +// var node: NodeInfoEntity? +// +// @State var hasChanges = false +// @State private var isPresentingEditView = false +// @State private var isPresentingSaveConfirm: Bool = false +// @State private var channelIndex: Int32 = 0 +// @State private var channelName = "" +// @State private var channelKeySize = 32 +// @State private var channelKey = "AQ==" +// @State private var channelRole = 0 +// @State private var uplink = false +// @State private var downlink = false +// +// var body: some View { +// +// NavigationStack { +// List { +// if node != nil && node?.myInfo != nil { +// ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in +// Button(action: { +// channelIndex = channel.index +// channelRole = Int(channel.role) +// channelKey = channel.psk?.base64EncodedString() ?? "" +// if channelKey.count == 0 { +// channelKeySize = 0 +// } else if channelKey == "AQ==" { +// channelKeySize = -1 +// } else if channelKey.count == 24 { +// channelKeySize = 16 +// } else if channelKey.count == 32 { +// channelKeySize = 24 +// } else if channelKey.count == 44 { +// channelKeySize = 32 +// } +// channelName = channel.name ?? "" +// uplink = channel.uplinkEnabled +// downlink = channel.downlinkEnabled +// isPresentingEditView = true +// hasChanges = false +// }) { +// VStack(alignment: .leading) { +// HStack { +// CircleText(text: String(channel.index), color: .accentColor, circleSize: 45, fontSize: 36, brightness: 0.1) +// .padding(.trailing, 5) +// VStack { +// HStack { +// if channel.name?.isEmpty ?? false { +// if channel.role == 1 { +// Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) +// } else { +// Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline) +// } +// } else { +// Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline) +// } +// } +// } +// } +// } +// } +// } +// } +// } +// if node?.myInfo?.channels?.array.count ?? 0 < 8 { +// +// Button { +// let key = generateChannelKey(size: 32) +// channelName = "" +// channelIndex = Int32(node!.myInfo!.channels!.array.count) +// channelRole = 2 +// channelKey = key +// uplink = false +// downlink = false +// hasChanges = false +// isPresentingEditView = true +// +// } label: { +// Label("Add Channel", systemImage: "plus.square") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding() +// .sheet(isPresented: $isPresentingEditView) { +// +// #if targetEnvironment(macCatalyst) +// Text("channel") +// .font(.largeTitle) +// .padding() +// #endif +// Form { +// HStack { +// Text("name") +// Spacer() +// TextField( +// "Channel Name", +// text: $channelName +// ) +// .disableAutocorrection(true) +// .keyboardType(.alphabet) +// .foregroundColor(Color.gray) +// .disabled(channelRole == 1 && channelName.count > 0) +// .onChange(of: channelName, perform: { value in +// channelName = channelName.replacing(" ", with: "") +// let totalBytes = channelName.utf8.count +// // Only mess with the value if it is too big +// if totalBytes > 11 { +// let firstNBytes = Data(channelName.utf8.prefix(11)) +// if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { +// // Set the channelName back to the last place where it was the right size +// channelName = maxBytesString +// } +// } +// hasChanges = true +// }) +// } +// HStack { +// Picker("Key Size", selection: $channelKeySize) { +// Text("Empty").tag(0) +// Text("Default").tag(-1) +// Text("1 bit").tag(1) +// Text("128 bit").tag(16) +// Text("192 bit").tag(24) +// Text("256 bit").tag(32) +// } +// .pickerStyle(DefaultPickerStyle()) +// Spacer() +// Button { +// if channelKeySize == -1 { +// channelKey = "AQ==" +// } else { +// let key = generateChannelKey(size: channelKeySize) +// channelKey = key +// } +// } label: { +// Image(systemName: "lock.rotation") +// .font(.title) +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.small) +// } +// HStack (alignment: .top) { +// Text("Key") +// Spacer() +// TextField ( +// "", +// text: $channelKey, +// axis: .vertical +// ) +// .foregroundColor(Color.gray) +// .disabled(true) +// +// } +// .textSelection(.enabled) +// Picker("Channel Role", selection: $channelRole) { +// if channelRole == 1 { +// Text("Primary").tag(1) +// } else{ +// Text("Disabled").tag(0) +// Text("Secondary").tag(2) +// } +// } +// .pickerStyle(DefaultPickerStyle()) +// .disabled(channelRole == 1) +// Toggle("Uplink Enabled", isOn: $uplink) +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// Toggle("Downlink Enabled", isOn: $downlink) +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// } +// .onSubmit { +// //validate(name: channelName) +// } +// .onChange(of: channelName) { newName in +// hasChanges = true +// } +// .onChange(of: channelKeySize) { newKeySize in +// if channelKeySize == -1 { +// channelKey = "AQ==" +// } else { +// let key = generateChannelKey(size: channelKeySize) +// channelKey = key +// } +// hasChanges = true +// } +// .onChange(of: channelKey) { newKey in +// hasChanges = true +// } +// .onChange(of: channelRole) { newRole in +// hasChanges = true +// } +// .onChange(of: uplink) { newUplink in +// hasChanges = true +// } +// .onChange(of: downlink) { newDownlink in +// hasChanges = true +// } +// HStack { +// Button { +// isPresentingSaveConfirm = true +// } label: { +// Label("save", systemImage: "square.and.arrow.down") +// } +// .disabled(bleManager.connectedPeripheral == nil || !hasChanges) +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// .confirmationDialog( +// "are.you.sure", +// isPresented: $isPresentingSaveConfirm, +// titleVisibility: .visible +// ) { +// Button("Save Channel \(channelIndex) to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { +// +// var channel = Channel() +// channel.index = channelIndex +// channel.settings.id = UInt32(channelIndex) +// channel.settings.name = channelName +// channel.settings.psk = Data(base64Encoded: channelKey) ?? Data() +// channel.role = ChannelRoles(rawValue: channelRole)?.protoEnumValue() ?? .secondary +// channel.settings.uplinkEnabled = uplink +// channel.settings.downlinkEnabled = downlink +// +// let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) +// +// if adminMessageId > 0 { +// // Should show a saved successfully alert once I know that to be true +// // for now just disable the button after a successful save +// channelName = "" +// hasChanges = false +// isPresentingEditView = false +// bleManager.disconnectPeripheral() +// } +// } +// } +// #if targetEnvironment(macCatalyst) +// Button { +// isPresentingEditView = false +// } label: { +// Label("Close", systemImage: "xmark") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// #endif +// } +// .presentationDetents([.medium, .large]) +// } +// } +// } +// .navigationTitle("channels") +// .navigationSplitViewStyle(.automatic) +// .navigationBarItems(trailing: +// ZStack { +// ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") +// }) +// .onAppear { +// bleManager.context = context +// } +// } +//} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 1ec48400..9bb805f7 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -59,16 +59,16 @@ struct Settings: View { Text("lora") } - NavigationLink() { - - Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) - } label: { - - Image(systemName: "fibrechannel") - .symbolRenderingMode(.hierarchical) - - Text("channels") - } +// NavigationLink() { +// +// Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) +// } label: { +// +// Image(systemName: "fibrechannel") +// .symbolRenderingMode(.hierarchical) +// +// Text("channels") +// } NavigationLink() { From df5b2e341616fd7853eb8dd146961d7ac8afced0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 28 Dec 2022 09:41:54 -0800 Subject: [PATCH 17/17] remove extra print --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 57bd1f01..24cfae62 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1038,7 +1038,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if let decodedData = Data(base64Encoded: decodedString) { do { let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData) - print(channelSet) var i:Int32 = 0 for cs in channelSet.settings { var chan = Channel() @@ -1095,6 +1094,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { MeshLogger.log("✈️ Sent a LoRaConfig for: \(String(self.connectedPeripheral.num))") } return true + } catch { return false }