Merge pull request #418 from meshtastic/2.2.9-Working_Changes

2.2.9 working changes
This commit is contained in:
Garth Vander Houwen 2023-10-21 15:51:06 -07:00 committed by GitHub
commit 13e752cd4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 214 additions and 117 deletions

View file

@ -1423,7 +1423,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.9;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1457,7 +1457,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.9;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1579,7 +1579,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.9;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1612,7 +1612,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.8;
MARKETING_VERSION = 2.2.9;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -85,6 +85,7 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable {
}
enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
case fiveSeconds = 5
case tenSeconds = 10
case fifteenSeconds = 15
case thirtySeconds = 30
@ -96,6 +97,8 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
var id: Int { self.rawValue }
var description: String {
switch self {
case .fiveSeconds:
return "interval.five.seconds".localized
case .tenSeconds:
return "interval.ten.seconds".localized
case .fifteenSeconds:

View file

@ -8,9 +8,6 @@
import Foundation
extension Date {
static var currentTimeStamp: Int64 {
return Int64(Date().timeIntervalSince1970 * 1000)
}
func formattedDate(format: String) -> String {
let dateformat = DateFormatter()

View file

@ -123,9 +123,6 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "enableMapPointsOfInterest")
}
}
static var enableOfflineMaps: Bool {
get {
UserDefaults.standard.bool(forKey: "enableOfflineMaps")

View file

@ -144,6 +144,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
func disconnectPeripheral(reconnect: Bool = true) {
guard let connectedPeripheral = connectedPeripheral else { return }
if mqttProxyConnected {
mqttManager.mqttClientProxy?.disconnect()
}
automaticallyReconnect = reconnect
centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral)
FROMRADIO_characteristic = nil
@ -272,6 +275,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
}
if ![FROMNUM_characteristic, TORADIO_characteristic].contains(nil) {
if mqttProxyConnected {
mqttManager.mqttClientProxy?.disconnect()
}
sendWantConfig()
}
}
@ -290,7 +296,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
func onMqttMessageReceived(message: CocoaMQTTMessage) {
print("📲 Mqtt Client Proxy onMqttMessageReceived for topic: \(message.topic)")
if message.topic.contains("/stat/") {
return
}
@ -305,7 +311,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
print("📲 Sent Mqtt client proxy message to the connected device.")
}
}
@ -443,7 +449,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
payload: [UInt8](decodedInfo.mqttClientProxyMessage.data),
retained: decodedInfo.mqttClientProxyMessage.retained
)
print("📲 Publish Mqtt client proxy message received on FromRadio to the Mqtt server \(message)")
mqttManager.mqttClientProxy?.publish(message)
}
@ -635,10 +640,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
// Use context to pass the radio name with the timer
// Use a RunLoop to prevent the timer from running on the main UI thread
if UserDefaults.provideLocation {
let interval = UserDefaults.provideLocationInterval > 0 ? UserDefaults.provideLocationInterval : 30
if positionTimer != nil {
positionTimer!.invalidate()
}
positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((UserDefaults.provideLocationInterval )), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true)
positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((UserDefaults.provideLocationInterval)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true)
if positionTimer != nil {
RunLoop.current.add(positionTimer!, forMode: .common)
}
@ -787,7 +792,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.from = fromNodeNum
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! waypoint.serializedData()
do {
dataMessage.payload = try waypoint.serializedData()
}
catch {
// Could not serialiaze the payload
return false
}
dataMessage.portnum = PortNum.waypointApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
@ -870,7 +882,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
success = true
let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum))
print(positionPacket)
MeshLogger.log("📍 \(logString)")
}
return success

View file

