diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index f3915cf1..60e70e47 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -176,6 +176,7 @@ DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelEntityExtension.swift; sourceTree = ""; }; DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV2.xcdatamodel; sourceTree = ""; }; DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthernetModes.swift; sourceTree = ""; }; + DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV7.xcdatamodel; sourceTree = ""; }; DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = ""; }; DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; @@ -1038,7 +1039,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.11; + MARKETING_VERSION = 2.0.12; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1071,7 +1072,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.11; + MARKETING_VERSION = 2.0.12; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1247,6 +1248,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */, DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */, DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */, DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */, @@ -1254,7 +1256,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */; + currentVersion = DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 1ad90961..f9335922 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -596,8 +596,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if positionTimer != nil { RunLoop.current.add(positionTimer!, forMode: .common) } - } - + } return } @@ -736,13 +735,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var success = false let fromNodeNum = UInt32(connectedPeripheral.num) - var waypointPacket = waypoint var meshPacket = MeshPacket() meshPacket.to = emptyNodeNum meshPacket.from = fromNodeNum meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! waypointPacket.serializedData() + dataMessage.payload = try! waypoint.serializedData() dataMessage.portnum = PortNum.waypointApp meshPacket.decoded = dataMessage var toRadio: ToRadio! @@ -758,7 +756,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { let wayPointEntity = getWaypoint(id: Int64(waypoint.id), context: context!) wayPointEntity.id = Int64(waypoint.id) - wayPointEntity.name = waypoint.name.count >= 1 ? waypointPacket.name : "Dropped Pin" + wayPointEntity.name = waypoint.name.count >= 1 ? waypoint.name : "Dropped Pin" wayPointEntity.longDescription = waypoint.description_p wayPointEntity.icon = Int64(waypoint.icon) wayPointEntity.latitudeI = waypoint.latitudeI @@ -862,7 +860,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { adminPacket.rebootSeconds = 5 var meshPacket: MeshPacket = MeshPacket() - meshPacket.to = UInt32(connectedPeripheral.num) + meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. applinks:meshtastic.org/e/* + com.apple.developer.weatherkit + com.apple.security.app-sandbox com.apple.security.device.bluetooth diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index a13f6b2e..aab08b69 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV6.xcdatamodel + MeshtasticDataModelV7.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents index 6f82e5d0..48da2809 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents @@ -282,9 +282,11 @@ + + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV7.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV7.xcdatamodel/contents new file mode 100644 index 00000000..6f82e5d0 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV7.xcdatamodel/contents @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/PositionEntityExtension.swift b/Meshtastic/Persistence/PositionEntityExtension.swift index ca7bd3e9..23ec54b2 100644 --- a/Meshtastic/Persistence/PositionEntityExtension.swift +++ b/Meshtastic/Persistence/PositionEntityExtension.swift @@ -32,6 +32,15 @@ extension PositionEntity { } } + var nodeLocation: CLLocation? { + if latitudeI != 0 && longitudeI != 0 { + let location = CLLocation(latitude: latitude!, longitude: longitude!) + return location + } else { + return nil + } + } + var annotaton: MKPointAnnotation { let pointAnn = MKPointAnnotation() if nodeCoordinate != nil { diff --git a/Meshtastic/Protobufs/config.pb.swift b/Meshtastic/Protobufs/config.pb.swift index 29ec3be8..65e3587d 100644 --- a/Meshtastic/Protobufs/config.pb.swift +++ b/Meshtastic/Protobufs/config.pb.swift @@ -188,12 +188,24 @@ struct Config { /// Router device role. /// Mesh packets will prefer to be routed over this node. This node will not be used by client apps. /// The wifi/ble radios and the oled screen will be put to sleep. + /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 /// /// Router Client device role /// Mesh packets will prefer to be routed over this node. The Router Client can be used as both a Router and an app connected Client. case routerClient // = 3 + + /// + /// Repeater device role + /// Mesh packets will simply be rebroadcasted over this node. Nodes under this role node will not originate NodeInfo, Position, Telemetry + /// or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factory, and coding rate. + case repeater // = 4 + + /// + /// Tracker device role + /// Position Mesh packets for will be higher priority and sent more frequently by default. + case tracker // = 5 case UNRECOGNIZED(Int) init() { @@ -206,6 +218,8 @@ struct Config { case 1: self = .clientMute case 2: self = .router case 3: self = .routerClient + case 4: self = .repeater + case 5: self = .tracker default: self = .UNRECOGNIZED(rawValue) } } @@ -216,6 +230,8 @@ struct Config { case .clientMute: return 1 case .router: return 2 case .routerClient: return 3 + case .repeater: return 4 + case .tracker: return 5 case .UNRECOGNIZED(let i): return i } } @@ -1048,6 +1064,10 @@ struct Config { /// /// Short Range - Fast case shortFast // = 6 + + /// + /// Long Range - Moderately Fast + case longModerate // = 7 case UNRECOGNIZED(Int) init() { @@ -1063,6 +1083,7 @@ struct Config { case 4: self = .mediumFast case 5: self = .shortSlow case 6: self = .shortFast + case 7: self = .longModerate default: self = .UNRECOGNIZED(rawValue) } } @@ -1076,6 +1097,7 @@ struct Config { case .mediumFast: return 4 case .shortSlow: return 5 case .shortFast: return 6 + case .longModerate: return 7 case .UNRECOGNIZED(let i): return i } } @@ -1159,6 +1181,8 @@ extension Config.DeviceConfig.Role: CaseIterable { .clientMute, .router, .routerClient, + .repeater, + .tracker, ] } @@ -1259,6 +1283,7 @@ extension Config.LoRaConfig.ModemPreset: CaseIterable { .mediumFast, .shortSlow, .shortFast, + .longModerate, ] } @@ -1520,6 +1545,8 @@ extension Config.DeviceConfig.Role: SwiftProtobuf._ProtoNameProviding { 1: .same(proto: "CLIENT_MUTE"), 2: .same(proto: "ROUTER"), 3: .same(proto: "ROUTER_CLIENT"), + 4: .same(proto: "REPEATER"), + 5: .same(proto: "TRACKER"), ] } @@ -2084,6 +2111,7 @@ extension Config.LoRaConfig.ModemPreset: SwiftProtobuf._ProtoNameProviding { 4: .same(proto: "MEDIUM_FAST"), 5: .same(proto: "SHORT_SLOW"), 6: .same(proto: "SHORT_FAST"), + 7: .same(proto: "LONG_MODERATE"), ] } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 032c72be..c00d475b 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -4,6 +4,7 @@ */ import SwiftUI +import WeatherKit import MapKit import CoreLocation @@ -11,6 +12,7 @@ struct NodeDetail: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + @Environment(\.colorScheme) var colorScheme: ColorScheme @State var satsInView = 0 @State private var mapType: MKMapType = .standard @State var waypointCoordinate: CLLocationCoordinate2D? @@ -35,6 +37,14 @@ struct NodeDetail: View { ), animation: .easeIn) private var waypoints: FetchedResults + /// The current weather condition for the city. + @State private var condition: WeatherCondition? + @State private var temperature: Measurement? + @State private var symbolName: String = "cloud.fill" + + @State private var attributionLink: URL? + @State private var attributionLogo: URL? + var body: some View { let hwModelString = node.user?.hwModel ?? "UNSET" @@ -44,7 +54,6 @@ struct NodeDetail: View { VStack { if node.positions?.count ?? 0 > 0 { let mostRecent = node.positions?.lastObject as! PositionEntity - let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!) ZStack { let annotations = node.positions?.array as! [PositionEntity] ZStack { @@ -63,17 +72,31 @@ struct NodeDetail: View { overlays: self.overlays ) - VStack { + VStack (alignment: .leading) { Spacer() - Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ") - .font(.caption) - .offset(y: 20) - Picker("Map Type", selection: $mapType) { - ForEach(MeshMapType.allCases) { map in - Text(map.description).tag(map.MKMapTypeValue()) + HStack (alignment: .bottom, spacing: 1) { + + Picker("Map Type", selection: $mapType) { + ForEach(MeshMapType.allCases) { map in + Text(map.description).tag(map.MKMapTypeValue()) + } } + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .pickerStyle(.menu) + .padding(5) + VStack { + if mostRecent.satsInView > 0 { + Text("Sats: \(mostRecent.satsInView)") + .font(.caption) + .padding(.bottom, 1) + } + Label(temperature?.formatted() ?? "??", systemImage: symbolName) + .font(.caption) + } + .padding(10) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .padding(5) } - .pickerStyle(.menu) } } .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) @@ -338,10 +361,9 @@ struct NodeDetail: View { HStack { - if hwModelString == "TBEAM" || hwModelString == "TECHO" || hwModelString.contains("4631") { + if node.metadata != nil && node.metadata?.canShutdown ?? false { Button(action: { - showingShutdownConfirm = true }) { @@ -373,7 +395,7 @@ struct NodeDetail: View { .controlSize(.large) .padding() .confirmationDialog("are.you.sure", - isPresented: $showingRebootConfirm + isPresented: $showingRebootConfirm ) { Button("reboot.node", role: .destructive) { if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) { @@ -383,7 +405,23 @@ struct NodeDetail: View { } } .padding(5) + Divider() } + + VStack { + AsyncImage(url: attributionLogo) { image in + image + .resizable() + .scaledToFit() + } placeholder: { + ProgressView() + .controlSize(.mini) + } + .frame(height: 15) + + Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!) + } + .font(.footnote) } } .edgesIgnoringSafeArea([.leading, .trailing]) @@ -403,6 +441,30 @@ struct NodeDetail: View { .onAppear { self.bleManager.context = context } + .task(id: node.num) { + do { + + if node.positions?.count ?? 0 > 0 { + + let mostRecent = node.positions?.lastObject as! PositionEntity + + let weather = try await WeatherService.shared.weather(for: mostRecent.nodeLocation!) + condition = weather.currentWeather.condition + temperature = weather.currentWeather.temperature + symbolName = weather.currentWeather.symbolName + + let attribution = try await WeatherService.shared.attribution + attributionLink = attribution.legalPageURL + attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL + + } + + } catch { + print("Could not gather weather information...", error.localizedDescription) + condition = .clear + symbolName = "cloud.fill" + } + } } } } diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 99f21bca..b57c4a59 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -80,7 +80,9 @@ struct NodeMap: View { Text(map.description).tag(map.MKMapTypeValue()) } } + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .pickerStyle(.menu) + .padding(.bottom, 5) } } .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index c4c5a00f..ffeb291d 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -132,7 +132,11 @@ struct BluetoothConfig: View { print("empty bluetooth config") } - let adminMessageId = bleManager.requestBluetoothConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + + // Need to request a BluetoothConfig from the remote node before allowing changes + if connectedNode != nil && node?.bluetoothConfig == nil { + _ = bleManager.requestBluetoothConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } } .onChange(of: enabled) { newEnabled in if node != nil && node!.bluetoothConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 7895d3ee..8b8d9b2e 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -116,9 +116,9 @@ struct LoRaConfig: View { self.hasChanges = false // Need to request a LoRaConfig from the remote node before allowing changes - if node?.loRaConfig == nil { + if connectedNode != nil && node?.loRaConfig == nil { print("empty lora config") - let adminMessageId = bleManager.requestLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + _ = bleManager.requestLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } } .onChange(of: region) { newRegion in diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 87cfd1a8..6712dc04 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -76,19 +76,19 @@ struct Settings: View { .pickerStyle(.menu) .labelsHidden() .onChange(of: selectedNode) { newValue in -// if selectedNode > 0 { -// let node = nodes.first(where: { $0.num == newValue }) -// let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) -// connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) -// -// if node?.metadata == nil && node!.num != connectedNodeNum { -// let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) -// -// if adminMessageId > 0 { -// print("Saved node metadata") -// } -// } -// } + if selectedNode > 0 { + let node = nodes.first(where: { $0.num == newValue }) + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) + + if node?.metadata == nil && node!.num != connectedNodeNum { + let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) + + if adminMessageId > 0 { + print("Saved node metadata") + } + } + } } } } @@ -114,7 +114,6 @@ struct Settings: View { Text("user") } .tag(SettingsSidebar.userConfig) - .disabled(selectedNode == 0) NavigationLink() { LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }), connectedNode: nodes.first(where: { $0.num == connectedNodeNum }))