Skip to content

Commit 7a2b108

Browse files
Copilotgarthvh
andauthored
Add Neighbor Info module config screen (#1860)
* Initial plan * Add Neighbor Info module configuration screen - Add NeighborInfoConfigEntity SwiftData model - Add neighborInfoConfig relationship to NodeInfoEntity - Add upsertNeighborInfoModuleConfigPacket persistence - Handle .neighborInfo case in MeshPackets moduleConfig dispatch - Add saveNeighborInfoModuleConfig and requestNeighborInfoModuleConfig - Add neighborInfo navigation state and wire up Settings navigation - Create NeighborInfoConfig SwiftUI view with all proto fields Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/8462931f-6005-44a9-a8b1-1793a4aca2a5 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Enforce minimum update interval (14400s) for neighbor info config Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/8462931f-6005-44a9-a8b1-1793a4aca2a5 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Help updates * Linting fixes * Cleanup neighbor config --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com>
1 parent 93f3265 commit 7a2b108

24 files changed

Lines changed: 486 additions & 3 deletions

File tree

Localizable.xcstrings

Lines changed: 170 additions & 0 deletions
Large diffs are not rendered by default.

Meshtastic.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
AA000001303050030000B001 /* Color+Brand.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002303050030000B001 /* Color+Brand.swift */; };
155155
AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; };
156156
AA0001032F07A4B000600001 /* TAKModuleConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001042F07A4B000600001 /* TAKModuleConfig.swift */; };
157+
AA0002022F07A4B000600001 /* NeighborInfoConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002012F07A4B000600001 /* NeighborInfoConfig.swift */; };
157158
AA000201CPLAY000000000002 /* CarPlaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000201CPLAY000000000001 /* CarPlaySceneDelegate.swift */; };
158159
AA000201CPLAY000000000004 /* CarPlayIntentDonation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000201CPLAY000000000003 /* CarPlayIntentDonation.swift */; };
159160
AA000301CPTST000000000002 /* CarPlayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000301CPTST000000000001 /* CarPlayTests.swift */; };
@@ -675,6 +676,7 @@
675676
AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = "<group>"; };
676677
AA0001022F07A4B000600001 /* MeshtasticDataModelV 57.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 57.xcdatamodel"; sourceTree = "<group>"; };
677678
AA0001042F07A4B000600001 /* TAKModuleConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TAKModuleConfig.swift; sourceTree = "<group>"; };
679+
AA0002012F07A4B000600001 /* NeighborInfoConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeighborInfoConfig.swift; sourceTree = "<group>"; };
678680
AA000201CPLAY000000000001 /* CarPlaySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlaySceneDelegate.swift; sourceTree = "<group>"; };
679681
AA000201CPLAY000000000003 /* CarPlayIntentDonation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayIntentDonation.swift; sourceTree = "<group>"; };
680682
AA000301CPTST000000000001 /* CarPlayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayTests.swift; sourceTree = "<group>"; };
@@ -1811,6 +1813,7 @@
18111813
DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */,
18121814
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */,
18131815
DD2160AE28C5552500C17253 /* MQTTConfig.swift */,
1816+
AA0002012F07A4B000600001 /* NeighborInfoConfig.swift */,
18141817
DD41582928585C32009B0E59 /* RangeTestConfig.swift */,
18151818
DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */,
18161819
DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */,
@@ -2705,6 +2708,7 @@
27052708
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */,
27062709
108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */,
27072710
DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */,
2711+
AA0002022F07A4B000600001 /* NeighborInfoConfig.swift in Sources */,
27082712
DDDB445229F8ACF900EE2349 /* Date.swift in Sources */,
27092713
2373AE132D0A216C0086C749 /* MetricsChartSeries.swift in Sources */,
27102714
23C2BD2B2EE8993800F6A997 /* DeviceHardwareImage.swift in Sources */,

Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,36 @@ extension AccessoryManager {
10831083
return Int64(meshPacket.id)
10841084
}
10851085