@ -258,6 +258,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
newNode.snr = nodeInfo.snr
if nodeInfo.hasUser {
let newUser = UserEntity(context: context)
newUser.userId = nodeInfo.user.id
newUser.num = Int64(nodeInfo.num)
@ -265,9 +266,19 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
newUser.shortName = nodeInfo.user.shortName
newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
newNode.user = newUser
} else {
let newUser = UserEntity(context: context)
newUser.num = Int64(nodeInfo.num)
let userId = String(format:"%2X", nodeInfo.num)
newUser.userId = "!\(userId)"
let last4 = String(userId.suffix(4))
newUser.longName = "Meshtastic \(last4)"
newUser.shortName = last4
newUser.hwModel = "UNSET"
newNode.user = newUser
}
if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
let position = PositionEntity(context: context)
position.latest = true
position.seqNo = Int32(nodeInfo.position.seqNumber)
@ -306,7 +317,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
} catch {
print("💥 Fetch MyInfo Error")
}
} else if nodeInfo.hasUser && nodeInfo.num > 0 {
} else if nodeInfo.num > 0 {
fetchedNode[0].id = Int64(nodeInfo.num)
fetchedNode[0].num = Int64(nodeInfo.num)
@ -323,6 +334,18 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
fetchedNode[0].user!.longName = nodeInfo.user.longName
fetchedNode[0].user!.shortName = nodeInfo.user.shortName
fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
} else {
if (fetchedNode[0].user == nil) {
let newUser = UserEntity(context: context)
newUser.num = Int64(nodeInfo.num)
let userId = String(format:"%2X", nodeInfo.num)
newUser.userId = "!\(userId)"
let last4 = String(userId.suffix(4))
newUser.longName = "Meshtastic \(last4)"
newUser.shortName = last4
newUser.hwModel = "UNSET"
fetchedNode[0].user = newUser
}
}
if nodeInfo.hasDeviceMetrics {
@ -340,7 +363,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
if nodeInfo.hasPosition {
if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
let position = PositionEntity(context: context)
position.latitudeI = nodeInfo.position.latitudeI

View file

@ -22,6 +22,7 @@ class MqttClientProxyManager {
weak var delegate: MqttClientProxyManagerDelegate?
var mqttClientProxy: CocoaMQTT?
var topic = "msh/2/c"
var debugLog = false
func connectFromConfigSettings(node: NodeInfoEntity) {
let defaultServerAddress = "mqtt.meshtastic.org"
let useSsl = node.mqttConfig?.tlsEnabled == true
@ -58,9 +59,9 @@ class MqttClientProxyManager {
mqttClient.password = password
mqttClient.keepAlive = 60
mqttClient.cleanSession = cleanSession
#if DEBUG
mqttClient.logLevel = .debug
#endif
if debugLog {
mqttClient.logLevel = .debug
}
mqttClient.willMessage = CocoaMQTTMessage(topic: "/will", string: "dieout")
mqttClient.autoReconnect = true
mqttClient.delegate = self
@ -82,7 +83,9 @@ class MqttClientProxyManager {
}
func publish(message: String, topic: String, qos: CocoaMQTTQoS) {
mqttClientProxy?.publish(topic, withString: message, qos: qos)
print("📲 MQTT Client Proxy publish for: " + topic)
if debugLog {
print("📲 MQTT Client Proxy publish for: " + topic)
}
}
func disconnect() {
if let client = mqttClientProxy {
@ -130,15 +133,21 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
delegate?.onMqttDisconnected()
}
func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
print("📲 MQTT Client Proxy didPublishMessage from MqttClientProxyManager: \(message)")
if debugLog {
print("📲 MQTT Client Proxy didPublishMessage from MqttClientProxyManager: \(message)")
}
}
func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) {
print("📲 MQTT Client Proxy didPublishAck from MqttClientProxyManager: \(id)")
if debugLog {
print("📲 MQTT Client Proxy didPublishAck from MqttClientProxyManager: \(id)")
}
}
public func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) {
delegate?.onMqttMessageReceived(message: message)
print("📲 MQTT Client Proxy message received on topic: \(message.topic)")
if debugLog {
print("📲 MQTT Client Proxy message received on topic: \(message.topic)")
}
}
func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
print("📲 MQTT Client Proxy didSubscribeTopics: \(success.allKeys.count) topics. failed: \(failed.count) topics")

View file

@ -178,6 +178,18 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
fetchedNode[0].user!.longName = nodeInfoMessage.user.longName
fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName
fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased()
} else {
if (fetchedNode[0].user == nil) {
let newUser = UserEntity(context: context)
newUser.num = Int64(nodeInfoMessage.num)
let userId = String(format:"%2X", nodeInfoMessage.num)
newUser.userId = "!\(userId)"
let last4 = String(userId.suffix(4))
newUser.longName = "Meshtastic \(last4)"
newUser.shortName = last4
newUser.hwModel = "UNSET"
fetchedNode[0].user! = newUser
}
}
}
do {
@ -225,7 +237,6 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
position.latest = false
}
}
print("Incoming position message: \n \(positionMessage)")
let position = PositionEntity(context: context)
position.latest = true
position.snr = packet.rxSnr

