Initial Project Commit

This commit is contained in:
Garth Vander Houwen 2021-08-20 07:56:05 -07:00
parent ff017aab5e
commit 8be3ac2bba
23 changed files with 898 additions and 166 deletions

View file

@ -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 = "<group>"; };
DDC2E15926CE248E0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
DDC2E16026CE248F0042C5E4 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
DDC2E16326CE248F0042C5E4 /* MeshtasticClient.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticClient.xcdatamodel; sourceTree = "<group>"; };
DDC2E16526CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
@ -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 = "<group>"; };
DDC2E17B26CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DDC2E18A26CE25690042C5E4 /* deviceData.json */ = {isa = PBXFileReference; explicitFileType = text.json; fileEncoding = 4; path = deviceData.json; sourceTree = "<group>"; };
DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
DDC2E19026CE26290042C5E4 /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = "<group>"; };
DDC2E19226CE266B0042C5E4 /* DeviceHome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHome.swift; sourceTree = "<group>"; };
DDC2E19426CE26760042C5E4 /* DeviceDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDetail.swift; sourceTree = "<group>"; };
DDC2E19626CE26840042C5E4 /* DeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRow.swift; sourceTree = "<group>"; };
DDC2E19826CE26940042C5E4 /* DeviceMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMap.swift; sourceTree = "<group>"; };
DDC2E19A26CE27150042C5E4 /* CircleImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleImage.swift; sourceTree = "<group>"; };
DDC2E19C26CE27580042C5E4 /* ModelData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelData.swift; sourceTree = "<group>"; };
DDC2E19E26CE27630042C5E4 /* Device.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = "<group>"; };
DDC2E1A326CE2F940042C5E4 /* DeviceBLE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceBLE.swift; sourceTree = "<group>"; };
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = "<group>"; };
DDC2E1A826CF85020042C5E4 /* DeviceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceList.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
DDC2E18726CE24E40042C5E4 /* Views */ = {
isa = PBXGroup;
children = (
DDC2E18C26CE25B00042C5E4 /* Devices */,
DDC2E18B26CE25A70042C5E4 /* Messages */,
DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */,
DDC2E18D26CE25CB0042C5E4 /* Helpers */,
);
path = Views;
sourceTree = "<group>";
};
DDC2E18826CE24EE0042C5E4 /* Model */ = {
isa = PBXGroup;
children = (
DDC2E19C26CE27580042C5E4 /* ModelData.swift */,
DDC2E19E26CE27630042C5E4 /* Device.swift */,
);
path = Model;
sourceTree = "<group>";
};
DDC2E18926CE24F70042C5E4 /* Resources */ = {
isa = PBXGroup;
children = (
DDC2E18A26CE25690042C5E4 /* deviceData.json */,
);
path = Resources;
sourceTree = "<group>";
};
DDC2E18B26CE25A70042C5E4 /* Messages */ = {
isa = PBXGroup;
children = (
DDC2E19026CE26290042C5E4 /* Messages.swift */,
);
path = Messages;
sourceTree = "<group>";
};
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 = "<group>";
};
DDC2E18D26CE25CB0042C5E4 /* Helpers */ = {
isa = PBXGroup;
children = (
DDC2E19A26CE27150042C5E4 /* CircleImage.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
DDC2E1A526CEB32B0042C5E4 /* Helpers */ = {
isa = PBXGroup;
children = (
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
/* 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 = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = DDC2E14C26CE248E0042C5E4 /* Project object */;
}

View file

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

View file

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -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<Item>
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)
}
}

View file

@ -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)")
}
}

View file

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to center maps of the mesh</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>

View file

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MeshtasticClient.xcdatamodel</string>
</dict>
<dict/>
</plist>

View file

@ -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)
}
}
}

View file

@ -0,0 +1,37 @@
/*
See LICENSE folder for this apps 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 }
}

View file

@ -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<T: Decodable>(_ 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)")
}
}

View file

@ -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)")
}
})
}
}

View file

@ -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
}
}]

View file

@ -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())
}
}

View file

@ -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")
}
}
}

View file

@ -0,0 +1,144 @@
/*
See LICENSE folder for this samples 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<MKCoordinateRegion>(
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)
}
}

View file

@ -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())
}
}

View file

@ -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())
}
}

View file

@ -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<MKCoordinateRegion>(
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)
}
}
}

View file

@ -0,0 +1,55 @@
/*
See LICENSE folder for this samples 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))
}
}

View file

@ -0,0 +1,25 @@
/*
See LICENSE folder for this samples 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"))
}
}

View file

@ -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")
}
}
}