diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 4c34f671..5ad9e5f7 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ DD0E21012B8A6F1300F2D100 /* DeviceHardware.json in Resources */ = {isa = PBXBuildFile; fileRef = DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */; }; DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */; }; DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */; }; + DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */; }; DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */; }; DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B828CDA93900720036 /* SerialConfigEnums.swift */; }; DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1933752B0835D500771CD5 /* PositionAltitudeChart.swift */; }; @@ -270,6 +271,7 @@ DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = ""; }; DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionPopover.swift; sourceTree = ""; }; DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV15.xcdatamodel; sourceTree = ""; }; + DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaxCounterConfig.swift; sourceTree = ""; }; DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfigEnums.swift; sourceTree = ""; }; DD1925B828CDA93900720036 /* SerialConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfigEnums.swift; sourceTree = ""; }; DD1933752B0835D500771CD5 /* PositionAltitudeChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAltitudeChart.swift; sourceTree = ""; }; @@ -682,6 +684,7 @@ DD6193782863875F00E59241 /* SerialConfig.swift */, DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */, DD415827285859C4009B0E59 /* TelemetryConfig.swift */, + DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */, ); path = Module; sourceTree = ""; @@ -1223,6 +1226,7 @@ DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */, DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */, DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, + DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */, DDDB445229F8ACF900EE2349 /* Date.swift in Sources */, DDC4D568275499A500A4208E /* Persistence.swift in Sources */, DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index d80dbd15..d6d2d36a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1704,6 +1704,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } + public func savePaxcounterModuleConfig(config: ModuleConfig.PaxcounterConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { + + var adminPacket = AdminMessage() + adminPacket.setModuleConfig.paxcounter = config + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.channel = UInt32(adminIndex) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() @@ -2204,6 +2231,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } + public func requestPaxCounterModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + + var adminPacket = AdminMessage() + adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.paxcounterConfig + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 760b5d08..6734a49b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -68,6 +68,8 @@ func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNu upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum, context: context) } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(config.mqtt) { upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.paxcounter(config.paxcounter) { + upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum, context: context) } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(config.rangeTest) { upsertRangeTestModuleConfigPacket(config: config.rangeTest, nodeNum: nodeNum, context: context) } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(config.serial) { @@ -247,7 +249,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje return nil } // Not Found Insert - if fetchedNode.isEmpty { + if fetchedNode.isEmpty && nodeInfo.num > 0 { let newNode = NodeInfoEntity(context: context) newNode.id = Int64(nodeInfo.num) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents index daadbce4..8448125f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -251,6 +251,8 @@ + + @@ -268,6 +270,18 @@ + + + + + + + + + + + + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 269cea3c..9b0786ad 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -949,6 +949,51 @@ func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfi } } +func upsertPaxCounterModuleConfigPacket(config: Meshtastic.ModuleConfig.PaxcounterConfig, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("mesh.log.paxcounter.config %@".localized, String(nodeNum)) + MeshLogger.log("📣 \(logString)") + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { + return + } + // Found a node, save PAX Counter Config + if !fetchedNode.isEmpty { + + if fetchedNode[0].paxCounterConfig == nil { + let newPaxCounterConfig = PaxCounterConfigEntity(context: context) + newPaxCounterConfig.enabled = config.enabled + newPaxCounterConfig.paxcounterUpdateInterval = Int32(config.paxcounterUpdateInterval) + + fetchedNode[0].paxCounterConfig = newPaxCounterConfig + + } else { + fetchedNode[0].paxCounterConfig?.enabled = config.enabled + fetchedNode[0].paxCounterConfig?.paxcounterUpdateInterval = Int32(config.paxcounterUpdateInterval) + } + + do { + try context.save() + print("💾 Updated PAX Counter Module Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data ExternalNotificationConfigEntity: \(nsError)") + } + } else { + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save PAX Counter Module Config") + } + } catch { + let nsError = error as NSError + print("💥 Fetching node for core data PaxCounterConfigEntity failed: \(nsError)") + } +} + func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.ringtone.config %@".localized, String(nodeNum)) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 83008279..4dd1bf7c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -68,6 +68,7 @@ struct NodeListItem: View { Text("Role: \(role?.name ?? "unknown".localized)") .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) + } if node.isStoreForwardRouter { HStack { diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift new file mode 100644 index 00000000..4a098d92 --- /dev/null +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -0,0 +1,114 @@ +// +// PaxCounterConfig.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 2/25/24. +// + +import SwiftUI + +struct PaxCounterConfig: View { + @Environment(\.managedObjectContext) private var context + @EnvironmentObject private var bleManager: BLEManager + @Environment(\.dismiss) private var goBack + + let node: NodeInfoEntity? + + @State private var enabled = false + @State private var paxcounterUpdateInterval = 0 + @State private var hasChanges: Bool = false + + var body: some View { + Form { + ConfigHeader(title: "paxcounter", config: \.powerConfig, node: node, onAppear: setPaxValues) + + Section { + Toggle(isOn: $enabled) { + Label("enabled", systemImage: "figure.walk.departure") + Text("config.module.paxcounter.enabled.description") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + if enabled { + Picker("config.module.paxcounter.updateinterval", selection: $paxcounterUpdateInterval) { + ForEach(UpdateIntervals.allCases) { at in + if at.rawValue >= 5 { + Text(at.description) + } + } + } + .pickerStyle(DefaultPickerStyle()) + .listRowSeparator(.hidden) + Text("config.module.paxcounter.updateinterval.description") + .foregroundColor(.gray) + .font(.callout) + } + } header: { + Text("options") + } + } + .disabled(self.bleManager.connectedPeripheral == nil || node?.powerConfig == nil) + .navigationTitle("config.module.paxcounter.title") + .navigationBarItems(trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" + ) + }) + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + + setPaxValues() + // Need to request a PAX Counter module config from the remote node before allowing changes + if bleManager.connectedPeripheral != nil && node?.paxCounterConfig == nil { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) + if node != nil && connectedNode != nil { + _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } + } + } + .onChange(of: enabled) { + if let val = node?.paxCounterConfig?.enabled { + hasChanges = $0 != val + } + } + .onChange(of: paxcounterUpdateInterval) { + if let val = node?.paxCounterConfig?.paxcounterUpdateInterval { + hasChanges = $0 != val + } + } + + SaveConfigButton(node: node, hasChanges: $hasChanges) { + guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context), + let fromUser = connectedNode.user, + let toUser = node?.user else { + return + } + + var config = ModuleConfig.PaxcounterConfig() + config.enabled = enabled + config.paxcounterUpdateInterval = UInt32(paxcounterUpdateInterval) + + let adminMessageId = bleManager.savePaxcounterModuleConfig( + config: config, + fromUser: fromUser, + toUser: toUser, + adminIndex: connectedNode.myInfo?.adminIndex ?? 0 + ) + 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() + } + } + } + + private func setPaxValues() { + enabled = node?.paxCounterConfig?.enabled ?? enabled + paxcounterUpdateInterval = Int(node?.paxCounterConfig?.paxcounterUpdateInterval ?? 900) + } +} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index c05335cd..3421bd17 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -27,6 +27,7 @@ struct Settings: View { case deviceConfig case displayConfig case networkConfig + case paxCounterConfig case positionConfig case powerConfig case ambientLightingConfig @@ -37,6 +38,7 @@ struct Settings: View { case rangeTestConfig case ringtoneConfig case serialConfig + case storeAndForwardConfig case telemetryConfig case meshLog case adminMessageLog @@ -283,6 +285,17 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("range.test") } + .tag(SettingsSidebar.rangeTestConfig) + if node?.metadata?.hasWifi ?? false { + NavigationLink { + PaxCounterConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "figure.walk.departure") + .symbolRenderingMode(.hierarchical) + Text("config.module.paxcounter.settings") + } + .tag(SettingsSidebar.paxCounterConfig) + } NavigationLink { RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { @@ -306,7 +319,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("storeforward") } - .tag(SettingsSidebar.serialConfig) + .tag(SettingsSidebar.storeAndForwardConfig) NavigationLink { TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index fed9f0e3..0c8a0fd5 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -51,6 +51,11 @@ "clear.app.data"="Clear App Data"; "clear.log"="Clear"; "close"="Close"; +"config.module.paxcounter.settings"="PAX Counter"; +"config.module.paxcounter.title"="PAX Counter Config"; +"config.module.paxcounter.enabled.description"="When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth."; +"config.module.paxcounter.updateinterval"="Update Interval"; +"config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected."; "config.save.confirm"="After config values save the node will reboot."; "communicating"="Communicating with device. ."; "connected.radio"="Connected Radio";