View file

@ -237,6 +237,13 @@ struct Config {
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
/// send environment telemetry, and then sleep for telemetry.environment_update_interval seconds.
case sensor // = 6
///
/// TAK device role
/// Used for nodes dedicated for connection to an ATAK EUD.
/// Turns off many of the routine broadcasts to favor CoT packet stream
/// from the Meshtastic ATAK plugin -> IMeshService -> Node
case tak // = 7
case UNRECOGNIZED(Int)
init() {
@ -252,6 +259,7 @@ struct Config {
case 4: self = .repeater
case 5: self = .tracker
case 6: self = .sensor
case 7: self = .tak
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -265,6 +273,7 @@ struct Config {
case .repeater: return 4
case .tracker: return 5
case .sensor: return 6
case .tak: return 7
case .UNRECOGNIZED(let i): return i
}
}
@ -1285,6 +1294,7 @@ extension Config.DeviceConfig.Role: CaseIterable {
.repeater,
.tracker,
.sensor,
.tak,
]
}
@ -1692,6 +1702,7 @@ extension Config.DeviceConfig.Role: SwiftProtobuf._ProtoNameProviding {
4: .same(proto: "REPEATER"),
5: .same(proto: "TRACKER"),
6: .same(proto: "SENSOR"),
7: .same(proto: "TAK"),
]
}

View file

@ -208,6 +208,10 @@ enum HardwareModel: SwiftProtobuf.Enum {
/// Heltec HT-CT62 with ESP32-C3 CPU and SX1262 LoRa
case heltecHt62 // = 53
///
/// EBYTE SPI LoRa module and ESP32-S3
case ebyteEsp32S3 // = 54
///
/// ------------------------------------------------------------------------------------------------------------------------------------------
/// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
@ -265,6 +269,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case 51: self = .tWatchS3
case 52: self = .picomputerS3
case 53: self = .heltecHt62
case 54: self = .ebyteEsp32S3
case 255: self = .privateHw
default: self = .UNRECOGNIZED(rawValue)
}
@ -316,6 +321,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case .tWatchS3: return 51
case .picomputerS3: return 52
case .heltecHt62: return 53
case .ebyteEsp32S3: return 54
case .privateHw: return 255
case .UNRECOGNIZED(let i): return i
}
@ -372,6 +378,7 @@ extension HardwareModel: CaseIterable {
.tWatchS3,
.picomputerS3,
.heltecHt62,
.ebyteEsp32S3,
.privateHw,
]
}
@ -2534,6 +2541,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
51: .same(proto: "T_WATCH_S3"),
52: .same(proto: "PICOMPUTER_S3"),
53: .same(proto: "HELTEC_HT62"),
54: .same(proto: "EBYTE_ESP32_S3"),
255: .same(proto: "PRIVATE_HW"),
]
}

View file

@ -21,7 +21,7 @@ struct CircleText: View {
.foregroundColor(color.isLight() ? .black : .white)
.font(.system(size: 500))
.minimumScaleFactor(0.001)
.frame(width: circleSize * 0.94, height: circleSize * 0.94, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
.frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
}
.aspectRatio(1, contentMode: .fit)
}

View file