1086+
public func saveNeighborInfoModuleConfig(config: ModuleConfig.NeighborInfoConfig, fromUser: UserEntity, toUser: UserEntity) async throws -> Int64 {
1087+
1088+
var adminPacket = AdminMessage()
1089+
adminPacket.setModuleConfig.neighborInfo = config
1090+
if fromUser != toUser {
1091+
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
1092+
}
1093+
var meshPacket: MeshPacket = MeshPacket()
1094+
meshPacket.to = UInt32(toUser.num)
1095+
meshPacket.from = UInt32(fromUser.num)
1096+
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
1097+
meshPacket.priority = MeshPacket.Priority.reliable
1098+
meshPacket.wantAck = true
1099+
1100+
var dataMessage = DataMessage()
1101+
guard let adminData: Data = try? adminPacket.serializedData() else {
1102+
throw AccessoryError.ioFailed("saveNeighborInfoModuleConfig: Unable to serialize admin packet")
1103+
}
1104+
dataMessage.payload = adminData
1105+
dataMessage.portnum = PortNum.adminApp
1106+
meshPacket.decoded = dataMessage
1107+
1108+
let messageDescription = "🛟 Saved Neighbor Info Module Config for \(toUser.longName ?? "Unknown".localized)"
1109+
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
1110+
1111+
await MeshPackets.shared.upsertNeighborInfoModuleConfigPacket(config: config, nodeNum: toUser.num)
1112+
1113+
return Int64(meshPacket.id)
1114+
}
1115+
10861116
public func savePaxcounterModuleConfig(config: ModuleConfig.PaxcounterConfig, fromUser: UserEntity, toUser: UserEntity) async throws -> Int64 {
10871117

10881118
var adminPacket = AdminMessage()
@@ -1314,6 +1344,30 @@ extension AccessoryManager {
13141344
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
13151345
}
13161346

1347+
public func requestNeighborInfoModuleConfig(fromUser: UserEntity, toUser: UserEntity) async throws {
1348+
var adminPacket = AdminMessage()
1349+
adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.neighborinfoConfig
1350+
var meshPacket: MeshPacket = MeshPacket()
1351+
meshPacket.to = UInt32(toUser.num)
1352+
meshPacket.from = UInt32(fromUser.num)
1353+
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
1354+
meshPacket.priority = MeshPacket.Priority.reliable
1355+
meshPacket.wantAck = true
1356+
1357+
var dataMessage = DataMessage()
1358+
guard let adminData: Data = try? adminPacket.serializedData() else {
1359+
throw AccessoryError.ioFailed("requestNeighborInfoModuleConfig: Unable to serialize admin packet")
1360+
}
1361+
dataMessage.payload = adminData
1362+
dataMessage.portnum = PortNum.adminApp
1363+
dataMessage.wantResponse = true
1364+
1365+
meshPacket.decoded = dataMessage
1366+
1367+
let messageDescription = "🛎️ Requested Neighbor Info Module Config using an admin key for node: \(toUser.longName ?? "Unknown".localized)"
1368+
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
1369+
}
1370+
13171371
public func requestPaxCounterModuleConfig(fromUser: UserEntity, toUser: UserEntity) async throws {
13181372
var adminPacket = AdminMessage()
13191373
adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.paxcounterConfig

Meshtastic/Enums/IntervalEnums.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum IntervalConfiguration: CaseIterable {
1515
case detectionSensorMinimum
1616
case detectionSensorState
1717
case nagTimeout
18+
case neighborInfo
1819
case paxCounter
1920
case rangeTestSender
2021
case smartBroadcastMinimum
@@ -35,6 +36,8 @@ enum IntervalConfiguration: CaseIterable {
3536
return [.unset, .fifteenMinutes, .thirtyMinutes, .oneHour, .twoHours, .threeHours, .fourHours, .fiveHours, .sixHours, .twelveHours, .eighteenHours, .twentyFourHours, .thirtySixHours, .fortyeightHours, .seventyTwoHours]
3637
case .nagTimeout:
3738
return [.unset, .oneSecond, .fiveSeconds, .tenSeconds, .fifteenSeconds, .thirtySeconds, .oneMinute]
39+
case .neighborInfo:
40+
return [.fourHours, .fiveHours, .sixHours, .twelveHours, .eighteenHours, .twentyFourHours, .thirtySixHours, .fortyeightHours, .seventyTwoHours]
3841
case .paxCounter:
3942
return [.fifteenMinutes, .thirtyMinutes, .oneHour, .twoHours, .threeHours, .fourHours, .fiveHours, .sixHours, .twelveHours, .eighteenHours, .twentyFourHours, .thirtySixHours, .fortyeightHours, .seventyTwoHours]
4043
case .rangeTestSender:

Meshtastic/Helpers/MeshPackets.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ actor MeshPackets {
183183
upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum)
184184
case .mqtt:
185185
upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum)
186+
case .neighborInfo:
187+
upsertNeighborInfoModuleConfigPacket(config: config.neighborInfo, nodeNum: nodeNum)
186188
case .paxcounter:
187189
upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum)
188190
case .rangeTest:

Meshtastic/Model/ConfigModels.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ final class NetworkConfigEntity {
201201
init() {}
202202
}
203203

204+
@Model
205+
final class NeighborInfoConfigEntity {
206+
var enabled: Bool = false
207+
var transmitOverLora: Bool = false
208+
var updateInterval: Int32 = 0
209+
var neighborInfoConfigNode: NodeInfoEntity?
210+
211+
init() {}
212+
}
213+
204214
@Model
205215
final class PaxCounterConfigEntity {
206216
var bleThreshold: Int32 = 0

Meshtastic/Model/NodeInfoEntity.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ final class NodeInfoEntity {
6060
@Relationship(deleteRule: .nullify, inverse: \MQTTConfigEntity.mqttConfigNode)
6161
var mqttConfig: MQTTConfigEntity?
6262

63+
@Relationship(deleteRule: .nullify, inverse: \NeighborInfoConfigEntity.neighborInfoConfigNode)
64+
var neighborInfoConfig: NeighborInfoConfigEntity?
65+
6366
@Relationship(deleteRule: .nullify, inverse: \MyInfoEntity.myInfoNode)
6467
var myInfo: MyInfoEntity?
6568

Meshtastic/Model/Schema/MeshtasticSchemaV1.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ enum MeshtasticSchemaV1: VersionedSchema {
4444
ExternalNotificationConfigEntity.self,
4545
LoRaConfigEntity.self,
4646
MQTTConfigEntity.self,
47+
NeighborInfoConfigEntity.self,
4748
NetworkConfigEntity.self,
4849
PaxCounterConfigEntity.self,
4950
PositionConfigEntity.self,

Meshtastic/Persistence/UpdateSwiftData.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,44 @@ extension MeshPackets {
13271327
}
13281328
}
13291329

1330+
func upsertNeighborInfoModuleConfigPacket(config: ModuleConfig.NeighborInfoConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) {
1331+
1332+
let logString = String.localizedStringWithFormat("Neighbor Info config received: %@".localized, String(nodeNum))
1333+
Logger.data.info("🏘️ \(logString, privacy: .public)")
1334+
1335+
let fetchNum = Int64(nodeNum)
1336+
var fetchNodeInfoRequest = FetchDescriptor<NodeInfoEntity>(predicate: #Predicate<NodeInfoEntity> { $0.num == fetchNum })
1337+
fetchNodeInfoRequest.fetchLimit = 1
1338+
do {
1339+
let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest)
1340+
if !fetchedNode.isEmpty {
1341+
if fetchedNode[0].neighborInfoConfig == nil {
1342+
let newConfig = NeighborInfoConfigEntity()
1343+
modelContext.insert(newConfig)
1344+
newConfig.enabled = config.enabled
1345+
newConfig.updateInterval = Int32(config.updateInterval)
1346+
newConfig.transmitOverLora = config.transmitOverLora
1347+
fetchedNode[0].neighborInfoConfig = newConfig
1348+
} else {
1349+
fetchedNode[0].neighborInfoConfig?.enabled = config.enabled
1350+
fetchedNode[0].neighborInfoConfig?.updateInterval = Int32(config.updateInterval)
1351+
fetchedNode[0].neighborInfoConfig?.transmitOverLora = config.transmitOverLora
1352+
}
1353+
if sessionPasskey != nil {
1354+
fetchedNode[0].sessionPasskey = sessionPasskey
1355+
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
1356+
}
1357+
savePendingChanges()
1358+
Logger.data.info("💾 [NeighborInfoConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)")
1359+
} else {
1360+
Logger.data.error("💥 [NeighborInfoConfigEntity] No Nodes found in local database matching node number \(nodeNum.toHex(), privacy: .public) unable to save Neighbor Info Module Config")
1361+
}
1362+
} catch {
1363+
let nsError = error as NSError
1364+
Logger.data.error("💥 [NeighborInfoConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
1365+
}
1366+
}
1367+
13301368
func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) {
13311369

13321370
let logString = String.localizedStringWithFormat("PAX Counter config received: %@".localized, String(nodeNum))

Meshtastic/Resources/docs/developer/deep-links.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ <h2>Settings</h2>
186186
<td>Ambient Lighting</td>
187187
</tr>
188188
<tr>
189+
<td><a href="meshtastic:///settings/audio"><code>meshtastic:///settings/audio</code></a></td>
190+
<td>Audio (Codec2, requires LORA_24 region)</td>
191+
</tr>
192+
<tr>
189193
<td><a href="meshtastic:///settings/cannedMessages"><code>meshtastic:///settings/cannedMessages</code></a></td>
190194
<td>Canned Messages</td>
191195
</tr>
@@ -202,6 +206,10 @@ <h2>Settings</h2>
202206
<td>MQTT</td>
203207
</tr>
204208
<tr>
209+
<td><a href="meshtastic:///settings/neighborInfo"><code>meshtastic:///settings/neighborInfo</code></a></td>
210+
<td>Neighbor Info</td>
211+
</tr>
212+
<tr>
205213
<td><a href="meshtastic:///settings/paxCounter"><code>meshtastic:///settings/paxCounter</code></a></td>
206214
<td>Pax Counter</td>
207215
</tr>

0 commit comments

Comments
 (0)