diff --git a/MeshtasticClient.xcodeproj/project.pbxproj b/MeshtasticClient.xcodeproj/project.pbxproj index bfddabbb..7eb6b71a 100644 --- a/MeshtasticClient.xcodeproj/project.pbxproj +++ b/MeshtasticClient.xcodeproj/project.pbxproj @@ -8,13 +8,23 @@ /* Begin PBXBuildFile section */ DDC2E15826CE248E0042C5E4 /* MeshtasticClientApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticClientApp.swift */; }; - DDC2E15A26CE248E0042C5E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15926CE248E0042C5E4 /* ContentView.swift */; }; DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; }; DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; }; - DDC2E16126CE248F0042C5E4 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E16026CE248F0042C5E4 /* Persistence.swift */; }; - DDC2E16426CE248F0042C5E4 /* MeshtasticClient.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E16226CE248F0042C5E4 /* MeshtasticClient.xcdatamodeld */; }; DDC2E16F26CE248F0042C5E4 /* MeshtasticClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E16E26CE248F0042C5E4 /* MeshtasticClientTests.swift */; }; DDC2E17A26CE248F0042C5E4 /* MeshtasticClientUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E17926CE248F0042C5E4 /* MeshtasticClientUITests.swift */; }; + DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */; }; + DDC2E19126CE26290042C5E4 /* Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E19026CE26290042C5E4 /* Messages.swift */; }; + DDC2E19326CE266B0042C5E4 /* DeviceHome.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E19226CE266B0042C5E4 /* DeviceHome.swift */; }; + DDC2E19526CE26760042C5E4 /* DeviceDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E19426CE26760042C5E4 /* DeviceDetail.swift */; }; + DDC2E19726CE26840042C5E4 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E19626CE26840042C5E4 /* DeviceRow.swift */; }; + DDC2E19926CE26940042C5E4 /* DeviceMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E19826CE26940042C5E4 /* DeviceMap.swift */; }; + DDC2E19B26CE27150042C5E4 /* CircleImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E19A26CE27150042C5E4 /* CircleImage.swift */; }; + DDC2E19D26CE27580042C5E4 /* ModelData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E19C26CE27580042C5E4 /* ModelData.swift */; }; + DDC2E19F26CE27630042C5E4 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E19E26CE27630042C5E4 /* Device.swift */; }; + DDC2E1A226CE29AC0042C5E4 /* deviceData.json in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E18A26CE25690042C5E4 /* deviceData.json */; }; + DDC2E1A426CE2F940042C5E4 /* DeviceBLE.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A326CE2F940042C5E4 /* DeviceBLE.swift */; }; + DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; }; + DDC2E1A926CF85020042C5E4 /* DeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A826CF85020042C5E4 /* DeviceList.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -37,11 +47,8 @@ /* Begin PBXFileReference section */ DDC2E15426CE248E0042C5E4 /* MeshtasticClient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MeshtasticClient.app; sourceTree = BUILT_PRODUCTS_DIR; }; DDC2E15726CE248E0042C5E4 /* MeshtasticClientApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticClientApp.swift; sourceTree = ""; }; - DDC2E15926CE248E0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - DDC2E16026CE248F0042C5E4 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; - DDC2E16326CE248F0042C5E4 /* MeshtasticClient.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticClient.xcdatamodel; sourceTree = ""; }; DDC2E16526CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DDC2E16A26CE248F0042C5E4 /* MeshtasticClientTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticClientTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DDC2E16E26CE248F0042C5E4 /* MeshtasticClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticClientTests.swift; sourceTree = ""; }; @@ -49,6 +56,19 @@ DDC2E17526CE248F0042C5E4 /* MeshtasticClientUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticClientUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DDC2E17926CE248F0042C5E4 /* MeshtasticClientUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticClientUITests.swift; sourceTree = ""; }; DDC2E17B26CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DDC2E18A26CE25690042C5E4 /* deviceData.json */ = {isa = PBXFileReference; explicitFileType = text.json; fileEncoding = 4; path = deviceData.json; sourceTree = ""; }; + DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + DDC2E19026CE26290042C5E4 /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = ""; }; + DDC2E19226CE266B0042C5E4 /* DeviceHome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHome.swift; sourceTree = ""; }; + DDC2E19426CE26760042C5E4 /* DeviceDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDetail.swift; sourceTree = ""; }; + DDC2E19626CE26840042C5E4 /* DeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRow.swift; sourceTree = ""; }; + DDC2E19826CE26940042C5E4 /* DeviceMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMap.swift; sourceTree = ""; }; + DDC2E19A26CE27150042C5E4 /* CircleImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleImage.swift; sourceTree = ""; }; + DDC2E19C26CE27580042C5E4 /* ModelData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelData.swift; sourceTree = ""; }; + DDC2E19E26CE27630042C5E4 /* Device.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; }; + DDC2E1A326CE2F940042C5E4 /* DeviceBLE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceBLE.swift; sourceTree = ""; }; + DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = ""; }; + DDC2E1A826CF85020042C5E4 /* DeviceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceList.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -99,12 +119,13 @@ DDC2E15626CE248E0042C5E4 /* MeshtasticClient */ = { isa = PBXGroup; children = ( + DDC2E1A526CEB32B0042C5E4 /* Helpers */, + DDC2E18726CE24E40042C5E4 /* Views */, + DDC2E18826CE24EE0042C5E4 /* Model */, + DDC2E18926CE24F70042C5E4 /* Resources */, DDC2E15726CE248E0042C5E4 /* MeshtasticClientApp.swift */, - DDC2E15926CE248E0042C5E4 /* ContentView.swift */, DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */, - DDC2E16026CE248F0042C5E4 /* Persistence.swift */, DDC2E16526CE248F0042C5E4 /* Info.plist */, - DDC2E16226CE248F0042C5E4 /* MeshtasticClient.xcdatamodeld */, DDC2E15D26CE248F0042C5E4 /* Preview Content */, ); path = MeshtasticClient; @@ -136,6 +157,71 @@ path = MeshtasticClientUITests; sourceTree = ""; }; + DDC2E18726CE24E40042C5E4 /* Views */ = { + isa = PBXGroup; + children = ( + DDC2E18C26CE25B00042C5E4 /* Devices */, + DDC2E18B26CE25A70042C5E4 /* Messages */, + DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */, + DDC2E18D26CE25CB0042C5E4 /* Helpers */, + ); + path = Views; + sourceTree = ""; + }; + DDC2E18826CE24EE0042C5E4 /* Model */ = { + isa = PBXGroup; + children = ( + DDC2E19C26CE27580042C5E4 /* ModelData.swift */, + DDC2E19E26CE27630042C5E4 /* Device.swift */, + ); + path = Model; + sourceTree = ""; + }; + DDC2E18926CE24F70042C5E4 /* Resources */ = { + isa = PBXGroup; + children = ( + DDC2E18A26CE25690042C5E4 /* deviceData.json */, + ); + path = Resources; + sourceTree = ""; + }; + DDC2E18B26CE25A70042C5E4 /* Messages */ = { + isa = PBXGroup; + children = ( + DDC2E19026CE26290042C5E4 /* Messages.swift */, + ); + path = Messages; + sourceTree = ""; + }; + DDC2E18C26CE25B00042C5E4 /* Devices */ = { + isa = PBXGroup; + children = ( + DDC2E19226CE266B0042C5E4 /* DeviceHome.swift */, + DDC2E19426CE26760042C5E4 /* DeviceDetail.swift */, + DDC2E19626CE26840042C5E4 /* DeviceRow.swift */, + DDC2E19826CE26940042C5E4 /* DeviceMap.swift */, + DDC2E1A326CE2F940042C5E4 /* DeviceBLE.swift */, + DDC2E1A826CF85020042C5E4 /* DeviceList.swift */, + ); + path = Devices; + sourceTree = ""; + }; + DDC2E18D26CE25CB0042C5E4 /* Helpers */ = { + isa = PBXGroup; + children = ( + DDC2E19A26CE27150042C5E4 /* CircleImage.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + DDC2E1A526CEB32B0042C5E4 /* Helpers */ = { + isa = PBXGroup; + children = ( + DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */, + ); + path = Helpers; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -241,6 +327,7 @@ files = ( DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */, DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */, + DDC2E1A226CE29AC0042C5E4 /* deviceData.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -265,9 +352,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DDC2E16126CE248F0042C5E4 /* Persistence.swift in Sources */, - DDC2E16426CE248F0042C5E4 /* MeshtasticClient.xcdatamodeld in Sources */, - DDC2E15A26CE248E0042C5E4 /* ContentView.swift in Sources */, + DDC2E1A926CF85020042C5E4 /* DeviceList.swift in Sources */, + DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */, + DDC2E19F26CE27630042C5E4 /* Device.swift in Sources */, + DDC2E19926CE26940042C5E4 /* DeviceMap.swift in Sources */, + DDC2E19526CE26760042C5E4 /* DeviceDetail.swift in Sources */, + DDC2E19726CE26840042C5E4 /* DeviceRow.swift in Sources */, + DDC2E19126CE26290042C5E4 /* Messages.swift in Sources */, + DDC2E19D26CE27580042C5E4 /* ModelData.swift in Sources */, + DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, + DDC2E19326CE266B0042C5E4 /* DeviceHome.swift in Sources */, + DDC2E19B26CE27150042C5E4 /* CircleImage.swift in Sources */, + DDC2E1A426CE2F940042C5E4 /* DeviceBLE.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticClientApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -588,19 +684,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCVersionGroup section */ - DDC2E16226CE248F0042C5E4 /* MeshtasticClient.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - DDC2E16326CE248F0042C5E4 /* MeshtasticClient.xcdatamodel */, - ); - currentVersion = DDC2E16326CE248F0042C5E4 /* MeshtasticClient.xcdatamodel */; - path = MeshtasticClient.xcdatamodeld; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; - }; -/* End XCVersionGroup section */ }; rootObject = DDC2E14C26CE248E0042C5E4 /* Project object */; } diff --git a/MeshtasticClient/Assets.xcassets/rak4631.imageset/Contents.json b/MeshtasticClient/Assets.xcassets/rak4631.imageset/Contents.json new file mode 100644 index 00000000..bcdf2ebd --- /dev/null +++ b/MeshtasticClient/Assets.xcassets/rak4631.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "rak4631@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MeshtasticClient/Assets.xcassets/rak4631.imageset/rak4631@3x.png b/MeshtasticClient/Assets.xcassets/rak4631.imageset/rak4631@3x.png new file mode 100644 index 00000000..442efe09 Binary files /dev/null and b/MeshtasticClient/Assets.xcassets/rak4631.imageset/rak4631@3x.png differ diff --git a/MeshtasticClient/Assets.xcassets/tbeam.imageset/Contents.json b/MeshtasticClient/Assets.xcassets/tbeam.imageset/Contents.json new file mode 100644 index 00000000..4f6ed0a0 --- /dev/null +++ b/MeshtasticClient/Assets.xcassets/tbeam.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "TTGO T-Beam ESP32 LoRa 923MHz+OLED WiFi GPS NEO-M8N 18650-1-550x550.jpg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MeshtasticClient/Assets.xcassets/tbeam.imageset/TTGO T-Beam ESP32 LoRa 923MHz+OLED WiFi GPS NEO-M8N 18650-1-550x550.jpg b/MeshtasticClient/Assets.xcassets/tbeam.imageset/TTGO T-Beam ESP32 LoRa 923MHz+OLED WiFi GPS NEO-M8N 18650-1-550x550.jpg new file mode 100644 index 00000000..69790047 Binary files /dev/null and b/MeshtasticClient/Assets.xcassets/tbeam.imageset/TTGO T-Beam ESP32 LoRa 923MHz+OLED WiFi GPS NEO-M8N 18650-1-550x550.jpg differ diff --git a/MeshtasticClient/ContentView.swift b/MeshtasticClient/ContentView.swift deleted file mode 100644 index 93727c16..00000000 --- a/MeshtasticClient/ContentView.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// ContentView.swift -// MeshtasticClient -// -// Created by Garth Vander Houwen on 8/18/21. -// - -import SwiftUI -import CoreData - -struct ContentView: View { - @Environment(\.managedObjectContext) private var viewContext - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], - animation: .default) - private var items: FetchedResults - - var body: some View { - List { - ForEach(items) { item in - Text("Item at \(item.timestamp!, formatter: itemFormatter)") - } - .onDelete(perform: deleteItems) - } - .toolbar { - #if os(iOS) - EditButton() - #endif - - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } - } - } - - private func addItem() { - withAnimation { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - offsets.map { items[$0] }.forEach(viewContext.delete) - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } -} - -private let itemFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter -}() - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/MeshtasticClient/Helpers/LocationHelper.swift b/MeshtasticClient/Helpers/LocationHelper.swift new file mode 100644 index 00000000..1e7e83fb --- /dev/null +++ b/MeshtasticClient/Helpers/LocationHelper.swift @@ -0,0 +1,48 @@ +// +// LocationHelper.swift +// RChat +// +// Created by Andrew Morgan on 24/11/2020. +// + +import CoreLocation + +struct MyAnnotationItem: Identifiable { + var coordinate: CLLocationCoordinate2D + let id = UUID() +} + +class LocationHelper: NSObject, ObservableObject { + + static let shared = LocationHelper() + static let DefaultLocation = CLLocationCoordinate2D(latitude: 51.506520923981554, longitude: -0.10689139236939127) + + static var currentLocation: CLLocationCoordinate2D { + guard let location = shared.locationManager.location else { + return DefaultLocation + } + return location.coordinate + } + + private let locationManager = CLLocationManager() + + private override init() { + super.init() + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.requestWhenInUseAuthorization() + locationManager.startUpdatingLocation() + } +} + +extension LocationHelper: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { } + + public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + print("Location manager failed with error: \(error.localizedDescription)") + } + + public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + print("Location manager changed the status: \(status)") + } +} diff --git a/MeshtasticClient/Info.plist b/MeshtasticClient/Info.plist index efc211a0..fb22c1b7 100644 --- a/MeshtasticClient/Info.plist +++ b/MeshtasticClient/Info.plist @@ -2,6 +2,8 @@ + NSLocationWhenInUseUsageDescription + We use your location to center maps of the mesh CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/MeshtasticClient/MeshtasticClient.xcdatamodeld/.xccurrentversion b/MeshtasticClient/MeshtasticClient.xcdatamodeld/.xccurrentversion index 0e918e50..0c67376e 100644 --- a/MeshtasticClient/MeshtasticClient.xcdatamodeld/.xccurrentversion +++ b/MeshtasticClient/MeshtasticClient.xcdatamodeld/.xccurrentversion @@ -1,8 +1,5 @@ - - _XCCurrentVersionName - MeshtasticClient.xcdatamodel - + diff --git a/MeshtasticClient/MeshtasticClientApp.swift b/MeshtasticClient/MeshtasticClientApp.swift index b1bd1267..d98de677 100644 --- a/MeshtasticClient/MeshtasticClientApp.swift +++ b/MeshtasticClient/MeshtasticClientApp.swift @@ -9,12 +9,12 @@ import SwiftUI @main struct MeshtasticClientApp: App { - let persistenceController = PersistenceController.shared + @StateObject private var modelData = ModelData() var body: some Scene { WindowGroup { ContentView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(modelData) } } } diff --git a/MeshtasticClient/Model/Device.swift b/MeshtasticClient/Model/Device.swift new file mode 100644 index 00000000..18295076 --- /dev/null +++ b/MeshtasticClient/Model/Device.swift @@ -0,0 +1,37 @@ +/* +See LICENSE folder for this app’s licensing information. + +Abstract: +A representation of a single device. +*/ + +import Foundation +import SwiftUI +import CoreLocation + +struct Device: Hashable, Codable, Identifiable { + + var longName: String + var shortName: String + var id: String + var region: String + var hasGPS: Bool + var isRouter: Bool + var firmwareVersion: String + var hardwareModel: String + var lastHeard: Double + var snr: Double + + private var imageName: String + var image: Image { + Image(imageName) + } + + var position: Position + + struct Position: Hashable, Codable { + var latitude: Double + var longitude: Double + var altitude: Int + var batteryLevel: Int } +} diff --git a/MeshtasticClient/Model/ModelData.swift b/MeshtasticClient/Model/ModelData.swift new file mode 100644 index 00000000..77097466 --- /dev/null +++ b/MeshtasticClient/Model/ModelData.swift @@ -0,0 +1,37 @@ +/* +Abstract: +Storage for model data. +*/ + +import Foundation +import Combine + +final class ModelData: ObservableObject { + @Published var devices: [Device] = load("deviceData.json") + + var nearby: [Device] { + devices + } +} + +func load(_ filename: String) -> T { + let data: Data + + guard let file = Bundle.main.url(forResource: filename, withExtension: nil) + else { + fatalError("Couldn't find \(filename) in main bundle.") + } + + do { + data = try Data(contentsOf: file) + } catch { + fatalError("Couldn't load \(filename) from main bundle:\n\(error)") + } + + do { + let decoder = JSONDecoder() + return try decoder.decode(T.self, from: data) + } catch { + fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)") + } +} diff --git a/MeshtasticClient/Persistence.swift b/MeshtasticClient/Persistence.swift deleted file mode 100644 index cf42e817..00000000 --- a/MeshtasticClient/Persistence.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Persistence.swift -// MeshtasticClient -// -// Created by Garth Vander Houwen on 8/18/21. -// - -import CoreData - -struct PersistenceController { - static let shared = PersistenceController() - - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - for _ in 0..<10 { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - } - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() - - let container: NSPersistentCloudKitContainer - - init(inMemory: Bool = false) { - container = NSPersistentCloudKitContainer(name: "MeshtasticClient") - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - } -} diff --git a/MeshtasticClient/Resources/deviceData.json b/MeshtasticClient/Resources/deviceData.json new file mode 100644 index 00000000..c6a53913 --- /dev/null +++ b/MeshtasticClient/Resources/deviceData.json @@ -0,0 +1,81 @@ +[ + { + "longName": "RAK GPS Node", + "shortName": "RGN", + "region": "US", + "id": "!438f02b0", + "firmwareVersion": "1.2.43", + "lastHeard": 1628145597, + "isRouter": true, + "hasGPS": true, + "hardwareModel": "RAK4631", + "macAddress": "MAC STRING", + "snr": 4.75, + "imageName": "rak4631", + "position": { + "longitude": -122.151405, + "latitude": 47.608330, + "batteryLevel": 55, + "altitude": 150 + } + }, + { + "longName": "RAK Solar 1", + "shortName": "RS1", + "region": "US", + "id": "!3ba37b3e", + "firmwareVersion": "1.2.43", + "lastHeard": 1628091161, + "isRouter": false, + "hasGPS": false, + "hardwareModel": "RAK4631", + "macAddress": "MAC STRING", + "snr": 4.75, + "imageName": "rak4631", + "position": { + "longitude": -122.145316, + "latitude": 47.601440, + "batteryLevel": 55, + "altitude": 150 + } + }, + { + "longName": "RAK Solar 2", + "shortName": "RS2", + "region": "US", + "id": "!a66c166f", + "firmwareVersion": "1.2.43", + "lastHeard": 1628094190, + "isRouter": false, + "hasGPS": false, + "hardwareModel": "RAK4631", + "macAddress": "MAC STRING", + "snr": 4.75, + "imageName": "rak4631", + "position": { + "longitude": -122.148599, + "latitude": 47.602106, + "batteryLevel": 77, + "altitude": 150 + } + }, + { + "longName": "Yellow Beam", + "shortName": "YB", + "region": "US", + "id": "!050c2538", + "firmwareVersion": "1.2.43", + "lastHeard": 1628091161, + "isRouter": false, + "hasGPS": true, + "hardwareModel": "TBEAM", + "macAddress": "MAC STRING", + "snr": 4.75, + "imageName": "tbeam", + "position": { + "longitude": -122.154392, + "latitude": 47.598788, + "batteryLevel": 55, + "altitude": 150 + } + }] diff --git a/MeshtasticClient/Views/ContentView.swift b/MeshtasticClient/Views/ContentView.swift new file mode 100644 index 00000000..bfa9f056 --- /dev/null +++ b/MeshtasticClient/Views/ContentView.swift @@ -0,0 +1,57 @@ +/* +Abstract: Default App View +*/ + +import SwiftUI + +struct ContentView: View { + @State private var selection: Tab = .devices + + enum Tab { + case messages + case devices + case map + case featured + case list + case ble + } + + var body: some View { + TabView(selection: $selection) { + + DeviceHome() + .tabItem { + Label("Devices", systemImage: "flipphone") + } + .tag(Tab.devices) + DeviceList() + .tabItem { + Label("Device List", systemImage: "list.bullet.rectangle") + } + .tag(Tab.devices) + DeviceMap() + .tabItem { + Label("Mesh Map", systemImage: "map") + } + .tag(Tab.map) + Messages() + .tabItem { + Label("Messages", systemImage: "message") + } + .tag(Tab.messages) + DeviceBLE() + .tabItem { + Label("Bluetooth", systemImage: "dot.radiowaves.left.and.right") + } + .tag(Tab.ble) + + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + .environmentObject(ModelData()) + } +} diff --git a/MeshtasticClient/Views/Devices/DeviceBLE.swift b/MeshtasticClient/Views/Devices/DeviceBLE.swift new file mode 100644 index 00000000..d970c564 --- /dev/null +++ b/MeshtasticClient/Views/Devices/DeviceBLE.swift @@ -0,0 +1,45 @@ +// +// DeviceBLE.swift +// MeshtasticClient +// +// Created by Garth Vander Houwen on 8/18/21. +// + +import CoreBluetooth +import SwiftUI +import MapKit +import CoreLocation + +struct DeviceBLE: View { + + @EnvironmentObject var modelData: ModelData + + var devices: [Device] { + modelData.devices + } + + var myPeripheal:CBPeripheral? + var myCharacteristic:CBCharacteristic? + var bleManager:CBCentralManager? + + let serviceUUID = CBUUID(string: "ab0828b1-198e-4351-b779-901fa0e0371e") + + + var body: some View { + NavigationView { + + ScrollView { + + + + + } + .navigationTitle("Bluetooth") + + + + + + } + } +} diff --git a/MeshtasticClient/Views/Devices/DeviceDetail.swift b/MeshtasticClient/Views/Devices/DeviceDetail.swift new file mode 100644 index 00000000..2a2b7065 --- /dev/null +++ b/MeshtasticClient/Views/Devices/DeviceDetail.swift @@ -0,0 +1,144 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view showing the details for a device. +*/ + +import SwiftUI +import MapKit +import CoreLocation +import CoreBluetooth + +struct DeviceDetail: View { + @EnvironmentObject var modelData: ModelData + var device: Device + + var deviceIndex: Int { + modelData.devices.firstIndex(where: { $0.id == device.id })! + } + + struct MapLocation: Identifiable { + let id = UUID() + let name: String + let coordinate: CLLocationCoordinate2D + } + var body: some View { + + let currentCoordinatePosition = CLLocationCoordinate2D(latitude: device.position.latitude, longitude: device.position.longitude) + let regionBinding = Binding( + get: { + MKCoordinateRegion(center: currentCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) + }, + set: { _ in } + ) + + VStack{ + // Map or Device Image + if(device.hasGPS) { + + let annotations = [ + MapLocation(name: device.shortName, coordinate: CLLocationCoordinate2D(latitude:device.position.latitude, longitude: device.position.longitude)) + ] + + Map(coordinateRegion: regionBinding, annotationItems: annotations) { location in + MapAnnotation( + coordinate: location.coordinate, + content: { + Text(device.shortName).font(.subheadline).foregroundColor(.white) + .background(Circle() + .fill(Color.blue) + .frame(width: 40, height: 40)) + } + ) + }.frame(minHeight: 150, maxHeight: 1000) + } + else + { + device.image + .resizable() + .frame(minHeight: 300, maxHeight: 1000) + } + } + + ZStack { + + VStack(alignment: .leading) { + + HStack { + if(device.hasGPS){ + device.image + .resizable() + .frame(width: 100, height: 100) + } + Text(device.longName).font(.largeTitle) + } + Divider() + HStack{ + Image(systemName: "clock").font(.title3).foregroundColor(.blue) + let lastHeard = Date(timeIntervalSince1970: device.lastHeard) + Text("Last Heard:").font(.headline) + Text(lastHeard, style: .date).font(.subheadline) + Text(lastHeard, style: .time).font(.subheadline) + } + Divider() + HStack { + Text("Meshtastic Version: " + device.firmwareVersion) + Spacer() + Text("Id: " + device.id) + } + .font(.subheadline) + .foregroundColor(.secondary) + HStack { + Text("Hardware Model: " + device.hardwareModel) + Spacer() + Text("Region: " + device.region) + + } + .font(.subheadline) + .foregroundColor(.secondary) + Divider() + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 14) { + Image(systemName: "antenna.radiowaves.left.and.right").font(.title).foregroundColor(.blue) + VStack(alignment: .leading) { + + Text("AKA").font(.caption) + Text(device.shortName).font(.caption2).foregroundColor(.gray) + } + VStack(alignment: .leading) { + + Text("Latitude").font(.caption) + Text(String(format: "%.4f", device.position.latitude) + "°").font(.caption2).foregroundColor(.gray) + } + VStack(alignment: .leading) { + + Text("Longitude").font(.caption) + let fourDecimalPlaces = String(format: "%.4f", device.position.longitude) + Text(String(fourDecimalPlaces) + "°").font(.caption2).foregroundColor(.gray) + } + VStack(alignment: .leading) { + + Text("Altitude").font(.caption) + Text(String(device.position.altitude) + " m").font(.caption2).foregroundColor(.gray) + } + VStack(alignment: .leading) { + Text("Battery").font(.caption) + Text(String(device.position.batteryLevel) + "%").font(.caption2).foregroundColor(.gray) + } + } + } + .padding() + } + .navigationTitle(device.longName) + .navigationBarTitleDisplayMode(.inline) + } +} + +struct DeviceDetail_Previews: PreviewProvider { + static let modelData = ModelData() + + static var previews: some View { + DeviceDetail(device: modelData.devices[0]) + .environmentObject(modelData) + } +} diff --git a/MeshtasticClient/Views/Devices/DeviceHome.swift b/MeshtasticClient/Views/Devices/DeviceHome.swift new file mode 100644 index 00000000..3a36016c --- /dev/null +++ b/MeshtasticClient/Views/Devices/DeviceHome.swift @@ -0,0 +1,48 @@ +// +// DeviceHome.swift +// Landmarks +// +// Created by Garth Vander Houwen on 8/7/21. +// See LICENSE folder for app licensing information. +// + +// Abstract: +// A view showing devices above a list of devices +// grouped by device. + +import SwiftUI + +struct DeviceHome: View { + @EnvironmentObject var modelData: ModelData + @State private var showGPSOnly = false + + var filteredDevices: [Device] { + modelData.devices.filter { device in + (!showGPSOnly || device.hasGPS) + } + } + var body: some View { + NavigationView { + + List { + Toggle(isOn: $showGPSOnly) { + Text("GPS only") + } + + ForEach(filteredDevices) { device in + NavigationLink(destination: DeviceDetail(device: device)) { + DeviceRow(device: device) + } + } + } + .navigationTitle("All Devices") + } + } +} + +struct DeviceHome_Previews: PreviewProvider { + static var previews: some View { + DeviceHome() + .environmentObject(ModelData()) + } +} diff --git a/MeshtasticClient/Views/Devices/DeviceList.swift b/MeshtasticClient/Views/Devices/DeviceList.swift new file mode 100644 index 00000000..7b30e808 --- /dev/null +++ b/MeshtasticClient/Views/Devices/DeviceList.swift @@ -0,0 +1,49 @@ +// +// DeviceHome.swift +// Landmarks +// +// Created by Garth Vander Houwen on 8/7/21. +// See LICENSE folder for app licensing information. +// + +// Abstract: +// A view showing devices above a list of devices +// grouped by device. + +import SwiftUI + +struct DeviceList: View { + @EnvironmentObject var modelData: ModelData + @State private var showGPSOnly = false + + var filteredDevices: [Device] { + modelData.devices.filter { device in + (!showGPSOnly || device.hasGPS) + } + } + var body: some View { + NavigationView { + + List { + Toggle(isOn: $showGPSOnly) { + Text("GPS only") + } + + ForEach(filteredDevices) { device in + NavigationLink(destination: DeviceDetail(device: device)) { + DeviceRow(device: device) + } + } + } + .navigationTitle("All Devices") + }.navigationViewStyle(StackNavigationViewStyle()) + + } +} + +struct DeviceList_Previews: PreviewProvider { + static var previews: some View { + DeviceHome() + .environmentObject(ModelData()) + } +} diff --git a/MeshtasticClient/Views/Devices/DeviceMap.swift b/MeshtasticClient/Views/Devices/DeviceMap.swift new file mode 100644 index 00000000..26b4d36e --- /dev/null +++ b/MeshtasticClient/Views/Devices/DeviceMap.swift @@ -0,0 +1,60 @@ +// +// DeviceMap.swift +// Landmarks +// +// Created by Garth Vander Houwen on 8/7/21. +// Copyright © 2021 Apple. All rights reserved. +// + +import SwiftUI +import MapKit +import CoreLocation + +struct DeviceMap: View { + + @EnvironmentObject var modelData: ModelData + + var devices: [Device] { + modelData.devices + } + + struct MapLocation: Identifiable { + let id = UUID() + let name: String + let coordinate: CLLocationCoordinate2D + } + + var body: some View { + let location = LocationHelper.currentLocation + let currentCoordinatePosition = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude) + let regionBinding = Binding( + get: { + MKCoordinateRegion(center: currentCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)) + }, + set: { _ in } + ) + let annotations = [ + MapLocation(name: devices[0].shortName, coordinate: CLLocationCoordinate2D(latitude: devices[0].position.latitude, longitude: devices[0].position.longitude)), + MapLocation(name: devices[1].shortName, coordinate: CLLocationCoordinate2D(latitude: devices[1].position.latitude, longitude: devices[1].position.longitude)), + MapLocation(name: devices[2].shortName, coordinate: CLLocationCoordinate2D(latitude: devices[2].position.latitude, longitude: devices[2].position.longitude)), + MapLocation(name: devices[3].shortName, coordinate: CLLocationCoordinate2D(latitude: devices[3].position.latitude, longitude: devices[3].position.longitude)) + ] + + ZStack { + Map(coordinateRegion: regionBinding, + interactionModes: [.all], + showsUserLocation: true, + userTrackingMode: .constant(.follow), annotationItems: annotations) { location in + + MapAnnotation( + coordinate: location.coordinate, + content: { + Text(location.name).font(.caption2).foregroundColor(.white) + .background(Circle() + .fill(Color.blue) + .frame(width: 40, height: 40)) } + ) + }.frame(maxHeight:.infinity) + } + } +} diff --git a/MeshtasticClient/Views/Devices/DeviceRow.swift b/MeshtasticClient/Views/Devices/DeviceRow.swift new file mode 100644 index 00000000..d306e62f --- /dev/null +++ b/MeshtasticClient/Views/Devices/DeviceRow.swift @@ -0,0 +1,55 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A single row to be displayed in a list of landmarks. +*/ + +import SwiftUI + +struct DeviceRow: View { + var device: Device + + var body: some View { + HStack { + + device.image.resizable().frame(width: 150, height: 150) + + VStack(alignment: .leading) { + + Text(device.longName).font(.title2) + HStack { + if device.hasGPS { + Image(systemName: "location.fill.viewfinder") + .foregroundColor(.blue).font(.title3) + } + if device.isRouter { + Image(systemName: "dot.radiowaves.left.and.right") + .foregroundColor(.blue).font(.title3) + } + if device.hardwareModel == "TBEAM" || device.hardwareModel == "TLORA" { + Image(systemName: "wifi") + .foregroundColor(.blue).font(.title3) + } + if false { + Image(systemName: "rectangle.connected.to.line.below") + .foregroundColor(.green).font(.title2) + } + } + } + Spacer() + } + } +} + +struct DeviceRow_Previews: PreviewProvider { + static var devices = ModelData().devices + + static var previews: some View { + Group { + DeviceRow(device: devices[0]) + DeviceRow(device: devices[1]) + } + .previewLayout(.fixed(width: 300, height: 70)) + } +} diff --git a/MeshtasticClient/Views/Helpers/CircleImage.swift b/MeshtasticClient/Views/Helpers/CircleImage.swift new file mode 100644 index 00000000..34dcbade --- /dev/null +++ b/MeshtasticClient/Views/Helpers/CircleImage.swift @@ -0,0 +1,25 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view that clips an image to a circle and adds a stroke and shadow. +*/ + +import SwiftUI + +struct CircleImage: View { + var image: Image + + var body: some View { + image + .clipShape(/*@START_MENU_TOKEN@*/Circle()/*@END_MENU_TOKEN@*/) + .overlay(Circle().stroke(Color.white, lineWidth: 4)) + .shadow(radius: 7) + } +} + +struct CircleImage_Previews: PreviewProvider { + static var previews: some View { + CircleImage(image: Image("tbeam")) + } +} diff --git a/MeshtasticClient/Views/Messages/Messages.swift b/MeshtasticClient/Views/Messages/Messages.swift new file mode 100644 index 00000000..daeb8d52 --- /dev/null +++ b/MeshtasticClient/Views/Messages/Messages.swift @@ -0,0 +1,57 @@ +import SwiftUI +import MapKit +import CoreLocation + +struct Messages: View { + var body: some View { + ZStack { + + List { + HStack { + Text("RGN").font(.subheadline).foregroundColor(.white).background(Circle().fill(Color.blue).frame(width: 40, height: 40)) + VStack(alignment: .trailing) { + Text("I sent a super great message with amazing text").padding().foregroundColor(.white).background(Capsule().fill(Color.green)) + Text("8/7/21 8:39 PM").font(.subheadline).foregroundColor(.gray) + } + + } + VStack { + HStack { + Text("RGN").font(.subheadline).foregroundColor(.white).background(Circle().fill(Color.blue).frame(width: 40, height: 40)) + HStack { + Text("I sent a super great message with amazing text").padding().foregroundColor(.white).background(Capsule().fill(Color.green)) + Text("8/7/21 8:39 PM").font(.subheadline).foregroundColor(.gray) + } + } + HStack { + Text("8/7/21 8:39 PM").font(.subheadline).foregroundColor(.gray) + } + + } + VStack { + HStack { + Text("RS1").font(.subheadline).foregroundColor(.white).background(Circle().fill(Color.blue).frame(width: 40, height: 40)) + Text("the best message").padding().foregroundColor(.white).background(Capsule().fill(Color.green)) + } + HStack { + Text("8/7/21 8:45 PM").font(.subheadline).foregroundColor(.gray) + } + + } + VStack { + HStack{ + Text("YB").font(.subheadline).foregroundColor(.white).background(Circle().fill(Color.green).frame(width: 40, height: 40)) + Spacer(minLength: 50) + Text("This is a terse response to an amazing text").padding().foregroundColor(.white).background(Capsule().fill(Color.green)) + } + HStack { + + Text("8/7/21 8:53 PM").font(.subheadline).foregroundColor(.gray) + Spacer() + } + } + } + .navigationTitle("Broadcast Channel") + } + } +}