@ -17,7 +17,7 @@ struct LastHeardText: View {
var body: some View {
if lastHeard != nil && lastHeard! >= sixMonthsAgo! {
Text("heard")+Text(" \(LastHeardText.formatter.localizedString(for: lastHeard!, relativeTo: Date.now))")
Text(lastHeard?.formatted() ?? "unknown.age".localized)
} else {
Text("unknown.age")
}

View file

@ -31,9 +31,9 @@ struct LoRaSignalStrengthMeter: View {
Gauge(value: Double(signalStrength.rawValue), in: 0...3) {
} currentValueLabel: {
Image(systemName: "dot.radiowaves.left.and.right")
.font(.caption)
.font(.callout)
Text("Signal \(signalStrength.description)")
.font(.caption)
.font(.callout)
}
.gaugeStyle(.accessoryLinear)
.tint(gradient)

View file

@ -26,14 +26,14 @@ struct NodeListItem: View {
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
if deviceMetrics?.count ?? 0 >= 1 {
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
BatteryLevelCompact(batteryLevel: mostRecent?.batteryLevel, font: .caption2, iconFont: .callout, color: .accentColor)
BatteryLevelCompact(batteryLevel: mostRecent?.batteryLevel, font: .caption, iconFont: .callout, color: .accentColor)
}
}
VStack(alignment: .leading) {
HStack {
Text(node.user?.longName ?? "unknown".localized)
.fontWeight(.medium)
.font(.callout)
.font(.headline)
if node.user?.vip ?? false {
Spacer()
Image(systemName: "star.fill")
@ -43,19 +43,19 @@ struct NodeListItem: View {
if connected {
HStack {
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
.font(.footnote)
.font(.callout)
.symbolRenderingMode(.hierarchical)
.foregroundColor(.green)
Text("connected").font(.caption)
Text("connected").font(.callout)
}
}
HStack {
Image(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill")
.font(.footnote)
.font(.callout)
.symbolRenderingMode(.hierarchical)
.foregroundColor(node.isOnline ? .green : .orange)
LastHeardText(lastHeard: node.lastHeard)
.font(.caption)
.font(.callout)
}
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
HStack {
@ -65,19 +65,19 @@ struct NodeListItem: View {
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
let metersAway = nodeCoord.distance(from: myCoord)
Image(systemName: "lines.measurement.horizontal")
.font(.footnote)
.font(.callout)
.symbolRenderingMode(.hierarchical)
DistanceText(meters: metersAway).font(.caption)
DistanceText(meters: metersAway).font(.callout)
}
}
}
if node.channel > 0 {
HStack {
Image(systemName: "fibrechannel")
.font(.footnote)
.font(.callout)
.symbolRenderingMode(.hierarchical)
Text("Channel: \(node.channel)")
.font(.caption)
.font(.callout)
}
}

View file

@ -20,7 +20,6 @@ struct NodeMapSwiftUI: View {
@ObservedObject var node: NodeInfoEntity
@State var showUserLocation: Bool = false
@State var positions: [PositionEntity] = []
//@State var waypoints: [WaypointEntity] = []
/// Map State User Defaults
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
@ -30,24 +29,20 @@ struct NodeMapSwiftUI: View {
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid
// Map Configuration
@Namespace var mapScope
@State private var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)
@State private var position = MapCameraPosition.automatic
@State private var scene: MKLookAroundScene?
@State private var isLookingAround = false
@State private var isEditingSettings = false
@State private var selected: PositionEntity?
@State private var selectedWaypoint: WaypointEntity?
@State private var selectedWaypointRect: CGRect = .zero
@State private var selectedWaypointPoint: CGPoint = .zero
@State private var showingPositionPopover = false
@State private var showingWaypointPopover = false
@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)
@State var position = MapCameraPosition.automatic
@State var scene: MKLookAroundScene?
@State var isLookingAround = false
@State var isEditingSettings = false
@State var selected: PositionEntity?
@State var selectedWaypoint: WaypointEntity?
@State var showingPositionPopover = false
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
predicate: NSPredicate(
format: "expire == nil || expire >= %@", Date() as NSDate
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
@State var waypoiintSelectionRect: CGRect = .zero
var body: some View {
@ -90,15 +85,10 @@ struct NodeMapSwiftUI: View {
Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) {
ZStack {
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35)
.onTapGesture(coordinateSpace: .global) { location in
.onTapGesture(coordinateSpace: .named("nodemap")) { location in
print("Tapped at \(location)")
let pinLocation = reader.convert(location, from: .local)
print(pinLocation)
let size = CGSize(width: 1, height: 50)
let rect = CGRect(origin: location, size: size)
selectedWaypointRect = rect
selectedWaypointPoint = location
showingWaypointPopover = true
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
}
}
@ -203,12 +193,11 @@ struct NodeMapSwiftUI: View {
.padding(.horizontal, 20)
}
}
.popover(item: $selectedWaypoint, attachmentAnchor: .rect(.rect(selectedWaypointRect)), arrowEdge: .bottom) { selection in
//.popover(isPresented: $showingWaypointPopover, arrowEdge: .bottom) {
.sheet(item: $selectedWaypoint) { selection in
WaypointPopover(waypoint: selection)
.presentationDetents([.fraction(0.3), .medium])
.padding()
.opacity(0.8)
.presentationCompactAdaptation(.popover)
}
.sheet(isPresented: $isEditingSettings) {
VStack {
@ -293,9 +282,7 @@ struct NodeMapSwiftUI: View {
.padding()
#endif
}
.presentationDetents([.fraction(0.60)])
//.presentationDetents([.medium, .large])
.presentationDragIndicator(.automatic)
.presentationDetents([.fraction(0.4), .medium])
}
.onChange(of: node) {
let mostRecent = node.positions?.lastObject as? PositionEntity
@ -353,10 +340,11 @@ struct NodeMapSwiftUI: View {
}
#if targetEnvironment(macCatalyst)
MapZoomStepper(scope: mapScope)
.mapControlVisibility(.visible)
MapPitchSlider(scope: mapScope)
.mapControlVisibility(.visible)
/// Hide non fuctional catalyst controls
// MapZoomStepper(scope: mapScope)
// .mapControlVisibility(.visible)
// MapPitchSlider(scope: mapScope)
// .mapControlVisibility(.visible)
#endif
}
.controlSize(.regular)

View file

@ -9,12 +9,13 @@ import SwiftUI
import MapKit
struct WaypointPopover: View {
@Environment(\.dismiss) private var dismiss
var waypoint: WaypointEntity
let distanceFormatter = MKDistanceFormatter()
var body: some View {
VStack {
HStack {
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.blue)
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange)
Text(waypoint.name ?? "?")
.font(.title3)
if waypoint.locked > 0 {
@ -31,7 +32,6 @@ struct WaypointPopover: View {
Label {
Text(waypoint.longDescription ?? "")
.foregroundColor(.primary)
.font(.footnote)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
} icon: {
@ -40,43 +40,57 @@ struct WaypointPopover: View {
.frame(width: 35)
}
.padding(.bottom, 5)
Divider()
}
/// Coordinate
Label {
Text("Coordinates: \(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
//.font(.footnote)
.textSelection(.enabled)
.foregroundColor(.primary)
} icon: {
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
}
.padding(.bottom, 5)
Divider()
/// Created
Label {
Text("Created: \(waypoint.created?.formatted() ?? "?")")
.foregroundColor(.primary)
.font(.footnote)
} icon: {
Image(systemName: "clock.badge.checkmark")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
}
.padding(.bottom, 5)
Divider()
/// Updated
if waypoint.lastUpdated != nil {
Label {
Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")")
.foregroundColor(.primary)
.font(.footnote)
} icon: {
Image(systemName: "clock.arrow.circlepath")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
}
.padding(.bottom, 5)
Divider()
}
/// Updated
/// Expires
if waypoint.expire != nil {
Label {
Text("Expires: \(waypoint.expire?.formatted() ?? "?")")
.foregroundColor(.primary)
.font(.footnote)
} icon: {
Image(systemName: "clock.badge.xmark")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
}
.padding(.bottom, 5)
Divider()
}
/// Distance
if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
@ -84,14 +98,26 @@ struct WaypointPopover: View {
Label {
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
.foregroundColor(.primary)
.font(.footnote)
} icon: {
Image(systemName: "lines.measurement.horizontal")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
}
.padding(.bottom, 5)
Divider()
}
}
#if targetEnvironment(macCatalyst)
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
#endif
}
.tag(waypoint.id)
}

View file

@ -22,7 +22,9 @@ struct AdminMessageList: View {
var body: some View {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current)
let localeTimeFormat = DateFormatter.dateFormat(fromTemplate: "h:mm:ss a", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a")
let timeFormatString = (localeTimeFormat ?? "h:mm:ss a")
List {
if user != nil {
@ -55,7 +57,7 @@ struct AdminMessageList: View {
}
if am.receivedACK && am.ackTimestamp > 0 {
Text(" \(Date(timeIntervalSince1970: TimeInterval(am.ackTimestamp)).formattedDate(format: "h:mm:ss a"))")
Text(" \(Date(timeIntervalSince1970: TimeInterval(am.ackTimestamp)).formattedDate(format: timeFormatString))")
.foregroundColor(am.realACK ? .gray : .orange)
.font(.caption2)
}

View file

@ -44,7 +44,7 @@ struct BluetoothConfig: View {
setBluetoothValues()
}
}
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? -1 {
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
} else {

View file

@ -14,7 +14,6 @@ struct Settings: View {
private var nodes: FetchedResults<NodeInfoEntity>
@State private var selectedNode: Int = 0
@State private var connectedNodeNum: Int = 0
@State private var initialLoad: Bool = true
@State private var selection: SettingsSidebar = .about
enum SettingsSidebar {
case appSettings
@ -59,39 +58,44 @@ struct Settings: View {
}
.tag(SettingsSidebar.appSettings)
let node = nodes.first(where: { $0.num == connectedNodeNum })
let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 ? true : false
if !(node?.deviceConfig?.isManaged ?? false) {
Section("Configure") {
Picker("Configuring Node", selection: $selectedNode) {
if selectedNode == 0 {
Text("Connect to a Node").tag(0)
}
ForEach(nodes) { node in
if node.num == bleManager.connectedPeripheral?.num ?? 0 {
Text("BLE Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
} else if node.metadata != nil {
Text("Remote Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
} else {
Text("Request Admin: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
if hasAdmin {
Picker("Configuring Node", selection: $selectedNode) {
if selectedNode == 0 {
Text("Connect to a Node").tag(0)
}
}
}
.pickerStyle(.automatic)
.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 : 0)
if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil {
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
if adminMessageId > 0 {
print("Sent node metadata request from node details")
ForEach(nodes) { node in
if node.num == bleManager.connectedPeripheral?.num ?? 0 {
Text("BLE Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
} else if node.metadata != nil {
Text("Remote Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
} else if hasAdmin {
Text("Request Admin: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
}
}
}
.pickerStyle(.automatic)
.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 : 0)
if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil {
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
if adminMessageId > 0 {
print("Sent node metadata request from node details")
}
}
}
}
} else {
Text("Configuring Node \(node?.user?.longName ?? "unknown".localized)")
}
}
Section("radio.configuration") {
@ -276,12 +280,11 @@ struct Settings: View {
}
}
.onAppear {
self.bleManager.context = context
self.connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
if initialLoad {
selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
initialLoad = false
if self.bleManager.context == nil {
self.bleManager.context = context
}
self.connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
}
.listStyle(GroupedListStyle())
.navigationTitle("settings")

View file

@ -23,7 +23,7 @@ struct UserConfig: View {
@State private var isPresentingSaveConfirm: Bool = false
@State var hasChanges = false
@State var shortName = ""
@State var longName = ""
@State var longName: String = ""
@State var isLicensed = false
@State var overrideDutyCycle = false
@State var overrideFrequency: Float = 0.0
@ -157,10 +157,11 @@ struct UserConfig: View {
}
} else {
var ham = HamParameters()
// ham.shortName = shortName
ham.shortName = shortName
ham.callSign = longName
ham.txPower = Int32(txPower)
ham.frequency = overrideFrequency
print(ham)
let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
if adminMessageId > 0 {
hasChanges = false
@ -201,7 +202,14 @@ struct UserConfig: View {
}
.onChange(of: isLicensed) { newIsLicensed in
if node != nil && node!.user != nil {
if newIsLicensed != node?.user!.isLicensed { hasChanges = true }
if newIsLicensed != node?.user!.isLicensed {
hasChanges = true
if newIsLicensed {
if node?.user?.longName?.count ?? 0 > 8 {
longName = ""
}
}
}
}
}
.onChange(of: overrideFrequency) { _ in