From 879e76d8994dc227f8e586775e66078e5c26808c Mon Sep 17 00:00:00 2001 From: BG6TNB Date: Sat, 7 Jan 2023 00:36:11 +0800 Subject: [PATCH 01/57] add zh-Hans --- Meshtastic.xcodeproj/project.pbxproj | 3 + zh-Hans.lproj/Localizable.strings | 183 +++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 zh-Hans.lproj/Localizable.strings diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 50e4621a..9b0658f4 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -126,6 +126,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; C9483F6C2773017500998F6B /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = ""; }; C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAnnotationView.swift; sourceTree = ""; }; @@ -651,6 +652,7 @@ en, de, Base, + "zh-Hans", ); mainGroup = DDC2E14B26CE248E0042C5E4; packageReferences = ( @@ -852,6 +854,7 @@ children = ( DDCDC6CC29481FCC004C1DDA /* en */, DDCDC6CE294821AD004C1DDA /* de */, + A65FA974296876BF00A97686 /* zh-Hans */, ); name = Localizable.strings; sourceTree = ""; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..0d78debb --- /dev/null +++ b/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,183 @@ +/* + Localizable.strings + Meshtastic + + Created by BG6TNB on 01/06/23. + +*/ +"about"="关于"; +"about.meshtastic"="关于 Meshtastic"; +"admin"="Admin"; +"admin.log"="Admin Message Log"; +"ago"="ago"; +"always.on"="常亮"; +"app.settings"="软件设置"; +"are.you.sure"="是否确认?"; +"ascii.capable"="ASCII Capable"; +"available.radios"="可获得的电台"; +"automatic.detection"="自动发现"; +"ble.name"="蓝牙名称"; +"bluetooth"="蓝牙"; +"bluetooth.config"="蓝牙配置"; +"bluetooth.mode.randompin"="随机 PIN 码"; +"bluetooth.mode.fixedpin"="固定 PIN 码"; +"bluetooth.mode.nopin"="不使用 PIN 码(直接使用)"; +"bytes"="Bytes"; +"cancel"="取消"; +"canned.messages"="快捷消息"; +"canned.messages.config"="快捷消息配置"; +"canned.messages.preset.manual"="手动配置"; +"canned.messages.preset.rakrotary"="RAK 旋转编码器"; +"canned.messages.preset.cardkb"="M5 Stack 卡片键盘 / RAK 键盘"; +"channel"="频道"; +"channel.role.disabled"="禁用"; +"channel.role.primary"="主要"; +"channel.role.secondary"="次要"; +"channels"="频道"; +"clear.app.data"="清除 App 数据"; +"connected.radio"="连接到电台"; +"communicating"="与电台进行通讯中. ."; +"connected"="已连接到电台"; +"connecting"="连接中. ."; +"contacts"="联系人"; +"copy"="复制"; +"default"="默认"; +"delete"="删除"; +"device"="电台"; +"device.config"="电台配置"; +"device.role.client"="标准模式 - App 可以连接到电台进行收发操作,并且会自动转发 Mesh 网络中其他节点的消息。"; +"device.role.clientmute"="静默模式 - 与标准模式类似,App 可以连接到电台进行收发操作,但不会转发 Mesh 网络中其他节点的消息。"; +"device.role.router"="纯中继模式 - 自动转发 Mesh 网络中其他节点的消息,中继模式下屏幕会熄灭,Wi-Fi 和蓝牙将会进入睡眠模式,App 将无法连接到电台进行收发操作。"; +"device.role.routerclient"="中继模式 - 优先转发 Mesh 网络中其他节点的消息,App 也可以连接到电台进行收发操作。"; +"direct.messages"="直接收到的消息"; +"dismiss.keyboard"="隐藏键盘"; +"display"="屏幕(电台屏幕)"; +"display.config"="屏幕配置"; +"distance"="距离"; +"disconnect"="断开连接"; +"echo"="回声"; +"email.address"="电子邮件"; +"enabled"="启用"; +"external.notification"="外部通知"; +"external.notification.config"="外部通知配置"; +"firmware.version"="固件版本"; +"gpsformat.dec"="十进制"; +"gpsformat.dms"="度分秒"; +"gpsformat.utm"="Universal Transverse Mercator"; +"gpsformat.mgrs"="Military Grid Reference System"; +"gpsformat.olc"="Open Location Code (aka Plus Codes)"; +"gpsformat.osgr"="Ordnance Survey Grid Reference"; +"heard"="收到"; +"heard.last"="最后收到"; +"hybrid"="混合"; +"inputevent.none"="无"; +"inputevent.up"="上"; +"inputevent.down"="下"; +"inputevent.left"="左"; +"inputevent.right"="右"; +"inputevent.select"="选择"; +"inputevent.back"="后退"; +"inputevent.cancel"="取消"; +"interval.one.second"="一秒"; +"interval.two.seconds"="两秒"; +"interval.five.seconds"="五秒"; +"interval.ten.seconds"="十秒"; +"interval.fifteen.seconds"="十五秒"; +"interval.twenty.seconds"="二十秒"; +"interval.twentyfive.seconds"="二十五秒"; +"interval.thirty.seconds"="三十秒"; +"interval.one.minute"="一分钟"; +"interval.two.minutes"="两分钟"; +"interval.five.minutes"="五分钟"; +"interval.ten.minutes"="十分钟"; +"interval.fifteen.minutes"="十五分钟"; +"interval.thirty.minutes"="三十分钟"; +"interval.one.hour"="一小时"; +"interval.six.hours"="六小时"; +"interval.twelve.hours"="十二小时"; +"interval.twentyfour.hours"="二十四小时"; +"keyboard.type"="键盘类型"; +"logging"="Logging"; +"lora"="LoRa"; +"lora.config"="LoRa 配置"; +"map"="Mesh 地图"; +"map.type"="Map 类型"; +"mesh.log"="Mesh 日志"; +"message"="消息"; +"message.details"="消息详情"; +"messages"="消息"; +"mode"="模式"; +"module.configuration"="模块配置"; +"mqtt"="MQTT"; +"mqtt.config"="MQTT 配置"; +"mqtt.username"="用户名称"; +"name"="名称"; +"network"="网络"; +"network.config"="网络配置"; +"nodes"="节点"; +"no.nodes"="未找到 Meshtastic 节点"; +"not.connected"="未连接到电台"; +"numbers.punctuation"="数字和标点符号"; +"off"="关闭"; +"on.boot"="仅在启动时"; +"options"="选项"; +"password"="密码"; +"phone.gps"="手机 GPS"; +"phone.gps.interval.description"="手机向电台刷新定位的时间间隔,但刷新 Mesh 网络中的定位的时间间隔由电台控制。"; +"position"="定位"; +"position.config"="定位配置"; +"preferred.radio"="首选电台"; +"provide.location"="提供定位到 Mesh 网络"; +"radio.configuration"="电台配置"; +"range.test"="距离测试"; +"range.test.config"="距离测试配置"; +"reply"="回复"; +"received.ack"="收到确认"; +"routing.acknowledged"="确认"; +"routing.noroute"="未转发"; +"routing.gotnak"="Received a negative acknowledgment"; +"routing.timeout"="超时"; +"routing.nointerface"="无连接"; +"routing.maxretransmit"="已达到最大重试次数"; +"routing.nochannel"="没有频道"; +"routing.toolarge"="数据包过大"; +"routing.noresponse"="无响应"; +"routing.dutycyclelimit"="已达到当前区域循环周期发射上限"; +"routing.badRequest"="错误请求"; +"routing.notauthorized"="未授权"; +"satellite"="卫星"; +"save"="保存"; +"serial"="串口"; +"serial.config"="串口配置"; +"serial.mode.default"="默认"; +"serial.mode.simple"="简单"; +"serial.mode.proto"="Protobufs"; +"serial.mode.txtmsg"="文本消息"; +"serial.mode.nmea"="NMEA 位置"; +"settings"="设置"; +"share.channels"="分享频道二维码"; +"share.position"="分享位置"; +"subscribed"="连接到 Mesh 网络"; +"select.contact"="选择一名联系人"; +"select.node"="选择一个节点"; +"select.menu.item"="从菜单选择一个选项"; +"set.region"="设置 LoRa 区域"; +"standard"="标准"; +"ssid"="SSID"; +"tapback"="Tapback Response"; +"tapback.heart"="Heart"; +"tapback.thumbsup"="Thumbs Up"; +"tapback.thumbsdown"="Thumbs Down"; +"tapback.haha"="HaHa"; +"tapback.exclamation"="Exclamation Mark"; +"tapback.question"="Question Mark"; +"tapback.poop"="Poop"; +"telemetry"="遥测(传感器)"; +"telemetry.config"="遥测配置"; +"timeout"="超时"; +"twitter"="Twitter"; +"unknown.age"="Unknown Age"; +"update.interval"="刷新间隔"; +"user"="用户"; +"user.details"="用户信息"; +"waiting"="等待. . ."; From 8bc645412b3f601c404e191d15d384ced7e74cdf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jan 2023 06:49:19 -0800 Subject: [PATCH 02/57] Cluster for map pins on the node details view --- Meshtastic.xcodeproj/project.pbxproj | 12 +- .../Persistence/PositionEntityExtension.swift | 13 +- Meshtastic/Views/Map/MapView.swift | 173 ------------------ Meshtastic/Views/Map/MapViewModule.swift | 2 +- Meshtastic/Views/Map/MapViewSwiftUI.swift | 57 ++++++ Meshtastic/Views/Nodes/NodeDetail.swift | 149 +++++++-------- Meshtastic/Views/Nodes/NodeList.swift | 4 +- 7 files changed, 144 insertions(+), 266 deletions(-) delete mode 100644 Meshtastic/Views/Map/MapView.swift create mode 100644 Meshtastic/Views/Map/MapViewSwiftUI.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index d28ae0a6..27ac9bee 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - C9483F6D2773017500998F6B /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9483F6C2773017500998F6B /* MapView.swift */; }; C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; C9A7BC1027759A9600760B50 /* PositionAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */; }; @@ -22,6 +21,7 @@ DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; }; DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; }; + DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; }; DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; }; DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; }; DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD35018A2852FC79000FC853 /* UserSettings.swift */; }; @@ -127,7 +127,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - C9483F6C2773017500998F6B /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = ""; }; C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAnnotationView.swift; sourceTree = ""; }; C9A88B54278B503C00BD810A /* MapViewModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapViewModule.swift; sourceTree = ""; }; @@ -141,6 +140,7 @@ DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = ""; }; DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = ""; }; + DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = ""; }; DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; DD35018A2852FC79000FC853 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; @@ -270,9 +270,9 @@ isa = PBXGroup; children = ( C9A7BC0E27759A6800760B50 /* Custom */, - C9483F6C2773017500998F6B /* MapView.swift */, C9A88B54278B503C00BD810A /* MapViewModule.swift */, C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */, + DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, ); path = Map; sourceTree = ""; @@ -744,6 +744,7 @@ DDC4D568275499A500A4208E /* Persistence.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, + DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */, @@ -783,7 +784,6 @@ DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */, DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */, DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */, - C9483F6D2773017500998F6B /* MapView.swift in Sources */, DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */, @@ -1006,7 +1006,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.10; + MARKETING_VERSION = 2.0.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1039,7 +1039,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.10; + MARKETING_VERSION = 2.0.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; diff --git a/Meshtastic/Persistence/PositionEntityExtension.swift b/Meshtastic/Persistence/PositionEntityExtension.swift index a17f7e06..05fb5d59 100644 --- a/Meshtastic/Persistence/PositionEntityExtension.swift +++ b/Meshtastic/Persistence/PositionEntityExtension.swift @@ -23,7 +23,7 @@ extension PositionEntity { return d / 1e7 } - var coordinate: CLLocationCoordinate2D? { + var nodeCoordinate: CLLocationCoordinate2D? { if latitudeI != 0 && longitudeI != 0 { let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) return coord @@ -31,12 +31,17 @@ extension PositionEntity { return nil } } - + var annotaton: MKPointAnnotation { let pointAnn = MKPointAnnotation() - if coordinate != nil { - pointAnn.coordinate = coordinate! + if nodeCoordinate != nil { + pointAnn.coordinate = nodeCoordinate! } return pointAnn } } + +extension PositionEntity: MKAnnotation { + public var coordinate: CLLocationCoordinate2D { nodeCoordinate! } + public var subtitle: String? { time?.formatted() } +} diff --git a/Meshtastic/Views/Map/MapView.swift b/Meshtastic/Views/Map/MapView.swift deleted file mode 100644 index 02c752be..00000000 --- a/Meshtastic/Views/Map/MapView.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// MapView.swift -// MeshtasticApple -// -// Created by Joshua Pirihi on 22/12/21. -// - -import Foundation -import UIKit -import MapKit -import SwiftUI -import CoreData -#if false -// wrap a MKMapView into something we can use in SwiftUI -struct MapView: UIViewRepresentable { - - var nodes: FetchedResults - - var mapViewDelegate = MapViewDelegate() - - // observe changes to the key in UserDefaults - @AppStorage("meshMapType") var type: String = "hybrid" - - func makeUIView(context: Context) -> MKMapView { - - let map = MKMapView(frame: .zero) - - map.userTrackingMode = .follow - - let region = MKCoordinateRegion( center: map.centerCoordinate, latitudinalMeters: CLLocationDistance(exactly: 500)!, longitudinalMeters: CLLocationDistance(exactly: 500)!) - map.setRegion(map.regionThatFits(region), animated: false) - - //self.updateMapType(map) - self.showNodePositions(to: map) - self.moveToMeshRegion(in: map) - - map.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self)) - - let overlay = MKTileOverlay(urlTemplate: //"http://tiles-a.data-cdn.linz.govt.nz/services;key=7fa19132d53240708c4ff436df5b9800/tiles/v4/layer=50767/EPSG:3857/{z}/{x}/{y}.png") - "http://10.147.253.250:5050/local/map/{z}/{x}/{y}.png") - overlay.canReplaceMapContent = true - self.mapViewDelegate.renderer = MKTileOverlayRenderer(tileOverlay: overlay) - map.addOverlay(overlay) - - return map - } - - func updateUIView(_ view: MKMapView, context: Context) { - view.delegate = mapViewDelegate // (1) This should be set in makeUIView, but it is getting reset to `nil` - view.translatesAutoresizingMaskIntoConstraints = false // (2) In the absence of this, we get constraints error on rotation; and again, it seems one should do this in makeUIView, but has to be here - - self.updateMapType(view) - - self.showNodePositions(to: view) - - //if (self.needToMoveToMeshRegion) { - // self.moveToMeshRegion(in: view) - // self.needToMoveToMeshRegion = false - //} - } - - func moveToMeshRegion(in mapView: MKMapView) { - //go through the annotations and create a bounding box that encloses them - - var minLat: CLLocationDegrees = 90.0 - var maxLat: CLLocationDegrees = -90.0 - var minLon: CLLocationDegrees = 180.0 - var maxLon: CLLocationDegrees = -180.0 - - for annotation in mapView.annotations { - if annotation.isKind(of: PositionAnnotation.self) { - minLat = min(minLat, annotation.coordinate.latitude) - maxLat = max(maxLat, annotation.coordinate.latitude) - minLon = min(minLon, annotation.coordinate.longitude) - maxLon = max(maxLon, annotation.coordinate.longitude) - } - } - - //check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location) - if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 { - return - } - - let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2) - - let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5) - - let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span)) - - mapView.setRegion(region, animated: true) - - - } - - func updateMapType(_ map: MKMapView) { - - switch self.type { - case "satellite": - map.mapType = .satellite - break - case "standard": - map.mapType = .standard - break - case "hybrid": - map.mapType = .hybrid - break - default: - map.mapType = .hybrid - } - } -} - -private extension MapView { - - func showNodePositions(to view: MKMapView) { - - // clear any existing annotations - if !view.annotations.isEmpty { - view.removeAnnotations(view.annotations) - } - - for node in self.nodes { - // try and get the last position - if (node.positions?.count ?? 0) > 0 && (node.positions!.lastObject as! PositionEntity).coordinate != nil { - let annotation = PositionAnnotation() - annotation.coordinate = (node.positions!.lastObject as! PositionEntity).coordinate! - annotation.title = node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") - annotation.shortName = node.user?.shortName?.uppercased() ?? "???" - - view.addAnnotation(annotation) - } - } - } -} - -class MapViewDelegate: NSObject, MKMapViewDelegate { - - var renderer: MKTileOverlayRenderer? - - func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - - guard !annotation.isKind(of: MKUserLocation.self) else { - // Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize. - return nil - } - - var annotationView: MKAnnotationView? - - if let annotation = annotation as? PositionAnnotation { - annotationView = self.setupPositionAnnotationView(for: annotation, on: mapView) - } - - return annotationView - } - - func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { - return self.renderer! - - } - - private func setupPositionAnnotationView(for annotation: PositionAnnotation, on mapView: MKMapView) -> PositionAnnotationView { - let identifier = NSStringFromClass(PositionAnnotationView.self) - - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? PositionAnnotationView ?? PositionAnnotationView() - - annotationView.name = annotation.shortName ?? "???" - - annotationView.canShowCallout = true - - return annotationView - } -} -#endif diff --git a/Meshtastic/Views/Map/MapViewModule.swift b/Meshtastic/Views/Map/MapViewModule.swift index 13d5bb77..bb3bfa82 100644 --- a/Meshtastic/Views/Map/MapViewModule.swift +++ b/Meshtastic/Views/Map/MapViewModule.swift @@ -223,7 +223,7 @@ public struct MapView: UIViewRepresentable { } let annotation = PositionAnnotation() - annotation.coordinate = position.coordinate! + annotation.coordinate = position.nodeCoordinate! annotation.title = position.nodePosition!.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") annotation.shortName = position.nodePosition!.user?.shortName?.uppercased() ?? "???" diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift new file mode 100644 index 00000000..e02ca6da --- /dev/null +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -0,0 +1,57 @@ +// +// MapViewSwitUI.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 1/9/23. +// + +import SwiftUI +import MapKit + +struct MapViewSwiftUI: UIViewRepresentable { + + let positions: [PositionEntity] + let region: MKCoordinateRegion + let mapViewType: MKMapType + + func makeUIView(context: Context) -> MKMapView { + let mapView = MKMapView() + mapView.mapType = mapViewType + mapView.setRegion(region, animated: true) + mapView.isRotateEnabled = true + mapView.addAnnotations(positions) + mapView.delegate = context.coordinator + return mapView + } + + func updateUIView(_ mapView: MKMapView, context: Context) { + mapView.mapType = mapViewType + } + + func makeCoordinator() -> MapCoordinator { + .init() + } + + final class MapCoordinator: NSObject, MKMapViewDelegate { + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + + switch annotation { + + case _ as MKClusterAnnotation: + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup") + annotationView.markerTintColor = .darkGray + return annotationView + case _ as PositionEntity: + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node") + annotationView.canShowCallout = true + annotationView.glyphText = "📟" + annotationView.clusteringIdentifier = "nodeGroup" + annotationView.markerTintColor = UIColor(.accentColor) + annotationView.titleVisibility = .visible + return annotationView + default: return nil + } + } + } +} diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 932ae13a..bae52138 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -1,73 +1,62 @@ /* -Abstract: -A view showing the details for a node. -*/ + Abstract: + A view showing the details for a node. + */ import SwiftUI import MapKit import CoreLocation struct NodeDetail: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + @State private var mapType: MKMapType = .standard + @State private var showingDetailsPopover = false @State var satsInView = 0 @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false - + var node: NodeInfoEntity - + var body: some View { let hwModelString = node.user?.hwModel ?? "UNSET" - + NavigationStack { GeometryReader { bounds in VStack { if node.positions?.count ?? 0 > 0 { let mostRecent = node.positions?.lastObject as! PositionEntity - if mostRecent.coordinate != nil { - let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!) - - let regionBinding = Binding( - get: { - MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) - }, - set: { _ in } - ) + let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!) + ZStack { + let annotations = node.positions?.array as! [PositionEntity] ZStack { - let annotations = node.positions?.array as! [PositionEntity] - - Map(coordinateRegion: regionBinding, - interactionModes: [.all], - showsUserLocation: true, - userTrackingMode: .constant(.follow), - annotationItems: annotations) { location in + MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType) + VStack { + Spacer() + Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ") + .font(.caption2) - return MapAnnotation( - coordinate: location.coordinate ?? CLLocationCoordinate2D(latitude: 0, longitude: 0), - content: { - - NodeAnnotation(time: location.time!) - } - ) + Picker("", selection: $mapType) { + Text("Standard").tag(MKMapType.standard) + Text("Hybrid").tag(MKMapType.hybrid) + Text("Satellite").tag(MKMapType.satellite) + } + .pickerStyle(SegmentedPickerStyle()) } - .ignoresSafeArea(.all, edges: [.leading, .trailing]) - .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2) - } - Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ") - .offset( y:-40) + .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) + .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65) } - } else { HStack { - + } - .padding([.top], 60) + .padding([.top], 20) } ScrollView { @@ -80,13 +69,13 @@ struct NodeDetail: View { Divider() VStack { if node.user != nil { - + Image(hwModelString) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) .cornerRadius(5) - + Text(String(hwModelString)) .foregroundColor(.gray) .font(.largeTitle).fixedSize() @@ -96,7 +85,7 @@ struct NodeDetail: View { if node.snr > 0 { Divider() VStack(alignment: .center) { - + Image(systemName: "waveform.path") .font(.title) .foregroundColor(.accentColor) @@ -109,15 +98,15 @@ struct NodeDetail: View { .fixedSize() } } - + if node.telemetries?.count ?? 0 >= 1 { - + let mostRecent = node.telemetries?.lastObject as! TelemetryEntity Divider() VStack(alignment: .center) { BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel)) if mostRecent.voltage > 0 { - + Text(String(format: "%.2f", mostRecent.voltage) + " V") .font(.title) .foregroundColor(.gray) @@ -147,7 +136,7 @@ struct NodeDetail: View { Divider() VStack { HStack { - Image(systemName: "number") + Image(systemName: "number") .font(.title2) .foregroundColor(.accentColor) .symbolRenderingMode(.hierarchical) @@ -160,8 +149,8 @@ struct NodeDetail: View { HStack { Image(systemName: "globe") .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) Text("MAC Address: ").font(.title) } @@ -174,8 +163,8 @@ struct NodeDetail: View { HStack { Image(systemName: "clock.badge.checkmark.fill") .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) Text("heard.last").font(.title)+Text(":").font(.title) } @@ -190,7 +179,7 @@ struct NodeDetail: View { } else { HStack { - + VStack(alignment: .center) { CircleText(text: node.user?.shortName ?? "???", color: .accentColor) } @@ -210,7 +199,7 @@ struct NodeDetail: View { if node.snr > 0 { Divider() VStack(alignment: .center) { - + Image(systemName: "waveform.path") .font(.title) .foregroundColor(.accentColor) @@ -223,15 +212,15 @@ struct NodeDetail: View { } .padding(5) } - + if node.telemetries?.count ?? 0 >= 1 { - + let mostRecent = node.telemetries?.lastObject as! TelemetryEntity - + Divider() - + VStack(alignment: .center) { - + BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel)) if mostRecent.voltage > 0 { @@ -262,7 +251,7 @@ struct NodeDetail: View { Divider() VStack { HStack { - Image(systemName: "number") + Image(systemName: "number") .font(.title2) .foregroundColor(.accentColor) .symbolRenderingMode(.hierarchical) @@ -275,9 +264,9 @@ struct NodeDetail: View { Divider() HStack { Image(systemName: "globe") - .font(.headline) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) + .font(.headline) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) Text("MAC Address: ") Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray) } @@ -334,16 +323,16 @@ struct NodeDetail: View { } if self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num && self.bleManager.connectedPeripheral.num == node.num { - + HStack { if hwModelString == "TBEAM" || hwModelString == "TECHO" || hwModelString.contains("4631") { - + Button(action: { showingShutdownConfirm = true }) { - + Label("Power Off", systemImage: "power") } .buttonStyle(.bordered) @@ -361,13 +350,13 @@ struct NodeDetail: View { } } } - + Button(action: { showingRebootConfirm = true }) { - + Label("reboot", systemImage: "arrow.triangle.2.circlepath") } .buttonStyle(.bordered) @@ -376,31 +365,31 @@ struct NodeDetail: View { .padding() .confirmationDialog("are.you.sure", - isPresented: $showingRebootConfirm - ) { - Button("reboot.node", role: .destructive) { - - if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) { - print("Reboot Failed") - } - } + isPresented: $showingRebootConfirm + ) { + Button("reboot.node", role: .destructive) { + + if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) { + print("Reboot Failed") } + } + } } .padding(5) } } - .offset( y:-40) + //.offset( y:-40) } .edgesIgnoringSafeArea([.leading, .trailing]) .navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline) .padding(.bottom, 10) .navigationBarItems(trailing: - ZStack { - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") - } + ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + } ) .onAppear { self.bleManager.context = context diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 1925c767..fb6980d7 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -53,8 +53,8 @@ struct NodeList: View { HStack(alignment: .bottom) { let lastPostion = node.positions!.reversed()[0] as! PositionEntity let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude) - if lastPostion.coordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude { - let nodeCoord = CLLocation(latitude: lastPostion.coordinate!.latitude, longitude: lastPostion.coordinate!.longitude) + if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude { + let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) let metersAway = nodeCoord.distance(from: myCoord) Image(systemName: "lines.measurement.horizontal") .font(.title3) From 02801ab16e285f0f34c2a0e75467d64114348af7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 11 Jan 2023 13:53:50 -0800 Subject: [PATCH 03/57] Waypoint form mockup Additional map fixes Emoji only keyboard and validation for waypoint emoji --- Meshtastic.xcodeproj/project.pbxproj | 8 ++ Meshtastic/Helpers/BLEManager.swift | 9 +- Meshtastic/Helpers/EmojiOnlyTextField.swift | 74 ++++++++++ Meshtastic/Helpers/Extensions.swift | 11 ++ Meshtastic/Helpers/LocationHelper.swift | 2 +- .../Persistence/PositionEntityExtension.swift | 3 +- Meshtastic/Protobufs/mesh.pb.swift | 12 +- Meshtastic/Views/Map/MapViewSwiftUI.swift | 20 ++- Meshtastic/Views/Map/WaypointFormView.swift | 127 ++++++++++++++++++ Meshtastic/Views/Nodes/NodeDetail.swift | 33 ++--- Meshtastic/Views/Settings/Channels.swift | 6 - 11 files changed, 266 insertions(+), 39 deletions(-) create mode 100644 Meshtastic/Helpers/EmojiOnlyTextField.swift create mode 100644 Meshtastic/Views/Map/WaypointFormView.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 27ac9bee..5f62f20e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -66,6 +66,8 @@ DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */; }; DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; }; DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; }; + DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; }; + DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormView.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; }; DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD994B68295F88B60013760A /* IntervalEnums.swift */; }; @@ -187,6 +189,8 @@ DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = ""; }; DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = ""; }; DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = ""; }; + DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = ""; }; + DD964FBE296E76EF007C176F /* WaypointFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormView.swift; sourceTree = ""; }; DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = ""; }; DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = ""; }; @@ -273,6 +277,7 @@ C9A88B54278B503C00BD810A /* MapViewModule.swift */, C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */, DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, + DD964FBE296E76EF007C176F /* WaypointFormView.swift */, ); path = Map; sourceTree = ""; @@ -546,6 +551,7 @@ DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, + DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, ); path = Helpers; sourceTree = ""; @@ -729,6 +735,7 @@ DDCFF601285453A7005FA625 /* localonly.pb.swift in Sources */, DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */, + DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */, DD3501892852FC3B000FC853 /* Settings.swift in Sources */, DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */, DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, @@ -743,6 +750,7 @@ DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, DDC4D568275499A500A4208E /* Persistence.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, + DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index d51723b0..58008d9e 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -789,9 +789,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var dataMessage = DataMessage() dataMessage.payload = try! positionPacket.serializedData() dataMessage.portnum = PortNum.positionApp - //if destNum != emptyNodeNum { - dataMessage.wantResponse = wantResponse - //} + dataMessage.wantResponse = wantResponse meshPacket.decoded = dataMessage var toRadio: ToRadio! @@ -809,18 +807,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } @objc func positionTimerFired(timer: Timer) { - // Check for connected node if connectedPeripheral != nil { - // Send a position out to the mesh if "share location with the mesh" is enabled in settings if userSettings!.provideLocation { - let success = sendPosition(destNum: connectedPeripheral.num, wantResponse: false) if !success { - print("Failed to send positon to device") - } } } diff --git a/Meshtastic/Helpers/EmojiOnlyTextField.swift b/Meshtastic/Helpers/EmojiOnlyTextField.swift new file mode 100644 index 00000000..ea30fb37 --- /dev/null +++ b/Meshtastic/Helpers/EmojiOnlyTextField.swift @@ -0,0 +1,74 @@ +// +// EmojiKeyboard.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 1/10/23. +// +import SwiftUI + +class SwiftUIEmojiTextField: UITextField { + + override func awakeFromNib() { + super.awakeFromNib() + } + + func setEmoji() { + _ = self.textInputMode + } + + override var textInputContextIdentifier: String? { + return "" + } + + override var textInputMode: UITextInputMode? { + for mode in UITextInputMode.activeInputModes { + if mode.primaryLanguage == "emoji" { + self.keyboardType = .default // do not remove this + return mode + } + } + return nil + } +} + +struct EmojiOnlyTextField: UIViewRepresentable { + @Binding var text: String + var placeholder: String = "" + + func makeUIView(context: Context) -> SwiftUIEmojiTextField { + let emojiTextField = SwiftUIEmojiTextField() + emojiTextField.placeholder = placeholder + emojiTextField.text = text + emojiTextField.delegate = context.coordinator + return emojiTextField + } + + func updateUIView(_ uiView: SwiftUIEmojiTextField, context: Context) { + uiView.text = text + } + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + class Coordinator: NSObject, UITextFieldDelegate { + var parent: EmojiOnlyTextField + init(parent: EmojiOnlyTextField) { + self.parent = parent + } + func textFieldDidChangeSelection(_ textField: UITextField) { + DispatchQueue.main.async { [weak self] in + self?.parent.text = textField.text ?? "" + } + } + } +} + +//struct EmojiContentView: View { +// +// @State private var text: String = "" +// +// var body: some View { +// EmojiTextField(text: $text, placeholder: "Enter emoji") +// } +//} diff --git a/Meshtastic/Helpers/Extensions.swift b/Meshtastic/Helpers/Extensions.swift index c1b41f99..38094e03 100644 --- a/Meshtastic/Helpers/Extensions.swift +++ b/Meshtastic/Helpers/Extensions.swift @@ -1,6 +1,13 @@ import Foundation import SwiftUI +extension Character { + var isEmoji: Bool { + guard let scalar = unicodeScalars.first else { return false } + return scalar.properties.isEmoji && (scalar.value >= 0x203C || unicodeScalars.count > 1) + } +} + extension Data { var macAddressString: String { let mac: String = reduce("") {$0 + String(format: "%02x:", $1)} @@ -73,6 +80,10 @@ extension String { return base64url } + func onlyEmojis() -> Bool { + return count > 0 && !contains { !$0.isEmoji } + } + func image(fontSize:CGFloat = 40, bgColor:UIColor = UIColor.clear, imageSize:CGSize? = nil) -> UIImage? { let font = UIFont.systemFont(ofSize: fontSize) diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift index db7ec2f8..dab6637d 100644 --- a/Meshtastic/Helpers/LocationHelper.swift +++ b/Meshtastic/Helpers/LocationHelper.swift @@ -6,7 +6,6 @@ class LocationHelper: NSObject, ObservableObject { // Apple Park static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) - static let DefaultAltitude = CLLocationDistance(integerLiteral: 0) static let DefaultSpeed = CLLocationSpeed(integerLiteral: 0) static let DefaultHeading = CLLocationDirection(integerLiteral: 0) @@ -82,6 +81,7 @@ class LocationHelper: NSObject, ObservableObject { super.init() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.allowsBackgroundLocationUpdates = true locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() } diff --git a/Meshtastic/Persistence/PositionEntityExtension.swift b/Meshtastic/Persistence/PositionEntityExtension.swift index 05fb5d59..7834b93b 100644 --- a/Meshtastic/Persistence/PositionEntityExtension.swift +++ b/Meshtastic/Persistence/PositionEntityExtension.swift @@ -43,5 +43,6 @@ extension PositionEntity { extension PositionEntity: MKAnnotation { public var coordinate: CLLocationCoordinate2D { nodeCoordinate! } - public var subtitle: String? { time?.formatted() } + public var title: String? { nodePosition?.user?.shortName ?? NSLocalizedString("unknown", comment: "Unknown") } + public var subtitle: String? { time?.formatted() } } diff --git a/Meshtastic/Protobufs/mesh.pb.swift b/Meshtastic/Protobufs/mesh.pb.swift index 05e93acc..d168c267 100644 --- a/Meshtastic/Protobufs/mesh.pb.swift +++ b/Meshtastic/Protobufs/mesh.pb.swift @@ -1150,10 +1150,14 @@ struct Waypoint { /// Name of the waypoint - max 30 chars var name: String = String() - ///* + /// /// Description of the waypoint - max 100 chars var description_p: String = String() + /// + /// Designator icon for the waypoint in the form of a unicode emoji + var emoji: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -2778,6 +2782,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 5: .same(proto: "locked"), 6: .same(proto: "name"), 7: .same(proto: "description"), + 8: .same(proto: "emoji"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2793,6 +2798,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 5: try { try decoder.decodeSingularBoolField(value: &self.locked) }() case 6: try { try decoder.decodeSingularStringField(value: &self.name) }() case 7: try { try decoder.decodeSingularStringField(value: &self.description_p) }() + case 8: try { try decoder.decodeSingularFixed32Field(value: &self.emoji) }() default: break } } @@ -2820,6 +2826,9 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if !self.description_p.isEmpty { try visitor.visitSingularStringField(value: self.description_p, fieldNumber: 7) } + if self.emoji != 0 { + try visitor.visitSingularFixed32Field(value: self.emoji, fieldNumber: 8) + } try unknownFields.traverse(visitor: &visitor) } @@ -2831,6 +2840,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if lhs.locked != rhs.locked {return false} if lhs.name != rhs.name {return false} if lhs.description_p != rhs.description_p {return false} + if lhs.emoji != rhs.emoji {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index e02ca6da..7aff279e 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -19,8 +19,18 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.mapType = mapViewType mapView.setRegion(region, animated: true) mapView.isRotateEnabled = true + mapView.isPitchEnabled = true + mapView.showsBuildings = true; mapView.addAnnotations(positions) + mapView.showsUserLocation = true + mapView.setUserTrackingMode(.followWithHeading, animated: true) + mapView.showsCompass = true + mapView.showsScale = true + mapView.isScrollEnabled = true mapView.delegate = context.coordinator + let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapMap(sender:))) + mapView.addGestureRecognizer(gestureRecognizer) + return mapView } @@ -33,14 +43,14 @@ struct MapViewSwiftUI: UIViewRepresentable { } final class MapCoordinator: NSObject, MKMapViewDelegate { - + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { switch annotation { case _ as MKClusterAnnotation: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup") - annotationView.markerTintColor = .darkGray + annotationView.markerTintColor = .systemRed return annotationView case _ as PositionEntity: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node") @@ -53,5 +63,11 @@ struct MapViewSwiftUI: UIViewRepresentable { default: return nil } } + @objc func tapMap(sender: UITapGestureRecognizer) { + if sender.state == .ended { + //let locationInMap = sender.location(in: control.mapView) + //let coordinateSet = control.mapView.convert(locationInMap, toCoordinateFrom: control.mapView) + } + } } } diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift new file mode 100644 index 00000000..6ce94398 --- /dev/null +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -0,0 +1,127 @@ +// +// WaypointFormView.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 1/10/23. +// + +import SwiftUI + +struct WaypointFormView: View { + + @Environment(\.dismiss) private var dismiss + @State private var id: Int32? + @State private var name: String = "" + @State private var description: String = "" + @State private var emoji: String = "" + @FocusState private var emojiIsFocused: Bool + @State private var latitude: Double = 0.0 + @State private var longitude: Double = 0.0 + @State private var expire: Date = Date.now.addingTimeInterval(60 * 60) + @State private var locked: Bool = false + + + + var body: some View { + + Form { + + Section(header: Text("Waypoint")) { + Text("Lat/Long ") + Text(" \(String(latitude) + "," + String(longitude))").foregroundColor(Color.gray) + HStack { + Text("Name") + Spacer() + TextField( + "Name", + text: $name + ) + .foregroundColor(Color.gray) + .onChange(of: name, perform: { value in + let totalBytes = name.utf8.count + // Only mess with the value if it is too big + if totalBytes > 30 { + let firstNBytes = Data(name.utf8.prefix(30)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the name back to the last place where it was the right size + name = maxBytesString + } + } + }) + } + HStack { + Text("Description") + Spacer() + TextField( + "Description", + text: $description, + axis: .vertical + ) + .foregroundColor(Color.gray) + .onChange(of: description, perform: { value in + let totalBytes = description.utf8.count + // Only mess with the value if it is too big + if totalBytes > 100 { + let firstNBytes = Data(description.utf8.prefix(100)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the name back to the last place where it was the right size + description = maxBytesString + } + } + }) + } + HStack { + Text("Emoji") + Spacer() + EmojiOnlyTextField(text: $emoji, placeholder: "emoji") + .font(.title) + .focused($emojiIsFocused) + .onChange(of: emoji) { value in + + // If you have anything other than emojis in your string make it empty + if !value.onlyEmojis() { + emoji = "" + } + // If a second emoji is entered delete the first one + if value.count >= 1 { + + if value.count > 1 { + let index = value.index(value.startIndex, offsetBy: 1) + emoji = String(value[index]) + } + emojiIsFocused = false + } + } + + } + Toggle(isOn: $locked) { + Label("Locked", systemImage: "lock") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + DatePicker("Expire", selection: $expire, in: Date.now...) + .datePickerStyle(.compact) + .font(.callout) + } + } + HStack { + Button { + dismiss() + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + + Button { + dismiss() + } label: { + Label("cancel", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + } + } +} diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index bae52138..7ed3b79a 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -19,6 +19,7 @@ struct NodeDetail: View { @State var satsInView = 0 @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false + @State var presentingWaypointForm = false var node: NodeInfoEntity @@ -35,7 +36,10 @@ struct NodeDetail: View { ZStack { let annotations = node.positions?.array as! [PositionEntity] ZStack { - MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType) + MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005) + //MKCoordinateSpan(latitudeDelta: 0.16405544070813249, longitudeDelta: 0.1232528799585566) + + ), mapViewType: mapType) VStack { Spacer() Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ") @@ -129,7 +133,6 @@ struct NodeDetail: View { .symbolRenderingMode(.hierarchical) Text("user").font(.title)+Text(":").font(.title) } - //Text(node.user?.userId ?? "??????").font(.title).foregroundColor(.gray) Text("!\(String(format:"%02x", node.num))") .font(.title).foregroundColor(.gray) } @@ -173,7 +176,6 @@ struct NodeDetail: View { .foregroundColor(.gray) } } - .padding() Divider() } else { @@ -194,7 +196,6 @@ struct NodeDetail: View { .font(.callout).fixedSize() } } - .padding(5) if node.snr > 0 { Divider() @@ -210,19 +211,13 @@ struct NodeDetail: View { .foregroundColor(.gray) .fixedSize() } - .padding(5) } if node.telemetries?.count ?? 0 >= 1 { - let mostRecent = node.telemetries?.lastObject as! TelemetryEntity - Divider() - VStack(alignment: .center) { - BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel)) - if mostRecent.voltage > 0 { Text(String(format: "%.2f", mostRecent.voltage) + " V") @@ -230,14 +225,11 @@ struct NodeDetail: View { .foregroundColor(.gray) .fixedSize() } - } } } Divider() HStack(alignment: .center) { - - VStack { HStack { Image(systemName: "person") @@ -260,7 +252,6 @@ struct NodeDetail: View { Text(String(node.num)).font(.title3).foregroundColor(.gray) } } - .padding(5) Divider() HStack { Image(systemName: "globe") @@ -270,7 +261,7 @@ struct NodeDetail: View { Text("MAC Address: ") Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray) } - .padding([.bottom], 0) + .padding([.bottom], 10) Divider() } @@ -374,15 +365,17 @@ struct NodeDetail: View { } } } - } - .padding(5) - } + } } } - //.offset( y:-40) } .edgesIgnoringSafeArea([.leading, .trailing]) + .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { + + WaypointFormView() + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) + } .navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline) - .padding(.bottom, 10) .navigationBarItems(trailing: ZStack { ConnectedDevice( diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 52a4d8ec..52ef1d8d 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -250,16 +250,10 @@ struct Channels: View { let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) if adminMessageId > 0 { - - // Should show a saved successfully alert once I know that to be true - // for now just disable the button after a successful save. - self.isPresentingEditView = false channelName = "" hasChanges = false - // Would rather send a getChannel but I can't seem serialize it properly yet bleManager.getChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) - //bleManager.sendWantConfig() } } label: { Label("save", systemImage: "square.and.arrow.down") From 67ca7befd82f69dd6759d4571e51faa673853ef0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 11 Jan 2023 18:25:37 -0800 Subject: [PATCH 04/57] Reduce shutdown and reboot time to 5 seconds --- Meshtastic/Helpers/BLEManager.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 58008d9e..5dbd0f8b 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -822,7 +822,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { public func sendShutdown(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() - adminPacket.shutdownSeconds = 10 + adminPacket.shutdownSeconds = 5 var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -845,7 +845,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { public func sendReboot(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() - adminPacket.rebootSeconds = 10 + adminPacket.rebootSeconds = 5 var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedPeripheral.num) @@ -853,7 +853,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Date: Wed, 11 Jan 2023 21:39:14 -0800 Subject: [PATCH 05/57] Get map coordinate from tap gesture --- Meshtastic/Views/Map/MapViewSwiftUI.swift | 34 +++++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 7aff279e..11891735 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -10,12 +10,12 @@ import MapKit struct MapViewSwiftUI: UIViewRepresentable { + let mapView = MKMapView() let positions: [PositionEntity] let region: MKCoordinateRegion let mapViewType: MKMapType func makeUIView(context: Context) -> MKMapView { - let mapView = MKMapView() mapView.mapType = mapViewType mapView.setRegion(region, animated: true) mapView.isRotateEnabled = true @@ -28,9 +28,6 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.showsScale = true mapView.isScrollEnabled = true mapView.delegate = context.coordinator - let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapMap(sender:))) - mapView.addGestureRecognizer(gestureRecognizer) - return mapView } @@ -39,11 +36,22 @@ struct MapViewSwiftUI: UIViewRepresentable { } func makeCoordinator() -> MapCoordinator { - .init() + return Coordinator(self) } - final class MapCoordinator: NSObject, MKMapViewDelegate { - + final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { + + var parent: MapViewSwiftUI + var gRecognizer = UITapGestureRecognizer() + + init(_ parent: MapViewSwiftUI) { + self.parent = parent + super.init() + self.gRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHandler)) + self.gRecognizer.delegate = self + self.parent.mapView.addGestureRecognizer(gRecognizer) + } + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { switch annotation { @@ -63,11 +71,13 @@ struct MapViewSwiftUI: UIViewRepresentable { default: return nil } } - @objc func tapMap(sender: UITapGestureRecognizer) { - if sender.state == .ended { - //let locationInMap = sender.location(in: control.mapView) - //let coordinateSet = control.mapView.convert(locationInMap, toCoordinateFrom: control.mapView) - } + + @objc func tapHandler(_ gesture: UITapGestureRecognizer) { + // Screen Position - CGPoint + let location = gRecognizer.location(in: self.parent.mapView) + // Map Coordinate - CLLocationCoordinate2D + let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) + print(coordinate) } } } From dcbc79af088d55e6e3118a5569a2d740b3dc6123 Mon Sep 17 00:00:00 2001 From: BG6TNB Date: Thu, 12 Jan 2023 16:08:44 +0800 Subject: [PATCH 06/57] finished the Chinese translation for Localizable.strings --- zh-Hans.lproj/Localizable.strings | 118 ++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 22 deletions(-) diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 0d78debb..cfba1be7 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -7,22 +7,32 @@ */ "about"="关于"; "about.meshtastic"="关于 Meshtastic"; -"admin"="Admin"; -"admin.log"="Admin Message Log"; +"admin"="管理员"; +"admin.log"="管理员消息日志"; "ago"="ago"; +"airtime"="广播时间"; "always.on"="常亮"; -"app.settings"="软件设置"; +"app.settings"="通用设置"; "are.you.sure"="是否确认?"; "ascii.capable"="ASCII Capable"; -"available.radios"="可获得的电台"; -"automatic.detection"="自动发现"; +"available.radios"="可用的电台"; +"automatic.detection"="自动识别"; +"battery.level"="电池电量"; +"battery.level.trend"="电池电量趋势"; "ble.name"="蓝牙名称"; +"ble.connection.timeout %d %@"="尝试连接%@失败,你可能需要在系统设置的蓝牙选项中忽略该电台。"; +"ble.errorcode.6 %@"="%@ 如果在首选电台的旁边,App 将会自动重连。"; +"ble.errorcode.14 %@"="%@ 这个错误通常无法自动修复,你需要在系统设置的蓝牙选项中忽略该电台并重新配对。"; +"ble.errorcode.pin %@"="%@ 请再次尝试连接并仔细检查 PIN 码。"; "bluetooth"="蓝牙"; +"bluetooth.off"="蓝牙已关闭"; "bluetooth.config"="蓝牙配置"; "bluetooth.mode.randompin"="随机 PIN 码"; "bluetooth.mode.fixedpin"="固定 PIN 码"; -"bluetooth.mode.nopin"="不使用 PIN 码(直接使用)"; -"bytes"="Bytes"; +"bluetooth.mode.nopin"="不使用 PIN 码(直接配对)"; +"bluetooth.pairingmode"="配对模式"; +"bluetooth.pin.validation"="蓝牙 PIN 码必须是 6 位数字。"; +"bytes"="字节"; "cancel"="取消"; "canned.messages"="快捷消息"; "canned.messages.config"="快捷消息配置"; @@ -33,18 +43,25 @@ "channel.role.disabled"="禁用"; "channel.role.primary"="主要"; "channel.role.secondary"="次要"; +"channel.utilization"="频道利用率"; "channels"="频道"; "clear.app.data"="清除 App 数据"; +"clear.log"="清除日志"; +"close"="关闭"; +"config.save.confirm"="电台将会在配置保存后重启。"; "connected.radio"="连接到电台"; -"communicating"="与电台进行通讯中. ."; +"communicating"="与电台进行通讯中..."; "connected"="已连接到电台"; -"connecting"="连接中. ."; +"connecting"="连接中..."; "contacts"="联系人"; "copy"="复制"; +"current"="当前"; "default"="默认"; "delete"="删除"; "device"="电台"; "device.config"="电台配置"; +"device.metrics.delete"="删除所有电台指标?"; +"device.metrics.log"="电台指标日志"; "device.role.client"="标准模式 - App 可以连接到电台进行收发操作,并且会自动转发 Mesh 网络中其他节点的消息。"; "device.role.clientmute"="静默模式 - 与标准模式类似,App 可以连接到电台进行收发操作,但不会转发 Mesh 网络中其他节点的消息。"; "device.role.router"="纯中继模式 - 自动转发 Mesh 网络中其他节点的消息,中继模式下屏幕会熄灭,Wi-Fi 和蓝牙将会进入睡眠模式,App 将无法连接到电台进行收发操作。"; @@ -56,20 +73,26 @@ "distance"="距离"; "disconnect"="断开连接"; "echo"="回声"; -"email.address"="电子邮件"; +"email.address"="邮件地址"; "enabled"="启用"; +"encrypted"="加密"; "external.notification"="外部通知"; "external.notification.config"="外部通知配置"; "firmware.version"="固件版本"; +"firmware.version.unsupported"="检测到不支持的固件版本,无法连接到电台。"; +"gas"="Gas"; +"gas.resistance"="Gas Resistance"; +"generate.qr.code"="生成二维码"; "gpsformat.dec"="十进制"; "gpsformat.dms"="度分秒"; -"gpsformat.utm"="Universal Transverse Mercator"; -"gpsformat.mgrs"="Military Grid Reference System"; -"gpsformat.olc"="Open Location Code (aka Plus Codes)"; -"gpsformat.osgr"="Ordnance Survey Grid Reference"; +"gpsformat.utm"="通用横轴墨卡托投影"; +"gpsformat.mgrs"="军事网格参考系统"; +"gpsformat.olc"="开放的位置代码(又称加码)"; +"gpsformat.osgr"="英国国土测量局网格参考"; "heard"="收到"; "heard.last"="最后收到"; "hybrid"="混合"; +"include"="包含"; "inputevent.none"="无"; "inputevent.up"="上"; "inputevent.down"="下"; @@ -80,6 +103,8 @@ "inputevent.cancel"="取消"; "interval.one.second"="一秒"; "interval.two.seconds"="两秒"; +"interval.three.seconds"="三秒"; +"interval.four.seconds"="四秒"; "interval.five.seconds"="五秒"; "interval.ten.seconds"="十秒"; "interval.fifteen.seconds"="十五秒"; @@ -93,16 +118,56 @@ "interval.fifteen.minutes"="十五分钟"; "interval.thirty.minutes"="三十分钟"; "interval.one.hour"="一小时"; +"interval.two.hours"="两小时"; +"interval.three.hours"="三小时"; +"interval.four.hours"="四小时"; +"interval.five.hours"="五小时"; "interval.six.hours"="六小时"; "interval.twelve.hours"="十二小时"; +"interval.eighteen.hours"="十八小时"; "interval.twentyfour.hours"="二十四小时"; +"interval.thirtysix.hours"="三十六小时"; +"interval.tyeight.hours"="四十八小时小时"; +"interval.eventytwo.hours"="七十二小时"; "keyboard.type"="键盘类型"; "logging"="Logging"; "lora"="LoRa"; "lora.config"="LoRa 配置"; "map"="Mesh 地图"; -"map.type"="Map 类型"; +"map.type"="地图类型"; "mesh.log"="Mesh 日志"; +"mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; +"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; +"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; +"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@"; +"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d"; +"mesh.log.channel.received %d %@"="Channel %d received from: %@"; +"mesh.log.device.config %@"="Device config received: %@"; +"mesh.log.display.config %@"="Display config received: %@"; +"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@"; +"mesh.log.externalnotification.config %@"="External Notifiation module config received: %@"; +"mesh.log.lora.config %@"="LoRa config received: %@"; +"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@"; +"mesh.log.mqtt.config %@"="MQTT module config received: %@"; +"mesh.log.myinfo %@"="MyInfo received: %@"; +"mesh.log.network.config %@"="Network config received: %@"; +"mesh.log.nodeinfo.received %@"="Node info received for: %@"; +"mesh.log.position.config %@"="Positon config received: %@"; +"mesh.log.position.received %@"="Position Packet received from node: %@"; +"mesh.log.rangetest.config %@"="Range Test module config received: %@"; +"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; +"mesh.log.serial.config %@"="Serial module config received: %@"; +"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; +"mesh.log.telemetry.config %@"="Telemetry module config received: %@"; +"mesh.log.telemetry.received %@"="Telemetry received for: %@"; +"mesh.log.textmessage.received"="Message received from the text message app."; +"mesh.log.textmessage.send.failed %@"="Message Send Failed, not properly connected to %@"; +"mesh.log.textmessage.sent %@ %@ %@"="Sent message %@ from %@ to %@"; +"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly."; +"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@"; +"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; +"mesh.log.wantconfig %@"="Issuing Want Config to %@"; +"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; "message"="消息"; "message.details"="消息详情"; "messages"="消息"; @@ -123,19 +188,22 @@ "options"="选项"; "password"="密码"; "phone.gps"="手机 GPS"; -"phone.gps.interval.description"="手机向电台刷新定位的时间间隔,但刷新 Mesh 网络中的定位的时间间隔由电台控制。"; +"phone.gps.interval.description"="电台通过手机刷新定位的时间间隔,但是向 Mesh 网络中刷新定位的时间间隔由电台控制。"; "position"="定位"; "position.config"="定位配置"; "preferred.radio"="首选电台"; "provide.location"="提供定位到 Mesh 网络"; "radio.configuration"="电台配置"; -"range.test"="距离测试"; -"range.test.config"="距离测试配置"; +"range.test"="拉距测试"; +"range.test.config"="拉距测试配置"; "reply"="回复"; +"reboot"="重启"; +"reboot.node"="重启节点?"; "received.ack"="收到确认"; +"received.ack.real"="收件人确认"; "routing.acknowledged"="确认"; -"routing.noroute"="未转发"; -"routing.gotnak"="Received a negative acknowledgment"; +"routing.noroute"="找不到目标"; +"routing.gotnak"="收到否认"; "routing.timeout"="超时"; "routing.nointerface"="无连接"; "routing.maxretransmit"="已达到最大重试次数"; @@ -147,6 +215,7 @@ "routing.notauthorized"="未授权"; "satellite"="卫星"; "save"="保存"; +"save.config %@"="保存%@的配置"; "serial"="串口"; "serial.config"="串口配置"; "serial.mode.default"="默认"; @@ -175,9 +244,14 @@ "telemetry"="遥测(传感器)"; "telemetry.config"="遥测配置"; "timeout"="超时"; +"timestamp"="时间戳"; "twitter"="Twitter"; +"unknown"="未知"; "unknown.age"="Unknown Age"; -"update.interval"="刷新间隔"; +"unset"="未设置"; +"update.firmware"="更新你的固件"; +"update.interval"="更新间隔"; "user"="用户"; "user.details"="用户信息"; -"waiting"="等待. . ."; +"voltage"="电压"; +"waiting"="等待中..."; From 3f220269a091037c492f7f2d7326ec75aff4585e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jan 2023 01:27:17 -0800 Subject: [PATCH 07/57] Fix up grid layout for position and metrics logs --- Meshtastic/Views/Map/MapViewSwiftUI.swift | 161 ++++++++++++++++-- Meshtastic/Views/Map/WaypointFormView.swift | 22 ++- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 10 +- .../Views/Nodes/EnvironmentMetricsLog.swift | 10 +- Meshtastic/Views/Nodes/NodeDetail.swift | 18 +- Meshtastic/Views/Nodes/PositionLog.swift | 24 ++- 6 files changed, 185 insertions(+), 60 deletions(-) diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 11891735..14a59fae 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -15,6 +15,15 @@ struct MapViewSwiftUI: UIViewRepresentable { let region: MKCoordinateRegion let mapViewType: MKMapType + + // Offline Maps + //make this view dependent on the UserDefault that is updated when importing a new map file + @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 + @State private var loadedLastUpdatedLocalMapFile = 0 + var customMapOverlay: CustomMapOverlay? + @State private var presentCustomMapOverlayHash: CustomMapOverlay? + var overlays: [Overlay] = [] + func makeUIView(context: Context) -> MKMapView { mapView.mapType = mapViewType mapView.setRegion(region, animated: true) @@ -42,14 +51,15 @@ struct MapViewSwiftUI: UIViewRepresentable { final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { var parent: MapViewSwiftUI - var gRecognizer = UITapGestureRecognizer() + var longPressRecognizer = UILongPressGestureRecognizer() init(_ parent: MapViewSwiftUI) { self.parent = parent super.init() - self.gRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHandler)) - self.gRecognizer.delegate = self - self.parent.mapView.addGestureRecognizer(gRecognizer) + self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler)) + self.longPressRecognizer.minimumPressDuration = 1.0 + self.longPressRecognizer.delegate = self + self.parent.mapView.addGestureRecognizer(longPressRecognizer) } func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { @@ -72,12 +82,143 @@ struct MapViewSwiftUI: UIViewRepresentable { } } - @objc func tapHandler(_ gesture: UITapGestureRecognizer) { - // Screen Position - CGPoint - let location = gRecognizer.location(in: self.parent.mapView) - // Map Coordinate - CLLocationCoordinate2D - let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - print(coordinate) + @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { + if gesture.state == .ended { + // Screen Position - CGPoint + let location = longPressRecognizer.location(in: self.parent.mapView) + // Map Coordinate - CLLocationCoordinate2D + let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) + print(coordinate) + } + } + } + + /// is supposed to be located in the folder with the map name + public struct DefaultTile: Hashable { + let tileName: String + let tileType: String + + public init(tileName: String, tileType: String) { + self.tileName = tileName + self.tileType = tileType + } + } + + public struct CustomMapOverlay: Equatable, Hashable { + let mapName: String + let tileType: String + var canReplaceMapContent: Bool + var minimumZoomLevel: Int? + var maximumZoomLevel: Int? + let defaultTile: DefaultTile? + + public init( + mapName: String, + tileType: String, + canReplaceMapContent: Bool = true, // false for transparent tiles + minimumZoomLevel: Int? = nil, + maximumZoomLevel: Int? = nil, + defaultTile: DefaultTile? = nil + ) { + + self.mapName = mapName + self.tileType = tileType + self.canReplaceMapContent = canReplaceMapContent + self.minimumZoomLevel = minimumZoomLevel + self.maximumZoomLevel = maximumZoomLevel + self.defaultTile = defaultTile + } + + public init?( + mapName: String?, + tileType: String, + canReplaceMapContent: Bool = true, // false for transparent tiles + minimumZoomLevel: Int? = nil, + maximumZoomLevel: Int? = nil, + defaultTile: DefaultTile? = nil + ) { + if (mapName == nil || mapName! == "") { + return nil + } + self.mapName = mapName! + self.tileType = tileType + self.canReplaceMapContent = canReplaceMapContent + self.minimumZoomLevel = minimumZoomLevel + self.maximumZoomLevel = maximumZoomLevel + self.defaultTile = defaultTile + } + } + + public class CustomMapOverlaySource: MKTileOverlay { + + // requires folder: tiles/{mapName}/z/y/y,{tileType} + private var parent: MapView + private let mapName: String + private let tileType: String + private let defaultTile: DefaultTile? + + public init( + parent: MapView, + mapName: String, + tileType: String, + defaultTile: DefaultTile? + ) { + self.parent = parent + self.mapName = mapName + self.tileType = tileType + self.defaultTile = defaultTile + super.init(urlTemplate: "") + } + + public override func url(forTilePath path: MKTileOverlayPath) -> URL { + if let tileUrl = Bundle.main.url( + forResource: "\(path.y)", + withExtension: self.tileType, + subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)", + localization: nil + ) { + return tileUrl + } else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url( + forResource: defaultTile.tileName, + withExtension: defaultTile.tileType, + subdirectory: "tiles/\(self.mapName)", + localization: nil + ) { + return defaultTileUrl + } else { + let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png" + return URL(string: urlstring)! + // Bundle.main.url(forResource: "surrounding", withExtension: "png", subdirectory: "tiles")! + } + + } + + } + + public struct Overlay { + + public static func == (lhs: MapViewSwiftUI.Overlay, rhs: MapViewSwiftUI.Overlay) -> Bool { + // maybe to use in the future for comparison of full array + lhs.shape.coordinate.latitude == rhs.shape.coordinate.latitude && + lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude && + lhs.fillColor == rhs.fillColor + } + + var shape: MKOverlay + var fillColor: UIColor? + var strokeColor: UIColor? + var lineWidth: CGFloat + + public init( + shape: MKOverlay, + fillColor: UIColor? = nil, + strokeColor: UIColor? = nil, + lineWidth: CGFloat = 0 + ) { + self.shape = shape + self.fillColor = fillColor + self.strokeColor = strokeColor + self.lineWidth = lineWidth } } } diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 6ce94398..9c1f4b3e 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -10,23 +10,21 @@ import SwiftUI struct WaypointFormView: View { @Environment(\.dismiss) private var dismiss + @FocusState private var emojiIsFocused: Bool @State private var id: Int32? @State private var name: String = "" @State private var description: String = "" - @State private var emoji: String = "" - @FocusState private var emojiIsFocused: Bool + @State private var emoji: String = "📍" @State private var latitude: Double = 0.0 @State private var longitude: Double = 0.0 - @State private var expire: Date = Date.now.addingTimeInterval(60 * 60) + @State private var expire: Date = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours @State private var locked: Bool = false - - var body: some View { - + Form { - - Section(header: Text("Waypoint")) { + Section(header: Text("Waypoint").font(.title3)) { + Text("Distance Away").foregroundColor(Color.gray) Text("Lat/Long ") + Text(" \(String(latitude) + "," + String(longitude))").foregroundColor(Color.gray) HStack { Text("Name") @@ -70,9 +68,9 @@ struct WaypointFormView: View { }) } HStack { - Text("Emoji") + Text("Icon") Spacer() - EmojiOnlyTextField(text: $emoji, placeholder: "emoji") + EmojiOnlyTextField(text: $emoji, placeholder: "Select an emoji") .font(.title) .focused($emojiIsFocused) .onChange(of: emoji) { value in @@ -111,7 +109,7 @@ struct WaypointFormView: View { .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) - .padding() + .padding(5) Button { dismiss() @@ -121,7 +119,7 @@ struct WaypointFormView: View { .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) - .padding() + .padding(5) } } } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 0925335d..98756ae8 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -78,11 +78,11 @@ struct DeviceMetricsLog: View { ScrollView { let columns = [ - GridItem(), - GridItem(), - GridItem(), - GridItem(), - GridItem(.fixed(140)) + GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 70), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 65), spacing: 0.1), + GridItem(spacing: 0) ] LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { GridRow { diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index e044a08f..3c48b671 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -65,11 +65,11 @@ struct EnvironmentMetricsLog: View { } else { ScrollView { let columns = [ - GridItem(), - GridItem(), - GridItem(), - GridItem(), - GridItem(.fixed(140)) + GridItem(spacing: 0.1), + GridItem(spacing: 0.1), + GridItem(spacing: 0.1), + GridItem(spacing: 0.1), + GridItem(spacing: 0) ] LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 7ed3b79a..c0fbd3e4 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -11,15 +11,12 @@ struct NodeDetail: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - - @State private var mapType: MKMapType = .standard - - @State private var showingDetailsPopover = false - @State var satsInView = 0 + @State private var mapType: MKMapType = .standard + @State private var showingDetailsPopover = false @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false - @State var presentingWaypointForm = false + @State private var presentingWaypointForm = true var node: NodeInfoEntity @@ -36,10 +33,7 @@ struct NodeDetail: View { ZStack { let annotations = node.positions?.array as! [PositionEntity] ZStack { - MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005) - //MKCoordinateSpan(latitudeDelta: 0.16405544070813249, longitudeDelta: 0.1232528799585566) - - ), mapViewType: mapType) + MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType) VStack { Spacer() Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ") @@ -58,7 +52,6 @@ struct NodeDetail: View { } } else { HStack { - } .padding([.top], 20) } @@ -73,7 +66,6 @@ struct NodeDetail: View { Divider() VStack { if node.user != nil { - Image(hwModelString) .resizable() .aspectRatio(contentMode: .fill) @@ -343,9 +335,7 @@ struct NodeDetail: View { } Button(action: { - showingRebootConfirm = true - }) { Label("reboot", systemImage: "arrow.triangle.2.circlepath") diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 2be8210c..1b98a7e5 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -31,10 +31,10 @@ struct PositionLog: View { Text(String(position.seqNo)) } TableColumn("Latitude") { position in - Text(String(format: "%.6f", position.latitude ?? 0)) + Text(String(format: "%.5f", position.latitude ?? 0)) } TableColumn("Longitude") { position in - Text(String(format: "%.6f", position.longitude ?? 0)) + Text(String(format: "%.5f", position.longitude ?? 0)) } TableColumn("Altitude") { position in Text(String(position.altitude)) @@ -61,11 +61,11 @@ struct PositionLog: View { ScrollView { // Use a grid on iOS as a table only shows a single column let columns = [ - GridItem(.fixed(90)), - GridItem(.fixed(95)), - GridItem(.fixed(45)), - GridItem(.fixed(40)), - GridItem(.fixed(140)) + GridItem(spacing: 0.1), + GridItem(spacing: 0.1), + GridItem(.flexible(minimum: 35, maximum: 40), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 35), spacing: 0.1), + GridItem(spacing: 0) ] LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { @@ -89,9 +89,9 @@ struct PositionLog: View { } ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in GridRow { - Text(String(format: "%.6f", mappin.latitude ?? 0)) + Text(String(format: "%.5f", mappin.latitude ?? 0)) .font(.caption2) - Text(String(format: "%.6f", mappin.longitude ?? 0)) + Text(String(format: "%.5f", mappin.longitude ?? 0)) .font(.caption2) Text(String(mappin.satsInView)) .font(.caption2) @@ -102,19 +102,15 @@ struct PositionLog: View { } } } - .padding(.leading, 15) - .padding(.trailing, 5) } + .padding(.leading) } HStack { Button(role: .destructive) { - isPresentingClearLogConfirm = true - } label: { - Label("Clear Log", systemImage: "trash.fill") } .buttonStyle(.bordered) From d5c5c2656ca0cd618cdb88057239e54d38f31fb9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jan 2023 01:30:06 -0800 Subject: [PATCH 08/57] Fix grid on environment metrics --- Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 3c48b671..e65a68a9 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -65,10 +65,10 @@ struct EnvironmentMetricsLog: View { } else { ScrollView { let columns = [ - GridItem(spacing: 0.1), - GridItem(spacing: 0.1), - GridItem(spacing: 0.1), - GridItem(spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), GridItem(spacing: 0) ] LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { From 3b4f504c102a4ca55b85410be6423ae8cf943f5f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jan 2023 07:41:31 -0800 Subject: [PATCH 09/57] Replace comma in CSV and grid date strings --- Meshtastic/Export/WriteCsvFile.swift | 6 +++--- Meshtastic/Views/Map/WaypointFormView.swift | 6 +++--- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 2 +- Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift | 2 +- Meshtastic/Views/Nodes/PositionLog.swift | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index 31d08e9e..7f3bbd0f 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -25,7 +25,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin csvString += ", " csvString += String(dm.airUtilTx) csvString += ", " - csvString += dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "") + csvString += dm.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? NSLocalizedString("unknown.age", comment: "") } } } else if metricsType == 1 { @@ -46,7 +46,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin csvString += ", " csvString += String(dm.current) csvString += ", " - csvString += dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "") + csvString += dm.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? NSLocalizedString("unknown.age", comment: "") } } } @@ -77,7 +77,7 @@ func PositionToCsvFile(positions: [PositionEntity]) -> String { csvString += ", " csvString += String(pos.snr) csvString += ", " - csvString += pos.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "") + csvString += pos.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? NSLocalizedString("unknown.age", comment: "") } return csvString } diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 9c1f4b3e..b1ade61c 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -91,13 +91,13 @@ struct WaypointFormView: View { } } + DatePicker("Expire", selection: $expire, in: Date.now...) + .datePickerStyle(.compact) + .font(.callout) Toggle(isOn: $locked) { Label("Locked", systemImage: "lock") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - DatePicker("Expire", selection: $expire, in: Date.now...) - .datePickerStyle(.compact) - .font(.callout) } } HStack { diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 98756ae8..da8d2df8 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -119,7 +119,7 @@ struct DeviceMetricsLog: View { Text("\(String(format: "%.2f", dm.airUtilTx))%") .font(.caption) - Text(dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) + Text(dm.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? "Unknown time") .font(.caption2) } } diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index e65a68a9..2189e5bb 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -58,7 +58,7 @@ struct EnvironmentMetricsLog: View { } TableColumn("timestamp") { em in if em.metricsType == 1 { - Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) + Text(em.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? "Unknown time") } } } diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 1b98a7e5..09e6f663 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -97,7 +97,7 @@ struct PositionLog: View { .font(.caption2) Text(String(mappin.altitude)) .font(.caption2) - Text(mappin.time?.formattedDate(format: dateFormatString) ?? "Unknown time") + Text(mappin.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? "Unknown time") .font(.caption2) } } From d49a2aab68796ba17df9ed029cb8aab3fd77a237 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jan 2023 07:57:24 -0800 Subject: [PATCH 10/57] Fix date format strings --- Meshtastic/Export/WriteCsvFile.swift | 10 +++++----- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 4 ++-- Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift | 4 ++-- Meshtastic/Views/Nodes/PositionLog.swift | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index 7f3bbd0f..057aaa96 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -10,7 +10,7 @@ import SwiftUI func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> String { var csvString: String = "" let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) - let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma") + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") if metricsType == 0 { // Create Device Metrics Header csvString = "\(NSLocalizedString("battery.level", comment: "")), \(NSLocalizedString("voltage", comment: "")), \(NSLocalizedString("channel.utilization", comment: "")), \(NSLocalizedString("airtime", comment: "")), \(NSLocalizedString("timestamp", comment: ""))" @@ -25,7 +25,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin csvString += ", " csvString += String(dm.airUtilTx) csvString += ", " - csvString += dm.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? NSLocalizedString("unknown.age", comment: "") + csvString += dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "") } } } else if metricsType == 1 { @@ -46,7 +46,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin csvString += ", " csvString += String(dm.current) csvString += ", " - csvString += dm.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? NSLocalizedString("unknown.age", comment: "") + csvString += dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "") } } } @@ -56,7 +56,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin func PositionToCsvFile(positions: [PositionEntity]) -> String { var csvString: String = "" let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) - let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma") + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: " ") // Create Position Header csvString = "SeqNo, Latitude, Longitude, Altitude, Sats, Speed, Heading, SNR, \(NSLocalizedString("timestamp", comment: ""))" for pos in positions { @@ -77,7 +77,7 @@ func PositionToCsvFile(positions: [PositionEntity]) -> String { csvString += ", " csvString += String(pos.snr) csvString += ", " - csvString += pos.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? NSLocalizedString("unknown.age", comment: "") + csvString += pos.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "") } return csvString } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index da8d2df8..c3ba0a1c 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -37,7 +37,7 @@ struct DeviceMetricsLog: View { } } let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) - let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma") + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { //Add a table for mac and ipad Table(node.telemetries!.reversed() as! [TelemetryEntity]) { @@ -119,7 +119,7 @@ struct DeviceMetricsLog: View { Text("\(String(format: "%.2f", dm.airUtilTx))%") .font(.caption) - Text(dm.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? "Unknown time") + Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown time") .font(.caption2) } } diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 2189e5bb..1fc40fa4 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -22,7 +22,7 @@ struct EnvironmentMetricsLog: View { NavigationStack { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) - let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma") + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { //Add a table for mac and ipad Table(node.telemetries!.reversed() as! [TelemetryEntity]) { @@ -58,7 +58,7 @@ struct EnvironmentMetricsLog: View { } TableColumn("timestamp") { em in if em.metricsType == 1 { - Text(em.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? "Unknown time") + Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) } } } diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 09e6f663..b4b67c23 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -22,7 +22,7 @@ struct PositionLog: View { NavigationStack { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) - let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma") + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { //Add a table for mac and ipad @@ -97,7 +97,7 @@ struct PositionLog: View { .font(.caption2) Text(String(mappin.altitude)) .font(.caption2) - Text(mappin.time?.formattedDate(format: dateFormatString).replacingOccurrences(of: ",", with: " ") ?? "Unknown time") + Text(mappin.time?.formattedDate(format: dateFormatString) ?? "Unknown time") .font(.caption2) } } From 1a80b474918df42a5dfff07427adb043df43fac7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jan 2023 08:35:05 -0800 Subject: [PATCH 11/57] Fix crash in PositionEntity extension --- Meshtastic/Persistence/PositionEntityExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Persistence/PositionEntityExtension.swift b/Meshtastic/Persistence/PositionEntityExtension.swift index 7834b93b..ef67f4a7 100644 --- a/Meshtastic/Persistence/PositionEntityExtension.swift +++ b/Meshtastic/Persistence/PositionEntityExtension.swift @@ -42,7 +42,7 @@ extension PositionEntity { } extension PositionEntity: MKAnnotation { - public var coordinate: CLLocationCoordinate2D { nodeCoordinate! } + public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0) } public var title: String? { nodePosition?.user?.shortName ?? NSLocalizedString("unknown", comment: "Unknown") } public var subtitle: String? { time?.formatted() } } From 93ff4b28bb447229929655f1383505c7cc3bf182 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jan 2023 10:51:51 -0800 Subject: [PATCH 12/57] Change location user tracking mode to use less battery --- Meshtastic/Persistence/PositionEntityExtension.swift | 2 +- Meshtastic/Views/Map/MapViewSwiftUI.swift | 2 +- Meshtastic/Views/Nodes/NodeDetail.swift | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Persistence/PositionEntityExtension.swift b/Meshtastic/Persistence/PositionEntityExtension.swift index ef67f4a7..ca7bd3e9 100644 --- a/Meshtastic/Persistence/PositionEntityExtension.swift +++ b/Meshtastic/Persistence/PositionEntityExtension.swift @@ -42,7 +42,7 @@ extension PositionEntity { } extension PositionEntity: MKAnnotation { - public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0) } + public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? LocationHelper.DefaultLocation } public var title: String? { nodePosition?.user?.shortName ?? NSLocalizedString("unknown", comment: "Unknown") } public var subtitle: String? { time?.formatted() } } diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 14a59fae..3dab1e36 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -32,7 +32,7 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.showsBuildings = true; mapView.addAnnotations(positions) mapView.showsUserLocation = true - mapView.setUserTrackingMode(.followWithHeading, animated: true) + mapView.setUserTrackingMode(.none, animated: false) mapView.showsCompass = true mapView.showsScale = true mapView.isScrollEnabled = true diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index c0fbd3e4..e44abb43 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -355,7 +355,9 @@ struct NodeDetail: View { } } } - } } + } + .padding(5) + } } } .edgesIgnoringSafeArea([.leading, .trailing]) From f745197664fb8e7ec4d6d14946751005141f885e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jan 2023 11:01:10 -0800 Subject: [PATCH 13/57] Remove stray space --- Meshtastic/Export/WriteCsvFile.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index 057aaa96..48d8d788 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -56,7 +56,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin func PositionToCsvFile(positions: [PositionEntity]) -> String { var csvString: String = "" let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) - let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: " ") + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") // Create Position Header csvString = "SeqNo, Latitude, Longitude, Altitude, Sats, Speed, Heading, SNR, \(NSLocalizedString("timestamp", comment: ""))" for pos in positions { From e4434c8605accd64848ccda6f53d1593a3b46234 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jan 2023 23:09:47 -0800 Subject: [PATCH 14/57] Hook up waypoint form to long press gesture --- Meshtastic/Views/Helpers/DistanceText.swift | 3 +- Meshtastic/Views/Map/MapViewSwiftUI.swift | 16 +++++-- Meshtastic/Views/Map/WaypointFormView.swift | 46 +++++++++++++++------ Meshtastic/Views/Nodes/NodeDetail.swift | 11 +++-- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/Meshtastic/Views/Helpers/DistanceText.swift b/Meshtastic/Views/Helpers/DistanceText.swift index 6028c1e5..5716ad57 100644 --- a/Meshtastic/Views/Helpers/DistanceText.swift +++ b/Meshtastic/Views/Helpers/DistanceText.swift @@ -12,7 +12,7 @@ import MapKit struct DistanceText: View { var meters: CLLocationDistance - + var body: some View { let distanceFormatter = MKDistanceFormatter() @@ -23,7 +23,6 @@ struct DistanceText_Previews: PreviewProvider { static var previews: some View { VStack { - DistanceText(meters: 100) DistanceText(meters: 1000) DistanceText(meters: 10000) diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 3dab1e36..d7942b22 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -10,6 +10,7 @@ import MapKit struct MapViewSwiftUI: UIViewRepresentable { + var onMarkerTap: (_ waypointCoordinate: CLLocationCoordinate2D? ) -> Void let mapView = MKMapView() let positions: [PositionEntity] let region: MKCoordinateRegion @@ -35,6 +36,7 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.setUserTrackingMode(.none, animated: false) mapView.showsCompass = true mapView.showsScale = true + mapView.isZoomEnabled = true mapView.isScrollEnabled = true mapView.delegate = context.coordinator return mapView @@ -57,7 +59,7 @@ struct MapViewSwiftUI: UIViewRepresentable { self.parent = parent super.init() self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler)) - self.longPressRecognizer.minimumPressDuration = 1.0 + self.longPressRecognizer.minimumPressDuration = 0.2 self.longPressRecognizer.delegate = self self.parent.mapView.addGestureRecognizer(longPressRecognizer) } @@ -83,13 +85,21 @@ struct MapViewSwiftUI: UIViewRepresentable { } @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { - if gesture.state == .ended { + //if gesture.state == .ended { // Screen Position - CGPoint let location = longPressRecognizer.location(in: self.parent.mapView) // Map Coordinate - CLLocationCoordinate2D let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) print(coordinate) - } + + // Add annotation: + let annotation = MKPointAnnotation() + annotation.title = "Dropped Pin" + annotation.coordinate = coordinate + parent.mapView.addAnnotation(annotation) + parent.onMarkerTap(coordinate) + UINotificationFeedbackGenerator().notificationOccurred(.success) + //} } } diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index b1ade61c..2d2884e7 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -6,26 +6,35 @@ // import SwiftUI +import CoreLocation struct WaypointFormView: View { + @State var coordinate: CLLocationCoordinate2D @Environment(\.dismiss) private var dismiss @FocusState private var emojiIsFocused: Bool @State private var id: Int32? @State private var name: String = "" @State private var description: String = "" @State private var emoji: String = "📍" - @State private var latitude: Double = 0.0 - @State private var longitude: Double = 0.0 - @State private var expire: Date = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours + @State private var expires: Bool = false + @State private var expire: Date = Date()// = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours @State private var locked: Bool = false - + var body: some View { - Form { - Section(header: Text("Waypoint").font(.title3)) { - Text("Distance Away").foregroundColor(Color.gray) - Text("Lat/Long ") + Text(" \(String(latitude) + "," + String(longitude))").foregroundColor(Color.gray) + let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) + Section(header: Text("Waypoint")) { + HStack { + Text("Location: \(String(format: "%.5f", coordinate.latitude ) + "," + String(format: "%.5f", coordinate.longitude ))") + .foregroundColor(Color.gray) + .font(.caption2) + if coordinate.latitude != LocationHelper.DefaultLocation.latitude && coordinate.longitude != LocationHelper.DefaultLocation.longitude { + DistanceText(meters: distance) + .foregroundColor(Color.gray) + .font(.caption2) + } + } HStack { Text("Name") Spacer() @@ -91,9 +100,15 @@ struct WaypointFormView: View { } } - DatePicker("Expire", selection: $expire, in: Date.now...) - .datePickerStyle(.compact) - .font(.callout) + Toggle(isOn: $expires) { + Label("Expires", systemImage: "clock.badge.xmark") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if expires { + DatePicker("Expire", selection: $expire, in: Date.now...) + .datePickerStyle(.compact) + .font(.callout) + } Toggle(isOn: $locked) { Label("Locked", systemImage: "lock") } @@ -103,13 +118,14 @@ struct WaypointFormView: View { HStack { Button { dismiss() + } label: { Label("save", systemImage: "square.and.arrow.down") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) - .padding(5) + .padding() Button { dismiss() @@ -119,7 +135,11 @@ struct WaypointFormView: View { .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) - .padding(5) + .padding() } } } +//var smiley = "😊" +//var data: NSData = smiley.dataUsingEncoding(NSUTF32LittleEndianStringEncoding, allowLossyConversion: false)! +//var unicode:UInt32 = UInt32() +//data.getBytes(&unicode) diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index e44abb43..39cc2546 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -13,10 +13,11 @@ struct NodeDetail: View { @EnvironmentObject var bleManager: BLEManager @State var satsInView = 0 @State private var mapType: MKMapType = .standard + @State var waypointCoordinate: CLLocationCoordinate2D? @State private var showingDetailsPopover = false @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false - @State private var presentingWaypointForm = true + @State private var presentingWaypointForm = false var node: NodeInfoEntity @@ -33,7 +34,10 @@ struct NodeDetail: View { ZStack { let annotations = node.positions?.array as! [PositionEntity] ZStack { - MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType) + MapViewSwiftUI(onMarkerTap: { coord in + presentingWaypointForm = true + waypointCoordinate = coord + }, positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType) VStack { Spacer() Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ") @@ -362,8 +366,7 @@ struct NodeDetail: View { } .edgesIgnoringSafeArea([.leading, .trailing]) .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { - - WaypointFormView() + WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation) .presentationDetents([.medium, .large]) .presentationDragIndicator(.automatic) } From a751bd1071f0b0b73c862015ad87728ef68ca143 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 13 Jan 2023 07:48:03 -0800 Subject: [PATCH 15/57] Add additional map types to the map picker --- Meshtastic/Views/Map/WaypointFormView.swift | 5 +++-- Meshtastic/Views/Nodes/NodeDetail.swift | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 2d2884e7..7af0dca7 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -2,7 +2,7 @@ // WaypointFormView.swift // Meshtastic // -// Created by Garth Vander Houwen on 1/10/23. +// Copyright Garth Vander Houwen 1/10/23. // import SwiftUI @@ -40,7 +40,8 @@ struct WaypointFormView: View { Spacer() TextField( "Name", - text: $name + text: $name, + axis: .vertical ) .foregroundColor(Color.gray) .onChange(of: name, perform: { value in diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 39cc2546..11162511 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -45,10 +45,14 @@ struct NodeDetail: View { Picker("", selection: $mapType) { Text("Standard").tag(MKMapType.standard) + Text("Muted").tag(MKMapType.mutedStandard) Text("Hybrid").tag(MKMapType.hybrid) + Text("Hybrid Flyover").tag(MKMapType.hybridFlyover) Text("Satellite").tag(MKMapType.satellite) + Text("Sat Flyover").tag(MKMapType.satelliteFlyover) } .pickerStyle(SegmentedPickerStyle()) + .padding(.bottom, 30) } } .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) @@ -341,7 +345,6 @@ struct NodeDetail: View { Button(action: { showingRebootConfirm = true }) { - Label("reboot", systemImage: "arrow.triangle.2.circlepath") } .buttonStyle(.bordered) @@ -349,11 +352,9 @@ struct NodeDetail: View { .controlSize(.large) .padding() .confirmationDialog("are.you.sure", - - isPresented: $showingRebootConfirm + isPresented: $showingRebootConfirm ) { Button("reboot.node", role: .destructive) { - if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) { print("Reboot Failed") } From 71aa8f93d810c9d7b8d2d0a6f2b8e272ed3fd1f2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 13 Jan 2023 09:40:52 -0800 Subject: [PATCH 16/57] Offline maps for node details --- Meshtastic/Views/Map/MapViewSwiftUI.swift | 130 +++++++++++++++++----- Meshtastic/Views/Nodes/NodeDetail.swift | 12 +- 2 files changed, 114 insertions(+), 28 deletions(-) diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index d7942b22..90c732be 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -44,6 +44,34 @@ struct MapViewSwiftUI: UIViewRepresentable { func updateUIView(_ mapView: MKMapView, context: Context) { mapView.mapType = mapViewType + + if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { + mapView.removeOverlays(mapView.overlays) + if self.customMapOverlay != nil { + + let fileManager = FileManager.default + let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! + let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path + if fileManager.fileExists(atPath: tilePath) { + //if let tilePath = Bundle.main.path(forResource: "offline_map", ofType: "mbtiles") { + + print("Loading local map file") + + if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { + + overlay.canReplaceMapContent = false//customMapOverlay.canReplaceMapContent + + mapView.addOverlay(overlay) + } + } else { + print("Couldn't find a local map file to load") + } + } + DispatchQueue.main.async { + self.presentCustomMapOverlayHash = self.customMapOverlay + self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile + } + } } func makeCoordinator() -> MapCoordinator { @@ -55,6 +83,8 @@ struct MapViewSwiftUI: UIViewRepresentable { var parent: MapViewSwiftUI var longPressRecognizer = UILongPressGestureRecognizer() + var overlays: [Overlay] = [] + init(_ parent: MapViewSwiftUI) { self.parent = parent super.init() @@ -62,6 +92,7 @@ struct MapViewSwiftUI: UIViewRepresentable { self.longPressRecognizer.minimumPressDuration = 0.2 self.longPressRecognizer.delegate = self self.parent.mapView.addGestureRecognizer(longPressRecognizer) + self.overlays = [] } func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { @@ -80,26 +111,73 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.markerTintColor = UIColor(.accentColor) annotationView.titleVisibility = .visible return annotationView + case _ as WaypointEntity: + return nil + default: return nil } } @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { - //if gesture.state == .ended { - // Screen Position - CGPoint - let location = longPressRecognizer.location(in: self.parent.mapView) - // Map Coordinate - CLLocationCoordinate2D - let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - print(coordinate) - - // Add annotation: - let annotation = MKPointAnnotation() - annotation.title = "Dropped Pin" - annotation.coordinate = coordinate - parent.mapView.addAnnotation(annotation) - parent.onMarkerTap(coordinate) - UINotificationFeedbackGenerator().notificationOccurred(.success) - //} + // Screen Position - CGPoint + let location = longPressRecognizer.location(in: self.parent.mapView) + // Map Coordinate - CLLocationCoordinate2D + let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) + print(coordinate) + + // Add annotation: + let annotation = MKPointAnnotation() + annotation.title = "Dropped Pin" + annotation.coordinate = coordinate + parent.mapView.addAnnotation(annotation) + parent.onMarkerTap(coordinate) + UINotificationFeedbackGenerator().notificationOccurred(.success) + } + + public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + + if let index = self.overlays.firstIndex(where: { overlay_ in overlay_.shape.hash == overlay.hash }) { + + let unwrappedOverlay = self.overlays[index] + if let circleOverlay = unwrappedOverlay.shape as? MKCircle { + let renderer = MKCircleRenderer(circle: circleOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + } else if let polygonOverlay = unwrappedOverlay.shape as? MKPolygon { + let renderer = MKPolygonRenderer(polygon: polygonOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + } else if let multiPolygonOverlay = unwrappedOverlay.shape as? MKMultiPolygon { + let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygonOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + } else if let polyLineOverlay = unwrappedOverlay.shape as? MKPolyline { + let renderer = MKPolylineRenderer(polyline: polyLineOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + } else if let multiPolylineOverlay = unwrappedOverlay.shape as? MKMultiPolyline { + let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolylineOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + } else { + return MKOverlayRenderer() + } + + } else if let tileOverlay = overlay as? MKTileOverlay { + return MKTileOverlayRenderer(tileOverlay: tileOverlay) + } else { + return MKOverlayRenderer() + } } } @@ -107,13 +185,13 @@ struct MapViewSwiftUI: UIViewRepresentable { public struct DefaultTile: Hashable { let tileName: String let tileType: String - + public init(tileName: String, tileType: String) { self.tileName = tileName self.tileType = tileType } } - + public struct CustomMapOverlay: Equatable, Hashable { let mapName: String let tileType: String @@ -121,7 +199,7 @@ struct MapViewSwiftUI: UIViewRepresentable { var minimumZoomLevel: Int? var maximumZoomLevel: Int? let defaultTile: DefaultTile? - + public init( mapName: String, tileType: String, @@ -158,15 +236,15 @@ struct MapViewSwiftUI: UIViewRepresentable { self.defaultTile = defaultTile } } - + public class CustomMapOverlaySource: MKTileOverlay { - + // requires folder: tiles/{mapName}/z/y/y,{tileType} private var parent: MapView private let mapName: String private let tileType: String private let defaultTile: DefaultTile? - + public init( parent: MapView, mapName: String, @@ -179,7 +257,7 @@ struct MapViewSwiftUI: UIViewRepresentable { self.defaultTile = defaultTile super.init(urlTemplate: "") } - + public override func url(forTilePath path: MKTileOverlayPath) -> URL { if let tileUrl = Bundle.main.url( forResource: "\(path.y)", @@ -198,11 +276,9 @@ struct MapViewSwiftUI: UIViewRepresentable { } else { let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png" return URL(string: urlstring)! - // Bundle.main.url(forResource: "surrounding", withExtension: "png", subdirectory: "tiles")! } - + } - } public struct Overlay { @@ -213,12 +289,12 @@ struct MapViewSwiftUI: UIViewRepresentable { lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude && lhs.fillColor == rhs.fillColor } - + var shape: MKOverlay var fillColor: UIColor? var strokeColor: UIColor? var lineWidth: CGFloat - + public init( shape: MKOverlay, fillColor: UIColor? = nil, diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 11162511..8278b6c6 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -18,6 +18,13 @@ struct NodeDetail: View { @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false @State private var presentingWaypointForm = false + @State private var showOverlays: Bool = true + @State private var overlays: [MapViewSwiftUI.Overlay] = [] + @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( + mapName: "offlinemap", + tileType: "png", + canReplaceMapContent: true + ) var node: NodeInfoEntity @@ -37,7 +44,10 @@ struct NodeDetail: View { MapViewSwiftUI(onMarkerTap: { coord in presentingWaypointForm = true waypointCoordinate = coord - }, positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType) + }, positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, + customMapOverlay: self.customMapOverlay, + overlays: self.overlays + ) VStack { Spacer() Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ") From 9157ca5d1732c06b1f5d6db3ff1ce70638538269 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 13 Jan 2023 15:49:25 -0800 Subject: [PATCH 17/57] Switch mesh map to new control, comment out old map view --- Meshtastic/Views/Map/MapViewModule.swift | 1230 ++++++++------------- Meshtastic/Views/Map/MapViewSwiftUI.swift | 40 +- Meshtastic/Views/Nodes/NodeMap.swift | 124 +-- 3 files changed, 553 insertions(+), 841 deletions(-) diff --git a/Meshtastic/Views/Map/MapViewModule.swift b/Meshtastic/Views/Map/MapViewModule.swift index bb3bfa82..4f782173 100644 --- a/Meshtastic/Views/Map/MapViewModule.swift +++ b/Meshtastic/Views/Map/MapViewModule.swift @@ -1,767 +1,469 @@ +//// +//// MapView.swift +//// MapViewTest +//// +//// Created by Cem Yilmaz on 05.07.21. +//// +//import SwiftUI +//import MapKit +//import CoreData // -// MapView.swift -// MapViewTest +//#if canImport(MapKit) && canImport(UIKit) +//public struct MapView: UIViewRepresentable { // -// Created by Cem Yilmaz on 05.07.21. +// @Environment(\.managedObjectContext) var context +// +// //var context: NSManagedObjectContext? +// +// //@Binding private var region: MKCoordinateRegion +// +// //make this view dependent on the UserDefault that is updated when importing a new map file +// @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 +// @State private var loadedLastUpdatedLocalMapFile = 0 +// +// private var customMapOverlay: CustomMapOverlay? +// @State private var presentCustomMapOverlayHash: CustomMapOverlay? +// +// private var mapType: MKMapType +// +// private var showZoomScale: Bool +// private var zoomEnabled: Bool +// private var zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?) +// +// private var scrollEnabled: Bool +// private var scrollBoundaries: MKCoordinateRegion? +// +// private var rotationEnabled: Bool +// private var showCompassWhenRotated: Bool +// +// private var showUserLocation: Bool +// private var userTrackingMode: MKUserTrackingMode +// @Binding private var userLocation: CLLocationCoordinate2D? +// +// private var overlays: [Overlay] +// +// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], animation: .default) +// private var positions: FetchedResults +// +// public init( +// customMapOverlay: CustomMapOverlay? = nil, +// mapType: String = "hybrid", +// zoomEnabled: Bool = true, +// showZoomScale: Bool = false, +// zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?) = (nil, nil), +// scrollEnabled: Bool = true, +// scrollBoundaries: MKCoordinateRegion? = nil, +// rotationEnabled: Bool = true, +// showCompassWhenRotated: Bool = true, +// showUserLocation: Bool = true, +// userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none, +// userLocation: Binding = .constant(nil), +// overlays: [Overlay] = [] +// ) { +// self.customMapOverlay = customMapOverlay +// +// switch mapType { +// case "satellite": +// self.mapType = .satellite +// break +// case "standard": +// self.mapType = .standard +// break +// case "hybrid": +// self.mapType = .hybrid +// break +// default: +// self.mapType = .hybrid +// } +// +// self.showZoomScale = showZoomScale +// self.zoomEnabled = zoomEnabled +// self.zoomRange = zoomRange +// +// self.scrollEnabled = scrollEnabled +// self.scrollBoundaries = scrollBoundaries +// +// self.rotationEnabled = rotationEnabled +// self.showCompassWhenRotated = showCompassWhenRotated +// +// self.showUserLocation = showUserLocation +// self.userTrackingMode = userTrackingMode +// self._userLocation = userLocation +// +// self.overlays = overlays +// +// } +// +// public func makeUIView(context: Context) -> MKMapView { +// let mapView = MKMapView() +// mapView.delegate = context.coordinator +// mapView.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self)) +// +// return mapView +// } +// +// +// public func updateUIView(_ mapView: MKMapView, context: Context) { +// +// if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { +// mapView.removeOverlays(mapView.overlays) +// if let customMapOverlay = self.customMapOverlay { +// +// let fileManager = FileManager.default +// let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! +// let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path +// if fileManager.fileExists(atPath: tilePath) { +// //if let tilePath = Bundle.main.path(forResource: "offline_map", ofType: "mbtiles") { +// +// print("Loading local map file") +// +// if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { +// +// overlay.canReplaceMapContent = false//customMapOverlay.canReplaceMapContent +// +// mapView.addOverlay(overlay) +// } +// } else { +// print("Couldn't find a local map file to load") +// } +// } +// DispatchQueue.main.async { +// self.presentCustomMapOverlayHash = self.customMapOverlay +// self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile +// } +// } +// +// if mapView.mapType != self.mapType { +// mapView.mapType = self.mapType +// } +// +// mapView.showsScale = self.zoomEnabled ? self.showZoomScale : false +// +// if mapView.isZoomEnabled != self.zoomEnabled { +// mapView.isZoomEnabled = self.zoomEnabled +// } +// +// if mapView.cameraZoomRange.minCenterCoordinateDistance != self.zoomRange.minHeight ?? 0 || +// mapView.cameraZoomRange.maxCenterCoordinateDistance != self.zoomRange.maxHeight ?? .infinity { +// mapView.cameraZoomRange = MKMapView.CameraZoomRange( +// minCenterCoordinateDistance: self.zoomRange.minHeight ?? 0, +// maxCenterCoordinateDistance: self.zoomRange.maxHeight ?? .infinity +// ) +// } +// +// mapView.isScrollEnabled = self.userTrackingMode == MKUserTrackingMode.none ? self.scrollEnabled : false +// +// if let scrollBoundary = self.scrollBoundaries, (mapView.cameraBoundary?.region.center.latitude != scrollBoundary.center.latitude || mapView.cameraBoundary?.region.center.longitude != scrollBoundary.center.longitude || mapView.cameraBoundary?.region.span.latitudeDelta != scrollBoundary.span.latitudeDelta || mapView.cameraBoundary?.region.span.longitudeDelta != scrollBoundary.span.longitudeDelta) { +// mapView.cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: scrollBoundary) +// } else if self.scrollBoundaries == nil && mapView.cameraBoundary != nil { +// mapView.cameraBoundary = nil +// } +// +// mapView.isRotateEnabled = self.userTrackingMode != .followWithHeading ? self.rotationEnabled : false +// mapView.showsCompass = self.userTrackingMode != .followWithHeading ? self.showCompassWhenRotated : false +// +// if mapView.showsUserLocation != self.showUserLocation { +// mapView.showsUserLocation = self.showUserLocation +// } +// +// if mapView.userTrackingMode != self.userTrackingMode { +// mapView.userTrackingMode = self.userTrackingMode +// } +// +// // clear any existing annotations +// var shouldMoveRegion = false +// if !mapView.annotations.isEmpty { +// mapView.removeAnnotations(mapView.annotations) +// } else { +// shouldMoveRegion = true +// } +// +// var displayedNodes: [Int64] = [] +// for position in self.positions { +// if position.nodePosition == nil || displayedNodes.contains(position.nodePosition!.num) || position.coordinate == nil { +// continue +// } +// +// let annotation = PositionAnnotation() +// annotation.coordinate = position.nodeCoordinate! +// annotation.title = position.nodePosition!.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") +// annotation.shortName = position.nodePosition!.user?.shortName?.uppercased() ?? "???" // -import SwiftUI -import MapKit -import CoreData - -#if canImport(MapKit) && canImport(UIKit) -public struct MapView: UIViewRepresentable { - - @Environment(\.managedObjectContext) var context - - //var context: NSManagedObjectContext? - - //@Binding private var region: MKCoordinateRegion - - //make this view dependent on the UserDefault that is updated when importing a new map file - @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 - @State private var loadedLastUpdatedLocalMapFile = 0 - - private var customMapOverlay: CustomMapOverlay? - @State private var presentCustomMapOverlayHash: CustomMapOverlay? - - private var mapType: MKMapType - - private var showZoomScale: Bool - private var zoomEnabled: Bool - private var zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?) - - private var scrollEnabled: Bool - private var scrollBoundaries: MKCoordinateRegion? - - private var rotationEnabled: Bool - private var showCompassWhenRotated: Bool - - private var showUserLocation: Bool - private var userTrackingMode: MKUserTrackingMode - @Binding private var userLocation: CLLocationCoordinate2D? - - //private var annotations: [MKPointAnnotation] - - private var overlays: [Overlay] - - //@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], animation: .default) - // private var locationNodes: FetchedResults - - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], animation: .default) - private var positions: FetchedResults - - //@State private var locationNodes: [NodeInfoEntity] - - public init( - //region: Binding = .constant(MKCoordinateRegion()), - customMapOverlay: CustomMapOverlay? = nil, - //mapType: MKMapType = MKMapType.standard, - mapType: String = "hybrid", - zoomEnabled: Bool = true, - showZoomScale: Bool = false, - zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?) = (nil, nil), - scrollEnabled: Bool = true, - scrollBoundaries: MKCoordinateRegion? = nil, - rotationEnabled: Bool = true, - showCompassWhenRotated: Bool = true, - showUserLocation: Bool = true, - userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none, - userLocation: Binding = .constant(nil), - //annotations: [MKPointAnnotation] = [], - //locationNodes: [NodeInfoEntity] = [], - overlays: [Overlay] = [] - //context: NSManagedObjectContext? = nil - ) { - //self._region = region - - self.customMapOverlay = customMapOverlay - - switch mapType { - case "satellite": - self.mapType = .satellite - break - case "standard": - self.mapType = .standard - break - case "hybrid": - self.mapType = .hybrid - break - default: - self.mapType = .hybrid - } - //self.mapType = mapType - - self.showZoomScale = showZoomScale - self.zoomEnabled = zoomEnabled - self.zoomRange = zoomRange - - self.scrollEnabled = scrollEnabled - self.scrollBoundaries = scrollBoundaries - - self.rotationEnabled = rotationEnabled - self.showCompassWhenRotated = showCompassWhenRotated - - self.showUserLocation = showUserLocation - self.userTrackingMode = userTrackingMode - self._userLocation = userLocation - - //self.annotations = annotations - - //self.locationNodes = locationNodes - - self.overlays = overlays - - } - - public func makeUIView(context: Context) -> MKMapView { - let mapView = MKMapView() - mapView.delegate = context.coordinator - mapView.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self)) - - return mapView - } - - - public func updateUIView(_ mapView: MKMapView, context: Context) { - - //if self.userTrackingMode == MKUserTrackingMode.none && (mapView.region.center.latitude != self.region.center.latitude || mapView.region.center.longitude != self.region.center.longitude) { - //mapView.region = self.region - //} - - if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { - mapView.removeOverlays(mapView.overlays) - if let customMapOverlay = self.customMapOverlay { - - let fileManager = FileManager.default - let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path - if fileManager.fileExists(atPath: tilePath) { - //if let tilePath = Bundle.main.path(forResource: "offline_map", ofType: "mbtiles") { - - print("Loading local map file") - - if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { - - overlay.canReplaceMapContent = false//customMapOverlay.canReplaceMapContent - - mapView.addOverlay(overlay) - } - } else { - print("Couldn't find a local map file to load") - } - } - DispatchQueue.main.async { - self.presentCustomMapOverlayHash = self.customMapOverlay - self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile - } - } - - /*if mapView.overlays.count != (self.overlays.count + (self.customMapOverlay == nil ? 0 : 1)) { - context.coordinator.overlays = self.overlays - mapView.overlays.forEach { overlay in - if !(overlay is MKTileOverlay) { - mapView.removeOverlay(overlay) - } - } - mapView.addOverlays(self.overlays.map { overlay in overlay.shape }) - }*/ - - if mapView.mapType != self.mapType { - mapView.mapType = self.mapType - } - - mapView.showsScale = self.zoomEnabled ? self.showZoomScale : false - - if mapView.isZoomEnabled != self.zoomEnabled { - mapView.isZoomEnabled = self.zoomEnabled - } - - if mapView.cameraZoomRange.minCenterCoordinateDistance != self.zoomRange.minHeight ?? 0 || - mapView.cameraZoomRange.maxCenterCoordinateDistance != self.zoomRange.maxHeight ?? .infinity { - mapView.cameraZoomRange = MKMapView.CameraZoomRange( - minCenterCoordinateDistance: self.zoomRange.minHeight ?? 0, - maxCenterCoordinateDistance: self.zoomRange.maxHeight ?? .infinity - ) - } - - mapView.isScrollEnabled = self.userTrackingMode == MKUserTrackingMode.none ? self.scrollEnabled : false - - if let scrollBoundary = self.scrollBoundaries, (mapView.cameraBoundary?.region.center.latitude != scrollBoundary.center.latitude || mapView.cameraBoundary?.region.center.longitude != scrollBoundary.center.longitude || mapView.cameraBoundary?.region.span.latitudeDelta != scrollBoundary.span.latitudeDelta || mapView.cameraBoundary?.region.span.longitudeDelta != scrollBoundary.span.longitudeDelta) { - mapView.cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: scrollBoundary) - } else if self.scrollBoundaries == nil && mapView.cameraBoundary != nil { - mapView.cameraBoundary = nil - } - - mapView.isRotateEnabled = self.userTrackingMode != .followWithHeading ? self.rotationEnabled : false - mapView.showsCompass = self.userTrackingMode != .followWithHeading ? self.showCompassWhenRotated : false - - if mapView.showsUserLocation != self.showUserLocation { - mapView.showsUserLocation = self.showUserLocation - } - - if mapView.userTrackingMode != self.userTrackingMode { - mapView.userTrackingMode = self.userTrackingMode - } - - //if mapView.annotations.filter({ annotation in !(annotation is MKUserLocation) }).count != self.annotations.count { - // mapView.removeAnnotations(mapView.annotations) - // mapView.addAnnotations(self.annotations) - //} - - // clear any existing annotations - var shouldMoveRegion = false - if !mapView.annotations.isEmpty { - mapView.removeAnnotations(mapView.annotations) - } else { - shouldMoveRegion = true - } - - var displayedNodes: [Int64] = [] - for position in self.positions { - if position.nodePosition == nil || displayedNodes.contains(position.nodePosition!.num) || position.coordinate == nil { - continue - } - - let annotation = PositionAnnotation() - annotation.coordinate = position.nodeCoordinate! - annotation.title = position.nodePosition!.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") - annotation.shortName = position.nodePosition!.user?.shortName?.uppercased() ?? "???" - - mapView.addAnnotation(annotation) - - displayedNodes.append(position.nodePosition!.num) - } - - if shouldMoveRegion { - self.moveToMeshRegion(mapView) - } - - - } - - func moveToMeshRegion(_ mapView: MKMapView) { - //go through the annotations and create a bounding box that encloses them - - var minLat: CLLocationDegrees = 90.0 - var maxLat: CLLocationDegrees = -90.0 - var minLon: CLLocationDegrees = 180.0 - var maxLon: CLLocationDegrees = -180.0 - - for annotation in mapView.annotations { - if annotation.isKind(of: PositionAnnotation.self) { - minLat = min(minLat, annotation.coordinate.latitude) - maxLat = max(maxLat, annotation.coordinate.latitude) - minLon = min(minLon, annotation.coordinate.longitude) - maxLon = max(maxLon, annotation.coordinate.longitude) - } - } - - //check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location) - if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 { - return - } else if minLat == maxLat && minLon == maxLon { - //then we are focussed on a single point (probably because there is only one node with a position) - //widen that out a little (don't zoom way in to that point) - - //0.001 degrees latitude is about 100m - //the mapView.regionThatFits call below will expand this out to a rectangle - minLat = minLat - 0.001 - maxLat = maxLat + 0.001 - } - - let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2) - - let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5) - - let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span)) - - mapView.setRegion(region, animated: true) - - - } - - public func makeCoordinator() -> Coordinator { - Coordinator(parent: self) - } - - public class Coordinator: NSObject, MKMapViewDelegate { - - private var parent: MapView - public var overlays: [Overlay] = [] - - init(parent: MapView) { - self.parent = parent - } - - /*public func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { - DispatchQueue.main.async { - self.parent.region = mapView.region - } - }*/ - - public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - - guard !annotation.isKind(of: MKUserLocation.self) else { - // Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize. - return nil - } - //var annotationView: MKAnnotationView? - - if let annotation = annotation as? PositionAnnotation { - - let annotationView = PositionAnnotationView(annotation: annotation, reuseIdentifier: "PositionAnnotation") - annotationView.name = annotation.shortName ?? "????" - annotationView.canShowCallout = true - - return annotationView - } - - return nil - } - - public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { - - if let index = self.overlays.firstIndex(where: { overlay_ in overlay_.shape.hash == overlay.hash }) { - - let unwrappedOverlay = self.overlays[index] - - if let circleOverlay = unwrappedOverlay.shape as? MKCircle { - - let renderer = MKCircleRenderer(circle: circleOverlay) - renderer.fillColor = unwrappedOverlay.fillColor - renderer.strokeColor = unwrappedOverlay.strokeColor - renderer.lineWidth = unwrappedOverlay.lineWidth - return renderer - - } else if let polygonOverlay = unwrappedOverlay.shape as? MKPolygon { - - let renderer = MKPolygonRenderer(polygon: polygonOverlay) - renderer.fillColor = unwrappedOverlay.fillColor - renderer.strokeColor = unwrappedOverlay.strokeColor - renderer.lineWidth = unwrappedOverlay.lineWidth - return renderer - - } else if let multiPolygonOverlay = unwrappedOverlay.shape as? MKMultiPolygon { - - let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygonOverlay) - renderer.fillColor = unwrappedOverlay.fillColor - renderer.strokeColor = unwrappedOverlay.strokeColor - renderer.lineWidth = unwrappedOverlay.lineWidth - return renderer - - } else if let polyLineOverlay = unwrappedOverlay.shape as? MKPolyline { - - let renderer = MKPolylineRenderer(polyline: polyLineOverlay) - renderer.fillColor = unwrappedOverlay.fillColor - renderer.strokeColor = unwrappedOverlay.strokeColor - renderer.lineWidth = unwrappedOverlay.lineWidth - return renderer - - } else if let multiPolylineOverlay = unwrappedOverlay.shape as? MKMultiPolyline { - - let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolylineOverlay) - renderer.fillColor = unwrappedOverlay.fillColor - renderer.strokeColor = unwrappedOverlay.strokeColor - renderer.lineWidth = unwrappedOverlay.lineWidth - return renderer - - } else { - - return MKOverlayRenderer() - - } - - } else if let tileOverlay = overlay as? MKTileOverlay { - - return MKTileOverlayRenderer(tileOverlay: tileOverlay) - - } else { - - return MKOverlayRenderer() - - } - - } - - - - } - - /// is supposed to be located in the folder with the map name - public struct DefaultTile: Hashable { - let tileName: String - let tileType: String - - public init(tileName: String, tileType: String) { - self.tileName = tileName - self.tileType = tileType - } - } - - public struct CustomMapOverlay: Equatable, Hashable { - let mapName: String - let tileType: String - var canReplaceMapContent: Bool - var minimumZoomLevel: Int? - var maximumZoomLevel: Int? - let defaultTile: DefaultTile? - - public init( - mapName: String, - tileType: String, - canReplaceMapContent: Bool = true, // false for transparent tiles - minimumZoomLevel: Int? = nil, - maximumZoomLevel: Int? = nil, - defaultTile: DefaultTile? = nil - ) { - - self.mapName = mapName - self.tileType = tileType - self.canReplaceMapContent = canReplaceMapContent - self.minimumZoomLevel = minimumZoomLevel - self.maximumZoomLevel = maximumZoomLevel - self.defaultTile = defaultTile - } - - public init?( - mapName: String?, - tileType: String, - canReplaceMapContent: Bool = true, // false for transparent tiles - minimumZoomLevel: Int? = nil, - maximumZoomLevel: Int? = nil, - defaultTile: DefaultTile? = nil - ) { - if (mapName == nil || mapName! == "") { - return nil - } - self.mapName = mapName! - self.tileType = tileType - self.canReplaceMapContent = canReplaceMapContent - self.minimumZoomLevel = minimumZoomLevel - self.maximumZoomLevel = maximumZoomLevel - self.defaultTile = defaultTile - } - } - - public class CustomMapOverlaySource: MKTileOverlay { - - // requires folder: tiles/{mapName}/z/y/y,{tileType} - private var parent: MapView - private let mapName: String - private let tileType: String - private let defaultTile: DefaultTile? - - public init( - parent: MapView, - mapName: String, - tileType: String, - defaultTile: DefaultTile? - ) { - self.parent = parent - self.mapName = mapName - self.tileType = tileType - self.defaultTile = defaultTile - super.init(urlTemplate: "") - } - - public override func url(forTilePath path: MKTileOverlayPath) -> URL { - if let tileUrl = Bundle.main.url( - forResource: "\(path.y)", - withExtension: self.tileType, - subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)", - localization: nil - ) { - return tileUrl - } else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url( - forResource: defaultTile.tileName, - withExtension: defaultTile.tileType, - subdirectory: "tiles/\(self.mapName)", - localization: nil - ) { - return defaultTileUrl - } else { - let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png" - return URL(string: urlstring)! - // Bundle.main.url(forResource: "surrounding", withExtension: "png", subdirectory: "tiles")! - } - - } - - } - - public struct Overlay { - - public static func == (lhs: MapView.Overlay, rhs: MapView.Overlay) -> Bool { - // maybe to use in the future for comparison of full array - lhs.shape.coordinate.latitude == rhs.shape.coordinate.latitude && - lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude && - lhs.fillColor == rhs.fillColor - } - - var shape: MKOverlay - var fillColor: UIColor? - var strokeColor: UIColor? - var lineWidth: CGFloat - - public init( - shape: MKOverlay, - fillColor: UIColor? = nil, - strokeColor: UIColor? = nil, - lineWidth: CGFloat = 0 - ) { - self.shape = shape - self.fillColor = fillColor - self.strokeColor = strokeColor - self.lineWidth = lineWidth - } - } - -} - -// MARK: End of implementation -// MARK: Demonstration -/* -public struct MapViewDemo: View { - - @State private var locationManager: CLLocationManager - - @State private var mapRegion: MKCoordinateRegion = MKCoordinateRegion( - center: CLLocationCoordinate2D( - latitude: -38.758247, - longitude: 175.360208 - ), - span: MKCoordinateSpan( - latitudeDelta: 0.01, - longitudeDelta: 0.01 - ) - ) - - @State private var customMapOverlay: MapView.CustomMapOverlay? - - @State private var mapType: MKMapType = MKMapType.standard - - @State private var zoomEnabled: Bool = true - @State private var showZoomScale: Bool = true - @State private var useMinZoomBoundary: Bool = false - @State private var minZoom: Double = 0 - @State private var useMaxZoomBoundary: Bool = false - @State private var maxZoom: Double = 3000000 - - @State private var scrollEnabled: Bool = true - @State private var useScrollBoundaries: Bool = false - @State private var scrollBoundaries: MKCoordinateRegion = MKCoordinateRegion() - - @State private var rotationEnabled: Bool = true - @State private var showCompassWhenRotated: Bool = true - - @State private var showUserLocation: Bool = true - @State private var userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none - @State private var userLocation: CLLocationCoordinate2D? - - @State private var showAnnotations: Bool = true - @State private var annotations: [MKPointAnnotation] = [] - - @State private var showOverlays: Bool = true - @State private var overlays: [MapView.Overlay] = [] - - @State private var showMapCenter: Bool = false - - public init() { - self.locationManager = CLLocationManager() - self.locationManager.requestWhenInUseAuthorization() - } - - public var body: some View { - - NavigationView { - - List { - - Section(header: Text("Scroll")) { - Toggle("Scroll enabled", isOn: self.$scrollEnabled) - Toggle("Use scroll boundaries", isOn: self.$useScrollBoundaries) - .onChange(of: self.useScrollBoundaries) { newValue in - if newValue { - self.scrollBoundaries = MKCoordinateRegion(center: self.mapRegion.center, span: MKCoordinateSpan()) - } - } - if self.useScrollBoundaries { - VStack(alignment: .leading) { - Text(String(format: "Vertical distance to center: %.2f m", self.scrollBoundaries.span.latitudeDelta * 10609)) - Slider(value: self.$scrollBoundaries.span.latitudeDelta, in: 0...(300/10609)) - } - VStack(alignment: .leading) { - Text(String(format: "Horizontal distance to center: %.2f m", self.self.scrollBoundaries.span.longitudeDelta * 10609)) - Slider(value: self.$scrollBoundaries.span.longitudeDelta, in: 0...(300/10609)) - } - } - } - - Section(header: Text("Zoom")) { - Toggle("Zoom enabled", isOn: self.$zoomEnabled) - Toggle("Show zoom scale", isOn: self.$showZoomScale) - Toggle("Use minimum zoom boundary", isOn: self.$useMinZoomBoundary) - if self.useMinZoomBoundary { - VStack(alignment: .leading) { - Text(String(format: "Minimum Height: %.2f m", self.minZoom)) - Slider(value: self.$minZoom, in: 0...(self.useMaxZoomBoundary ? self.maxZoom : 3000000), step: 10) - } - } - Toggle("Use maximum zoom boundary", isOn: self.$useMaxZoomBoundary) - if self.useMaxZoomBoundary { - VStack(alignment: .leading) { - Text(String(format: "Maximum Height: %.2f m", self.maxZoom)) - Slider(value: self.$maxZoom, in: (self.useMinZoomBoundary ? self.minZoom : 0)...3000000, step: 10) - } - } - } - - Section(header: Text("Rotation")) { - Toggle("Rotation enabled", isOn: self.$rotationEnabled) - Toggle("Show compass when rotated", isOn: self.$showCompassWhenRotated) - } - - Section { - Toggle("Show map Center", isOn: self.$showMapCenter) - } - - Section(header: Text("User Location")) { - Toggle("Show User Location", isOn: self.$showUserLocation) - Picker("Follow Mode", selection: self.$userTrackingMode) { - Text("Nicht folgen").tag(MKUserTrackingMode.none) - Text("Folgen").tag(MKUserTrackingMode.follow) - Text("Richtung folgen").tag(MKUserTrackingMode.followWithHeading) - }.pickerStyle(MenuPickerStyle()) - - } - - Section(header: Text("Annotations")) { - Toggle("Show Annotations", isOn: self.$showAnnotations) - Button("Add Annotation") { - let annotation = MKPointAnnotation() - annotation.coordinate = self.mapRegion.center - annotation.title = "Title" - annotation.subtitle = "Subtitle" - self.annotations.append(annotation) - } - - Button("Delete all") { self.annotations = [] }.foregroundColor(.red) - } - - Section(header: Text("Overlays")) { - Toggle("Show Overlays", isOn: self.$showOverlays) - Button("Add circle") { - self.overlays.append(MapView.Overlay( - shape: MKCircle( - center: self.mapRegion.center, - radius: 20 - ), - strokeColor: UIColor.systemBlue, - lineWidth: 10 - )) - } - - Button("Delete all") { self.overlays = [] }.foregroundColor(.red) - } - - Section(header: Text("Custom Map Overlay")) { - Button("Keine") { self.customMapOverlay = nil } - Button("OSM Online") { - self.customMapOverlay = MapView.CustomMapOverlay( - mapName: "https://tile.openstreetmap.org/", - tileType: "png", - canReplaceMapContent: true - ) - } - Button("Farm Map") { - self.customMapOverlay = MapView.CustomMapOverlay( - mapName: "http://10.147.253.250:5050/local/map/", - tileType: "png", - canReplaceMapContent: true - ) - } - } - - }.listStyle(GroupedListStyle()) - .navigationBarTitle("Map Configuration", displayMode: NavigationBarItem.TitleDisplayMode.inline) - - ZStack { - - MapView( - region: self.$mapRegion, - customMapOverlay: self.customMapOverlay, - mapType: self.mapType, - zoomEnabled: self.zoomEnabled, - showZoomScale: self.showZoomScale, - zoomRange: (minHeight: self.useMinZoomBoundary ? self.minZoom : 0, maxHeight: self.useMaxZoomBoundary ? self.maxZoom : .infinity), - scrollEnabled: self.scrollEnabled, - scrollBoundaries: self.useScrollBoundaries ? self.scrollBoundaries : nil, - rotationEnabled: self.rotationEnabled, - showCompassWhenRotated: self.showCompassWhenRotated, - showUserLocation: self.showUserLocation, - userTrackingMode: self.userTrackingMode, - userLocation: self.$userLocation, - annotations: self.showAnnotations ? self.annotations : [], - overlays: self.showOverlays ? self.overlays : [] - ) - - VStack { - - Spacer() - - HStack { - if let userLocation = self.userLocation, self.showUserLocation { - VStack(alignment: .leading) { - Button("Center user location") { - self.mapRegion.center = userLocation - } - Text("User Location").bold() - Text("\(userLocation.latitude)") - Text("\(userLocation.longitude)") - } - } - - Spacer() - - VStack(alignment: .leading) { - Text("Map Center").bold() - Text("\(self.mapRegion.center.latitude)") - Text("\(self.mapRegion.center.longitude)") - } - } - - Picker("", selection: self.$mapType) { - Text("Standard").tag(MKMapType.standard) - Text("Muted Standard").tag(MKMapType.mutedStandard) - Text("Satellite").tag(MKMapType.satellite) - Text("Satellite Flyover").tag(MKMapType.satelliteFlyover) - Text("Hybrid").tag(MKMapType.hybrid) - Text("Hybrid Flyover").tag(MKMapType.hybridFlyover) - }.pickerStyle(SegmentedPickerStyle()) - - if self.showMapCenter { - Circle().frame(width: 8, height: 8).foregroundColor(.red) - } - - }.padding() - - }.navigationBarTitle("SwiftUI MapView", displayMode: NavigationBarItem.TitleDisplayMode.inline) - .ignoresSafeArea(edges: .bottom) - - } - - } - -} - - -public struct MapView_Previews: PreviewProvider { - - public static var previews: some View { - - MapViewDemo() - - } - -}*/ -#endif +// mapView.addAnnotation(annotation) +// +// displayedNodes.append(position.nodePosition!.num) +// } +// +// if shouldMoveRegion { +// self.moveToMeshRegion(mapView) +// } +// +// +// } +// +// func moveToMeshRegion(_ mapView: MKMapView) { +// //go through the annotations and create a bounding box that encloses them +// +// var minLat: CLLocationDegrees = 90.0 +// var maxLat: CLLocationDegrees = -90.0 +// var minLon: CLLocationDegrees = 180.0 +// var maxLon: CLLocationDegrees = -180.0 +// +// for annotation in mapView.annotations { +// if annotation.isKind(of: PositionAnnotation.self) { +// minLat = min(minLat, annotation.coordinate.latitude) +// maxLat = max(maxLat, annotation.coordinate.latitude) +// minLon = min(minLon, annotation.coordinate.longitude) +// maxLon = max(maxLon, annotation.coordinate.longitude) +// } +// } +// +// //check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location) +// if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 { +// return +// } else if minLat == maxLat && minLon == maxLon { +// //then we are focussed on a single point (probably because there is only one node with a position) +// //widen that out a little (don't zoom way in to that point) +// +// //0.001 degrees latitude is about 100m +// //the mapView.regionThatFits call below will expand this out to a rectangle +// minLat = minLat - 0.001 +// maxLat = maxLat + 0.001 +// } +// +// let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2) +// +// let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5) +// +// let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span)) +// +// mapView.setRegion(region, animated: true) +// } +// +// public func makeCoordinator() -> Coordinator { +// Coordinator(parent: self) +// } +// +// public class Coordinator: NSObject, MKMapViewDelegate { +// +// private var parent: MapView +// public var overlays: [Overlay] = [] +// +// init(parent: MapView) { +// self.parent = parent +// } +// +// public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { +// +// guard !annotation.isKind(of: MKUserLocation.self) else { +// // Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize. +// return nil +// } +// +// if let annotation = annotation as? PositionAnnotation { +// +// let annotationView = PositionAnnotationView(annotation: annotation, reuseIdentifier: "PositionAnnotation") +// annotationView.name = annotation.shortName ?? "????" +// annotationView.canShowCallout = true +// +// return annotationView +// } +// +// return nil +// } +// +// public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { +// +// if let index = self.overlays.firstIndex(where: { overlay_ in overlay_.shape.hash == overlay.hash }) { +// +// let unwrappedOverlay = self.overlays[index] +// +// if let circleOverlay = unwrappedOverlay.shape as? MKCircle { +// +// let renderer = MKCircleRenderer(circle: circleOverlay) +// renderer.fillColor = unwrappedOverlay.fillColor +// renderer.strokeColor = unwrappedOverlay.strokeColor +// renderer.lineWidth = unwrappedOverlay.lineWidth +// return renderer +// +// } else if let polygonOverlay = unwrappedOverlay.shape as? MKPolygon { +// +// let renderer = MKPolygonRenderer(polygon: polygonOverlay) +// renderer.fillColor = unwrappedOverlay.fillColor +// renderer.strokeColor = unwrappedOverlay.strokeColor +// renderer.lineWidth = unwrappedOverlay.lineWidth +// return renderer +// +// } else if let multiPolygonOverlay = unwrappedOverlay.shape as? MKMultiPolygon { +// +// let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygonOverlay) +// renderer.fillColor = unwrappedOverlay.fillColor +// renderer.strokeColor = unwrappedOverlay.strokeColor +// renderer.lineWidth = unwrappedOverlay.lineWidth +// return renderer +// +// } else if let polyLineOverlay = unwrappedOverlay.shape as? MKPolyline { +// +// let renderer = MKPolylineRenderer(polyline: polyLineOverlay) +// renderer.fillColor = unwrappedOverlay.fillColor +// renderer.strokeColor = unwrappedOverlay.strokeColor +// renderer.lineWidth = unwrappedOverlay.lineWidth +// return renderer +// +// } else if let multiPolylineOverlay = unwrappedOverlay.shape as? MKMultiPolyline { +// +// let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolylineOverlay) +// renderer.fillColor = unwrappedOverlay.fillColor +// renderer.strokeColor = unwrappedOverlay.strokeColor +// renderer.lineWidth = unwrappedOverlay.lineWidth +// return renderer +// +// } else { +// +// return MKOverlayRenderer() +// +// } +// +// } else if let tileOverlay = overlay as? MKTileOverlay { +// +// return MKTileOverlayRenderer(tileOverlay: tileOverlay) +// +// } else { +// return MKOverlayRenderer() +// } +// } +// } +// +// /// is supposed to be located in the folder with the map name +// public struct DefaultTile: Hashable { +// let tileName: String +// let tileType: String +// +// public init(tileName: String, tileType: String) { +// self.tileName = tileName +// self.tileType = tileType +// } +// } +// +// public struct CustomMapOverlay: Equatable, Hashable { +// let mapName: String +// let tileType: String +// var canReplaceMapContent: Bool +// var minimumZoomLevel: Int? +// var maximumZoomLevel: Int? +// let defaultTile: DefaultTile? +// +// public init( +// mapName: String, +// tileType: String, +// canReplaceMapContent: Bool = true, // false for transparent tiles +// minimumZoomLevel: Int? = nil, +// maximumZoomLevel: Int? = nil, +// defaultTile: DefaultTile? = nil +// ) { +// +// self.mapName = mapName +// self.tileType = tileType +// self.canReplaceMapContent = canReplaceMapContent +// self.minimumZoomLevel = minimumZoomLevel +// self.maximumZoomLevel = maximumZoomLevel +// self.defaultTile = defaultTile +// } +// +// public init?( +// mapName: String?, +// tileType: String, +// canReplaceMapContent: Bool = true, // false for transparent tiles +// minimumZoomLevel: Int? = nil, +// maximumZoomLevel: Int? = nil, +// defaultTile: DefaultTile? = nil +// ) { +// if (mapName == nil || mapName! == "") { +// return nil +// } +// self.mapName = mapName! +// self.tileType = tileType +// self.canReplaceMapContent = canReplaceMapContent +// self.minimumZoomLevel = minimumZoomLevel +// self.maximumZoomLevel = maximumZoomLevel +// self.defaultTile = defaultTile +// } +// } +// +// public class CustomMapOverlaySource: MKTileOverlay { +// +// // requires folder: tiles/{mapName}/z/y/y,{tileType} +// private var parent: MapView +// private let mapName: String +// private let tileType: String +// private let defaultTile: DefaultTile? +// +// public init( +// parent: MapView, +// mapName: String, +// tileType: String, +// defaultTile: DefaultTile? +// ) { +// self.parent = parent +// self.mapName = mapName +// self.tileType = tileType +// self.defaultTile = defaultTile +// super.init(urlTemplate: "") +// } +// +// public override func url(forTilePath path: MKTileOverlayPath) -> URL { +// if let tileUrl = Bundle.main.url( +// forResource: "\(path.y)", +// withExtension: self.tileType, +// subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)", +// localization: nil +// ) { +// return tileUrl +// } else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url( +// forResource: defaultTile.tileName, +// withExtension: defaultTile.tileType, +// subdirectory: "tiles/\(self.mapName)", +// localization: nil +// ) { +// return defaultTileUrl +// } else { +// let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png" +// return URL(string: urlstring)! +// // Bundle.main.url(forResource: "surrounding", withExtension: "png", subdirectory: "tiles")! +// } +// +// } +// +// } +// +// public struct Overlay { +// +// public static func == (lhs: MapView.Overlay, rhs: MapView.Overlay) -> Bool { +// // maybe to use in the future for comparison of full array +// lhs.shape.coordinate.latitude == rhs.shape.coordinate.latitude && +// lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude && +// lhs.fillColor == rhs.fillColor +// } +// +// var shape: MKOverlay +// var fillColor: UIColor? +// var strokeColor: UIColor? +// var lineWidth: CGFloat +// +// public init( +// shape: MKOverlay, +// fillColor: UIColor? = nil, +// strokeColor: UIColor? = nil, +// lineWidth: CGFloat = 0 +// ) { +// self.shape = shape +// self.fillColor = fillColor +// self.strokeColor = strokeColor +// self.lineWidth = lineWidth +// } +// } +// +//} +// +//// MARK: End of implementation +//#endif diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 90c732be..84688b81 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -72,12 +72,48 @@ struct MapViewSwiftUI: UIViewRepresentable { self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile } } + self.moveToMeshRegion(mapView) } func makeCoordinator() -> MapCoordinator { return Coordinator(self) } + func moveToMeshRegion(_ mapView: MKMapView) { + //go through the annotations and create a bounding box that encloses them + var minLat: CLLocationDegrees = 90.0 + var maxLat: CLLocationDegrees = -90.0 + var minLon: CLLocationDegrees = 180.0 + var maxLon: CLLocationDegrees = -180.0 + + for annotation in mapView.annotations { + if annotation.isKind(of: MKAnnotation.self) { + minLat = min(minLat, annotation.coordinate.latitude) + maxLat = max(maxLat, annotation.coordinate.latitude) + minLon = min(minLon, annotation.coordinate.longitude) + maxLon = max(maxLon, annotation.coordinate.longitude) + } + } + + //check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location) + if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 { + return + } else if minLat == maxLat && minLon == maxLon { + //then we are focussed on a single point (probably because there is only one node with a position) + //widen that out a little (don't zoom way in to that point) + + //0.001 degrees latitude is about 100m + //the mapView.regionThatFits call below will expand this out to a rectangle + minLat = minLat - 0.001 + maxLat = maxLat + 0.001 + } + + let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2) + let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5) + let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span)) + mapView.setRegion(region, animated: true) + } + final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { var parent: MapViewSwiftUI @@ -240,13 +276,13 @@ struct MapViewSwiftUI: UIViewRepresentable { public class CustomMapOverlaySource: MKTileOverlay { // requires folder: tiles/{mapName}/z/y/y,{tileType} - private var parent: MapView + private var parent: MapViewSwiftUI private let mapName: String private let tileType: String private let defaultTile: DefaultTile? public init( - parent: MapView, + parent: MapViewSwiftUI, mapName: String, tileType: String, defaultTile: DefaultTile? diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 3f968bab..ebf038d8 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -15,14 +15,13 @@ struct NodeMap: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings - @AppStorage("meshMapType") var type: String = "hybrid" @AppStorage("meshMapCustomTileServer") var customTileServer: String = "" { didSet { if customTileServer == "" { self.customMapOverlay = nil } else { - self.customMapOverlay = MapView.CustomMapOverlay( + self.customMapOverlay = MapViewSwiftUI.CustomMapOverlay( mapName: customTileServer, tileType: "png", canReplaceMapContent: true @@ -30,94 +29,71 @@ struct NodeMap: View { } } } + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], animation: .default) + private var positions: FetchedResults - @State private var showLabels: Bool = false - - //@State private var annotationItems: [MapLocation] = [] - //@FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \NodeInfoEntity.lastHeard, ascending: false)], animation: .default) - //private var locationNodes: FetchedResults - - /*@State private var mapRegion: MKCoordinateRegion = MKCoordinateRegion( - center: CLLocationCoordinate2D( - latitude: -38.758247, - longitude: 175.360208 - ), - span: MKCoordinateSpan( - latitudeDelta: 0.01, - longitudeDelta: 0.01 - ) - )*/ - - @State private var customMapOverlay: MapView.CustomMapOverlay? = MapView.CustomMapOverlay( + @State private var mapType: MKMapType = .standard + @State var waypointCoordinate: CLLocationCoordinate2D? + @State private var presentingWaypointForm = false + @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( mapName: "offlinemap", tileType: "png", canReplaceMapContent: true ) - - //@State private var mapType: MKMapType = MKMapType.standard - - @State private var zoomEnabled: Bool = true - @State private var showZoomScale: Bool = true - @State private var useMinZoomBoundary: Bool = false - @State private var minZoom: Double = 0 - @State private var useMaxZoomBoundary: Bool = false - @State private var maxZoom: Double = 3000000 - - @State private var scrollEnabled: Bool = true - @State private var useScrollBoundaries: Bool = false - @State private var scrollBoundaries: MKCoordinateRegion = MKCoordinateRegion() - - @State private var rotationEnabled: Bool = true - @State private var showCompassWhenRotated: Bool = true - - @State private var showUserLocation: Bool = true - @State private var userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none - @State private var userLocation: CLLocationCoordinate2D? = LocationHelper.currentLocation - - @State private var showAnnotations: Bool = true - @State private var annotations: [MKPointAnnotation] = [] - - @State private var showOverlays: Bool = true - @State private var overlays: [MapView.Overlay] = [] - - @State private var showMapCenter: Bool = false + @State private var overlays: [MapViewSwiftUI.Overlay] = [] + + //@State private var showLabels: Bool = false + //@State private var zoomEnabled: Bool = true + //@State private var showZoomScale: Bool = true + //@State private var useMinZoomBoundary: Bool = false + //@State private var minZoom: Double = 0 + //@State private var useMaxZoomBoundary: Bool = false + //@State private var maxZoom: Double = 3000000 + //@State private var scrollEnabled: Bool = true + //@State private var useScrollBoundaries: Bool = false + //@State private var scrollBoundaries: MKCoordinateRegion = MKCoordinateRegion() + //@State private var rotationEnabled: Bool = true + //@State private var showCompassWhenRotated: Bool = true + //@State private var showUserLocation: Bool = true + //@State private var userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none + //@State private var userLocation: CLLocationCoordinate2D? = LocationHelper.currentLocation + //@State private var showAnnotations: Bool = true + //@State private var annotations: [MKPointAnnotation] = [] + //@State private var showOverlays: Bool = true + //@State private var showMapCenter: Bool = false var body: some View { NavigationStack { - ZStack { - - //MapView(nodes: self.locationNodes)//.environmentObject(bleManager) - // } - MapView( - //region: self.$mapRegion, + ZStack { + MapViewSwiftUI(onMarkerTap: { coord in + presentingWaypointForm = true + waypointCoordinate = coord + }, positions: Array(positions), region: MKCoordinateRegion(center: LocationHelper.currentLocation, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, customMapOverlay: self.customMapOverlay, - mapType: self.type, - zoomEnabled: self.zoomEnabled, - showZoomScale: self.showZoomScale, - zoomRange: (minHeight: self.useMinZoomBoundary ? self.minZoom : 0, maxHeight: self.useMaxZoomBoundary ? self.maxZoom : .infinity), - scrollEnabled: self.scrollEnabled, - scrollBoundaries: self.useScrollBoundaries ? self.scrollBoundaries : nil, - rotationEnabled: self.rotationEnabled, - showCompassWhenRotated: self.showCompassWhenRotated, - showUserLocation: self.showUserLocation, - userTrackingMode: self.userTrackingMode, - userLocation: self.$userLocation, - //annotations: self.annotations, - //locationNodes: self.locationNodes.map({ nodeinfo in return nodeinfo }), overlays: self.overlays - //context: self.context ) - - .frame(maxHeight: .infinity) - .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) + VStack { + Spacer() + + Picker("", selection: $mapType) { + Text("Standard").tag(MKMapType.standard) + Text("Muted").tag(MKMapType.mutedStandard) + Text("Hybrid").tag(MKMapType.hybrid) + Text("Hybrid Flyover").tag(MKMapType.hybridFlyover) + Text("Satellite").tag(MKMapType.satellite) + Text("Sat Flyover").tag(MKMapType.satelliteFlyover) + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.bottom, 30) + } } + .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) + .frame(maxHeight: .infinity) } - .navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack { - ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, @@ -125,10 +101,8 @@ struct NodeMap: View { "????") }) .onAppear(perform: { - self.bleManager.context = context self.bleManager.userSettings = userSettings - }) } } From 8e5bb482f07bfbbbf39d28ebf200ba7e16181476 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 13 Jan 2023 15:58:20 -0800 Subject: [PATCH 18/57] Dynamic region bool --- Meshtastic/Views/Map/MapViewSwiftUI.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 84688b81..04b85520 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -16,7 +16,6 @@ struct MapViewSwiftUI: UIViewRepresentable { let region: MKCoordinateRegion let mapViewType: MKMapType - // Offline Maps //make this view dependent on the UserDefault that is updated when importing a new map file @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @@ -24,6 +23,7 @@ struct MapViewSwiftUI: UIViewRepresentable { var customMapOverlay: CustomMapOverlay? @State private var presentCustomMapOverlayHash: CustomMapOverlay? var overlays: [Overlay] = [] + let dynamicRegion: Bool = true func makeUIView(context: Context) -> MKMapView { mapView.mapType = mapViewType @@ -72,7 +72,9 @@ struct MapViewSwiftUI: UIViewRepresentable { self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile } } - self.moveToMeshRegion(mapView) + if dynamicRegion { + self.moveToMeshRegion(mapView) + } } func makeCoordinator() -> MapCoordinator { From dbe61f94c48dd8758ad118ccdbf69903860b5f6e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 13 Jan 2023 16:07:38 -0800 Subject: [PATCH 19/57] Add waypoint sheet to mesh map --- Meshtastic/Views/Map/MapViewSwiftUI.swift | 2 -- Meshtastic/Views/Nodes/NodeMap.swift | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 04b85520..84256020 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -161,7 +161,6 @@ struct MapViewSwiftUI: UIViewRepresentable { let location = longPressRecognizer.location(in: self.parent.mapView) // Map Coordinate - CLLocationCoordinate2D let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - print(coordinate) // Add annotation: let annotation = MKPointAnnotation() @@ -210,7 +209,6 @@ struct MapViewSwiftUI: UIViewRepresentable { } else { return MKOverlayRenderer() } - } else if let tileOverlay = overlay as? MKTileOverlay { return MKTileOverlayRenderer(tileOverlay: tileOverlay) } else { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index ebf038d8..fafc1e9b 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -90,6 +90,11 @@ struct NodeMap: View { } .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) .frame(maxHeight: .infinity) + .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { + WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) + } } .navigationBarItems(leading: MeshtasticLogo(), trailing: From 50943ea5fc269b6065f44cb27258ed2045aec0f2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 13 Jan 2023 22:30:10 -0800 Subject: [PATCH 20/57] More waypoints --- Meshtastic.xcodeproj/project.pbxproj | 8 +- Meshtastic/Helpers/BLEManager.swift | 4 +- Meshtastic/Helpers/MeshPackets.swift | 65 ++++ .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 3 +- .../contents | 280 ++++++++++++++++++ .../Persistence/WaypointEntityExtension.swift | 54 ++++ Meshtastic/Protobufs/mesh.pb.swift | 12 +- Meshtastic/Views/Map/LocalMBTileOverlay.swift | 21 +- Meshtastic/Views/Map/MapViewModule.swift | 2 +- Meshtastic/Views/Map/MapViewSwiftUI.swift | 36 ++- Meshtastic/Views/Map/WaypointFormView.swift | 1 + Meshtastic/Views/Nodes/NodeDetail.swift | 14 +- Meshtastic/Views/Nodes/NodeMap.swift | 44 +-- 14 files changed, 461 insertions(+), 85 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents create mode 100644 Meshtastic/Persistence/WaypointEntityExtension.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b145995c..464ac36c 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; }; DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; }; DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormView.swift */; }; + DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; }; DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD994B68295F88B60013760A /* IntervalEnums.swift */; }; @@ -192,6 +193,8 @@ DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = ""; }; DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = ""; }; DD964FBE296E76EF007C176F /* WaypointFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormView.swift; sourceTree = ""; }; + DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = ""; }; + DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = ""; }; DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = ""; }; DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = ""; }; @@ -565,6 +568,7 @@ DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */, DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */, DD3CC6C128EB9D4900FA9159 /* UpdateCoreData.swift */, + DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */, ); path = Persistence; sourceTree = ""; @@ -777,6 +781,7 @@ DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */, C9A7BC1027759A9600760B50 /* PositionAnnotationView.swift in Sources */, DD882F5D2772E4640005BF05 /* Contacts.swift in Sources */, + DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */, DD47E3CE26F103C600029299 /* NodeList.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, @@ -1226,13 +1231,14 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */, DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */, DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */, DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */, DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */; + currentVersion = DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5dbd0f8b..65cfc18c 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -506,7 +506,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .positionApp: positionPacket(packet: decodedInfo.packet, context: context!) case .waypointApp: - MeshLogger.log("🕸️ MESH PACKET received for Waypoint App UNHANDLED \(try! decodedInfo.packet.jsonString())") + waypointPacket(packet: decodedInfo.packet, context: context!) case .nodeinfoApp: if !invalidVersion { nodeInfoAppPacket(packet: decodedInfo.packet, context: context!) } case .routingApp: @@ -627,7 +627,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil { connectTo(peripheral: preferredPeripheral!.peripheral) } - let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown")) + let nodeName = connectedPeripheral?.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown")) let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.textmessage.send.failed %@", comment: "Message Send Failed, not properly connected to %@"), nodeName) MeshLogger.log("🚫 \(logString)") diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d07bb2ba..10406180 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1311,3 +1311,68 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM } } } + +func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.waypoint.received %@", comment: "Waypoint Packet received from node: %@"), String(packet.from)) + MeshLogger.log("📍 \(logString)") + + let fetchWaypointRequest: NSFetchRequest = NSFetchRequest.init(entityName: "WaypointEntity") + fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(packet.id)) + + do { + + if let waypointMessage = try? Waypoint(serializedData: packet.decoded.payload) { + // Don't save empty waypoint packets + if waypointMessage.longitudeI > 0 || waypointMessage.latitudeI > 0 { + let fetchedWaypoint = try context.fetch(fetchWaypointRequest) as! [WaypointEntity] + if fetchedWaypoint.isEmpty { + let waypoint = WaypointEntity(context: context) + + waypoint.id = Int64(packet.id) + waypoint.name = waypointMessage.name + waypoint.longDescription = waypointMessage.description_p + waypoint.latitudeI = waypointMessage.latitudeI + waypoint.longitudeI = waypointMessage.longitudeI + //waypoint.icon = Int32(waypointMessage.icon) + waypoint.locked = waypointMessage.locked + if waypointMessage.expire != 0 { + waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } + do { + try context.save() + print("💾 Updated Node Waypoint App Packet For: \(waypoint.id)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") + } + } else { + fetchedWaypoint[0].id = Int64(packet.id) + fetchedWaypoint[0].name = waypointMessage.name + fetchedWaypoint[0].longDescription = waypointMessage.description_p + fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI + fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI + //fetchedWaypoint[0].icon = Int32(waypointMessage.icon) + fetchedWaypoint[0].locked = waypointMessage.locked + if waypointMessage.expire != 0 { + fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } + do { + try context.save() + print("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") + } + } + } else { + print("💥 Empty WAYPOINT_APP Packet") + print(try! packet.jsonString()) + } + } + } catch { + print("💥 Error Deserializing WAYPOINT_APP packet.") + } +} diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 1b3c637e..a13f6b2e 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV5.xcdatamodel + MeshtasticDataModelV6.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV5.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV5.xcdatamodel/contents index 1f463d49..4bb57e13 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV5.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV5.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -269,6 +269,7 @@ + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents new file mode 100644 index 00000000..792586e0 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/WaypointEntityExtension.swift b/Meshtastic/Persistence/WaypointEntityExtension.swift new file mode 100644 index 00000000..1ee65fa4 --- /dev/null +++ b/Meshtastic/Persistence/WaypointEntityExtension.swift @@ -0,0 +1,54 @@ +// +// WaypointEntityExtension.swift +// Meshtastic +// +// Copyright (c) Garth Vander Houwen 1/13/23. +// +import CoreData +import CoreLocation +import MapKit +import SwiftUI + +extension WaypointEntity { + + var latitude: Double? { + + let d = Double(latitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var longitude: Double? { + + let d = Double(longitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var waypointCoordinate: CLLocationCoordinate2D? { + if latitudeI != 0 && longitudeI != 0 { + let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) + return coord + } else { + return nil + } + } + + var annotaton: MKPointAnnotation { + let pointAnn = MKPointAnnotation() + if waypointCoordinate != nil { + pointAnn.coordinate = waypointCoordinate! + } + return pointAnn + } +} + +extension WaypointEntity: MKAnnotation { + public var coordinate: CLLocationCoordinate2D { waypointCoordinate ?? LocationHelper.DefaultLocation } + public var title: String? { self.title ?? "" } + public var subtitle: String? { self.description } +} diff --git a/Meshtastic/Protobufs/mesh.pb.swift b/Meshtastic/Protobufs/mesh.pb.swift index d168c267..9a7101aa 100644 --- a/Meshtastic/Protobufs/mesh.pb.swift +++ b/Meshtastic/Protobufs/mesh.pb.swift @@ -1156,7 +1156,7 @@ struct Waypoint { /// /// Designator icon for the waypoint in the form of a unicode emoji - var emoji: UInt32 = 0 + var icon: UInt32 = 0 var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2782,7 +2782,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 5: .same(proto: "locked"), 6: .same(proto: "name"), 7: .same(proto: "description"), - 8: .same(proto: "emoji"), + 8: .same(proto: "icon"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2798,7 +2798,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 5: try { try decoder.decodeSingularBoolField(value: &self.locked) }() case 6: try { try decoder.decodeSingularStringField(value: &self.name) }() case 7: try { try decoder.decodeSingularStringField(value: &self.description_p) }() - case 8: try { try decoder.decodeSingularFixed32Field(value: &self.emoji) }() + case 8: try { try decoder.decodeSingularFixed32Field(value: &self.icon) }() default: break } } @@ -2826,8 +2826,8 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if !self.description_p.isEmpty { try visitor.visitSingularStringField(value: self.description_p, fieldNumber: 7) } - if self.emoji != 0 { - try visitor.visitSingularFixed32Field(value: self.emoji, fieldNumber: 8) + if self.icon != 0 { + try visitor.visitSingularFixed32Field(value: self.icon, fieldNumber: 8) } try unknownFields.traverse(visitor: &visitor) } @@ -2840,7 +2840,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if lhs.locked != rhs.locked {return false} if lhs.name != rhs.name {return false} if lhs.description_p != rhs.description_p {return false} - if lhs.emoji != rhs.emoji {return false} + if lhs.icon != rhs.icon {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/Map/LocalMBTileOverlay.swift b/Meshtastic/Views/Map/LocalMBTileOverlay.swift index 83409f4b..11c5af3e 100644 --- a/Meshtastic/Views/Map/LocalMBTileOverlay.swift +++ b/Meshtastic/Views/Map/LocalMBTileOverlay.swift @@ -2,7 +2,7 @@ // LocalMBTileOverlay.swift // MeshtasticApple // -// Created by Joshua Pirihi on 16/01/22. +// Copyright(c) Joshua Pirihi 16/01/22. // import UIKit @@ -41,9 +41,7 @@ enum MapTileError: Error { class LocalMBTileOverlay: MKTileOverlay { var path: String! - var mb: Connection! - private var _boundingMapRect: MKMapRect! override var boundingMapRect: MKMapRect { get { @@ -55,7 +53,6 @@ class LocalMBTileOverlay: MKTileOverlay { super.init(urlTemplate: nil) self.path = path - do { self.mb = try Connection(self.path, readonly: true) let metadata = Table("metadata") @@ -87,43 +84,29 @@ class LocalMBTileOverlay: MKTileOverlay { ] self._boundingMapRect = MKMapRect(coordinates: coords) - } catch { print("💥 Map tile error: \(error)") return nil } - - } override func loadTile(at path: MKTileOverlayPath, result: @escaping (Data?, Error?) -> Void) { - //} - - //override func loadTile(at path: MKTileOverlayPath) async throws -> Data { - let tileX = Int64(path.x) let tileY = Int64(path.y) - let tileZ = Int64(path.z) - + let tileZ = Int64(path.z) let tileData = Expression("tile_data") let zoomLevel = Expression("zoom_level") let tileColumn = Expression("tile_column") let tileRow = Expression("tile_row") if let dataQuery = try? self.mb.pluck(Table("tiles").select(tileData).filter(zoomLevel == tileZ).filter(tileColumn == tileX).filter(tileRow == tileY)) { - let data = Data(bytes: dataQuery[tileData].bytes, count: dataQuery[tileData].bytes.count)//dataQuery![tileData].bytes - - //return data result(data, nil) - } else { print("💥 No tile here: x:\(tileX) y:\(tileY) z:\(tileZ)") - //return Data() let error = NSError(domain: "LocalMBTileOverlay", code: 1, userInfo: ["reason": "no_tile"]) result(nil, error) } } - } diff --git a/Meshtastic/Views/Map/MapViewModule.swift b/Meshtastic/Views/Map/MapViewModule.swift index 4f782173..cba18980 100644 --- a/Meshtastic/Views/Map/MapViewModule.swift +++ b/Meshtastic/Views/Map/MapViewModule.swift @@ -153,7 +153,7 @@ // // mapView.isScrollEnabled = self.userTrackingMode == MKUserTrackingMode.none ? self.scrollEnabled : false // -// if let scrollBoundary = self.scrollBoundaries, (mapView.cameraBoundary?.region.center.latitude != scrollBoundary.center.latitude || mapView.cameraBoundary?.region.center.longitude != scrollBoundary.center.longitude || mapView.cameraBoundary?.region.span.latitudeDelta != scrollBoundary.span.latitudeDelta || mapView.cameraBoundary?.region.span.longitudeDelta != scrollBoundary.span.longitudeDelta) { +// if let scrollBoundary = self.scrollBoundaries, (mapView.cameraBoundary?.region.center.latitude != scrollBoundary.center.latitude || mapView.cameraBoundary?.region.center.longitude != scrollBoundary.center.longitude || mapView.camera Boundary?.region.span.latitudeDelta != scrollBoundary.span.latitudeDelta || mapView.cameraBoundary?.region.span.longitudeDelta != scrollBoundary.span.longitudeDelta) { // mapView.cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: scrollBoundary) // } else if self.scrollBoundaries == nil && mapView.cameraBoundary != nil { // mapView.cameraBoundary = nil diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 84256020..cfcbd5b3 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -2,7 +2,7 @@ // MapViewSwitUI.swift // Meshtastic // -// Copyright(c) Garth Vander Houwen 1/9/23. +// Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22. // import SwiftUI @@ -13,6 +13,7 @@ struct MapViewSwiftUI: UIViewRepresentable { var onMarkerTap: (_ waypointCoordinate: CLLocationCoordinate2D? ) -> Void let mapView = MKMapView() let positions: [PositionEntity] + let waypoints: [WaypointEntity] let region: MKCoordinateRegion let mapViewType: MKMapType @@ -26,18 +27,24 @@ struct MapViewSwiftUI: UIViewRepresentable { let dynamicRegion: Bool = true func makeUIView(context: Context) -> MKMapView { + // Parameters + mapView.addAnnotations(positions) mapView.mapType = mapViewType mapView.setRegion(region, animated: true) - mapView.isRotateEnabled = true - mapView.isPitchEnabled = true - mapView.showsBuildings = true; - mapView.addAnnotations(positions) - mapView.showsUserLocation = true mapView.setUserTrackingMode(.none, animated: false) + // Other MKMapView Settings + mapView.isPitchEnabled = true + mapView.isRotateEnabled = true + mapView.isScrollEnabled = true + mapView.isZoomEnabled = true + mapView.showsBuildings = true mapView.showsCompass = true mapView.showsScale = true - mapView.isZoomEnabled = true - mapView.isScrollEnabled = true + mapView.showsTraffic = true + mapView.showsUserLocation = true + #if targetEnvironment(macCatalyst) + mapView.showsZoomControls = true + #endif mapView.delegate = context.coordinator return mapView } @@ -149,9 +156,14 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.markerTintColor = UIColor(.accentColor) annotationView.titleVisibility = .visible return annotationView - case _ as WaypointEntity: - return nil - + case let waypointAnnotation as WaypointEntity: + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint") + annotationView.canShowCallout = true + annotationView.glyphText = "🪧" + annotationView.clusteringIdentifier = "waypointGroup" + annotationView.markerTintColor = UIColor(.green) + annotationView.titleVisibility = .visible + return annotationView default: return nil } } @@ -161,7 +173,6 @@ struct MapViewSwiftUI: UIViewRepresentable { let location = longPressRecognizer.location(in: self.parent.mapView) // Map Coordinate - CLLocationCoordinate2D let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - // Add annotation: let annotation = MKPointAnnotation() annotation.title = "Dropped Pin" @@ -244,7 +255,6 @@ struct MapViewSwiftUI: UIViewRepresentable { maximumZoomLevel: Int? = nil, defaultTile: DefaultTile? = nil ) { - self.mapName = mapName self.tileType = tileType self.canReplaceMapContent = canReplaceMapContent diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 7af0dca7..6433ed85 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -27,6 +27,7 @@ struct WaypointFormView: View { Section(header: Text("Waypoint")) { HStack { Text("Location: \(String(format: "%.5f", coordinate.latitude ) + "," + String(format: "%.5f", coordinate.longitude ))") + .textSelection(.enabled) .foregroundColor(Color.gray) .font(.caption2) if coordinate.latitude != LocationHelper.DefaultLocation.latitude && coordinate.longitude != LocationHelper.DefaultLocation.longitude { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 8278b6c6..4aad6646 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -28,6 +28,9 @@ struct NodeDetail: View { var node: NodeInfoEntity + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], animation: .default) + private var waypoints: FetchedResults + var body: some View { let hwModelString = node.user?.hwModel ?? "UNSET" @@ -44,16 +47,16 @@ struct NodeDetail: View { MapViewSwiftUI(onMarkerTap: { coord in presentingWaypointForm = true waypointCoordinate = coord - }, positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, + }, positions: annotations, waypoints: Array(waypoints), region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, customMapOverlay: self.customMapOverlay, overlays: self.overlays ) VStack { Spacer() Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ") - .font(.caption2) - - Picker("", selection: $mapType) { + .font(.caption) + .padding() + Picker("Map Type", selection: $mapType) { Text("Standard").tag(MKMapType.standard) Text("Muted").tag(MKMapType.mutedStandard) Text("Hybrid").tag(MKMapType.hybrid) @@ -61,8 +64,7 @@ struct NodeDetail: View { Text("Satellite").tag(MKMapType.satellite) Text("Sat Flyover").tag(MKMapType.satelliteFlyover) } - .pickerStyle(SegmentedPickerStyle()) - .padding(.bottom, 30) + .pickerStyle(.menu) } } .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index fafc1e9b..7f5498a7 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -32,6 +32,9 @@ struct NodeMap: View { @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], animation: .default) private var positions: FetchedResults + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], animation: .default) + private var waypoints: FetchedResults + @State private var mapType: MKMapType = .standard @State var waypointCoordinate: CLLocationCoordinate2D? @State private var presentingWaypointForm = false @@ -42,50 +45,29 @@ struct NodeMap: View { ) @State private var overlays: [MapViewSwiftUI.Overlay] = [] - //@State private var showLabels: Bool = false - //@State private var zoomEnabled: Bool = true - //@State private var showZoomScale: Bool = true - //@State private var useMinZoomBoundary: Bool = false - //@State private var minZoom: Double = 0 - //@State private var useMaxZoomBoundary: Bool = false - //@State private var maxZoom: Double = 3000000 - //@State private var scrollEnabled: Bool = true - //@State private var useScrollBoundaries: Bool = false - //@State private var scrollBoundaries: MKCoordinateRegion = MKCoordinateRegion() - //@State private var rotationEnabled: Bool = true - //@State private var showCompassWhenRotated: Bool = true - //@State private var showUserLocation: Bool = true - //@State private var userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none - //@State private var userLocation: CLLocationCoordinate2D? = LocationHelper.currentLocation - //@State private var showAnnotations: Bool = true - //@State private var annotations: [MKPointAnnotation] = [] - //@State private var showOverlays: Bool = true - //@State private var showMapCenter: Bool = false - var body: some View { NavigationStack { ZStack { + MapViewSwiftUI(onMarkerTap: { coord in presentingWaypointForm = true waypointCoordinate = coord - }, positions: Array(positions), region: MKCoordinateRegion(center: LocationHelper.currentLocation, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, + }, positions: Array(positions), waypoints: Array(waypoints), region: MKCoordinateRegion(center: LocationHelper.currentLocation, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, customMapOverlay: self.customMapOverlay, overlays: self.overlays ) VStack { Spacer() - - Picker("", selection: $mapType) { + Picker("Map Type", selection: $mapType) { Text("Standard").tag(MKMapType.standard) - Text("Muted").tag(MKMapType.mutedStandard) + Text("Standard Muted").tag(MKMapType.mutedStandard) Text("Hybrid").tag(MKMapType.hybrid) Text("Hybrid Flyover").tag(MKMapType.hybridFlyover) Text("Satellite").tag(MKMapType.satellite) - Text("Sat Flyover").tag(MKMapType.satelliteFlyover) + Text("Satellite Flyover").tag(MKMapType.satelliteFlyover) } - .pickerStyle(SegmentedPickerStyle()) - .padding(.bottom, 30) + .pickerStyle(.menu) } } .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) @@ -111,11 +93,3 @@ struct NodeMap: View { }) } } - -struct NodeMap_Previews: PreviewProvider { - static let bleManager = BLEManager() - - static var previews: some View { - NodeMap() - } -} From 83bdcaf42a88e59e2ee45610d1cdc244eb9a97fa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 13 Jan 2023 23:11:16 -0800 Subject: [PATCH 21/57] Random waypointid for now --- Meshtastic/Helpers/MeshPackets.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 10406180..fa8fee4b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1329,7 +1329,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { if fetchedWaypoint.isEmpty { let waypoint = WaypointEntity(context: context) - waypoint.id = Int64(packet.id) + waypoint.id = Int64(UInt32.random(in: UInt32(UInt8.max).. Date: Sat, 14 Jan 2023 09:42:09 -0800 Subject: [PATCH 22/57] Hook up waypoint form --- Meshtastic/Helpers/BLEManager.swift | 22 +++++++---------- Meshtastic/Views/Map/WaypointFormView.swift | 26 ++++++++++++++------- Meshtastic/Views/Nodes/NodeDetail.swift | 2 +- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 65cfc18c..45147035 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -727,24 +727,18 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return success } - public func sendWaypoint(destNum: Int64, name: String, wantAck: Bool) -> Bool { + public func sendWaypoint(waypoint: Waypoint) -> Bool { var success = false - let fromNodeNum = connectedPeripheral.num - if fromNodeNum <= 0 || (LocationHelper.currentLocation.latitude == LocationHelper.DefaultLocation.latitude && LocationHelper.currentLocation.longitude == LocationHelper.DefaultLocation.longitude) { - return false - } - var waypointPacket = Waypoint() - waypointPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) - waypointPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) - let oneWeekFromNow = Calendar.current.date(byAdding: .day, value: 7, to: Date()) - waypointPacket.expire = UInt32(oneWeekFromNow!.timeIntervalSince1970) - waypointPacket.name = name + let fromNodeNum = UInt32(connectedPeripheral.num) + var waypointPacket = waypoint + waypointPacket.id = UInt32.random(in: UInt32(UInt8.max)..= 1 { if value.count > 1 { let index = value.index(value.startIndex, offsetBy: 1) - emoji = String(value[index]) + icon = String(value[index]) } - emojiIsFocused = false + iconIsFocused = false } } @@ -119,6 +120,15 @@ struct WaypointFormView: View { } HStack { Button { + var newWaypoint = Waypoint() + newWaypoint.name = name.count < 1 ? "Dropped Pin" : name + newWaypoint.description_p = description + newWaypoint.latitudeI = Int32(coordinate.latitude * 1e7) + newWaypoint.longitudeI = Int32(coordinate.longitude * 1e7) + //newWaypoint.icon = icon + newWaypoint.locked = locked + //newWaypoint.expire = expire + bleManager.sendWaypoint(waypoint: newWaypoint) dismiss() } label: { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 4aad6646..4d0e89b0 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -55,7 +55,7 @@ struct NodeDetail: View { Spacer() Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ") .font(.caption) - .padding() + .offset(y: 20) Picker("Map Type", selection: $mapType) { Text("Standard").tag(MKMapType.standard) Text("Muted").tag(MKMapType.mutedStandard) From 7450df4bcc81113b0f025882affa7aea778bf26f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jan 2023 09:58:28 -0800 Subject: [PATCH 23/57] Set waypoint ids --- Meshtastic/Helpers/BLEManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 45147035..21f598aa 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -736,9 +736,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var meshPacket = MeshPacket() meshPacket.to = emptyNodeNum meshPacket.from = fromNodeNum - //meshPacket.wantAck = true + meshPacket.id = waypointPacket.id + meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.wantResponse = true dataMessage.payload = try! waypointPacket.serializedData() dataMessage.portnum = PortNum.waypointApp meshPacket.decoded = dataMessage From f1d70a4103af56c953f920cbff389ec488069610 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jan 2023 11:26:32 -0800 Subject: [PATCH 24/57] Assorted waypoint updates --- Meshtastic/Helpers/MeshPackets.swift | 8 ++++---- .../MeshtasticDataModelV6.xcdatamodel/contents | 2 +- Meshtastic/Persistence/WaypointEntityExtension.swift | 4 ++-- Meshtastic/Views/Map/MapViewSwiftUI.swift | 7 ++++++- Meshtastic/Views/Map/WaypointFormView.swift | 11 +++++------ 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index fa8fee4b..c37cac19 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1314,7 +1314,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.waypoint.received %@", comment: "Waypoint Packet received from node: %@"), String(packet.from)) +let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.waypoint.received %@", comment: "Waypoint Packet received from node: %@"), String(packet.from)) MeshLogger.log("📍 \(logString)") let fetchWaypointRequest: NSFetchRequest = NSFetchRequest.init(entityName: "WaypointEntity") @@ -1329,12 +1329,12 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { if fetchedWaypoint.isEmpty { let waypoint = WaypointEntity(context: context) - waypoint.id = Int64(UInt32.random(in: UInt32(UInt8.max).. - + \ No newline at end of file diff --git a/Meshtastic/Persistence/WaypointEntityExtension.swift b/Meshtastic/Persistence/WaypointEntityExtension.swift index 1ee65fa4..2de1ae62 100644 --- a/Meshtastic/Persistence/WaypointEntityExtension.swift +++ b/Meshtastic/Persistence/WaypointEntityExtension.swift @@ -49,6 +49,6 @@ extension WaypointEntity { extension WaypointEntity: MKAnnotation { public var coordinate: CLLocationCoordinate2D { waypointCoordinate ?? LocationHelper.DefaultLocation } - public var title: String? { self.title ?? "" } - public var subtitle: String? { self.description } + public var title: String? { name ?? "Dropped Pin" } + public var subtitle: String? { longDescription } } diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index cfcbd5b3..113ed20b 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -29,6 +29,7 @@ struct MapViewSwiftUI: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { // Parameters mapView.addAnnotations(positions) + mapView.addAnnotations(waypoints) mapView.mapType = mapViewType mapView.setRegion(region, animated: true) mapView.setUserTrackingMode(.none, animated: false) @@ -159,7 +160,11 @@ struct MapViewSwiftUI: UIViewRepresentable { case let waypointAnnotation as WaypointEntity: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint") annotationView.canShowCallout = true - annotationView.glyphText = "🪧" + if waypointAnnotation.icon == 0 { + annotationView.glyphText = "🪧" + } else { + + } annotationView.clusteringIdentifier = "waypointGroup" annotationView.markerTintColor = UIColor(.green) annotationView.titleVisibility = .visible diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 1cbf10dd..aeef93a9 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -17,7 +17,7 @@ struct WaypointFormView: View { @State private var id: Int32? @State private var name: String = "" @State private var description: String = "" - @State private var icon: String = "📍" + @State private var icon: String = "🪧" @State private var expires: Bool = false @State private var expire: Date = Date()// = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours @State private var locked: Bool = false @@ -125,6 +125,9 @@ struct WaypointFormView: View { newWaypoint.description_p = description newWaypoint.latitudeI = Int32(coordinate.latitude * 1e7) newWaypoint.longitudeI = Int32(coordinate.longitude * 1e7) + let uni = icon.unicodeScalars // Unicode scalar values of the string + let unicode = uni[uni.startIndex].value // First element as an UInt32 + newWaypoint.icon = unicode //newWaypoint.icon = icon newWaypoint.locked = locked //newWaypoint.expire = expire @@ -132,7 +135,7 @@ struct WaypointFormView: View { dismiss() } label: { - Label("save", systemImage: "square.and.arrow.down") + Label("Send", systemImage: "arrow.up") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) @@ -151,7 +154,3 @@ struct WaypointFormView: View { } } } -//var smiley = "😊" -//var data: NSData = smiley.dataUsingEncoding(NSUTF32LittleEndianStringEncoding, allowLossyConversion: false)! -//var unicode:UInt32 = UInt32() -//data.getBytes(&unicode) From a6c4f0639e396cfc25fd97d060f58095daaba162 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jan 2023 13:22:28 -0800 Subject: [PATCH 25/57] More cleanup for maps and waypoints --- Meshtastic/Helpers/LocationHelper.swift | 4 +++- Meshtastic/Views/Map/MapViewSwiftUI.swift | 2 +- Meshtastic/Views/Nodes/NodeDetail.swift | 2 +- Meshtastic/Views/Nodes/NodeMap.swift | 9 +++++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift index dab6637d..2d1c7a85 100644 --- a/Meshtastic/Helpers/LocationHelper.swift +++ b/Meshtastic/Helpers/LocationHelper.swift @@ -80,8 +80,10 @@ class LocationHelper: NSObject, ObservableObject { super.init() locationManager.delegate = self - locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters + locationManager.pausesLocationUpdatesAutomatically = true locationManager.allowsBackgroundLocationUpdates = true + locationManager.activityType = .otherNavigation locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() } diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 113ed20b..6f085198 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -166,7 +166,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } annotationView.clusteringIdentifier = "waypointGroup" - annotationView.markerTintColor = UIColor(.green) + annotationView.markerTintColor = UIColor(.indigo) annotationView.titleVisibility = .visible return annotationView default: return nil diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 4d0e89b0..e7f54145 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -45,8 +45,8 @@ struct NodeDetail: View { let annotations = node.positions?.array as! [PositionEntity] ZStack { MapViewSwiftUI(onMarkerTap: { coord in - presentingWaypointForm = true waypointCoordinate = coord + presentingWaypointForm = true }, positions: annotations, waypoints: Array(waypoints), region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, customMapOverlay: self.customMapOverlay, overlays: self.overlays diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 7f5498a7..44003ba7 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -51,9 +51,14 @@ struct NodeMap: View { ZStack { MapViewSwiftUI(onMarkerTap: { coord in - presentingWaypointForm = true waypointCoordinate = coord - }, positions: Array(positions), waypoints: Array(waypoints), region: MKCoordinateRegion(center: LocationHelper.currentLocation, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, + if waypointCoordinate?.latitude ?? LocationHelper.DefaultLocation.latitude == LocationHelper.DefaultLocation.latitude { + presentingWaypointForm = false + } else { + presentingWaypointForm = true + } + + }, positions: Array(positions), waypoints: Array(waypoints), region: MKCoordinateRegion(center: LocationHelper.currentLocation, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)), mapViewType: mapType, customMapOverlay: self.customMapOverlay, overlays: self.overlays ) From bc6a76e57ed0229092a8beebc5f9761168aea674 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jan 2023 19:13:38 -0800 Subject: [PATCH 26/57] Refresh pins on update --- Meshtastic/Views/Map/MapViewSwiftUI.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 6f085198..f3980c95 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -83,6 +83,9 @@ struct MapViewSwiftUI: UIViewRepresentable { if dynamicRegion { self.moveToMeshRegion(mapView) } + mapView.removeAnnotations(mapView.annotations) + mapView.addAnnotations(positions) + mapView.addAnnotations(waypoints) } func makeCoordinator() -> MapCoordinator { From c023ede407fbe85d6c745a40e64da71aa0f6813a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jan 2023 20:27:05 -0800 Subject: [PATCH 27/57] Waypoint cleanup --- Meshtastic/Helpers/BLEManager.swift | 15 ++ Meshtastic/Helpers/MeshPackets.swift | 225 +++++++++++++-------------- Meshtastic/Views/Nodes/NodeMap.swift | 5 +- 3 files changed, 128 insertions(+), 117 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 21f598aa..278762c4 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -752,6 +752,21 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) success = true + let wayPointEntity = WaypointEntity(context: context!) + wayPointEntity.id = Int64(waypointPacket.id) + wayPointEntity.name = waypointPacket.name.count >= 1 ? waypointPacket.name : "Dropped Pin" + wayPointEntity.longDescription = waypointPacket.description_p + wayPointEntity.icon = Int32(waypointPacket.icon) + wayPointEntity.latitudeI = waypointPacket.latitudeI + wayPointEntity.longitudeI = waypointPacket.longitudeI + do { + try context!.save() + print("💾 Updated Waypoint from Waypoint App Packet From: \(fromNodeNum)") + } catch { + context!.rollback() + let nsError = error as NSError + print("💥 Error Saving NodeInfoEntity from WAYPOINT_APP \(nsError)") + } } return success } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index c37cac19..f1de489f 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -16,7 +16,7 @@ func generateMessageMarkdown (message: String) -> String { let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count)) var messageWithMarkdown = message if matches.count > 0 { - + for match in matches { guard let range = Range(match.range, in: message) else { continue } if match.resultType == .address { @@ -48,7 +48,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save Device Config if !fetchedNode.isEmpty { @@ -88,7 +88,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save Device Config if !fetchedNode.isEmpty { @@ -131,7 +131,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save Device Config @@ -149,7 +149,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 fetchedNode[0].displayConfig = newDisplayConfig } else { - + fetchedNode[0].displayConfig?.gpsFormat = Int32(config.display.gpsFormat.rawValue) fetchedNode[0].displayConfig?.screenOnSeconds = Int32(config.display.screenOnSecs) fetchedNode[0].displayConfig?.screenCarouselInterval = Int32(config.display.autoScreenCarouselSecs) @@ -159,14 +159,14 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 } do { - + try context.save() print("💾 Updated Display Config for node number: \(String(nodeNum))") - + } catch { - + context.rollback() - + let nsError = error as NSError print("💥 Error Updating Core Data DisplayConfigEntity: \(nsError)") } @@ -181,7 +181,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 print("💥 Fetching node for core data DisplayConfigEntity failed: \(nsError)") } } - + if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config %@", comment: "LoRa config received: %@"), String(nodeNum)) @@ -191,7 +191,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save LoRa Config if !fetchedNode.isEmpty { @@ -240,15 +240,15 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 } if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { - + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.network.config %@", comment: "Network config received: %@"), String(nodeNum)) MeshLogger.log("🌐 \(logString)") - + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save WiFi Config if !fetchedNode.isEmpty { @@ -265,7 +265,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 do { try context.save() print("💾 Updated Network Config for node number: \(String(nodeNum))") - + } catch { context.rollback() let nsError = error as NSError @@ -289,7 +289,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save LoRa Config if !fetchedNode.isEmpty { @@ -341,7 +341,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save Canned Message Config @@ -350,7 +350,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum if fetchedNode[0].cannedMessageConfig == nil { let newCannedMessageConfig = CannedMessageConfigEntity(context: context) - + newCannedMessageConfig.enabled = config.cannedMessage.enabled newCannedMessageConfig.sendBell = config.cannedMessage.sendBell newCannedMessageConfig.rotary1Enabled = config.cannedMessage.rotary1Enabled @@ -361,7 +361,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum newCannedMessageConfig.inputbrokerEventCw = Int32(config.cannedMessage.inputbrokerEventCw.rawValue) newCannedMessageConfig.inputbrokerEventCcw = Int32(config.cannedMessage.inputbrokerEventCcw.rawValue) newCannedMessageConfig.inputbrokerEventPress = Int32(config.cannedMessage.inputbrokerEventPress.rawValue) - + fetchedNode[0].cannedMessageConfig = newCannedMessageConfig } else { @@ -405,7 +405,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save External Notificaitone Config if !fetchedNode.isEmpty { @@ -471,7 +471,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save MQTT Config if !fetchedNode.isEmpty { @@ -519,7 +519,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save Device Config if !fetchedNode.isEmpty { @@ -550,7 +550,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum print("💥 Fetching node for core data RangeTestConfigEntity failed: \(nsError)") } } - + if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(config.serial) { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.serial.config %@", comment: "Serial module config received: %@"), String(nodeNum)) @@ -560,7 +560,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save Device Config @@ -591,11 +591,11 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum do { try context.save() print("💾 Updated Serial Module Config for node number: \(String(nodeNum))") - + } catch { - + context.rollback() - + let nsError = error as NSError print("💥 Error Updating Core Data SerialConfigEntity: \(nsError)") } @@ -621,7 +621,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save Telemetry Config if !fetchedNode.isEmpty { @@ -650,7 +650,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum do { try context.save() print("💾 Updated Telemetry Module Config for node number: \(String(nodeNum))") - + } catch { context.rollback() let nsError = error as NSError @@ -675,7 +675,7 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(myInfo.myNodeNum)) - + do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] // Not Found Insert @@ -705,7 +705,7 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)") } } else { - + fetchedMyInfo[0].peripheralId = peripheralId fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum) fetchedMyInfo[0].hasGps = myInfo.hasGps_p @@ -737,7 +737,7 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectContext) { if channel.isInitialized && channel.hasSettings && channel.role != Channel.Role.disabled { - + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.channel.received %d %@", comment: "Channel %d received from: %@"), channel.index, String(fromNum)) MeshLogger.log("🎛️ \(logString)") @@ -787,13 +787,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeInfo.num)) - + do { - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Not Found Insert if fetchedNode.isEmpty && nodeInfo.hasUser { - + let newNode = NodeInfoEntity(context: context) newNode.id = Int64(nodeInfo.num) newNode.num = Int64(nodeInfo.num) @@ -822,7 +822,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() newNode.user = newUser } - + if nodeInfo.position.latitudeI > 0 || nodeInfo.position.longitudeI > 0 { let position = PositionEntity(context: context) position.seqNo = Int32(nodeInfo.position.seqNumber) @@ -837,13 +837,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newPostions.append(position) newNode.positions? = NSOrderedSet(array: newPostions) } - + // Look for a MyInfo let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) - + do { - + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] if fetchedMyInfo.count > 0 { newNode.myInfo = fetchedMyInfo[0] @@ -860,16 +860,16 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje print("💥 Fetch MyInfo Error") } } else if nodeInfo.hasUser && nodeInfo.num > 0 { - + fetchedNode[0].id = Int64(nodeInfo.num) fetchedNode[0].num = Int64(nodeInfo.num) fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) fetchedNode[0].snr = nodeInfo.snr fetchedNode[0].channel = Int32(channel) - + if nodeInfo.hasUser { - + fetchedNode[0].user!.userId = nodeInfo.user.id fetchedNode[0].user!.num = Int64(nodeInfo.num) fetchedNode[0].user!.longName = nodeInfo.user.longName @@ -877,7 +877,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user!.macaddr = nodeInfo.user.macaddr fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() } - + if nodeInfo.hasDeviceMetrics { let newTelemetry = TelemetryEntity(context: context) @@ -890,7 +890,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje } if nodeInfo.hasPosition { - + let position = PositionEntity(context: context) position.latitudeI = nodeInfo.position.latitudeI position.longitudeI = nodeInfo.position.longitudeI @@ -900,11 +900,11 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet } - + // Look for a MyInfo let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) - + do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] if fetchedMyInfo.count > 0 { @@ -930,17 +930,17 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje } func nodeInfoAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { - + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.nodeinfo.received %@", comment: "Node info received for: %@"), String(packet.from)) MeshLogger.log("📟 \(logString)") let fetchNodeInfoAppRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - + do { - + let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) as? [NodeInfoEntity] ?? [] - + if fetchedNode.count == 1 { fetchedNode[0].id = Int64(packet.from) fetchedNode[0].num = Int64(packet.from) @@ -991,7 +991,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { if let cmmc = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) { - + if !cmmc.messages.isEmpty { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.cannedmessages.messages.received %@", comment: "Canned Messages Messages Received For: %@"), String(packet.from)) @@ -1036,7 +1036,7 @@ func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.received %@", comment: "Position Packet received from node: %@"), String(packet.from)) MeshLogger.log("📍 \(logString)") - + let fetchNodePositionRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) @@ -1047,7 +1047,7 @@ func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) { if positionMessage.longitudeI > 0 || positionMessage.latitudeI > 0 { let fetchedNode = try context.fetch(fetchNodePositionRequest) as! [NodeInfoEntity] if fetchedNode.count == 1 { - + let position = PositionEntity(context: context) position.snr = packet.rxSnr position.seqNo = Int32(positionMessage.seqNumber) @@ -1097,10 +1097,10 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana let routingErrorString = routingError?.display ?? NSLocalizedString("unknown", comment: "") let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.routing.message %@ %@", comment: "Routing received for RequestID: %@ Ack Status: %@"), String(packet.decoded.requestID), routingErrorString) MeshLogger.log("🕸️ \(logString)") - + let fetchMessageRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MessageEntity") fetchMessageRequest.predicate = NSPredicate(format: "messageId == %lld", Int64(packet.decoded.requestID)) - + do { let fetchedMessage = try context.fetch(fetchMessageRequest) as? [MessageEntity] if fetchedMessage?.count ?? 0 > 0 { @@ -1130,14 +1130,14 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana if fetchedMyInfo?.count ?? 0 > 0 { for ch in fetchedMyInfo![0].channels!.array as! [ChannelEntity] { - + if ch.index == packet.channel { ch.objectWillChange.send() } } } } catch { - + } } @@ -1153,7 +1153,7 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana } } } - + func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) { @@ -1168,9 +1168,9 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage let fetchNodeTelemetryRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeTelemetryRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - + do { - + let fetchedNode = try context.fetch(fetchNodeTelemetryRequest) as! [NodeInfoEntity] if fetchedNode.count == 1 { if telemetryMessage.variant == Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) { @@ -1213,16 +1213,16 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage } func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { - + if let messageText = String(bytes: packet.decoded.payload, encoding: .utf8) { - + MeshLogger.log("💬 \(NSLocalizedString("mesh.log.textmessage.received", comment: "Message received from the text message app"))") let messageUsers: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from]) - + do { - + let fetchedUsers = try context.fetch(messageUsers) as! [UserEntity] let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) @@ -1235,7 +1235,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM if packet.decoded.replyID > 0 { newMessage.replyID = Int64(packet.decoded.replyID) } - + if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != 4294967295 { newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) } @@ -1244,14 +1244,14 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM } newMessage.messagePayload = messageText newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText) - + newMessage.fromUser?.objectWillChange.send() newMessage.toUser?.objectWillChange.send() var messageSaved = false - + do { - + try context.save() print("💾 Saved a new message for \(newMessage.messageId)") messageSaved = true @@ -1274,7 +1274,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) - + do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { @@ -1314,62 +1314,57 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { -let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.waypoint.received %@", comment: "Waypoint Packet received from node: %@"), String(packet.from)) + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.waypoint.received %@", comment: "Waypoint Packet received from node: %@"), String(packet.from)) MeshLogger.log("📍 \(logString)") - + let fetchWaypointRequest: NSFetchRequest = NSFetchRequest.init(entityName: "WaypointEntity") fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(packet.id)) do { if let waypointMessage = try? Waypoint(serializedData: packet.decoded.payload) { - // Don't save empty waypoint packets - if waypointMessage.longitudeI > 0 || waypointMessage.latitudeI > 0 { - let fetchedWaypoint = try context.fetch(fetchWaypointRequest) as! [WaypointEntity] - if fetchedWaypoint.isEmpty { - let waypoint = WaypointEntity(context: context) - waypoint.id = Int64(packet.id) - waypoint.name = waypointMessage.name - waypoint.longDescription = waypointMessage.description_p - waypoint.latitudeI = waypointMessage.latitudeI - waypoint.longitudeI = waypointMessage.longitudeI - waypoint.icon = Int32(waypointMessage.icon) - waypoint.locked = waypointMessage.locked - if waypointMessage.expire != 0 { - waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) - } - do { - try context.save() - print("💾 Updated Node Waypoint App Packet For: \(waypoint.id)") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") - } - } else { - fetchedWaypoint[0].id = Int64(packet.id) - fetchedWaypoint[0].name = waypointMessage.name - fetchedWaypoint[0].longDescription = waypointMessage.description_p - fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI - fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI - fetchedWaypoint[0].icon = Int32(waypointMessage.icon) - fetchedWaypoint[0].locked = waypointMessage.locked - if waypointMessage.expire != 0 { - fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) - } - do { - try context.save() - print("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id)") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") - } + let fetchedWaypoint = try context.fetch(fetchWaypointRequest) as! [WaypointEntity] + if fetchedWaypoint.isEmpty { + let waypoint = WaypointEntity(context: context) + + waypoint.id = Int64(packet.id) + waypoint.name = waypointMessage.name + waypoint.longDescription = waypointMessage.description_p + waypoint.latitudeI = waypointMessage.latitudeI + waypoint.longitudeI = waypointMessage.longitudeI + waypoint.icon = Int32(waypointMessage.icon) + waypoint.locked = waypointMessage.locked + if waypointMessage.expire != 0 { + waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } + do { + try context.save() + print("💾 Updated Node Waypoint App Packet For: \(waypoint.id)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") } } else { - print("💥 Empty WAYPOINT_APP Packet") - print(try! packet.jsonString()) + fetchedWaypoint[0].id = Int64(packet.id) + fetchedWaypoint[0].name = waypointMessage.name + fetchedWaypoint[0].longDescription = waypointMessage.description_p + fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI + fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI + fetchedWaypoint[0].icon = Int32(waypointMessage.icon) + fetchedWaypoint[0].locked = waypointMessage.locked + if waypointMessage.expire != 0 { + fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } + do { + try context.save() + print("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") + } } } } catch { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 44003ba7..5c52a284 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -29,10 +29,11 @@ struct NodeMap: View { } } } - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], animation: .default) + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], + predicate: NSPredicate(format: "time >= %@", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .easeIn) private var positions: FetchedResults - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], animation: .default) + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], animation: .easeIn) private var waypoints: FetchedResults @State private var mapType: MKMapType = .standard From a98bd6ad99072f778207684ddab608e0189880da Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jan 2023 21:40:31 -0800 Subject: [PATCH 28/57] Hook up emoji to annotations --- Meshtastic/Views/Map/MapViewSwiftUI.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index f3980c95..9f2657b1 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -68,7 +68,6 @@ struct MapViewSwiftUI: UIViewRepresentable { if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { overlay.canReplaceMapContent = false//customMapOverlay.canReplaceMapContent - mapView.addOverlay(overlay) } } else { @@ -164,9 +163,10 @@ struct MapViewSwiftUI: UIViewRepresentable { let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint") annotationView.canShowCallout = true if waypointAnnotation.icon == 0 { + print(waypointAnnotation.icon) annotationView.glyphText = "🪧" } else { - + annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "🪧") } annotationView.clusteringIdentifier = "waypointGroup" annotationView.markerTintColor = UIColor(.indigo) From 4a3895daf2bbf5e2ca865150c3d2e17e63119c8f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jan 2023 21:57:39 -0800 Subject: [PATCH 29/57] Fix emoji parsing --- Meshtastic/Helpers/BLEManager.swift | 2 +- Meshtastic/Helpers/MeshPackets.swift | 4 ++-- .../MeshtasticDataModelV6.xcdatamodel/contents | 2 +- Meshtastic/Views/Map/MapViewSwiftUI.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 278762c4..c4d0e3d9 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -756,7 +756,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { wayPointEntity.id = Int64(waypointPacket.id) wayPointEntity.name = waypointPacket.name.count >= 1 ? waypointPacket.name : "Dropped Pin" wayPointEntity.longDescription = waypointPacket.description_p - wayPointEntity.icon = Int32(waypointPacket.icon) + wayPointEntity.icon = Int64(waypointPacket.icon) wayPointEntity.latitudeI = waypointPacket.latitudeI wayPointEntity.longitudeI = waypointPacket.longitudeI do { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f1de489f..df86d362 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1333,7 +1333,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { waypoint.longDescription = waypointMessage.description_p waypoint.latitudeI = waypointMessage.latitudeI waypoint.longitudeI = waypointMessage.longitudeI - waypoint.icon = Int32(waypointMessage.icon) + waypoint.icon = Int64(waypointMessage.icon) waypoint.locked = waypointMessage.locked if waypointMessage.expire != 0 { waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) @@ -1352,7 +1352,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { fetchedWaypoint[0].longDescription = waypointMessage.description_p fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI - fetchedWaypoint[0].icon = Int32(waypointMessage.icon) + fetchedWaypoint[0].icon = Int64(waypointMessage.icon) fetchedWaypoint[0].locked = waypointMessage.locked if waypointMessage.expire != 0 { fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents index 524054e2..cab2cacc 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents @@ -269,7 +269,7 @@ - + diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 9f2657b1..2e3570cc 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -149,7 +149,7 @@ struct MapViewSwiftUI: UIViewRepresentable { case _ as MKClusterAnnotation: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup") - annotationView.markerTintColor = .systemRed + annotationView.markerTintColor = .brown//.systemRed return annotationView case _ as PositionEntity: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node") From b11ae85104a0e8e69f372c424ed706a69ccdf775 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jan 2023 08:49:17 -0800 Subject: [PATCH 30/57] More waypoint cleanup --- Meshtastic/Helpers/MeshPackets.swift | 4 ++-- .../Persistence/WaypointEntityExtension.swift | 5 ++++- Meshtastic/Views/Map/MapViewSwiftUI.swift | 4 ++-- Meshtastic/Views/Map/WaypointFormView.swift | 22 ++++++++++++------- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index df86d362..fc23f06e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1335,7 +1335,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { waypoint.longitudeI = waypointMessage.longitudeI waypoint.icon = Int64(waypointMessage.icon) waypoint.locked = waypointMessage.locked - if waypointMessage.expire != 0 { + if waypointMessage.expire > 0 { waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) } do { @@ -1354,7 +1354,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI fetchedWaypoint[0].icon = Int64(waypointMessage.icon) fetchedWaypoint[0].locked = waypointMessage.locked - if waypointMessage.expire != 0 { + if waypointMessage.expire > 0 { fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) } do { diff --git a/Meshtastic/Persistence/WaypointEntityExtension.swift b/Meshtastic/Persistence/WaypointEntityExtension.swift index 2de1ae62..4edd9566 100644 --- a/Meshtastic/Persistence/WaypointEntityExtension.swift +++ b/Meshtastic/Persistence/WaypointEntityExtension.swift @@ -50,5 +50,8 @@ extension WaypointEntity { extension WaypointEntity: MKAnnotation { public var coordinate: CLLocationCoordinate2D { waypointCoordinate ?? LocationHelper.DefaultLocation } public var title: String? { name ?? "Dropped Pin" } - public var subtitle: String? { longDescription } + public var subtitle: String? { + (longDescription ?? "") + + String(expire != nil ? "\n⌛ Expires \(String(describing: expire?.formatted()))" : "") + + String(locked ? "\n🔒 Locked" : "") } } diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 2e3570cc..4bdd7001 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -164,9 +164,9 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.canShowCallout = true if waypointAnnotation.icon == 0 { print(waypointAnnotation.icon) - annotationView.glyphText = "🪧" + annotationView.glyphText = "📍" } else { - annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "🪧") + annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍") } annotationView.clusteringIdentifier = "waypointGroup" annotationView.markerTintColor = UIColor(.indigo) diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index aeef93a9..5d9bf432 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -17,7 +17,7 @@ struct WaypointFormView: View { @State private var id: Int32? @State private var name: String = "" @State private var description: String = "" - @State private var icon: String = "🪧" + @State private var icon: String = "📍" @State private var expires: Bool = false @State private var expire: Date = Date()// = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours @State private var locked: Bool = false @@ -125,21 +125,27 @@ struct WaypointFormView: View { newWaypoint.description_p = description newWaypoint.latitudeI = Int32(coordinate.latitude * 1e7) newWaypoint.longitudeI = Int32(coordinate.longitude * 1e7) - let uni = icon.unicodeScalars // Unicode scalar values of the string - let unicode = uni[uni.startIndex].value // First element as an UInt32 + // Unicode scalar value for the icon emoji string + let unicodeScalers = icon.unicodeScalars + // First element as an UInt32 + let unicode = unicodeScalers[unicodeScalers.startIndex].value newWaypoint.icon = unicode - //newWaypoint.icon = icon newWaypoint.locked = locked - //newWaypoint.expire = expire - bleManager.sendWaypoint(waypoint: newWaypoint) - dismiss() - + if expire.timeIntervalSince1970 > 0 { + newWaypoint.expire = UInt32(expire.timeIntervalSince1970) + } + if bleManager.sendWaypoint(waypoint: newWaypoint) { + dismiss() + } else { + + } } label: { Label("Send", systemImage: "arrow.up") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) + .disabled(bleManager.connectedPeripheral == nil) .padding() Button { From 5fd3215921a5d9cb67763f96b9d99bebae7c1bb5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jan 2023 09:18:50 -0800 Subject: [PATCH 31/57] Prevent waypoint form from opening up with apple park as the location --- Meshtastic/Views/Map/MapViewSwiftUI.swift | 2 +- Meshtastic/Views/Map/WaypointFormView.swift | 3 ++- Meshtastic/Views/Nodes/NodeDetail.swift | 14 ++++++++++---- Meshtastic/Views/Nodes/NodeMap.swift | 10 ++++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 4bdd7001..171c9c58 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -181,12 +181,12 @@ struct MapViewSwiftUI: UIViewRepresentable { let location = longPressRecognizer.location(in: self.parent.mapView) // Map Coordinate - CLLocationCoordinate2D let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) + parent.onMarkerTap(coordinate) // Add annotation: let annotation = MKPointAnnotation() annotation.title = "Dropped Pin" annotation.coordinate = coordinate parent.mapView.addAnnotation(annotation) - parent.onMarkerTap(coordinate) UINotificationFeedbackGenerator().notificationOccurred(.success) } diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 5d9bf432..0e6712e2 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -11,8 +11,9 @@ import CoreLocation struct WaypointFormView: View { @EnvironmentObject var bleManager: BLEManager - @State var coordinate: CLLocationCoordinate2D @Environment(\.dismiss) private var dismiss + @State var coordinate: CLLocationCoordinate2D + @FocusState private var iconIsFocused: Bool @State private var id: Int32? @State private var name: String = "" diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index e7f54145..71117497 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -46,7 +46,11 @@ struct NodeDetail: View { ZStack { MapViewSwiftUI(onMarkerTap: { coord in waypointCoordinate = coord - presentingWaypointForm = true + if waypointCoordinate == nil { + presentingWaypointForm = false + } else { + presentingWaypointForm = true + } }, positions: annotations, waypoints: Array(waypoints), region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, customMapOverlay: self.customMapOverlay, overlays: self.overlays @@ -379,9 +383,11 @@ struct NodeDetail: View { } .edgesIgnoringSafeArea([.leading, .trailing]) .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { - WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) + if waypointCoordinate != nil { + WaypointFormView(coordinate: waypointCoordinate!) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) + } } .navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline) .navigationBarItems(trailing: diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 5c52a284..06593a6f 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -53,7 +53,7 @@ struct NodeMap: View { MapViewSwiftUI(onMarkerTap: { coord in waypointCoordinate = coord - if waypointCoordinate?.latitude ?? LocationHelper.DefaultLocation.latitude == LocationHelper.DefaultLocation.latitude { + if waypointCoordinate == nil { presentingWaypointForm = false } else { presentingWaypointForm = true @@ -79,9 +79,11 @@ struct NodeMap: View { .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) .frame(maxHeight: .infinity) .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { - WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) + if waypointCoordinate != nil { + WaypointFormView(coordinate: waypointCoordinate!) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) + } } } .navigationBarItems(leading: From 547d250ec20857d31c6d8806090ea1d74e3e879e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jan 2023 10:25:24 -0800 Subject: [PATCH 32/57] Fit extension for map annotations --- Meshtastic.xcodeproj/project.pbxproj | 4 ++ .../Views/Map/MapViewFitExtension.swift | 37 +++++++++++++ Meshtastic/Views/Map/MapViewSwiftUI.swift | 52 ++----------------- Meshtastic/Views/Nodes/NodeDetail.swift | 2 +- Meshtastic/Views/Nodes/NodeMap.swift | 2 +- 5 files changed, 46 insertions(+), 51 deletions(-) create mode 100644 Meshtastic/Views/Map/MapViewFitExtension.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 464ac36c..e078d344 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; }; DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormView.swift */; }; DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; }; + DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC32974767D007C176F /* MapViewFitExtension.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; }; DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD994B68295F88B60013760A /* IntervalEnums.swift */; }; @@ -195,6 +196,7 @@ DD964FBE296E76EF007C176F /* WaypointFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormView.swift; sourceTree = ""; }; DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = ""; }; DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = ""; }; + DD964FC32974767D007C176F /* MapViewFitExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewFitExtension.swift; sourceTree = ""; }; DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = ""; }; DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = ""; }; @@ -282,6 +284,7 @@ C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */, DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, DD964FBE296E76EF007C176F /* WaypointFormView.swift */, + DD964FC32974767D007C176F /* MapViewFitExtension.swift */, ); path = Map; sourceTree = ""; @@ -784,6 +787,7 @@ DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */, DD47E3CE26F103C600029299 /* NodeList.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, + DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DD17E5DE277D49D400010EC2 /* storeforward.pb.swift in Sources */, diff --git a/Meshtastic/Views/Map/MapViewFitExtension.swift b/Meshtastic/Views/Map/MapViewFitExtension.swift new file mode 100644 index 00000000..52c13eaa --- /dev/null +++ b/Meshtastic/Views/Map/MapViewFitExtension.swift @@ -0,0 +1,37 @@ +// +// MapViewFitExtension.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 1/15/23. +// + +import MapKit + +extension MKMapView { + + func fitAllAnnotations(with padding: UIEdgeInsets = UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100)) { + var zoomRect: MKMapRect = .null + annotations.forEach({ + let annotationPoint = MKMapPoint($0.coordinate) + let pointRect = MKMapRect(x: annotationPoint.x, y: annotationPoint.y, width: 0.01, height: 0.01) + zoomRect = zoomRect.union(pointRect) + }) + + setVisibleMapRect(zoomRect, edgePadding: padding, animated: true) + } + + func fit(annotations: [MKAnnotation], andShow show: Bool, with padding: UIEdgeInsets = UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100)) { + var zoomRect: MKMapRect = .null + annotations.forEach({ + let aPoint = MKMapPoint($0.coordinate) + let rect = MKMapRect(x: aPoint.x, y: aPoint.y, width: 0.1, height: 0.1) + zoomRect = zoomRect.isNull ? rect : zoomRect.union(rect) + }) + + if show { + addAnnotations(annotations) + } + + setVisibleMapRect(zoomRect, edgePadding: padding, animated: true) + } +} diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/MapViewSwiftUI.swift index 171c9c58..a8e37692 100644 --- a/Meshtastic/Views/Map/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/MapViewSwiftUI.swift @@ -14,7 +14,6 @@ struct MapViewSwiftUI: UIViewRepresentable { let mapView = MKMapView() let positions: [PositionEntity] let waypoints: [WaypointEntity] - let region: MKCoordinateRegion let mapViewType: MKMapType // Offline Maps @@ -28,11 +27,10 @@ struct MapViewSwiftUI: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { // Parameters - mapView.addAnnotations(positions) + mapView.fit(annotations: positions, andShow: true) mapView.addAnnotations(waypoints) mapView.mapType = mapViewType - mapView.setRegion(region, animated: true) - mapView.setUserTrackingMode(.none, animated: false) + mapView.setUserTrackingMode(.none, animated: true) // Other MKMapView Settings mapView.isPitchEnabled = true mapView.isRotateEnabled = true @@ -61,12 +59,8 @@ struct MapViewSwiftUI: UIViewRepresentable { let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path if fileManager.fileExists(atPath: tilePath) { - //if let tilePath = Bundle.main.path(forResource: "offline_map", ofType: "mbtiles") { - print("Loading local map file") - if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { - overlay.canReplaceMapContent = false//customMapOverlay.canReplaceMapContent mapView.addOverlay(overlay) } @@ -79,9 +73,6 @@ struct MapViewSwiftUI: UIViewRepresentable { self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile } } - if dynamicRegion { - self.moveToMeshRegion(mapView) - } mapView.removeAnnotations(mapView.annotations) mapView.addAnnotations(positions) mapView.addAnnotations(waypoints) @@ -91,53 +82,17 @@ struct MapViewSwiftUI: UIViewRepresentable { return Coordinator(self) } - func moveToMeshRegion(_ mapView: MKMapView) { - //go through the annotations and create a bounding box that encloses them - var minLat: CLLocationDegrees = 90.0 - var maxLat: CLLocationDegrees = -90.0 - var minLon: CLLocationDegrees = 180.0 - var maxLon: CLLocationDegrees = -180.0 - - for annotation in mapView.annotations { - if annotation.isKind(of: MKAnnotation.self) { - minLat = min(minLat, annotation.coordinate.latitude) - maxLat = max(maxLat, annotation.coordinate.latitude) - minLon = min(minLon, annotation.coordinate.longitude) - maxLon = max(maxLon, annotation.coordinate.longitude) - } - } - - //check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location) - if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 { - return - } else if minLat == maxLat && minLon == maxLon { - //then we are focussed on a single point (probably because there is only one node with a position) - //widen that out a little (don't zoom way in to that point) - - //0.001 degrees latitude is about 100m - //the mapView.regionThatFits call below will expand this out to a rectangle - minLat = minLat - 0.001 - maxLat = maxLat + 0.001 - } - - let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2) - let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5) - let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span)) - mapView.setRegion(region, animated: true) - } - final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { var parent: MapViewSwiftUI var longPressRecognizer = UILongPressGestureRecognizer() - var overlays: [Overlay] = [] init(_ parent: MapViewSwiftUI) { self.parent = parent super.init() self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler)) - self.longPressRecognizer.minimumPressDuration = 0.2 + self.longPressRecognizer.minimumPressDuration = 0.3 self.longPressRecognizer.delegate = self self.parent.mapView.addGestureRecognizer(longPressRecognizer) self.overlays = [] @@ -163,7 +118,6 @@ struct MapViewSwiftUI: UIViewRepresentable { let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint") annotationView.canShowCallout = true if waypointAnnotation.icon == 0 { - print(waypointAnnotation.icon) annotationView.glyphText = "📍" } else { annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍") diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 71117497..f1d8a03b 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -51,7 +51,7 @@ struct NodeDetail: View { } else { presentingWaypointForm = true } - }, positions: annotations, waypoints: Array(waypoints), region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType, + }, positions: annotations, waypoints: Array(waypoints), mapViewType: mapType, customMapOverlay: self.customMapOverlay, overlays: self.overlays ) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 06593a6f..30aede82 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -59,7 +59,7 @@ struct NodeMap: View { presentingWaypointForm = true } - }, positions: Array(positions), waypoints: Array(waypoints), region: MKCoordinateRegion(center: LocationHelper.currentLocation, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)), mapViewType: mapType, + }, positions: Array(positions), waypoints: Array(waypoints), mapViewType: mapType, customMapOverlay: self.customMapOverlay, overlays: self.overlays ) From 3d1111972c043374f2fe67d9e491ab9edb0d8213 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jan 2023 10:39:08 -0800 Subject: [PATCH 33/57] Organize map views, comment out un used controls --- Meshtastic.xcodeproj/project.pbxproj | 8 +- .../Map/{ => Custom}/LocalMBTileOverlay.swift | 0 .../{ => Custom}/MapViewFitExtension.swift | 0 .../Map/{ => Custom}/MapViewModule.swift | 0 .../Map/{ => Custom}/MapViewSwiftUI.swift | 0 .../Map/Custom/PositionAnnotationView.swift | 114 +++++++++--------- 6 files changed, 61 insertions(+), 61 deletions(-) rename Meshtastic/Views/Map/{ => Custom}/LocalMBTileOverlay.swift (100%) rename Meshtastic/Views/Map/{ => Custom}/MapViewFitExtension.swift (100%) rename Meshtastic/Views/Map/{ => Custom}/MapViewModule.swift (100%) rename Meshtastic/Views/Map/{ => Custom}/MapViewSwiftUI.swift (100%) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e078d344..7e342fb5 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -280,11 +280,7 @@ isa = PBXGroup; children = ( C9A7BC0E27759A6800760B50 /* Custom */, - C9A88B54278B503C00BD810A /* MapViewModule.swift */, - C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */, - DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, DD964FBE296E76EF007C176F /* WaypointFormView.swift */, - DD964FC32974767D007C176F /* MapViewFitExtension.swift */, ); path = Map; sourceTree = ""; @@ -292,6 +288,10 @@ C9A7BC0E27759A6800760B50 /* Custom */ = { isa = PBXGroup; children = ( + C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */, + DD964FC32974767D007C176F /* MapViewFitExtension.swift */, + DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, + C9A88B54278B503C00BD810A /* MapViewModule.swift */, C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */, ); path = Custom; diff --git a/Meshtastic/Views/Map/LocalMBTileOverlay.swift b/Meshtastic/Views/Map/Custom/LocalMBTileOverlay.swift similarity index 100% rename from Meshtastic/Views/Map/LocalMBTileOverlay.swift rename to Meshtastic/Views/Map/Custom/LocalMBTileOverlay.swift diff --git a/Meshtastic/Views/Map/MapViewFitExtension.swift b/Meshtastic/Views/Map/Custom/MapViewFitExtension.swift similarity index 100% rename from Meshtastic/Views/Map/MapViewFitExtension.swift rename to Meshtastic/Views/Map/Custom/MapViewFitExtension.swift diff --git a/Meshtastic/Views/Map/MapViewModule.swift b/Meshtastic/Views/Map/Custom/MapViewModule.swift similarity index 100% rename from Meshtastic/Views/Map/MapViewModule.swift rename to Meshtastic/Views/Map/Custom/MapViewModule.swift diff --git a/Meshtastic/Views/Map/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift similarity index 100% rename from Meshtastic/Views/Map/MapViewSwiftUI.swift rename to Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift diff --git a/Meshtastic/Views/Map/Custom/PositionAnnotationView.swift b/Meshtastic/Views/Map/Custom/PositionAnnotationView.swift index 3c3e0f79..cfb843c3 100644 --- a/Meshtastic/Views/Map/Custom/PositionAnnotationView.swift +++ b/Meshtastic/Views/Map/Custom/PositionAnnotationView.swift @@ -1,60 +1,60 @@ +//// +//// PositionAnnotationView.swift +//// MeshtasticApple +//// +//// Created by Joshua Pirihi on 24/12/21. +//// // -// PositionAnnotationView.swift -// MeshtasticApple +//import UIKit +//import MapKit +//import SwiftUI // -// Created by Joshua Pirihi on 24/12/21. +//// a simple circle annotation, with a string in it +//class PositionAnnotation: NSObject, MKAnnotation { // - -import UIKit -import MapKit -import SwiftUI - -// a simple circle annotation, with a string in it -class PositionAnnotation: NSObject, MKAnnotation { - - // This property must be key-value observable, which the `@objc dynamic` attributes provide. - @objc dynamic var coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) - - // Required if you set the annotation view's `canShowCallout` property to `true` - // this string fills the callout label when you tap an annotation - var title: String? - - // the text to appear inside the little circle - var shortName: String? - -} - -class PositionAnnotationView: MKAnnotationView { - - private let annotationFrame = CGRect(x: 0, y: 0, width: 40, height: 40) - private let label: UILabel - - override init(annotation: MKAnnotation?, reuseIdentifier: String?) { - self.label = UILabel(frame: annotationFrame.offsetBy(dx: 0, dy: 0)) - super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) - self.frame = annotationFrame - self.label.font = UIFont.preferredFont(forTextStyle: .caption2) - self.label.textColor = .white - self.label.textAlignment = .center - self.backgroundColor = .clear - self.addSubview(label) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) not implemented!") - } - - public var name: String = "" { - didSet { - self.label.text = name - } - } - - override func draw(_ rect: CGRect) { - guard let context = UIGraphicsGetCurrentContext() else { return } - - let circleRect = CGRect(x: 1, y: 1, width: 38, height: 38) - context.setFillColor(Color.accentColor.cgColor ?? CGColor(red: 0, green: 0.5, blue: 1.0, alpha: 1.0)) - context.fillEllipse(in: circleRect) - } -} +// // This property must be key-value observable, which the `@objc dynamic` attributes provide. +// @objc dynamic var coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) +// +// // Required if you set the annotation view's `canShowCallout` property to `true` +// // this string fills the callout label when you tap an annotation +// var title: String? +// +// // the text to appear inside the little circle +// var shortName: String? +// +//} +// +//class PositionAnnotationView: MKAnnotationView { +// +// private let annotationFrame = CGRect(x: 0, y: 0, width: 40, height: 40) +// private let label: UILabel +// +// override init(annotation: MKAnnotation?, reuseIdentifier: String?) { +// self.label = UILabel(frame: annotationFrame.offsetBy(dx: 0, dy: 0)) +// super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) +// self.frame = annotationFrame +// self.label.font = UIFont.preferredFont(forTextStyle: .caption2) +// self.label.textColor = .white +// self.label.textAlignment = .center +// self.backgroundColor = .clear +// self.addSubview(label) +// } +// +// required init?(coder aDecoder: NSCoder) { +// fatalError("init(coder:) not implemented!") +// } +// +// public var name: String = "" { +// didSet { +// self.label.text = name +// } +// } +// +// override func draw(_ rect: CGRect) { +// guard let context = UIGraphicsGetCurrentContext() else { return } +// +// let circleRect = CGRect(x: 1, y: 1, width: 38, height: 38) +// context.setFillColor(Color.accentColor.cgColor ?? CGColor(red: 0, green: 0.5, blue: 1.0, alpha: 1.0)) +// context.fillEllipse(in: circleRect) +// } +//} From f59b4a8a868285d4f8f31d753eb0629165824355 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jan 2023 12:47:42 -0800 Subject: [PATCH 34/57] Center the node detail map on positions, node map on both positions and waypoints --- Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift | 8 +++++++- Meshtastic/Views/Nodes/NodeDetail.swift | 2 ++ Meshtastic/Views/Nodes/NodeMap.swift | 6 +++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index a8e37692..9438e038 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -15,6 +15,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let positions: [PositionEntity] let waypoints: [WaypointEntity] let mapViewType: MKMapType + let centerOnPositionsOnly: Bool // Offline Maps //make this view dependent on the UserDefault that is updated when importing a new map file @@ -27,8 +28,13 @@ struct MapViewSwiftUI: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { // Parameters - mapView.fit(annotations: positions, andShow: true) mapView.addAnnotations(waypoints) + if centerOnPositionsOnly { + mapView.fit(annotations: positions, andShow: true) + } else { + mapView.addAnnotations(positions) + mapView.fitAllAnnotations() + } mapView.mapType = mapViewType mapView.setUserTrackingMode(.none, animated: true) // Other MKMapView Settings diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index f1d8a03b..c456e246 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -52,8 +52,10 @@ struct NodeDetail: View { presentingWaypointForm = true } }, positions: annotations, waypoints: Array(waypoints), mapViewType: mapType, + centerOnPositionsOnly: true, customMapOverlay: self.customMapOverlay, overlays: self.overlays + ) VStack { Spacer() diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 30aede82..efe976f1 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -33,6 +33,10 @@ struct NodeMap: View { predicate: NSPredicate(format: "time >= %@", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .easeIn) private var positions: FetchedResults + //@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], + // predicate: NSPredicate(format: "expire >= %@", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .easeIn) + //private var waypoints: FetchedResults + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], animation: .easeIn) private var waypoints: FetchedResults @@ -58,8 +62,8 @@ struct NodeMap: View { } else { presentingWaypointForm = true } - }, positions: Array(positions), waypoints: Array(waypoints), mapViewType: mapType, + centerOnPositionsOnly: false, customMapOverlay: self.customMapOverlay, overlays: self.overlays ) From ac6cca636ff1673aca5673666897b81616ba56c6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jan 2023 19:15:00 -0800 Subject: [PATCH 35/57] Updates to locked waypoints --- Meshtastic/Enums/AppSettingsEnums.swift | 40 ++++++++++++++++--- Meshtastic/Helpers/MeshPackets.swift | 4 +- .../contents | 2 +- .../Persistence/WaypointEntityExtension.swift | 2 +- Meshtastic/Protobufs/mesh.pb.swift | 15 +++---- Meshtastic/Views/Map/WaypointFormView.swift | 4 +- Meshtastic/Views/Nodes/NodeDetail.swift | 5 ++- Meshtastic/Views/Nodes/NodeMap.swift | 9 ++--- de.lproj/Localizable.strings | 3 ++ en.lproj/Localizable.strings | 3 ++ zh-Hans.lproj/Localizable.strings | 3 ++ 11 files changed, 66 insertions(+), 24 deletions(-) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index f94a9b6c..53ce177b 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -6,6 +6,7 @@ // import Foundation +import MapKit enum KeyboardType: Int, CaseIterable, Identifiable { @@ -36,24 +37,51 @@ enum KeyboardType: Int, CaseIterable, Identifiable { enum MeshMapType: String, CaseIterable, Identifiable { - case satellite = "satellite" + case standard = "standard" + case mutedStandard = "mutedStandard" case hybrid = "hybrid" - case standard = "standard" + case hybridFlyover = "hybridFlyover" + case satellite = "satellite" + case satelliteFlyover = "satelliteFlyover" + var id: String { self.rawValue } var description: String { get { switch self { - case .satellite: - return NSLocalizedString("satellite", comment: "Satellite Map Type") case .standard: - return NSLocalizedString("standard", comment: "Standard Map Type") + return NSLocalizedString("standard", comment: "Standard") + case .mutedStandard: + return NSLocalizedString("standard.muted", comment: "Standard Muted") case .hybrid: - return NSLocalizedString("hybrid", comment: "Hybrid Map Type") + return NSLocalizedString("hybrid", comment: "Hybrid") + case .hybridFlyover: + return NSLocalizedString("hybrid.flyover", comment: "Hybrid Flyover") + case .satellite: + return NSLocalizedString("satellite", comment: "Satellite") + case .satelliteFlyover: + return NSLocalizedString("satellite.flyover", comment: "Satellite Flyover") } } } + func MKMapTypeValue() -> MKMapType { + + switch self { + case .standard: + return MKMapType.standard + case .mutedStandard: + return MKMapType.mutedStandard + case .hybrid: + return MKMapType.hybrid + case .hybridFlyover: + return MKMapType.hybridFlyover + case .satellite: + return MKMapType.satellite + case .satelliteFlyover: + return MKMapType.satelliteFlyover + } + } } enum LocationUpdateInterval: Int, CaseIterable, Identifiable { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index fc23f06e..795ee220 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1334,7 +1334,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { waypoint.latitudeI = waypointMessage.latitudeI waypoint.longitudeI = waypointMessage.longitudeI waypoint.icon = Int64(waypointMessage.icon) - waypoint.locked = waypointMessage.locked + waypoint.locked = Int64(waypointMessage.lockedTo) if waypointMessage.expire > 0 { waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) } @@ -1353,7 +1353,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI fetchedWaypoint[0].icon = Int64(waypointMessage.icon) - fetchedWaypoint[0].locked = waypointMessage.locked + fetchedWaypoint[0].locked = Int64(waypointMessage.lockedTo) if waypointMessage.expire > 0 { fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents index cab2cacc..da2cb3ef 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents @@ -272,7 +272,7 @@ - + diff --git a/Meshtastic/Persistence/WaypointEntityExtension.swift b/Meshtastic/Persistence/WaypointEntityExtension.swift index 4edd9566..bc897615 100644 --- a/Meshtastic/Persistence/WaypointEntityExtension.swift +++ b/Meshtastic/Persistence/WaypointEntityExtension.swift @@ -53,5 +53,5 @@ extension WaypointEntity: MKAnnotation { public var subtitle: String? { (longDescription ?? "") + String(expire != nil ? "\n⌛ Expires \(String(describing: expire?.formatted()))" : "") + - String(locked ? "\n🔒 Locked" : "") } + String(locked > 0 ? "\n🔒 Locked" : "") } } diff --git a/Meshtastic/Protobufs/mesh.pb.swift b/Meshtastic/Protobufs/mesh.pb.swift index 9a7101aa..997e627f 100644 --- a/Meshtastic/Protobufs/mesh.pb.swift +++ b/Meshtastic/Protobufs/mesh.pb.swift @@ -1143,8 +1143,9 @@ struct Waypoint { var expire: UInt32 = 0 /// - /// If true, only allow the original sender to update the waypoint. - var locked: Bool = false + /// If greater than zero, treat the value as a nodenum only allowing them to update the waypoint. + /// If zero, the waypoint is open to be edited by any member of the mesh. + var lockedTo: UInt32 = 0 /// /// Name of the waypoint - max 30 chars @@ -2779,7 +2780,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 2: .standard(proto: "latitude_i"), 3: .standard(proto: "longitude_i"), 4: .same(proto: "expire"), - 5: .same(proto: "locked"), + 5: .standard(proto: "locked_to"), 6: .same(proto: "name"), 7: .same(proto: "description"), 8: .same(proto: "icon"), @@ -2795,7 +2796,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 2: try { try decoder.decodeSingularSFixed32Field(value: &self.latitudeI) }() case 3: try { try decoder.decodeSingularSFixed32Field(value: &self.longitudeI) }() case 4: try { try decoder.decodeSingularUInt32Field(value: &self.expire) }() - case 5: try { try decoder.decodeSingularBoolField(value: &self.locked) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self.lockedTo) }() case 6: try { try decoder.decodeSingularStringField(value: &self.name) }() case 7: try { try decoder.decodeSingularStringField(value: &self.description_p) }() case 8: try { try decoder.decodeSingularFixed32Field(value: &self.icon) }() @@ -2817,8 +2818,8 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if self.expire != 0 { try visitor.visitSingularUInt32Field(value: self.expire, fieldNumber: 4) } - if self.locked != false { - try visitor.visitSingularBoolField(value: self.locked, fieldNumber: 5) + if self.lockedTo != 0 { + try visitor.visitSingularUInt32Field(value: self.lockedTo, fieldNumber: 5) } if !self.name.isEmpty { try visitor.visitSingularStringField(value: self.name, fieldNumber: 6) @@ -2837,7 +2838,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if lhs.latitudeI != rhs.latitudeI {return false} if lhs.longitudeI != rhs.longitudeI {return false} if lhs.expire != rhs.expire {return false} - if lhs.locked != rhs.locked {return false} + if lhs.lockedTo != rhs.lockedTo {return false} if lhs.name != rhs.name {return false} if lhs.description_p != rhs.description_p {return false} if lhs.icon != rhs.icon {return false} diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 0e6712e2..d0aecc58 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -131,7 +131,9 @@ struct WaypointFormView: View { // First element as an UInt32 let unicode = unicodeScalers[unicodeScalers.startIndex].value newWaypoint.icon = unicode - newWaypoint.locked = locked + if locked { + newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) + } if expire.timeIntervalSince1970 > 0 { newWaypoint.expire = UInt32(expire.timeIntervalSince1970) } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index c456e246..8dcc538d 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -28,7 +28,10 @@ struct NodeDetail: View { var node: NodeInfoEntity - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], animation: .default) + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], + predicate: NSPredicate( + format: "expire == nil || expire >= %@", Date() as NSDate + ), animation: .easeIn) private var waypoints: FetchedResults var body: some View { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index efe976f1..8ca536f8 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -33,11 +33,10 @@ struct NodeMap: View { predicate: NSPredicate(format: "time >= %@", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .easeIn) private var positions: FetchedResults - //@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], - // predicate: NSPredicate(format: "expire >= %@", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .easeIn) - //private var waypoints: FetchedResults - - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], animation: .easeIn) + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], + predicate: NSPredicate( + format: "expire == nil || expire >= %@", Date() as NSDate + ), animation: .easeIn) private var waypoints: FetchedResults @State private var mapType: MKMapType = .standard diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 54aeb93d..87117b20 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -92,6 +92,7 @@ "heard"="Gehört"; "heard.last"="Zuletzt gehört"; "hybrid"="Hybrid"; +"hybrid.flyover"="Hybrid Flyover"; "include"="Include"; "inputevent.none"="Keins"; "inputevent.up"="Hoch"; @@ -214,6 +215,7 @@ "routing.badRequest"="Bad Request"; "routing.notauthorized"="Nicht authorisiert"; "satellite"="Satellit"; +"satellite.flyover"="Satellite Flyover"; "save"="Speichern"; "save.config %@"="Save Config for %@"; "serial"="Serial"; @@ -232,6 +234,7 @@ "select.menu.item"="Wähle einen Menüeintrag aus"; "set.region"="Setze LoRa Region"; "standard"="Standard"; +"standard.muted"="Standard Muted"; "ssid"="SSID"; "tapback"="Tapback Response"; "tapback.heart"="Gehört"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 6b772ea7..fd37f919 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -92,6 +92,7 @@ "heard"="Heard"; "heard.last"="Last Heard"; "hybrid"="Hybrid"; +"hybrid.flyover"="Hybrid Flyover"; "include"="Include"; "inputevent.none"="None"; "inputevent.up"="Up"; @@ -214,6 +215,7 @@ "routing.badRequest"="Bad Request"; "routing.notauthorized"="Not Authorized"; "satellite"="Satellite"; +"satellite.flyover"="Satellite Flyover"; "save"="Save"; "save.config %@"="Save Config for %@"; "serial"="Serial"; @@ -232,6 +234,7 @@ "select.menu.item"="Select an item from the menu"; "set.region"="Set LoRa Region"; "standard"="Standard"; +"standard.muted"="Standard Muted"; "ssid"="SSID"; "tapback"="Tapback Response"; "tapback.heart"="Heart"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index cfba1be7..946d2694 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -92,6 +92,7 @@ "heard"="收到"; "heard.last"="最后收到"; "hybrid"="混合"; +"hybrid.flyover"="Hybrid Flyover"; "include"="包含"; "inputevent.none"="无"; "inputevent.up"="上"; @@ -214,6 +215,7 @@ "routing.badRequest"="错误请求"; "routing.notauthorized"="未授权"; "satellite"="卫星"; +"satellite.flyover"="Satellite Flyover"; "save"="保存"; "save.config %@"="保存%@的配置"; "serial"="串口"; @@ -232,6 +234,7 @@ "select.menu.item"="从菜单选择一个选项"; "set.region"="设置 LoRa 区域"; "standard"="标准"; +"standard.muted"="Standard Muted"; "ssid"="SSID"; "tapback"="Tapback Response"; "tapback.heart"="Heart"; From 5cb9f9d9f6b5eaa3d657650d858b22ceb86c514d Mon Sep 17 00:00:00 2001 From: Brad Midgley Date: Sun, 15 Jan 2023 22:17:21 -0700 Subject: [PATCH 36/57] typo --- .../Views/Settings/Config/Module/CannedMessagesConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 46071f37..c9c5374f 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -74,7 +74,7 @@ struct CannedMessagesConfig: View { HStack { Label("Messages", systemImage: "message.fill") - TextField("Messages seperate with |", text: $messages, axis: .vertical) + TextField("Messages separate with |", text: $messages, axis: .vertical) .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) From b00f8c2daa82fdccf95a21668278d2eb74496e94 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 16 Jan 2023 17:40:28 -0800 Subject: [PATCH 37/57] Janky waypoint edit --- Meshtastic.xcodeproj/project.pbxproj | 4 +++ Meshtastic/Helpers/BLEManager.swift | 15 +++++------ Meshtastic/Persistence/QueryCoreData.swift | 24 +++++++++++++++++ .../Views/Map/Custom/MapViewSwiftUI.swift | 21 ++++++++++++--- Meshtastic/Views/Map/WaypointFormView.swift | 27 +++++++++++++++++-- Meshtastic/Views/Nodes/NodeDetail.swift | 21 ++++++--------- Meshtastic/Views/Nodes/NodeMap.swift | 15 +++++------ Meshtastic/Views/Settings/AppSettings.swift | 3 +++ 8 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 Meshtastic/Persistence/QueryCoreData.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 7e342fb5..a18269eb 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -70,6 +70,7 @@ DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormView.swift */; }; DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; }; DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC32974767D007C176F /* MapViewFitExtension.swift */; }; + DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC52975DBFD007C176F /* QueryCoreData.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; }; DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD994B68295F88B60013760A /* IntervalEnums.swift */; }; @@ -197,6 +198,7 @@ DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = ""; }; DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = ""; }; DD964FC32974767D007C176F /* MapViewFitExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewFitExtension.swift; sourceTree = ""; }; + DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = ""; }; DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = ""; }; DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = ""; }; @@ -570,6 +572,7 @@ DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */, DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */, DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */, + DD964FC52975DBFD007C176F /* QueryCoreData.swift */, DD3CC6C128EB9D4900FA9159 /* UpdateCoreData.swift */, DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */, ); @@ -804,6 +807,7 @@ DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */, DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */, DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */, + DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */, DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c4d0e3d9..65fe7a12 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -732,7 +732,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var success = false let fromNodeNum = UInt32(connectedPeripheral.num) var waypointPacket = waypoint - waypointPacket.id = UInt32.random(in: UInt32(UInt8.max)..= 1 ? waypointPacket.name : "Dropped Pin" - wayPointEntity.longDescription = waypointPacket.description_p - wayPointEntity.icon = Int64(waypointPacket.icon) - wayPointEntity.latitudeI = waypointPacket.latitudeI - wayPointEntity.longitudeI = waypointPacket.longitudeI + + let wayPointEntity = getWaypoint(id: Int64(waypoint.id), context: context!) + wayPointEntity.name = waypoint.name.count >= 1 ? waypointPacket.name : "Dropped Pin" + wayPointEntity.longDescription = waypoint.description_p + wayPointEntity.icon = Int64(waypoint.icon) + wayPointEntity.latitudeI = waypoint.latitudeI + wayPointEntity.longitudeI = waypoint.longitudeI do { try context!.save() print("💾 Updated Waypoint from Waypoint App Packet From: \(fromNodeNum)") diff --git a/Meshtastic/Persistence/QueryCoreData.swift b/Meshtastic/Persistence/QueryCoreData.swift new file mode 100644 index 00000000..3ddfe46d --- /dev/null +++ b/Meshtastic/Persistence/QueryCoreData.swift @@ -0,0 +1,24 @@ +// +// QueryCoreData.swift +// Meshtastic +// +// Created(c) Garth Vander Houwen 1/16/23. +// + +import CoreData + +public func getWaypoint(id: Int64, context: NSManagedObjectContext) -> WaypointEntity { + + let fetchWaypointRequest: NSFetchRequest = NSFetchRequest.init(entityName: "WaypointEntity") + fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(id)) + + do { + let fetchedWaypoint = try context.fetch(fetchWaypointRequest) as! [WaypointEntity] + if fetchedWaypoint.count == 1 { + return fetchedWaypoint[0] + } + } catch { + return WaypointEntity() + } + return WaypointEntity() +} diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 9438e038..5b9d54ab 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -3,14 +3,13 @@ // Meshtastic // // Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22. -// import SwiftUI import MapKit struct MapViewSwiftUI: UIViewRepresentable { - var onMarkerTap: (_ waypointCoordinate: CLLocationCoordinate2D? ) -> Void + var onMarkerTap: (_ waypointCoordinate: CLLocationCoordinate2D?, _ tag: Int? ) -> Void let mapView = MKMapView() let positions: [PositionEntity] let waypoints: [WaypointEntity] @@ -111,9 +110,11 @@ struct MapViewSwiftUI: UIViewRepresentable { case _ as MKClusterAnnotation: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup") annotationView.markerTintColor = .brown//.systemRed + annotationView.tag = -1 return annotationView case _ as PositionEntity: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node") + annotationView.tag = -1 annotationView.canShowCallout = true annotationView.glyphText = "📟" annotationView.clusteringIdentifier = "nodeGroup" @@ -122,6 +123,7 @@ struct MapViewSwiftUI: UIViewRepresentable { return annotationView case let waypointAnnotation as WaypointEntity: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint") + annotationView.tag = Int(waypointAnnotation.id) annotationView.canShowCallout = true if waypointAnnotation.icon == 0 { annotationView.glyphText = "📍" @@ -136,12 +138,24 @@ struct MapViewSwiftUI: UIViewRepresentable { } } + func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) + { + // Only Allow Edit for annotations with a tag + if view.tag > 0 { + // Screen Position - CGPoint + let location = longPressRecognizer.location(in: self.parent.mapView) + // Map Coordinate - CLLocationCoordinate2D + let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) + parent.onMarkerTap(coordinate, view.tag) + } + } + @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { // Screen Position - CGPoint let location = longPressRecognizer.location(in: self.parent.mapView) // Map Coordinate - CLLocationCoordinate2D let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - parent.onMarkerTap(coordinate) + parent.onMarkerTap(coordinate, 0) // Add annotation: let annotation = MKPointAnnotation() annotation.title = "Dropped Pin" @@ -291,7 +305,6 @@ struct MapViewSwiftUI: UIViewRepresentable { let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png" return URL(string: urlstring)! } - } } diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index d0aecc58..cea01009 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -13,9 +13,10 @@ struct WaypointFormView: View { @EnvironmentObject var bleManager: BLEManager @Environment(\.dismiss) private var dismiss @State var coordinate: CLLocationCoordinate2D + @State var id: Int = 0 @FocusState private var iconIsFocused: Bool - @State private var id: Int32? + @State private var name: String = "" @State private var description: String = "" @State private var icon: String = "📍" @@ -26,7 +27,7 @@ struct WaypointFormView: View { var body: some View { Form { let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) - Section(header: Text("Waypoint")) { + Section(header: Text((id > 0) ? "Editing Waypoint" : "Create Waypoint")) { HStack { Text("Location: \(String(format: "%.5f", coordinate.latitude ) + "," + String(format: "%.5f", coordinate.longitude ))") .textSelection(.enabled) @@ -121,7 +122,14 @@ struct WaypointFormView: View { } HStack { Button { + var newWaypoint = Waypoint() + + if id == 0 { + newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 { + let waypoint = getWaypoint(id: Int64(id), context: bleManager.context!) + id = Int(waypoint.id) + name = waypoint.name ?? "Dropped Pin" + description = waypoint.longDescription ?? "" + icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍") + if waypoint.expire != nil { + expires = true + expire = waypoint.expire ?? Date() + } else { + expires = false + } + } + } } } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 8dcc538d..e8a83316 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -14,6 +14,7 @@ struct NodeDetail: View { @State var satsInView = 0 @State private var mapType: MKMapType = .standard @State var waypointCoordinate: CLLocationCoordinate2D? + @State var editingWaypoint: Int = 0 @State private var showingDetailsPopover = false @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false @@ -47,11 +48,10 @@ struct NodeDetail: View { ZStack { let annotations = node.positions?.array as! [PositionEntity] ZStack { - MapViewSwiftUI(onMarkerTap: { coord in + MapViewSwiftUI(onMarkerTap: { coord, id in waypointCoordinate = coord - if waypointCoordinate == nil { - presentingWaypointForm = false - } else { + editingWaypoint = id ?? 0 + if waypointCoordinate != nil { presentingWaypointForm = true } }, positions: annotations, waypoints: Array(waypoints), mapViewType: mapType, @@ -66,12 +66,9 @@ struct NodeDetail: View { .font(.caption) .offset(y: 20) Picker("Map Type", selection: $mapType) { - Text("Standard").tag(MKMapType.standard) - Text("Muted").tag(MKMapType.mutedStandard) - Text("Hybrid").tag(MKMapType.hybrid) - Text("Hybrid Flyover").tag(MKMapType.hybridFlyover) - Text("Satellite").tag(MKMapType.satellite) - Text("Sat Flyover").tag(MKMapType.satelliteFlyover) + ForEach(MeshMapType.allCases) { map in + Text(map.description).tag(map.MKMapTypeValue()) + } } .pickerStyle(.menu) } @@ -388,11 +385,9 @@ struct NodeDetail: View { } .edgesIgnoringSafeArea([.leading, .trailing]) .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { - if waypointCoordinate != nil { - WaypointFormView(coordinate: waypointCoordinate!) + WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation, id: editingWaypoint) .presentationDetents([.medium, .large]) .presentationDragIndicator(.automatic) - } } .navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline) .navigationBarItems(trailing: diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 8ca536f8..1642c62f 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -41,6 +41,7 @@ struct NodeMap: View { @State private var mapType: MKMapType = .standard @State var waypointCoordinate: CLLocationCoordinate2D? + @State var editingWaypoint: Int = 0 @State private var presentingWaypointForm = false @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( mapName: "offlinemap", @@ -54,7 +55,8 @@ struct NodeMap: View { NavigationStack { ZStack { - MapViewSwiftUI(onMarkerTap: { coord in + MapViewSwiftUI(onMarkerTap: { coord, id in + editingWaypoint = id ?? 0 waypointCoordinate = coord if waypointCoordinate == nil { presentingWaypointForm = false @@ -69,12 +71,9 @@ struct NodeMap: View { VStack { Spacer() Picker("Map Type", selection: $mapType) { - Text("Standard").tag(MKMapType.standard) - Text("Standard Muted").tag(MKMapType.mutedStandard) - Text("Hybrid").tag(MKMapType.hybrid) - Text("Hybrid Flyover").tag(MKMapType.hybridFlyover) - Text("Satellite").tag(MKMapType.satellite) - Text("Satellite Flyover").tag(MKMapType.satelliteFlyover) + ForEach(MeshMapType.allCases) { map in + Text(map.description).tag(map.MKMapTypeValue()) + } } .pickerStyle(.menu) } @@ -83,7 +82,7 @@ struct NodeMap: View { .frame(maxHeight: .infinity) .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { if waypointCoordinate != nil { - WaypointFormView(coordinate: waypointCoordinate!) + WaypointFormView(coordinate: waypointCoordinate!, id: editingWaypoint) .presentationDetents([.medium, .large]) .presentationDragIndicator(.automatic) } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index a9d9ada1..9e2a2af7 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -102,6 +102,9 @@ struct AppSettings: View { .onAppear { self.bleManager.context = context } + .onChange(of: userSettings.provideLocation) { newProvideLocation in + self.bleManager.sendWantConfig() + } } } From 33d843937e6d22bf3870ac6ffdf337ce0b94ff74 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 16 Jan 2023 23:16:57 -0800 Subject: [PATCH 38/57] Edit waypoint round 2 --- Meshtastic/Helpers/BLEManager.swift | 2 +- Meshtastic/Persistence/QueryCoreData.swift | 4 +- .../Views/Map/Custom/MapViewSwiftUI.swift | 39 +++++++++++++------ Meshtastic/Views/Map/WaypointFormView.swift | 11 +++++- Meshtastic/Views/Nodes/NodeDetail.swift | 8 +++- Meshtastic/Views/Nodes/NodeMap.swift | 8 +++- 6 files changed, 52 insertions(+), 20 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 65fe7a12..4e2c12c9 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -735,7 +735,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var meshPacket = MeshPacket() meshPacket.to = emptyNodeNum meshPacket.from = fromNodeNum - meshPacket.id = waypointPacket.id meshPacket.wantAck = true var dataMessage = DataMessage() dataMessage.payload = try! waypointPacket.serializedData() @@ -753,6 +752,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { success = true let wayPointEntity = getWaypoint(id: Int64(waypoint.id), context: context!) + wayPointEntity.id = Int64(waypoint.id) wayPointEntity.name = waypoint.name.count >= 1 ? waypointPacket.name : "Dropped Pin" wayPointEntity.longDescription = waypoint.description_p wayPointEntity.icon = Int64(waypoint.icon) diff --git a/Meshtastic/Persistence/QueryCoreData.swift b/Meshtastic/Persistence/QueryCoreData.swift index 3ddfe46d..72692819 100644 --- a/Meshtastic/Persistence/QueryCoreData.swift +++ b/Meshtastic/Persistence/QueryCoreData.swift @@ -18,7 +18,7 @@ public func getWaypoint(id: Int64, context: NSManagedObjectContext) -> WaypointE return fetchedWaypoint[0] } } catch { - return WaypointEntity() + return WaypointEntity(context: context) } - return WaypointEntity() + return WaypointEntity(context: context) } diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 5b9d54ab..a3a83077 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -9,7 +9,8 @@ import MapKit struct MapViewSwiftUI: UIViewRepresentable { - var onMarkerTap: (_ waypointCoordinate: CLLocationCoordinate2D?, _ tag: Int? ) -> Void + var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D?, _ tag: Int ) -> Void + var onWaypointEdit: (_ waypointId: Int ) -> Void let mapView = MKMapView() let positions: [PositionEntity] let waypoints: [WaypointEntity] @@ -124,6 +125,7 @@ struct MapViewSwiftUI: UIViewRepresentable { case let waypointAnnotation as WaypointEntity: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint") annotationView.tag = Int(waypointAnnotation.id) + annotationView.isEnabled = true annotationView.canShowCallout = true if waypointAnnotation.icon == 0 { annotationView.glyphText = "📍" @@ -133,29 +135,42 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.clusteringIdentifier = "waypointGroup" annotationView.markerTintColor = UIColor(.indigo) annotationView.titleVisibility = .visible + let editWaypoint = UIButton(type: .detailDisclosure) + editWaypoint.setImage(UIImage(systemName: "square.and.pencil"), for: .normal) + annotationView.rightCalloutAccessoryView = editWaypoint return annotationView default: return nil } } - func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) - { - // Only Allow Edit for annotations with a tag - if view.tag > 0 { - // Screen Position - CGPoint - let location = longPressRecognizer.location(in: self.parent.mapView) - // Map Coordinate - CLLocationCoordinate2D - let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - parent.onMarkerTap(coordinate, view.tag) - } + func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { + let control = view.annotation as! WaypointEntity + print(control) + // Only Allow Edit for annotations with a id + //if control.id > 0 { + print(view.tag) + parent.onWaypointEdit(view.tag) + //} } +// func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) +// { +// // Only Allow Edit for annotations with a tag +// if view.tag > 0 { +// // Screen Position - CGPoint +// let location = longPressRecognizer.location(in: self.parent.mapView) +// // Map Coordinate - CLLocationCoordinate2D +// let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) +// parent.onMarkerTap(coordinate, view.tag) +// } +// } + @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { // Screen Position - CGPoint let location = longPressRecognizer.location(in: self.parent.mapView) // Map Coordinate - CLLocationCoordinate2D let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - parent.onMarkerTap(coordinate, 0) + parent.onLongPress(coordinate, 0) // Add annotation: let annotation = MKPointAnnotation() annotation.title = "Dropped Pin" diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index cea01009..3b0f72fd 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -21,7 +21,7 @@ struct WaypointFormView: View { @State private var description: String = "" @State private var icon: String = "📍" @State private var expires: Bool = false - @State private var expire: Date = Date()// = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours + @State private var expire: Date = Date() // = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours @State private var locked: Bool = false var body: some View { @@ -126,6 +126,7 @@ struct WaypointFormView: View { var newWaypoint = Waypoint() if id == 0 { + newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. Date: Wed, 18 Jan 2023 06:57:23 -0800 Subject: [PATCH 39/57] Updated gen_protos --- gen_protos.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gen_protos.sh b/gen_protos.sh index 94c685c2..4cf7e405 100755 --- a/gen_protos.sh +++ b/gen_protos.sh @@ -1,8 +1,8 @@ #!/bin/bash # simple sanity checking for repo -if [ ! -d "../Meshtastic-protobufs" ]; then - echo "Please check out the https://github.com/meshtastic/Meshtastic-protobufs the parent directory." +if [ ! -d "../protobufs/meshtastic" ]; then + echo "Please check out the https://github.com/meshtastic/protobufs parent directory." exit fi @@ -12,15 +12,15 @@ if [ ! -x "`which protoc`" ]; then exit fi -pdir=$(realpath "../Meshtastic-protobufs") +pdir=$(realpath "../protobufs/meshtastic") sdir=$(realpath "./Meshtastic/Protobufs") echo "pdir:$pdir sdir:$sdir" -pfiles="admin.proto apponly.proto cannedmessages.proto channel.proto config.proto device_metadata.proto deviceonly.proto localonly.proto mesh.proto module_config.proto mqtt.proto portnums.proto remote_hardware.proto -storeforward.proto telemetry.proto" +pfiles="admin.proto apponly.proto cannedmessages.proto channel.proto config.proto device_metadata.proto deviceonly.proto localonly.proto mesh.proto module_config.proto mqtt.proto portnums.proto remote_hardware.proto rtttl.proto storeforward.proto telemetry.proto xmodem.proto" for pf in $pfiles do echo "Generating $pf..." protoc --swift_out=${sdir} --proto_path=${pdir} $pf + echo protoc --swift_out=${sdir} --proto_path=${pdir} $pf done echo "Done generating the swift files from the proto files." echo "Build, test, and commit changes." From 76847d9db3e494affe97e39418d467dc837888a4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jan 2023 08:45:47 -0800 Subject: [PATCH 40/57] Waypoint editor update --- Meshtastic/Views/Map/WaypointFormView.swift | 32 ++++++++++++++------- Meshtastic/Views/Nodes/NodeDetail.swift | 9 +++--- Meshtastic/Views/Nodes/NodeMap.swift | 2 +- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 3b0f72fd..0419207e 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -13,13 +13,15 @@ struct WaypointFormView: View { @EnvironmentObject var bleManager: BLEManager @Environment(\.dismiss) private var dismiss @State var coordinate: CLLocationCoordinate2D - @State var id: Int = 0 + @State var waypointId : Int = 0 @FocusState private var iconIsFocused: Bool @State private var name: String = "" @State private var description: String = "" @State private var icon: String = "📍" + @State private var latitude: Double = 0 + @State private var longitude: Double = 0 @State private var expires: Bool = false @State private var expire: Date = Date() // = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours @State private var locked: Bool = false @@ -27,9 +29,9 @@ struct WaypointFormView: View { var body: some View { Form { let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) - Section(header: Text((id > 0) ? "Editing Waypoint" : "Create Waypoint")) { + Section(header: Text((waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) { HStack { - Text("Location: \(String(format: "%.5f", coordinate.latitude ) + "," + String(format: "%.5f", coordinate.longitude ))") + Text("Location: \(String(format: "%.5f", latitude) + "," + String(format: "%.5f", longitude))") .textSelection(.enabled) .foregroundColor(Color.gray) .font(.caption2) @@ -125,11 +127,10 @@ struct WaypointFormView: View { var newWaypoint = Waypoint() - if id == 0 { - - newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 { + newWaypoint.id = UInt32(waypointId) } else { - newWaypoint.id = UInt32(id) + newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 { - let waypoint = getWaypoint(id: Int64(id), context: bleManager.context!) - id = Int(waypoint.id) + if waypointId > 0 { + let waypoint = getWaypoint(id: Int64(waypointId), context: bleManager.context!) + waypointId = Int(waypoint.id) name = waypoint.name ?? "Dropped Pin" description = waypoint.longDescription ?? "" icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍") + latitude = Double(waypoint.latitudeI) / 1e7 + longitude = Double(waypoint.longitudeI) / 1e7 if waypoint.expire != nil { expires = true expire = waypoint.expire ?? Date() } else { expires = false } + } else { + latitude = coordinate.latitude + longitude = coordinate.longitude } } } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 1849a136..78c6d78f 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -55,9 +55,10 @@ struct NodeDetail: View { presentingWaypointForm = true } }, onWaypointEdit: { wpId in - editingWaypoint = wpId - presentingWaypointForm = true - + if wpId > 0 { + editingWaypoint = wpId + presentingWaypointForm = true + } }, positions: annotations, waypoints: Array(waypoints), mapViewType: mapType, centerOnPositionsOnly: true, customMapOverlay: self.customMapOverlay, @@ -389,7 +390,7 @@ struct NodeDetail: View { } .edgesIgnoringSafeArea([.leading, .trailing]) .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { - WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation, id: editingWaypoint) + WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation, waypointId: editingWaypoint) .presentationDetents([.medium, .large]) .presentationDragIndicator(.automatic) } diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 7a6c7ef7..8952f3f5 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -86,7 +86,7 @@ struct NodeMap: View { .frame(maxHeight: .infinity) .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { if waypointCoordinate != nil { - WaypointFormView(coordinate: waypointCoordinate!, id: editingWaypoint) + WaypointFormView(coordinate: waypointCoordinate!, waypointId: editingWaypoint) .presentationDetents([.medium, .large]) .presentationDragIndicator(.automatic) } From 73bf7d1283b693fffe1368bdbfef9dbe2b399a50 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 18 Jan 2023 11:33:45 -0600 Subject: [PATCH 41/57] Fix proto generation script --- gen_protos.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gen_protos.sh b/gen_protos.sh index 4cf7e405..da96dd76 100755 --- a/gen_protos.sh +++ b/gen_protos.sh @@ -12,8 +12,17 @@ if [ ! -x "`which protoc`" ]; then exit fi +if [ ! -x "`which gsed`" ]; then + echo "Please install gnu-sed by running: brew install gnu-sed" + exit +fi + pdir=$(realpath "../protobufs/meshtastic") sdir=$(realpath "./Meshtastic/Protobufs") + +gsed -i 's/import "meshtastic\//import "/g' ../protobufs/meshtastic/* +gsed -i 's/package meshtastic;//g' ../protobufs/meshtastic/* + echo "pdir:$pdir sdir:$sdir" pfiles="admin.proto apponly.proto cannedmessages.proto channel.proto config.proto device_metadata.proto deviceonly.proto localonly.proto mesh.proto module_config.proto mqtt.proto portnums.proto remote_hardware.proto rtttl.proto storeforward.proto telemetry.proto xmodem.proto" for pf in $pfiles @@ -24,3 +33,5 @@ do done echo "Done generating the swift files from the proto files." echo "Build, test, and commit changes." + +cd ../protobufs/meshtastic && git reset --hard \ No newline at end of file From 0aee940794c8665097b36236d9a0e437505a6828 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jan 2023 11:38:43 -0800 Subject: [PATCH 42/57] Waypoint editing and new protos --- Meshtastic.xcodeproj/project.pbxproj | 14 +- Meshtastic/Protobufs/config.pb.swift | 16 ++ Meshtastic/Protobufs/device_metadata.pb.swift | 20 ++ Meshtastic/Protobufs/mesh.pb.swift | 72 +++++++- Meshtastic/Protobufs/rtttl.pb.swift | 75 ++++++++ Meshtastic/Protobufs/xmodem.pb.swift | 173 ++++++++++++++++++ Meshtastic/Views/Map/WaypointFormView.swift | 19 +- Meshtastic/Views/Nodes/NodeMap.swift | 13 +- gen_protos.sh | 3 +- 9 files changed, 381 insertions(+), 24 deletions(-) create mode 100644 Meshtastic/Protobufs/rtttl.pb.swift create mode 100644 Meshtastic/Protobufs/xmodem.pb.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index a18269eb..f3915cf1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; }; DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; }; DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; }; + DD2F145129787595009E4638 /* xmodem.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F144F29787595009E4638 /* xmodem.pb.swift */; }; + DD2F145229787595009E4638 /* rtttl.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F145029787595009E4638 /* rtttl.pb.swift */; }; DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; }; DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD35018A2852FC79000FC853 /* UserSettings.swift */; }; DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; }; @@ -148,6 +150,8 @@ DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = ""; }; DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = ""; }; DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = ""; }; + DD2F144F29787595009E4638 /* xmodem.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = xmodem.pb.swift; sourceTree = ""; }; + DD2F145029787595009E4638 /* rtttl.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = rtttl.pb.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; DD35018A2852FC79000FC853 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = ""; }; @@ -405,21 +409,23 @@ DDAF8C5626ED07740058C060 /* Protobufs */ = { isa = PBXGroup; children = ( - DDB3107128A6224100F1DE3D /* device_metadata.pb.swift */, - DDCFF600285453A7005FA625 /* localonly.pb.swift */, - DD4DED8F27AD2975004BA27E /* cannedmessages.pb.swift */, DDAF8C6126ED0A230058C060 /* admin.pb.swift */, C9A88B56278B559900BD810A /* apponly.pb.swift */, + DD4DED8F27AD2975004BA27E /* cannedmessages.pb.swift */, DDAF8C6426ED0A490058C060 /* channel.pb.swift */, DD4C158D2824AA7E0032668E /* config.pb.swift */, DDAF8C6826ED0D070058C060 /* deviceonly.pb.swift */, + DDB3107128A6224100F1DE3D /* device_metadata.pb.swift */, + DDCFF600285453A7005FA625 /* localonly.pb.swift */, DDAF8C5726ED07FD0058C060 /* mesh.pb.swift */, DD4C158B2824A91E0032668E /* module_config.pb.swift */, DDAF8C6026ED0A230058C060 /* mqtt.pb.swift */, DDAF8C5C26ED09490058C060 /* portnums.pb.swift */, DDAF8C6626ED0C8C0058C060 /* remote_hardware.pb.swift */, + DD2F145029787595009E4638 /* rtttl.pb.swift */, DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */, DDB2CC6D27F3EB47009C5FCC /* telemetry.pb.swift */, + DD2F144F29787595009E4638 /* xmodem.pb.swift */, ); path = Protobufs; sourceTree = ""; @@ -755,6 +761,7 @@ DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */, DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */, DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */, + DD2F145129787595009E4638 /* xmodem.pb.swift in Sources */, DD4C158C2824A91E0032668E /* module_config.pb.swift in Sources */, DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */, DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */, @@ -832,6 +839,7 @@ C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */, DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, + DD2F145229787595009E4638 /* rtttl.pb.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */, diff --git a/Meshtastic/Protobufs/config.pb.swift b/Meshtastic/Protobufs/config.pb.swift index bc6b37eb..62f472e7 100644 --- a/Meshtastic/Protobufs/config.pb.swift +++ b/Meshtastic/Protobufs/config.pb.swift @@ -941,6 +941,14 @@ struct Config { /// /// WLAN Band case lora24 // = 13 + + /// + /// Ukraine 433mhz + case ua433 // = 14 + + /// + /// Ukraine 868mhz + case ua868 // = 15 case UNRECOGNIZED(Int) init() { @@ -963,6 +971,8 @@ struct Config { case 11: self = .nz865 case 12: self = .th case 13: self = .lora24 + case 14: self = .ua433 + case 15: self = .ua868 default: self = .UNRECOGNIZED(rawValue) } } @@ -983,6 +993,8 @@ struct Config { case .nz865: return 11 case .th: return 12 case .lora24: return 13 + case .ua433: return 14 + case .ua868: return 15 case .UNRECOGNIZED(let i): return i } } @@ -1217,6 +1229,8 @@ extension Config.LoRaConfig.RegionCode: CaseIterable { .nz865, .th, .lora24, + .ua433, + .ua868, ] } @@ -2028,6 +2042,8 @@ extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding { 11: .same(proto: "NZ_865"), 12: .same(proto: "TH"), 13: .same(proto: "LORA_24"), + 14: .same(proto: "UA_433"), + 15: .same(proto: "UA_868"), ] } diff --git a/Meshtastic/Protobufs/device_metadata.pb.swift b/Meshtastic/Protobufs/device_metadata.pb.swift index 5e6698dc..a13084c7 100644 --- a/Meshtastic/Protobufs/device_metadata.pb.swift +++ b/Meshtastic/Protobufs/device_metadata.pb.swift @@ -51,6 +51,14 @@ struct DeviceMetadata { /// Indicates that the device has an ethernet peripheral var hasEthernet_p: Bool = false + /// + /// Indicates that the device's role in the mesh + var role: Config.DeviceConfig.Role = .client + + /// + /// Indicates the device's current enabled position flags + var positionFlags: Config.PositionConfig.PositionFlags = .unset + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -71,6 +79,8 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement 4: .same(proto: "hasWifi"), 5: .same(proto: "hasBluetooth"), 6: .same(proto: "hasEthernet"), + 7: .same(proto: "role"), + 8: .standard(proto: "position_flags"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -85,6 +95,8 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement case 4: try { try decoder.decodeSingularBoolField(value: &self.hasWifi_p) }() case 5: try { try decoder.decodeSingularBoolField(value: &self.hasBluetooth_p) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.hasEthernet_p) }() + case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }() + case 8: try { try decoder.decodeSingularEnumField(value: &self.positionFlags) }() default: break } } @@ -109,6 +121,12 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if self.hasEthernet_p != false { try visitor.visitSingularBoolField(value: self.hasEthernet_p, fieldNumber: 6) } + if self.role != .client { + try visitor.visitSingularEnumField(value: self.role, fieldNumber: 7) + } + if self.positionFlags != .unset { + try visitor.visitSingularEnumField(value: self.positionFlags, fieldNumber: 8) + } try unknownFields.traverse(visitor: &visitor) } @@ -119,6 +137,8 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if lhs.hasWifi_p != rhs.hasWifi_p {return false} if lhs.hasBluetooth_p != rhs.hasBluetooth_p {return false} if lhs.hasEthernet_p != rhs.hasEthernet_p {return false} + if lhs.role != rhs.role {return false} + if lhs.positionFlags != rhs.positionFlags {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/mesh.pb.swift b/Meshtastic/Protobufs/mesh.pb.swift index 997e627f..c2a49acf 100644 --- a/Meshtastic/Protobufs/mesh.pb.swift +++ b/Meshtastic/Protobufs/mesh.pb.swift @@ -1929,7 +1929,8 @@ struct FromRadio { set {_uniqueStorage()._payloadVariant = .channel(newValue)} } - /// Queue status info + /// + /// Queue status info var queueStatus: QueueStatus { get { if case .queueStatus(let v)? = _storage._payloadVariant {return v} @@ -1938,6 +1939,16 @@ struct FromRadio { set {_uniqueStorage()._payloadVariant = .queueStatus(newValue)} } + /// + /// File Transfer Chunk + var xmodemPacket: XModem { + get { + if case .xmodemPacket(let v)? = _storage._payloadVariant {return v} + return XModem() + } + set {_uniqueStorage()._payloadVariant = .xmodemPacket(newValue)} + } + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1978,8 +1989,12 @@ struct FromRadio { /// /// One packet is sent for each channel case channel(Channel) - /// Queue status info + /// + /// Queue status info case queueStatus(QueueStatus) + /// + /// File Transfer Chunk + case xmodemPacket(XModem) #if !swift(>=4.1) static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { @@ -2027,6 +2042,10 @@ struct FromRadio { guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() } return l == r }() + case (.xmodemPacket, .xmodemPacket): return { + guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -2089,6 +2108,14 @@ struct ToRadio { set {payloadVariant = .disconnect(newValue)} } + var xmodemPacket: XModem { + get { + if case .xmodemPacket(let v)? = payloadVariant {return v} + return XModem() + } + set {payloadVariant = .xmodemPacket(newValue)} + } + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -2112,6 +2139,7 @@ struct ToRadio { /// This is useful for serial links where there is no hardware/protocol based notification that the client has dropped the link. /// (Sending this message is optional for clients) case disconnect(Bool) + case xmodemPacket(XModem) #if !swift(>=4.1) static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { @@ -2131,6 +2159,10 @@ struct ToRadio { guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() } return l == r }() + case (.xmodemPacket, .xmodemPacket): return { + guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -3366,6 +3398,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 9: .same(proto: "moduleConfig"), 10: .same(proto: "channel"), 11: .same(proto: "queueStatus"), + 12: .same(proto: "xmodemPacket"), ] fileprivate class _StorageClass { @@ -3518,6 +3551,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation _storage._payloadVariant = .queueStatus(v) } }() + case 12: try { + var v: XModem? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .xmodemPacket(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .xmodemPacket(v) + } + }() default: break } } @@ -3574,6 +3620,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .queueStatus(let v)? = _storage._payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 11) }() + case .xmodemPacket?: try { + guard case .xmodemPacket(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + }() case nil: break } } @@ -3602,6 +3652,7 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa 1: .same(proto: "packet"), 3: .standard(proto: "want_config_id"), 4: .same(proto: "disconnect"), + 5: .same(proto: "xmodemPacket"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3639,6 +3690,19 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa self.payloadVariant = .disconnect(v) } }() + case 5: try { + var v: XModem? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .xmodemPacket(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .xmodemPacket(v) + } + }() default: break } } @@ -3662,6 +3726,10 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa guard case .disconnect(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 4) }() + case .xmodemPacket?: try { + guard case .xmodemPacket(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) diff --git a/Meshtastic/Protobufs/rtttl.pb.swift b/Meshtastic/Protobufs/rtttl.pb.swift new file mode 100644 index 00000000..3fc7fa98 --- /dev/null +++ b/Meshtastic/Protobufs/rtttl.pb.swift @@ -0,0 +1,75 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: rtttl.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// +/// Canned message module configuration. +struct RTTTLConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// Ringtone for PWM Buzzer in RTTTL Format. + var ringtone: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension RTTTLConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension RTTTLConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "RTTTLConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "ringtone"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.ringtone) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.ringtone.isEmpty { + try visitor.visitSingularStringField(value: self.ringtone, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: RTTTLConfig, rhs: RTTTLConfig) -> Bool { + if lhs.ringtone != rhs.ringtone {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Meshtastic/Protobufs/xmodem.pb.swift b/Meshtastic/Protobufs/xmodem.pb.swift new file mode 100644 index 00000000..fc57c812 --- /dev/null +++ b/Meshtastic/Protobufs/xmodem.pb.swift @@ -0,0 +1,173 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: xmodem.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct XModem { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var control: XModem.Control = .nul + + var seq: UInt32 = 0 + + var crc16: UInt32 = 0 + + var buffer: Data = Data() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum Control: SwiftProtobuf.Enum { + typealias RawValue = Int + case nul // = 0 + case soh // = 1 + case stx // = 2 + case eot // = 4 + case ack // = 6 + case nak // = 21 + case can // = 24 + case ctrlz // = 26 + case UNRECOGNIZED(Int) + + init() { + self = .nul + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .nul + case 1: self = .soh + case 2: self = .stx + case 4: self = .eot + case 6: self = .ack + case 21: self = .nak + case 24: self = .can + case 26: self = .ctrlz + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .nul: return 0 + case .soh: return 1 + case .stx: return 2 + case .eot: return 4 + case .ack: return 6 + case .nak: return 21 + case .can: return 24 + case .ctrlz: return 26 + case .UNRECOGNIZED(let i): return i + } + } + + } + + init() {} +} + +#if swift(>=4.2) + +extension XModem.Control: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static var allCases: [XModem.Control] = [ + .nul, + .soh, + .stx, + .eot, + .ack, + .nak, + .can, + .ctrlz, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension XModem: @unchecked Sendable {} +extension XModem.Control: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension XModem: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "XModem" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "control"), + 2: .same(proto: "seq"), + 3: .same(proto: "crc16"), + 4: .same(proto: "buffer"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.control) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.seq) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.crc16) }() + case 4: try { try decoder.decodeSingularBytesField(value: &self.buffer) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.control != .nul { + try visitor.visitSingularEnumField(value: self.control, fieldNumber: 1) + } + if self.seq != 0 { + try visitor.visitSingularUInt32Field(value: self.seq, fieldNumber: 2) + } + if self.crc16 != 0 { + try visitor.visitSingularUInt32Field(value: self.crc16, fieldNumber: 3) + } + if !self.buffer.isEmpty { + try visitor.visitSingularBytesField(value: self.buffer, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: XModem, rhs: XModem) -> Bool { + if lhs.control != rhs.control {return false} + if lhs.seq != rhs.seq {return false} + if lhs.crc16 != rhs.crc16 {return false} + if lhs.buffer != rhs.buffer {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension XModem.Control: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "NUL"), + 1: .same(proto: "SOH"), + 2: .same(proto: "STX"), + 4: .same(proto: "EOT"), + 6: .same(proto: "ACK"), + 21: .same(proto: "NAK"), + 24: .same(proto: "CAN"), + 26: .same(proto: "CTRLZ"), + ] +} diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 0419207e..f1b6af54 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -124,7 +124,7 @@ struct WaypointFormView: View { } HStack { Button { - + var newWaypoint = Waypoint() if waypointId > 0 { @@ -132,7 +132,7 @@ struct WaypointFormView: View { } else { newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 { let waypoint = getWaypoint(id: Int64(waypointId), context: bleManager.context!) waypointId = Int(waypoint.id) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 8952f3f5..7772f05d 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -56,17 +56,16 @@ struct NodeMap: View { ZStack { MapViewSwiftUI(onLongPress: { coord, id in - print(id) waypointCoordinate = coord - if waypointCoordinate == nil { - presentingWaypointForm = false - } else { + + if waypointCoordinate != nil { presentingWaypointForm = true } }, onWaypointEdit: { wpId in - editingWaypoint = wpId - presentingWaypointForm = true - + if wpId > 0 { + editingWaypoint = wpId + presentingWaypointForm = true + } }, positions: Array(positions), waypoints: Array(waypoints), mapViewType: mapType, centerOnPositionsOnly: false, customMapOverlay: self.customMapOverlay, diff --git a/gen_protos.sh b/gen_protos.sh index da96dd76..fd756c64 100755 --- a/gen_protos.sh +++ b/gen_protos.sh @@ -29,9 +29,8 @@ for pf in $pfiles do echo "Generating $pf..." protoc --swift_out=${sdir} --proto_path=${pdir} $pf - echo protoc --swift_out=${sdir} --proto_path=${pdir} $pf done echo "Done generating the swift files from the proto files." echo "Build, test, and commit changes." -cd ../protobufs/meshtastic && git reset --hard \ No newline at end of file +cd ../protobufs/meshtastic && git reset --hard From fac854f43ec25fd6672eb6eb64ad4d25ccb7aebf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jan 2023 15:27:53 -0800 Subject: [PATCH 43/57] Update node details to have better switching between editing and creating of waypoints --- Meshtastic/Protobufs/config.pb.swift | 10 ++++++++++ Meshtastic/Protobufs/device_metadata.pb.swift | 8 ++++---- Meshtastic/Views/Map/WaypointFormView.swift | 12 ++++++------ Meshtastic/Views/Nodes/NodeDetail.swift | 1 + Meshtastic/Views/Nodes/NodeMap.swift | 1 + 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Protobufs/config.pb.swift b/Meshtastic/Protobufs/config.pb.swift index 62f472e7..546c28b4 100644 --- a/Meshtastic/Protobufs/config.pb.swift +++ b/Meshtastic/Protobufs/config.pb.swift @@ -487,6 +487,10 @@ struct Config { /// Clears the value of `ipv4Config`. Subsequent reads from it will return its default value. mutating func clearIpv4Config() {self._ipv4Config = nil} + /// + /// rsyslog Server and Port + var rsyslogServer: String = String() + var unknownFields = SwiftProtobuf.UnknownStorage() enum AddressMode: SwiftProtobuf.Enum { @@ -1688,6 +1692,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp 6: .standard(proto: "eth_enabled"), 7: .standard(proto: "address_mode"), 8: .standard(proto: "ipv4_config"), + 9: .standard(proto: "rsyslog_server"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1703,6 +1708,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp case 6: try { try decoder.decodeSingularBoolField(value: &self.ethEnabled) }() case 7: try { try decoder.decodeSingularEnumField(value: &self.addressMode) }() case 8: try { try decoder.decodeSingularMessageField(value: &self._ipv4Config) }() + case 9: try { try decoder.decodeSingularStringField(value: &self.rsyslogServer) }() default: break } } @@ -1734,6 +1740,9 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp try { if let v = self._ipv4Config { try visitor.visitSingularMessageField(value: v, fieldNumber: 8) } }() + if !self.rsyslogServer.isEmpty { + try visitor.visitSingularStringField(value: self.rsyslogServer, fieldNumber: 9) + } try unknownFields.traverse(visitor: &visitor) } @@ -1745,6 +1754,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if lhs.ethEnabled != rhs.ethEnabled {return false} if lhs.addressMode != rhs.addressMode {return false} if lhs._ipv4Config != rhs._ipv4Config {return false} + if lhs.rsyslogServer != rhs.rsyslogServer {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/device_metadata.pb.swift b/Meshtastic/Protobufs/device_metadata.pb.swift index a13084c7..7c41e60f 100644 --- a/Meshtastic/Protobufs/device_metadata.pb.swift +++ b/Meshtastic/Protobufs/device_metadata.pb.swift @@ -57,7 +57,7 @@ struct DeviceMetadata { /// /// Indicates the device's current enabled position flags - var positionFlags: Config.PositionConfig.PositionFlags = .unset + var positionFlags: UInt32 = 0 var unknownFields = SwiftProtobuf.UnknownStorage() @@ -96,7 +96,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement case 5: try { try decoder.decodeSingularBoolField(value: &self.hasBluetooth_p) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.hasEthernet_p) }() case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }() - case 8: try { try decoder.decodeSingularEnumField(value: &self.positionFlags) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }() default: break } } @@ -124,8 +124,8 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if self.role != .client { try visitor.visitSingularEnumField(value: self.role, fieldNumber: 7) } - if self.positionFlags != .unset { - try visitor.visitSingularEnumField(value: self.positionFlags, fieldNumber: 8) + if self.positionFlags != 0 { + try visitor.visitSingularUInt32Field(value: self.positionFlags, fieldNumber: 8) } try unknownFields.traverse(visitor: &visitor) } diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index f1b6af54..59a149a9 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -177,12 +177,6 @@ struct WaypointFormView: View { } .onAppear { - name = "" - description = "" - locked = false - expires = false - expire = Date.now.addingTimeInterval(60 * 120) - icon = "📍" if waypointId > 0 { let waypoint = getWaypoint(id: Int64(waypointId), context: bleManager.context!) waypointId = Int(waypoint.id) @@ -198,6 +192,12 @@ struct WaypointFormView: View { expires = false } } else { + name = "" + description = "" + locked = false + expires = false + expire = Date.now.addingTimeInterval(60 * 120) + icon = "📍" latitude = coordinate.latitude longitude = coordinate.longitude } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 78c6d78f..abd8ed10 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -52,6 +52,7 @@ struct NodeDetail: View { waypointCoordinate = coord if waypointCoordinate != nil { + editingWaypoint = 0 presentingWaypointForm = true } }, onWaypointEdit: { wpId in diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 7772f05d..9e026e8a 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -59,6 +59,7 @@ struct NodeMap: View { waypointCoordinate = coord if waypointCoordinate != nil { + editingWaypoint = 0 presentingWaypointForm = true } }, onWaypointEdit: { wpId in From 56d51aa9b88831708b40956f97bc8484f9545a78 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jan 2023 16:57:44 -0800 Subject: [PATCH 44/57] Clean up the waypoint annotation --- .../Views/Map/Custom/MapViewSwiftUI.swift | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index a3a83077..ebd26065 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -135,36 +135,26 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.clusteringIdentifier = "waypointGroup" annotationView.markerTintColor = UIColor(.indigo) annotationView.titleVisibility = .visible - let editWaypoint = UIButton(type: .detailDisclosure) - editWaypoint.setImage(UIImage(systemName: "square.and.pencil"), for: .normal) - annotationView.rightCalloutAccessoryView = editWaypoint + let subtitle = UILabel() + subtitle.text = waypointAnnotation.longDescription + subtitle.numberOfLines = 0 + let editIcon = UIButton(type: .detailDisclosure) + editIcon.setImage(UIImage(systemName: "square.and.pencil"), for: .normal) + // Build the Callout + annotationView.detailCalloutAccessoryView = subtitle; + annotationView.rightCalloutAccessoryView = editIcon return annotationView default: return nil } } func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { - let control = view.annotation as! WaypointEntity - print(control) - // Only Allow Edit for annotations with a id - //if control.id > 0 { - print(view.tag) - parent.onWaypointEdit(view.tag) - //} + // Only Allow Edit for waypoint annotations with a id + if view.tag > 0 { + parent.onWaypointEdit(view.tag) + } } -// func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) -// { -// // Only Allow Edit for annotations with a tag -// if view.tag > 0 { -// // Screen Position - CGPoint -// let location = longPressRecognizer.location(in: self.parent.mapView) -// // Map Coordinate - CLLocationCoordinate2D -// let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) -// parent.onMarkerTap(coordinate, view.tag) -// } -// } - @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { // Screen Position - CGPoint let location = longPressRecognizer.location(in: self.parent.mapView) From eb16c58f14dfda4931aa02f8137eb0727407df21 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jan 2023 00:16:48 -0800 Subject: [PATCH 45/57] Annotation cleanup, core data for device metadata --- Meshtastic/Helpers/BLEManager.swift | 15 +++++++-------- .../MeshtasticDataModelV6.xcdatamodel/contents | 12 ++++++++++++ Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift | 11 +++++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 4e2c12c9..6f8a654a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -291,9 +291,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } } - func requestDeviceMetadata() { + func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, context: NSManagedObjectContext) -> Int64 { - guard (connectedPeripheral!.peripheral.state == CBPeripheralState.connected) else { return } + guard (connectedPeripheral!.peripheral.state == CBPeripheralState.connected) else { return 0 } let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown")) let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.devicemetadata %@", @@ -310,12 +310,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - var toRadio: ToRadio = ToRadio() - toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() - connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - // Either Read the config complete value or from num notify value - connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) + let messageDescription = "Requested Device Metadata for node \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown")) by \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + return Int64(meshPacket.id) + } + return 0 } func sendTraceRouteRequest(destNum: Int64, wantResponse: Bool) -> Bool { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents index da2cb3ef..f658ef98 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents @@ -47,6 +47,17 @@ + + + + + + + + + + + @@ -170,6 +181,7 @@ + diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index ebd26065..19e219b4 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -119,7 +119,7 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.canShowCallout = true annotationView.glyphText = "📟" annotationView.clusteringIdentifier = "nodeGroup" - annotationView.markerTintColor = UIColor(.accentColor) + annotationView.markerTintColor = UIColor(.indigo) annotationView.titleVisibility = .visible return annotationView case let waypointAnnotation as WaypointEntity: @@ -133,15 +133,18 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍") } annotationView.clusteringIdentifier = "waypointGroup" - annotationView.markerTintColor = UIColor(.indigo) + annotationView.markerTintColor = UIColor(.accentColor) + annotationView.displayPriority = .required annotationView.titleVisibility = .visible + let leftIcon = UIImageView(image: annotationView.glyphText?.image()) + leftIcon.backgroundColor = UIColor(.accentColor) + annotationView.leftCalloutAccessoryView = leftIcon let subtitle = UILabel() subtitle.text = waypointAnnotation.longDescription subtitle.numberOfLines = 0 + annotationView.detailCalloutAccessoryView = subtitle let editIcon = UIButton(type: .detailDisclosure) editIcon.setImage(UIImage(systemName: "square.and.pencil"), for: .normal) - // Build the Callout - annotationView.detailCalloutAccessoryView = subtitle; annotationView.rightCalloutAccessoryView = editIcon return annotationView default: return nil From 79bc59adc99983dcd617758552dca524b1f79340 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jan 2023 22:04:18 -0800 Subject: [PATCH 46/57] One working admin channel settings message --- Meshtastic/Helpers/BLEManager.swift | 16 +-- Meshtastic/Helpers/MeshPackets.swift | 60 +++++++++- .../contents | 1 + .../Views/Map/Custom/MapViewSwiftUI.swift | 18 +-- Meshtastic/Views/Nodes/NodeDetail.swift | 5 +- Meshtastic/Views/Settings/Settings.swift | 106 +++++++++++++----- Meshtastic/Views/Settings/UserConfig.swift | 5 +- 7 files changed, 162 insertions(+), 49 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 6f8a654a..7eeb0ddf 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -291,7 +291,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } } - func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, context: NSManagedObjectContext) -> Int64 { + func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 { guard (connectedPeripheral!.peripheral.state == CBPeripheralState.connected) else { return 0 } @@ -303,14 +303,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { adminPacket.getDeviceMetadataRequest = true var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config var meshPacket: MeshPacket = MeshPacket() - meshPacket.to = UInt32(connectedPeripheral.num) - meshPacket.from = 0 //UInt32(connectedPeripheral.num) + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchedNodeRequest.predicate = NSPredicate(format: "num == %lld", fromNum) + + do { + + let fetchedNode = try context.fetch(fetchedNodeRequest) as! [NodeInfoEntity] + if fetchedNode.count == 1 { + let newMetadata = DeviceMetadataEntity(context: context) + newMetadata.firmwareVersion = metadata.firmwareVersion + newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion) + newMetadata.canShutdown = metadata.canShutdown + newMetadata.hasWifi = metadata.hasWifi_p + newMetadata.hasBluetooth = metadata.hasBluetooth_p + newMetadata.hasEthernet = metadata.hasEthernet_p + newMetadata.role = Int32(metadata.role.rawValue) + newMetadata.positionFlags = Int32(metadata.positionFlags) + + fetchedNode[0].metadata = newMetadata + + do { + try context.save() + } catch { + print("Failed to save device metadata") + } + print("💾 Updated Device Metadata from Admin App Packet For: \(fromNum)") + } + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError)") + } + } +} + func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.nodeinfo.received %@", comment: "Node info received for: %@"), String(nodeInfo.num)) @@ -986,8 +1030,12 @@ func nodeInfoAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { + MeshLogger.log("🕸️ MESH PACKET received for Admin App \(try! packet.decoded.jsonString())") + if let adminMessage = try? AdminMessage(serializedData: packet.decoded.payload) { + MeshLogger.log("🕸️ MESH PACKET received for Admin App \(adminMessage.getDeviceMetadataResponse)") + if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { if let cmmc = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) { @@ -1022,14 +1070,16 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } } } - } - - else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { - + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) { + deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), context: context) + + print(try! adminMessage.getDeviceMetadataResponse.jsonString()) + + } } - } func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents index f658ef98..cabe58ef 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents @@ -135,6 +135,7 @@ + diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 19e219b4..649b5064 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -98,7 +98,9 @@ struct MapViewSwiftUI: UIViewRepresentable { self.parent = parent super.init() self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler)) - self.longPressRecognizer.minimumPressDuration = 0.3 + self.longPressRecognizer.minimumPressDuration = 0.4 + //self.longPressRecognizer.numberOfTouchesRequired = 1 + //self.longPressRecognizer.cancelsTouchesInView = true self.longPressRecognizer.delegate = self self.parent.mapView.addGestureRecognizer(longPressRecognizer) self.overlays = [] @@ -163,13 +165,15 @@ struct MapViewSwiftUI: UIViewRepresentable { let location = longPressRecognizer.location(in: self.parent.mapView) // Map Coordinate - CLLocationCoordinate2D let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - parent.onLongPress(coordinate, 0) // Add annotation: - let annotation = MKPointAnnotation() - annotation.title = "Dropped Pin" - annotation.coordinate = coordinate - parent.mapView.addAnnotation(annotation) - UINotificationFeedbackGenerator().notificationOccurred(.success) + if coordinate.longitude != LocationHelper.DefaultLocation.longitude && coordinate.latitude != LocationHelper.DefaultLocation.latitude { + let annotation = MKPointAnnotation() + annotation.title = "Dropped Pin" + annotation.coordinate = coordinate + parent.mapView.addAnnotation(annotation) + UINotificationFeedbackGenerator().notificationOccurred(.success) + parent.onLongPress(coordinate, 0) + } } public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index abd8ed10..ccd84b5e 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -397,13 +397,12 @@ struct NodeDetail: View { } .navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline) .navigationBarItems(trailing: - ZStack { + ZStack { ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") - } - ) + }) .onAppear { self.bleManager.context = context } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index d4728fc1..e3b953ca 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -12,22 +12,59 @@ struct Settings: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], animation: .default) + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default) private var nodes: FetchedResults + @State private var selectedNode: Int = 0 + @State private var connectedNodeNum: Int = 0 + @State private var initialLoad: Bool = true var body: some View { NavigationSplitView { List { - let connectedNodeNum = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0 - NavigationLink() { AppSettings() } label: { - Image(systemName: "gearshape") .symbolRenderingMode(.hierarchical) Text("app.settings") } + Section("Configure") { + Picker("Configuring Node", selection: $selectedNode) { + if connectedNodeNum == 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 ?? NSLocalizedString("unknown", comment: "Unknown"))") + .tag(Int(node.num)) + } else if node.metadata != nil { + Text("Remote Config: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + .tag(Int(node.num)) + } else { + Text("Request Admin: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + .tag(Int(node.num)) + } + } + } + .pickerStyle(.menu) + .labelsHidden() + .onChange(of: selectedNode) { newValue in + if selectedNode > 0 { + let node = nodes.first(where: { $0.num == newValue }) + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) + + if node?.metadata == nil && node!.num != connectedNodeNum { + let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) + + if adminMessageId > 0 { + print("Saved node metadata") + } + } + } + } + } + Section("radio.configuration") { NavigationLink { @@ -37,85 +74,87 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("share.channels") } - + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { - UserConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + UserConfig(node: nodes.first(where: { $0.num == selectedNode }), connectedNode: nodes.first(where: { $0.num == connectedNodeNum })) } label: { Image(systemName: "person.crop.rectangle.fill") .symbolRenderingMode(.hierarchical) Text("user") } + .disabled(selectedNode == 0) NavigationLink() { - - LoRaConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + LoRaConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "dot.radiowaves.left.and.right") .symbolRenderingMode(.hierarchical) Text("lora") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink() { - Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) } label: { - Image(systemName: "fibrechannel") .symbolRenderingMode(.hierarchical) - Text("channels") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink() { - - BluetoothConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "antenna.radiowaves.left.and.right") .symbolRenderingMode(.hierarchical) Text("bluetooth") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { - DeviceConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + DeviceConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "flipphone") .symbolRenderingMode(.hierarchical) Text("device") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { - DisplayConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + DisplayConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "display") .symbolRenderingMode(.hierarchical) Text("display") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { - NetworkConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + NetworkConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "network") .symbolRenderingMode(.hierarchical) Text("network") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { - PositionConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + PositionConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "location") .symbolRenderingMode(.hierarchical) Text("position") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) } Section("module.configuration") { NavigationLink { - CannedMessagesConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "list.bullet.rectangle.fill") @@ -123,42 +162,52 @@ struct Settings: View { Text("canned.messages") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { - ExternalNotificationConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "megaphone") .symbolRenderingMode(.hierarchical) Text("external.notification") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + NavigationLink { - MQTTConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + MQTTConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "dot.radiowaves.right") .symbolRenderingMode(.hierarchical) Text("mqtt") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + NavigationLink { - RangeTestConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "point.3.connected.trianglepath.dotted") .symbolRenderingMode(.hierarchical) Text("range.test") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + NavigationLink { - SerialConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "terminal") .symbolRenderingMode(.hierarchical) Text("serial") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + NavigationLink { - TelemetryConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) + TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Image(systemName: "chart.xyaxis.line") .symbolRenderingMode(.hierarchical) Text("telemetry") } + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) } Section(header: Text("logging")) { NavigationLink { @@ -169,7 +218,7 @@ struct Settings: View { Text("mesh.log") } NavigationLink { - let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + let connectedNode = nodes.first(where: { $0.num == selectedNode }) AdminMessageList(user: connectedNode?.user) } label: { Image(systemName: "building.columns") @@ -191,6 +240,13 @@ struct Settings: View { .onAppear { self.bleManager.context = context self.bleManager.userSettings = userSettings + if initialLoad { + selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) + initialLoad = false + } + connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) + //let node = nodes.first(where: { $0.num == connectedNodeNum }) + //selectedNode = node?.num ?? 0 } .listStyle(GroupedListStyle()) .navigationTitle("settings") diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index f372499e..4b3dedc1 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -13,6 +13,7 @@ struct UserConfig: View { @Environment(\.dismiss) private var goBack var node: NodeInfoEntity? + var connectedNode: NodeInfoEntity? @State private var isPresentingFactoryResetConfirm: Bool = false @State private var isPresentingSaveConfirm: Bool = false @@ -84,11 +85,11 @@ struct UserConfig: View { isPresented: $isPresentingSaveConfirm, titleVisibility: .visible ) { - Button("Save User Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { + Button("Save User Config to \(node?.user?.longName ?? "Unknown")?") { var u = User() u.shortName = shortName u.longName = longName - let adminMessageId = bleManager.saveUser(config: u, fromUser: node!.user!, toUser: node!.user!) + let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { hasChanges = false goBack() From 338cfa946f3e13684b6c4b51e512bdacec9e5b58 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jan 2023 19:14:49 -0800 Subject: [PATCH 47/57] Admin messages updates --- Meshtastic/Helpers/BLEManager.swift | 39 +++++++++-- Meshtastic/Helpers/MeshPackets.swift | 65 +++---------------- Meshtastic/Persistence/UpdateCoreData.swift | 58 +++++++++++++++++ Meshtastic/Views/Bluetooth/Connect.swift | 2 +- .../Views/Settings/Config/LoRaConfig.swift | 15 ++++- Meshtastic/Views/Settings/Settings.swift | 2 +- 6 files changed, 115 insertions(+), 66 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 7eeb0ddf..27b04646 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1188,17 +1188,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return 0 } - public func saveLoRaConfig(config: Config.LoRaConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { + public func saveLoRaConfig(config: Config.LoRaConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.lora = config var meshPacket: MeshPacket = MeshPacket() - meshPacket.to = UInt32(connectedPeripheral.num) - meshPacket.from = 0 //UInt32(connectedPeripheral.num) + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. 0 { + meshPacket.channel = UInt32(adminIndex) + } var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp @@ -1392,6 +1394,35 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return false } + public func getLoRaConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + + var adminPacket = AdminMessage() + adminPacket.getConfigRequest = AdminMessage.ConfigType.loraConfig + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 9d874f35..d95c2126 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -184,59 +184,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config %@", comment: "LoRa config received: %@"), String(nodeNum)) - MeshLogger.log("📻 \(logString)") - - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - - let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - // Found a node, save LoRa Config - if !fetchedNode.isEmpty { - if fetchedNode[0].loRaConfig == nil { - let newLoRaConfig = LoRaConfigEntity(context: context) - newLoRaConfig.regionCode = Int32(config.lora.region.rawValue) - newLoRaConfig.usePreset = config.lora.usePreset - newLoRaConfig.modemPreset = Int32(config.lora.modemPreset.rawValue) - newLoRaConfig.bandwidth = Int32(config.lora.bandwidth) - newLoRaConfig.spreadFactor = Int32(config.lora.spreadFactor) - newLoRaConfig.codingRate = Int32(config.lora.codingRate) - newLoRaConfig.frequencyOffset = config.lora.frequencyOffset - newLoRaConfig.hopLimit = Int32(config.lora.hopLimit) - newLoRaConfig.txPower = Int32(config.lora.txPower) - newLoRaConfig.txEnabled = config.lora.txEnabled - newLoRaConfig.channelNum = Int32(config.lora.channelNum) - fetchedNode[0].loRaConfig = newLoRaConfig - } else { - fetchedNode[0].loRaConfig?.regionCode = Int32(config.lora.region.rawValue) - fetchedNode[0].loRaConfig?.usePreset = config.lora.usePreset - fetchedNode[0].loRaConfig?.modemPreset = Int32(config.lora.modemPreset.rawValue) - fetchedNode[0].loRaConfig?.bandwidth = Int32(config.lora.bandwidth) - fetchedNode[0].loRaConfig?.spreadFactor = Int32(config.lora.spreadFactor) - fetchedNode[0].loRaConfig?.codingRate = Int32(config.lora.codingRate) - fetchedNode[0].loRaConfig?.frequencyOffset = config.lora.frequencyOffset - fetchedNode[0].loRaConfig?.hopLimit = Int32(config.lora.hopLimit) - fetchedNode[0].loRaConfig?.txPower = Int32(config.lora.txPower) - fetchedNode[0].loRaConfig?.txEnabled = config.lora.txEnabled - fetchedNode[0].loRaConfig?.channelNum = Int32(config.lora.channelNum) - } - do { - try context.save() - print("💾 Updated LoRa Config for node number: \(String(nodeNum))") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Updating Core Data LoRaConfigEntity: \(nsError)") - } - } else { - print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Lora Config") - } - } catch { - let nsError = error as NSError - print("💥 Fetching node for core data LoRaConfigEntity failed: \(nsError)") - } + upsertLoraConfigPacket(config: config, nodeNum: nodeNum, context: context) } if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { @@ -911,7 +859,6 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].snr = nodeInfo.snr fetchedNode[0].channel = Int32(channel) - if nodeInfo.hasUser { fetchedNode[0].user!.userId = nodeInfo.user.id @@ -1074,10 +1021,14 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) { deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), context: context) + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) { - print(try! adminMessage.getDeviceMetadataResponse.jsonString()) - - + if let config = try? Config(serializedData: packet.decoded.payload) { + + if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { + upsertLoraConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + } + } } } } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 40bc81b9..a97c70ce 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -100,3 +100,61 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext) { } } } + +func upsertLoraConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config %@", comment: "LoRa config received: %@"), String(nodeNum)) + MeshLogger.log("📻 \(logString)") + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", nodeNum) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] + // Found a node, save LoRa Config + if !fetchedNode.isEmpty { + if fetchedNode[0].loRaConfig == nil { + let newLoRaConfig = LoRaConfigEntity(context: context) + newLoRaConfig.regionCode = Int32(config.lora.region.rawValue) + newLoRaConfig.usePreset = config.lora.usePreset + newLoRaConfig.modemPreset = Int32(config.lora.modemPreset.rawValue) + newLoRaConfig.bandwidth = Int32(config.lora.bandwidth) + newLoRaConfig.spreadFactor = Int32(config.lora.spreadFactor) + newLoRaConfig.codingRate = Int32(config.lora.codingRate) + newLoRaConfig.frequencyOffset = config.lora.frequencyOffset + newLoRaConfig.hopLimit = Int32(config.lora.hopLimit) + newLoRaConfig.txPower = Int32(config.lora.txPower) + newLoRaConfig.txEnabled = config.lora.txEnabled + newLoRaConfig.channelNum = Int32(config.lora.channelNum) + fetchedNode[0].loRaConfig = newLoRaConfig + } else { + fetchedNode[0].loRaConfig?.regionCode = Int32(config.lora.region.rawValue) + fetchedNode[0].loRaConfig?.usePreset = config.lora.usePreset + fetchedNode[0].loRaConfig?.modemPreset = Int32(config.lora.modemPreset.rawValue) + fetchedNode[0].loRaConfig?.bandwidth = Int32(config.lora.bandwidth) + fetchedNode[0].loRaConfig?.spreadFactor = Int32(config.lora.spreadFactor) + fetchedNode[0].loRaConfig?.codingRate = Int32(config.lora.codingRate) + fetchedNode[0].loRaConfig?.frequencyOffset = config.lora.frequencyOffset + fetchedNode[0].loRaConfig?.hopLimit = Int32(config.lora.hopLimit) + fetchedNode[0].loRaConfig?.txPower = Int32(config.lora.txPower) + fetchedNode[0].loRaConfig?.txEnabled = config.lora.txEnabled + fetchedNode[0].loRaConfig?.channelNum = Int32(config.lora.channelNum) + } + do { + try context.save() + context.refreshAllObjects() + print("💾 Updated LoRa Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data LoRaConfigEntity: \(nsError)") + } + } else { + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Lora Config") + } + } catch { + let nsError = error as NSError + print("💥 Fetching node for core data LoRaConfigEntity failed: \(nsError)") + } +} diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index d9e29e1b..fded4ec7 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -111,7 +111,7 @@ struct Connect: View { if isUnsetRegion { HStack { NavigationLink { - LoRaConfig(node: node) + LoRaConfig(node: node, connectedNode: node) } label: { Label("set.region", systemImage: "globe.americas.fill") .foregroundColor(.red) diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index dc4ea18e..ac4d277a 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -14,6 +14,7 @@ struct LoRaConfig: View { @Environment(\.dismiss) private var goBack var node: NodeInfoEntity? + var connectedNode: NodeInfoEntity? @State var isPresentingSaveConfirm = false @State var hasChanges = false @@ -75,7 +76,7 @@ struct LoRaConfig: View { isPresented: $isPresentingSaveConfirm, titleVisibility: .visible ) { - let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown") + let nodeName = node?.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName) Button(buttonText) { var lc = Config.LoRaConfig() @@ -84,7 +85,7 @@ struct LoRaConfig: View { lc.modemPreset = ModemPresets(rawValue: modemPreset)!.protoEnumValue() lc.usePreset = true lc.txEnabled = true - let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: node!.user!, toUser: node!.user!) + let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: node?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -102,14 +103,22 @@ struct LoRaConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") }) .onAppear { + self.bleManager.context = context - self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 0) + self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3) self.region = Int(node?.loRaConfig?.regionCode ?? 0) self.usePreset = node?.loRaConfig?.usePreset ?? true self.modemPreset = Int(node?.loRaConfig?.modemPreset ?? 0) self.txEnabled = node?.loRaConfig?.txEnabled ?? true self.txPower = Int(node?.loRaConfig?.txPower ?? 0) self.hasChanges = false + + // Need to request a LoRaConfig from the remote node before allowing changes + if node?.loRaConfig == nil { + + let adminMessageId = bleManager.getLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } + } .onChange(of: region) { newRegion in if node != nil && node!.loRaConfig != nil { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index e3b953ca..647242dc 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -86,7 +86,7 @@ struct Settings: View { .disabled(selectedNode == 0) NavigationLink() { - LoRaConfig(node: nodes.first(where: { $0.num == selectedNode })) + LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }), connectedNode: nodes.first(where: { $0.num == connectedNodeNum })) } label: { Image(systemName: "dot.radiowaves.left.and.right") .symbolRenderingMode(.hierarchical) From c819d21d893e58c920a4b33fc022d046359a2e5d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jan 2023 21:49:54 -0800 Subject: [PATCH 48/57] Delete button for waypoints --- .../Views/Map/Custom/MapViewSwiftUI.swift | 4 +-- Meshtastic/Views/Map/WaypointFormView.swift | 30 +++++++++++++++---- Meshtastic/Views/Nodes/NodeDetail.swift | 9 ++---- Meshtastic/Views/Nodes/NodeMap.swift | 9 ++---- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 649b5064..d4de2fb5 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -9,7 +9,7 @@ import MapKit struct MapViewSwiftUI: UIViewRepresentable { - var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D?, _ tag: Int ) -> Void + var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void var onWaypointEdit: (_ waypointId: Int ) -> Void let mapView = MKMapView() let positions: [PositionEntity] @@ -172,7 +172,7 @@ struct MapViewSwiftUI: UIViewRepresentable { annotation.coordinate = coordinate parent.mapView.addAnnotation(annotation) UINotificationFeedbackGenerator().notificationOccurred(.success) - parent.onLongPress(coordinate, 0) + parent.onLongPress(coordinate) } } diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 59a149a9..bf8fdee5 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -27,6 +27,7 @@ struct WaypointFormView: View { @State private var locked: Bool = false var body: some View { + Form { let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) Section(header: Text((waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) { @@ -132,7 +133,7 @@ struct WaypointFormView: View { } else { newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 ? name : "Dropped Pin" newWaypoint.description_p = description newWaypoint.latitudeI = Int32(coordinate.latitude * 1e7) newWaypoint.longitudeI = Int32(coordinate.longitude * 1e7) @@ -160,17 +161,36 @@ struct WaypointFormView: View { .buttonBorderShape(.capsule) .controlSize(.large) .disabled(bleManager.connectedPeripheral == nil) - .padding() + .padding(.bottom) - Button { + Button(role:.cancel) { dismiss() } label: { - Label("cancel", systemImage: "xmark") + Label("cancel", systemImage: "x.circle") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) - .padding() + .padding(.bottom) + + if waypointId > 0 { + Button(role: .destructive) { + let waypoint = getWaypoint(id: Int64(waypointId), context: bleManager.context!) + bleManager.context!.delete(waypoint) + do { + try bleManager.context!.save() + } catch { + bleManager.context!.rollback() + } + dismiss() + } label: { + Label("delete", systemImage: "trash") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + } } .onChange(of: waypointId) { newId in print(newId) diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index ccd84b5e..032c72be 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -48,13 +48,10 @@ struct NodeDetail: View { ZStack { let annotations = node.positions?.array as! [PositionEntity] ZStack { - MapViewSwiftUI(onLongPress: { coord, id in + MapViewSwiftUI(onLongPress: { coord in waypointCoordinate = coord - - if waypointCoordinate != nil { - editingWaypoint = 0 - presentingWaypointForm = true - } + editingWaypoint = 0 + presentingWaypointForm = true }, onWaypointEdit: { wpId in if wpId > 0 { editingWaypoint = wpId diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 9e026e8a..75b85af9 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -55,13 +55,10 @@ struct NodeMap: View { NavigationStack { ZStack { - MapViewSwiftUI(onLongPress: { coord, id in + MapViewSwiftUI(onLongPress: { coord in waypointCoordinate = coord - - if waypointCoordinate != nil { - editingWaypoint = 0 - presentingWaypointForm = true - } + editingWaypoint = 0 + presentingWaypointForm = true }, onWaypointEdit: { wpId in if wpId > 0 { editingWaypoint = wpId From 2ce5cab3c1b2ba8a245e32634b6835d54786eebc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jan 2023 21:58:20 -0800 Subject: [PATCH 49/57] Only show remote node picker if you have an admin channel --- Meshtastic/Views/Settings/Settings.swift | 59 +++++++++++++----------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 647242dc..263c0975 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -28,37 +28,40 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("app.settings") } - Section("Configure") { - Picker("Configuring Node", selection: $selectedNode) { - if connectedNodeNum == 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 ?? NSLocalizedString("unknown", comment: "Unknown"))") - .tag(Int(node.num)) - } else if node.metadata != nil { - Text("Remote Config: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") - .tag(Int(node.num)) - } else { - Text("Request Admin: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") - .tag(Int(node.num)) + let node = nodes.first(where: { $0.num == connectedNodeNum }) + if node?.myInfo?.adminIndex ?? 0 > 0 { + Section("Configure") { + Picker("Configuring Node", selection: $selectedNode) { + if connectedNodeNum == 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 ?? NSLocalizedString("unknown", comment: "Unknown"))") + .tag(Int(node.num)) + } else if node.metadata != nil { + Text("Remote Config: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + .tag(Int(node.num)) + } else { + Text("Request Admin: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + .tag(Int(node.num)) + } } } - } - .pickerStyle(.menu) - .labelsHidden() - .onChange(of: selectedNode) { newValue in - if selectedNode > 0 { - let node = nodes.first(where: { $0.num == newValue }) - let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) - connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) - - if node?.metadata == nil && node!.num != connectedNodeNum { - let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) + .pickerStyle(.menu) + .labelsHidden() + .onChange(of: selectedNode) { newValue in + if selectedNode > 0 { + let node = nodes.first(where: { $0.num == newValue }) + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) + + if node?.metadata == nil && node!.num != connectedNodeNum { + let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) - if adminMessageId > 0 { - print("Saved node metadata") + if adminMessageId > 0 { + print("Saved node metadata") + } } } } From c5f43acec6c5e9faf122c312ccd820f12f9f261d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jan 2023 22:20:35 -0800 Subject: [PATCH 50/57] dont hide waypoint form --- Meshtastic/Views/Nodes/NodeMap.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 75b85af9..f6bb191a 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -82,11 +82,10 @@ struct NodeMap: View { .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) .frame(maxHeight: .infinity) .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { - if waypointCoordinate != nil { - WaypointFormView(coordinate: waypointCoordinate!, waypointId: editingWaypoint) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) - } + WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation, waypointId: editingWaypoint) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) + } } .navigationBarItems(leading: From 55edc42321f64d96f64632882899022648db5ce4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 21 Jan 2023 07:28:50 -0800 Subject: [PATCH 51/57] Waypoint fixes --- Meshtastic/Helpers/BLEManager.swift | 4 ++++ Meshtastic/Helpers/Extensions.swift | 12 ++++++++++++ Meshtastic/Helpers/MeshPackets.swift | 19 +++++++++---------- Meshtastic/Persistence/UpdateCoreData.swift | 1 - .../Views/Map/Custom/MapViewSwiftUI.swift | 9 +++++---- Meshtastic/Views/Map/WaypointFormView.swift | 9 +++++++++ Meshtastic/Views/Nodes/NodeMap.swift | 10 +++++++--- en.lproj/Localizable.strings | 2 +- 8 files changed, 47 insertions(+), 19 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 27b04646..76be7679 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -730,6 +730,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { public func sendWaypoint(waypoint: Waypoint) -> Bool { + if waypoint.latitudeI == 373346000 && waypoint.longitudeI == -1220090000 { + return false + } + var success = false let fromNodeNum = UInt32(connectedPeripheral.num) var waypointPacket = waypoint diff --git a/Meshtastic/Helpers/Extensions.swift b/Meshtastic/Helpers/Extensions.swift index 38094e03..39a2b6af 100644 --- a/Meshtastic/Helpers/Extensions.swift +++ b/Meshtastic/Helpers/Extensions.swift @@ -1,5 +1,6 @@ import Foundation import SwiftUI +import MapKit extension Character { var isEmoji: Bool { @@ -8,6 +9,17 @@ extension Character { } } +extension CLLocationCoordinate2D { + /// Returns distance from coordianate in meters. + /// - Parameter from: coordinate which will be used as end point. + /// - Returns: Returns distance in meters. + func distance(from: CLLocationCoordinate2D) -> CLLocationDistance { + let from = CLLocation(latitude: from.latitude, longitude: from.longitude) + let to = CLLocation(latitude: self.latitude, longitude: self.longitude) + return from.distance(from: to) + } +} + extension Data { var macAddressString: String { let mac: String = reduce("") {$0 + String(format: "%02x:", $1)} diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d95c2126..efc248f4 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -734,9 +734,8 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NSManagedObjectContext) { if metadata.isInitialized { - - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.device.metadata.received %@", comment: "Device Metadata received from: %@"), String(fromNum)) - MeshLogger.log("🎛️ \(logString)") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.device.metadata.received %@", comment: "Device Metadata admin message received from: %@"), String(fromNum)) + MeshLogger.log("🏷️ \(logString)") let fetchedNodeRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchedNodeRequest.predicate = NSPredicate(format: "num == %lld", fromNum) @@ -977,12 +976,8 @@ func nodeInfoAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { - MeshLogger.log("🕸️ MESH PACKET received for Admin App \(try! packet.decoded.jsonString())") - if let adminMessage = try? AdminMessage(serializedData: packet.decoded.payload) { - MeshLogger.log("🕸️ MESH PACKET received for Admin App \(adminMessage.getDeviceMetadataResponse)") - if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { if let cmmc = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) { @@ -1019,16 +1014,20 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) { deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), context: context) - } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) { + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) { if let config = try? Config(serializedData: packet.decoded.payload) { - - if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { + if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { + //upsertLoraConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { upsertLoraConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) } } + } else { + MeshLogger.log("🕸️ MESH PACKET received for Admin App \(try! packet.decoded.jsonString())") } } } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index a97c70ce..53c05627 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -143,7 +143,6 @@ func upsertLoraConfigPacket(config: Config, nodeNum: Int64, context: NSManagedOb } do { try context.save() - context.refreshAllObjects() print("💾 Updated LoRa Config for node number: \(String(nodeNum))") } catch { context.rollback() diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index d4de2fb5..caa9f52e 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -98,9 +98,9 @@ struct MapViewSwiftUI: UIViewRepresentable { self.parent = parent super.init() self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler)) - self.longPressRecognizer.minimumPressDuration = 0.4 + self.longPressRecognizer.minimumPressDuration = 0.5 //self.longPressRecognizer.numberOfTouchesRequired = 1 - //self.longPressRecognizer.cancelsTouchesInView = true + self.longPressRecognizer.cancelsTouchesInView = true self.longPressRecognizer.delegate = self self.parent.mapView.addGestureRecognizer(longPressRecognizer) self.overlays = [] @@ -113,6 +113,7 @@ struct MapViewSwiftUI: UIViewRepresentable { case _ as MKClusterAnnotation: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup") annotationView.markerTintColor = .brown//.systemRed + annotationView.displayPriority = .defaultLow annotationView.tag = -1 return annotationView case _ as PositionEntity: @@ -122,7 +123,7 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.glyphText = "📟" annotationView.clusteringIdentifier = "nodeGroup" annotationView.markerTintColor = UIColor(.indigo) - annotationView.titleVisibility = .visible + annotationView.titleVisibility = .adaptive return annotationView case let waypointAnnotation as WaypointEntity: let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint") @@ -137,7 +138,7 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.clusteringIdentifier = "waypointGroup" annotationView.markerTintColor = UIColor(.accentColor) annotationView.displayPriority = .required - annotationView.titleVisibility = .visible + annotationView.titleVisibility = .adaptive let leftIcon = UIImageView(image: annotationView.glyphText?.image()) leftIcon.backgroundColor = UIColor(.accentColor) annotationView.leftCalloutAccessoryView = leftIcon diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index bf8fdee5..201ba6d9 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -152,6 +152,8 @@ struct WaypointFormView: View { waypointId = 0 dismiss() } else { + waypointId = 0 + dismiss() print("Send waypoint failed") } } label: { @@ -221,6 +223,13 @@ struct WaypointFormView: View { latitude = coordinate.latitude longitude = coordinate.longitude } + + if coordinate.distance(from: LocationHelper.DefaultLocation) == 0.0 { + // Too close to apple park, bail out + waypointId = 0 + //dismiss() + //print(coordinate.distance(from: LocationHelper.DefaultLocation)) + } } } } diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index f6bb191a..99f21bca 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -40,7 +40,7 @@ struct NodeMap: View { private var waypoints: FetchedResults @State private var mapType: MKMapType = .standard - @State var waypointCoordinate: CLLocationCoordinate2D? + @State var waypointCoordinate: CLLocationCoordinate2D = LocationHelper.DefaultLocation @State var editingWaypoint: Int = 0 @State private var presentingWaypointForm = false @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( @@ -58,7 +58,11 @@ struct NodeMap: View { MapViewSwiftUI(onLongPress: { coord in waypointCoordinate = coord editingWaypoint = 0 - presentingWaypointForm = true + if waypointCoordinate.distance(from: LocationHelper.DefaultLocation) == 0.0 { + print("Apple Park") + } else { + presentingWaypointForm = true + } }, onWaypointEdit: { wpId in if wpId > 0 { editingWaypoint = wpId @@ -82,7 +86,7 @@ struct NodeMap: View { .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) .frame(maxHeight: .infinity) .sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) { - WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation, waypointId: editingWaypoint) + WaypointFormView(coordinate: waypointCoordinate, waypointId: editingWaypoint) .presentationDetents([.medium, .large]) .presentationDragIndicator(.automatic) diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index fd37f919..7f67069a 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -21,7 +21,7 @@ "battery.level.trend"="Battery Level Trend"; "ble.name"="BLE Name"; "ble.connection.timeout %d %@"="Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth."; -"ble.errorcode.6 %@"="%@ The app will automatically reconnect to the preferred radio if it come back in range."; +"ble.errorcode.6 %@"="%@ The app will automatically reconnect to the preferred radio if it comes back in range."; "ble.errorcode.14 %@"="%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."; "ble.errorcode.pin %@"="%@ Please try connecting again and check the PIN carefully."; "bluetooth"="Bluetooth"; From 0a4d440f6c7baa010359f1adc22cb371dbfcda92 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Jan 2023 17:56:04 -0800 Subject: [PATCH 52/57] Cleanup --- Meshtastic/Helpers/BLEManager.swift | 37 ++- Meshtastic/Helpers/MeshPackets.swift | 256 ++---------------- .../contents | 2 +- Meshtastic/Persistence/UpdateCoreData.swift | 238 +++++++++++++++- .../Settings/Config/BluetoothConfig.swift | 8 + .../Views/Settings/Config/LoRaConfig.swift | 8 +- Meshtastic/Views/Settings/Settings.swift | 11 +- 7 files changed, 307 insertions(+), 253 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 76be7679..2063cddb 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -312,7 +312,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - let messageDescription = "Requested Device Metadata for node \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown")) by \(fromUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" + let messageDescription = "🛎️ Requested Device Metadata for node \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown")) by \(fromUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return Int64(meshPacket.id) } @@ -1398,7 +1398,36 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return false } - public func getLoRaConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + public func requestBluetoothConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + + var adminPacket = AdminMessage() + adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.loraConfig @@ -1417,7 +1446,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.decoded = dataMessage - let messageDescription = "🛎️ Sent a Get LoRa Config request on the admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" + let messageDescription = "🛎️ Requested LoRa Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { @@ -1578,7 +1607,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { do { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) try context!.save() - print("⚙️ \(adminDescription)") + print(adminDescription) return true } catch { context!.rollback() diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index efc248f4..628339b8 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -40,241 +40,17 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 // We don't care about any of the Power settings, config is available for everyting else if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { - - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.bluetooth.config %@", comment: "Bluetooth config received: %@"), String(nodeNum)) - MeshLogger.log("📶 \(logString)") - - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - - let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - // Found a node, save Device Config - if !fetchedNode.isEmpty { - if fetchedNode[0].bluetoothConfig == nil { - let newBluetoothConfig = BluetoothConfigEntity(context: context) - newBluetoothConfig.enabled = config.bluetooth.enabled - newBluetoothConfig.mode = Int32(config.bluetooth.mode.rawValue) - newBluetoothConfig.fixedPin = Int32(config.bluetooth.fixedPin) - fetchedNode[0].bluetoothConfig = newBluetoothConfig - } else { - fetchedNode[0].bluetoothConfig?.enabled = config.bluetooth.enabled - fetchedNode[0].bluetoothConfig?.mode = Int32(config.bluetooth.mode.rawValue) - fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.bluetooth.fixedPin) - } - do { - try context.save() - print("💾 Updated Bluetooth Config for node number: \(String(nodeNum))") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)") - } - } else { - print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Bluetooth Config") - } - } catch { - let nsError = error as NSError - print("💥 Fetching node for core data BluetoothConfigEntity failed: \(nsError)") - } - } - - if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { - - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.device.config %@", comment: "Device config received: %@"), String(nodeNum)) - MeshLogger.log("📟 \(logString)") - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - - let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - // Found a node, save Device Config - if !fetchedNode.isEmpty { - if fetchedNode[0].deviceConfig == nil { - let newDeviceConfig = DeviceConfigEntity(context: context) - newDeviceConfig.role = Int32(config.device.role.rawValue) - newDeviceConfig.serialEnabled = config.device.serialEnabled - newDeviceConfig.debugLogEnabled = config.device.debugLogEnabled - newDeviceConfig.buttonGpio = Int32(config.device.buttonGpio) - newDeviceConfig.buzzerGpio = Int32(config.device.buzzerGpio) - fetchedNode[0].deviceConfig = newDeviceConfig - } else { - fetchedNode[0].deviceConfig?.role = Int32(config.device.role.rawValue) - fetchedNode[0].deviceConfig?.serialEnabled = config.device.serialEnabled - fetchedNode[0].deviceConfig?.debugLogEnabled = config.device.debugLogEnabled - fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.device.buttonGpio) - fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.device.buzzerGpio) - } - do { - try context.save() - print("💾 Updated Device Config for node number: \(String(nodeNum))") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Updating Core Data DeviceConfigEntity: \(nsError)") - } - } - } catch { - let nsError = error as NSError - print("💥 Fetching node for core data DeviceConfigEntity failed: \(nsError)") - } - } - - if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { - - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.display.config %@", comment: "Display config received: %@"), String(nodeNum)) - MeshLogger.log("🖥️ \(logString)") - - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - - let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - - // Found a node, save Device Config - if !fetchedNode.isEmpty { - - if fetchedNode[0].displayConfig == nil { - - let newDisplayConfig = DisplayConfigEntity(context: context) - newDisplayConfig.gpsFormat = Int32(config.display.gpsFormat.rawValue) - newDisplayConfig.screenOnSeconds = Int32(config.display.screenOnSecs) - newDisplayConfig.screenCarouselInterval = Int32(config.display.autoScreenCarouselSecs) - newDisplayConfig.compassNorthTop = config.display.compassNorthTop - newDisplayConfig.flipScreen = config.display.flipScreen - newDisplayConfig.oledType = Int32(config.display.oled.rawValue) - fetchedNode[0].displayConfig = newDisplayConfig - - } else { - - fetchedNode[0].displayConfig?.gpsFormat = Int32(config.display.gpsFormat.rawValue) - fetchedNode[0].displayConfig?.screenOnSeconds = Int32(config.display.screenOnSecs) - fetchedNode[0].displayConfig?.screenCarouselInterval = Int32(config.display.autoScreenCarouselSecs) - fetchedNode[0].displayConfig?.compassNorthTop = config.display.compassNorthTop - fetchedNode[0].displayConfig?.flipScreen = config.display.flipScreen - fetchedNode[0].displayConfig?.oledType = Int32(config.display.oled.rawValue) - } - - do { - - try context.save() - print("💾 Updated Display Config for node number: \(String(nodeNum))") - - } catch { - - context.rollback() - - let nsError = error as NSError - print("💥 Error Updating Core Data DisplayConfigEntity: \(nsError)") - } - } else { - - print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Display Config") - } - - } catch { - - let nsError = error as NSError - print("💥 Fetching node for core data DisplayConfigEntity failed: \(nsError)") - } - } - - if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { - - upsertLoraConfigPacket(config: config, nodeNum: nodeNum, context: context) - } - - if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { - - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.network.config %@", comment: "Network config received: %@"), String(nodeNum)) - MeshLogger.log("🌐 \(logString)") - - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - - let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - // Found a node, save WiFi Config - if !fetchedNode.isEmpty { - if fetchedNode[0].networkConfig == nil { - let newNetworkConfig = NetworkConfigEntity(context: context) - newNetworkConfig.wifiSsid = config.network.wifiSsid - newNetworkConfig.wifiPsk = config.network.wifiPsk - fetchedNode[0].networkConfig = newNetworkConfig - } else { - fetchedNode[0].networkConfig?.wifiSsid = config.network.wifiSsid - fetchedNode[0].networkConfig?.wifiPsk = config.network.wifiPsk - } - - do { - try context.save() - print("💾 Updated Network Config for node number: \(String(nodeNum))") - - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Updating Core Data WiFiConfigEntity: \(nsError)") - } - } else { - print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Network Config") - } - } catch { - let nsError = error as NSError - print("💥 Fetching node for core data NetworkConfigEntity failed: \(nsError)") - } - } - - if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { - - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.config %@", comment: "Positon config received: %@"), String(nodeNum)) - MeshLogger.log("🗺️ \(logString)") - - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - - let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - // Found a node, save LoRa Config - if !fetchedNode.isEmpty { - if fetchedNode[0].positionConfig == nil { - let newPositionConfig = PositionConfigEntity(context: context) - newPositionConfig.smartPositionEnabled = config.position.positionBroadcastSmartEnabled - newPositionConfig.deviceGpsEnabled = config.position.gpsEnabled - newPositionConfig.fixedPosition = config.position.fixedPosition - newPositionConfig.gpsUpdateInterval = Int32(config.position.gpsUpdateInterval) - newPositionConfig.gpsAttemptTime = Int32(config.position.gpsAttemptTime) - newPositionConfig.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs) - newPositionConfig.positionFlags = Int32(config.position.positionFlags) - fetchedNode[0].positionConfig = newPositionConfig - } else { - fetchedNode[0].positionConfig?.smartPositionEnabled = config.position.positionBroadcastSmartEnabled - fetchedNode[0].positionConfig?.deviceGpsEnabled = config.position.gpsEnabled - fetchedNode[0].positionConfig?.fixedPosition = config.position.fixedPosition - fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.position.gpsUpdateInterval) - fetchedNode[0].positionConfig?.gpsAttemptTime = Int32(config.position.gpsAttemptTime) - fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs) - fetchedNode[0].positionConfig?.positionFlags = Int32(config.position.positionFlags) - } - do { - try context.save() - print("💾 Updated Position Config for node number: \(String(nodeNum))") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Updating Core Data PositionConfigEntity: \(nsError)") - } - } else { - print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Position Config") - } - } catch { - let nsError = error as NSError - print("💥 Fetching node for core data PositionConfigEntity failed: \(nsError)") - } + upsertBluetoothConfigPacket(config: config, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { + upsertDeviceConfigPacket(config: config, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { + upsertDisplayConfigPacket(config: config, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { + upsertLoRaConfigPacket(config: config, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { + upsertNetworkConfigPacket(config: config, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { + upsertPositionConfigPacket(config: config, nodeNum: nodeNum, context: context) } } @@ -1020,10 +796,16 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) { if let config = try? Config(serializedData: packet.decoded.payload) { + if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { - //upsertLoraConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + upsertBluetoothConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + + } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { + upsertDeviceConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { - upsertLoraConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + upsertLoRaConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + } } } else { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents index cabe58ef..5602f840 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents @@ -274,7 +274,7 @@ - + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 53c05627..c739ecde 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -101,7 +101,150 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext) { } } -func upsertLoraConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertBluetoothConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.bluetooth.config %@", comment: "Bluetooth config received: %@"), String(nodeNum)) + MeshLogger.log("📶 \(logString)") + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] + // Found a node, save Device Config + if !fetchedNode.isEmpty { + if fetchedNode[0].bluetoothConfig == nil { + let newBluetoothConfig = BluetoothConfigEntity(context: context) + newBluetoothConfig.enabled = config.bluetooth.enabled + newBluetoothConfig.mode = Int32(config.bluetooth.mode.rawValue) + newBluetoothConfig.fixedPin = Int32(config.bluetooth.fixedPin) + fetchedNode[0].bluetoothConfig = newBluetoothConfig + } else { + fetchedNode[0].bluetoothConfig?.enabled = config.bluetooth.enabled + fetchedNode[0].bluetoothConfig?.mode = Int32(config.bluetooth.mode.rawValue) + fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.bluetooth.fixedPin) + } + do { + try context.save() + print("💾 Updated Bluetooth Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)") + } + } else { + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Bluetooth Config") + } + } catch { + let nsError = error as NSError + print("💥 Fetching node for core data BluetoothConfigEntity failed: \(nsError)") + } +} + +func upsertDeviceConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.device.config %@", comment: "Device config received: %@"), String(nodeNum)) + MeshLogger.log("📟 \(logString)") + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] + // Found a node, save Device Config + if !fetchedNode.isEmpty { + if fetchedNode[0].deviceConfig == nil { + let newDeviceConfig = DeviceConfigEntity(context: context) + newDeviceConfig.role = Int32(config.device.role.rawValue) + newDeviceConfig.serialEnabled = config.device.serialEnabled + newDeviceConfig.debugLogEnabled = config.device.debugLogEnabled + newDeviceConfig.buttonGpio = Int32(config.device.buttonGpio) + newDeviceConfig.buzzerGpio = Int32(config.device.buzzerGpio) + fetchedNode[0].deviceConfig = newDeviceConfig + } else { + fetchedNode[0].deviceConfig?.role = Int32(config.device.role.rawValue) + fetchedNode[0].deviceConfig?.serialEnabled = config.device.serialEnabled + fetchedNode[0].deviceConfig?.debugLogEnabled = config.device.debugLogEnabled + fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.device.buttonGpio) + fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.device.buzzerGpio) + } + do { + try context.save() + print("💾 Updated Device Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data DeviceConfigEntity: \(nsError)") + } + } + } catch { + let nsError = error as NSError + print("💥 Fetching node for core data DeviceConfigEntity failed: \(nsError)") + } +} + +func upsertDisplayConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.display.config %@", comment: "Display config received: %@"), String(nodeNum)) + MeshLogger.log("🖥️ \(logString)") + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] + + // Found a node, save Device Config + if !fetchedNode.isEmpty { + + if fetchedNode[0].displayConfig == nil { + + let newDisplayConfig = DisplayConfigEntity(context: context) + newDisplayConfig.gpsFormat = Int32(config.display.gpsFormat.rawValue) + newDisplayConfig.screenOnSeconds = Int32(config.display.screenOnSecs) + newDisplayConfig.screenCarouselInterval = Int32(config.display.autoScreenCarouselSecs) + newDisplayConfig.compassNorthTop = config.display.compassNorthTop + newDisplayConfig.flipScreen = config.display.flipScreen + newDisplayConfig.oledType = Int32(config.display.oled.rawValue) + fetchedNode[0].displayConfig = newDisplayConfig + + } else { + + fetchedNode[0].displayConfig?.gpsFormat = Int32(config.display.gpsFormat.rawValue) + fetchedNode[0].displayConfig?.screenOnSeconds = Int32(config.display.screenOnSecs) + fetchedNode[0].displayConfig?.screenCarouselInterval = Int32(config.display.autoScreenCarouselSecs) + fetchedNode[0].displayConfig?.compassNorthTop = config.display.compassNorthTop + fetchedNode[0].displayConfig?.flipScreen = config.display.flipScreen + fetchedNode[0].displayConfig?.oledType = Int32(config.display.oled.rawValue) + } + + do { + + try context.save() + print("💾 Updated Display Config for node number: \(String(nodeNum))") + + } catch { + + context.rollback() + + let nsError = error as NSError + print("💥 Error Updating Core Data DisplayConfigEntity: \(nsError)") + } + } else { + + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Display Config") + } + + } catch { + + let nsError = error as NSError + print("💥 Fetching node for core data DisplayConfigEntity failed: \(nsError)") + } +} + +func upsertLoRaConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config %@", comment: "LoRa config received: %@"), String(nodeNum)) MeshLogger.log("📻 \(logString)") @@ -113,8 +256,9 @@ func upsertLoraConfigPacket(config: Config, nodeNum: Int64, context: NSManagedOb let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save LoRa Config - if !fetchedNode.isEmpty { + if fetchedNode.count > 0 { if fetchedNode[0].loRaConfig == nil { + // No lora config for node, save a new lora config let newLoRaConfig = LoRaConfigEntity(context: context) newLoRaConfig.regionCode = Int32(config.lora.region.rawValue) newLoRaConfig.usePreset = config.lora.usePreset @@ -157,3 +301,93 @@ func upsertLoraConfigPacket(config: Config, nodeNum: Int64, context: NSManagedOb print("💥 Fetching node for core data LoRaConfigEntity failed: \(nsError)") } } + +func upsertNetworkConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.network.config %@", comment: "Network config received: %@"), String(nodeNum)) + MeshLogger.log("🌐 \(logString)") + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] + // Found a node, save WiFi Config + if !fetchedNode.isEmpty { + if fetchedNode[0].networkConfig == nil { + let newNetworkConfig = NetworkConfigEntity(context: context) + newNetworkConfig.wifiSsid = config.network.wifiSsid + newNetworkConfig.wifiPsk = config.network.wifiPsk + fetchedNode[0].networkConfig = newNetworkConfig + } else { + fetchedNode[0].networkConfig?.wifiSsid = config.network.wifiSsid + fetchedNode[0].networkConfig?.wifiPsk = config.network.wifiPsk + } + + do { + try context.save() + print("💾 Updated Network Config for node number: \(String(nodeNum))") + + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data WiFiConfigEntity: \(nsError)") + } + } else { + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Network Config") + } + } catch { + let nsError = error as NSError + print("💥 Fetching node for core data NetworkConfigEntity failed: \(nsError)") + } +} + +func upsertPositionConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.config %@", comment: "Positon config received: %@"), String(nodeNum)) + MeshLogger.log("🗺️ \(logString)") + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] + // Found a node, save LoRa Config + if !fetchedNode.isEmpty { + if fetchedNode[0].positionConfig == nil { + let newPositionConfig = PositionConfigEntity(context: context) + newPositionConfig.smartPositionEnabled = config.position.positionBroadcastSmartEnabled + newPositionConfig.deviceGpsEnabled = config.position.gpsEnabled + newPositionConfig.fixedPosition = config.position.fixedPosition + newPositionConfig.gpsUpdateInterval = Int32(config.position.gpsUpdateInterval) + newPositionConfig.gpsAttemptTime = Int32(config.position.gpsAttemptTime) + newPositionConfig.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs) + newPositionConfig.positionFlags = Int32(config.position.positionFlags) + fetchedNode[0].positionConfig = newPositionConfig + } else { + fetchedNode[0].positionConfig?.smartPositionEnabled = config.position.positionBroadcastSmartEnabled + fetchedNode[0].positionConfig?.deviceGpsEnabled = config.position.gpsEnabled + fetchedNode[0].positionConfig?.fixedPosition = config.position.fixedPosition + fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.position.gpsUpdateInterval) + fetchedNode[0].positionConfig?.gpsAttemptTime = Int32(config.position.gpsAttemptTime) + fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs) + fetchedNode[0].positionConfig?.positionFlags = Int32(config.position.positionFlags) + } + do { + try context.save() + print("💾 Updated Position Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data PositionConfigEntity: \(nsError)") + } + } else { + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Position Config") + } + } catch { + let nsError = error as NSError + print("💥 Fetching node for core data PositionConfigEntity failed: \(nsError)") + } +} diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index dcc10981..c4c5a00f 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -14,6 +14,7 @@ struct BluetoothConfig: View { @Environment(\.dismiss) private var goBack var node: NodeInfoEntity? + var connectedNode: NodeInfoEntity? @State private var isPresentingSaveConfirm: Bool = false @State var hasChanges = false @@ -125,6 +126,13 @@ struct BluetoothConfig: View { self.mode = Int(node?.bluetoothConfig?.mode ?? 0) self.fixedPin = String(node?.bluetoothConfig?.fixedPin ?? 123456) self.hasChanges = false + + // Need to request a LoRaConfig from the remote node before allowing changes + if node?.bluetoothConfig == nil { + print("empty bluetooth config") + + } + let adminMessageId = bleManager.requestBluetoothConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } .onChange(of: enabled) { newEnabled in if node != nil && node!.bluetoothConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index ac4d277a..bf96724a 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -51,6 +51,8 @@ struct LoRaConfig: View { } Section(header: Text("Mesh Options")) { Picker("Number of hops", selection: $hopLimit) { + Text("Please Select") + .tag(0) ForEach(HopValues.allCases) { hop in Text(hop.description) } @@ -115,10 +117,10 @@ struct LoRaConfig: View { // Need to request a LoRaConfig from the remote node before allowing changes if node?.loRaConfig == nil { - - let adminMessageId = bleManager.getLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + print("empty lora config") + let adminMessageId = bleManager.requestLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } - + print(node?.loRaConfig?.regionCode) } .onChange(of: region) { newRegion in if node != nil && node!.loRaConfig != nil { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 263c0975..258db8e0 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -107,13 +107,13 @@ struct Settings: View { .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink() { - BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) + BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode }), connectedNode: nodes.first(where: { $0.num == connectedNodeNum })) } label: { Image(systemName: "antenna.radiowaves.left.and.right") .symbolRenderingMode(.hierarchical) Text("bluetooth") } - .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + .disabled(selectedNode == 0) NavigationLink { DeviceConfig(node: nodes.first(where: { $0.num == selectedNode })) @@ -221,7 +221,7 @@ struct Settings: View { Text("mesh.log") } NavigationLink { - let connectedNode = nodes.first(where: { $0.num == selectedNode }) + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) AdminMessageList(user: connectedNode?.user) } label: { Image(systemName: "building.columns") @@ -243,13 +243,12 @@ struct Settings: View { .onAppear { self.bleManager.context = context self.bleManager.userSettings = userSettings + self.connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) if initialLoad { selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) initialLoad = false } - connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) - //let node = nodes.first(where: { $0.num == connectedNodeNum }) - //selectedNode = node?.num ?? 0 + } .listStyle(GroupedListStyle()) .navigationTitle("settings") From 3d21d714d945d60b1a7f6e2c4bf58916919baa99 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Jan 2023 18:05:45 -0800 Subject: [PATCH 53/57] Hide non working pages from the remote admin nav --- Meshtastic/Views/Settings/Settings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 258db8e0..8420efb6 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -113,7 +113,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("bluetooth") } - .disabled(selectedNode == 0) + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { DeviceConfig(node: nodes.first(where: { $0.num == selectedNode })) From 6b4240d7fb2a522366280a42dfd6356171446841 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 24 Jan 2023 20:33:48 -0800 Subject: [PATCH 54/57] Updates to settings views --- Meshtastic/Helpers/BLEManager.swift | 7 +- Meshtastic/Helpers/MeshPackets.swift | 11 ++- .../Views/Settings/Config/LoRaConfig.swift | 1 - .../Views/Settings/Config/NetworkConfig.swift | 2 +- Meshtastic/Views/Settings/Settings.swift | 74 +++++++++++++++---- 5 files changed, 73 insertions(+), 22 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 2063cddb..808b9bd7 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -779,9 +779,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var success = false let fromNodeNum = connectedPeripheral.num - if fromNodeNum <= 0 || (LocationHelper.currentLocation.latitude == LocationHelper.DefaultLocation.latitude && LocationHelper.currentLocation.longitude == LocationHelper.DefaultLocation.longitude) { + if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { return false } + var positionPacket = Position() positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) @@ -826,7 +827,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if userSettings!.provideLocation { let success = sendPosition(destNum: connectedPeripheral.num, wantResponse: false) if !success { - print("Failed to send positon to device") + print("Failed to send position to device") } } } @@ -1247,7 +1248,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return 0 } - public func saveWiFiConfig(config: Config.NetworkConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { + public func saveNetworkConfig(config: Config.NetworkConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.network = config diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 628339b8..ae101ad9 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -38,7 +38,7 @@ func generateMessageMarkdown (message: String) -> String { func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - // We don't care about any of the Power settings, config is available for everyting else + // We don't care about any of the Power settings, config is available for everything else if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { upsertBluetoothConfigPacket(config: config, nodeNum: nodeNum, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { @@ -519,7 +519,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS do { let fetchedNode = try context.fetch(fetchedNodeRequest) as! [NodeInfoEntity] - if fetchedNode.count == 1 { + if fetchedNode.count > 0 { let newMetadata = DeviceMetadataEntity(context: context) newMetadata.firmwareVersion = metadata.firmwareVersion newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion) @@ -529,7 +529,6 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS newMetadata.hasEthernet = metadata.hasEthernet_p newMetadata.role = Int32(metadata.role.rawValue) newMetadata.positionFlags = Int32(metadata.positionFlags) - fetchedNode[0].metadata = newMetadata do { @@ -806,6 +805,12 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { upsertLoRaConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { + upsertNetworkConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + + } else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { + upsertPositionConfigPacket(config: config, nodeNum: Int64(packet.from), context: context) + } } } else { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index bf96724a..7895d3ee 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -120,7 +120,6 @@ struct LoRaConfig: View { print("empty lora config") let adminMessageId = bleManager.requestLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } - print(node?.loRaConfig?.regionCode) } .onChange(of: region) { newRegion in if node != nil && node!.loRaConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 65e73b2c..58710789 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -118,7 +118,7 @@ struct NetworkConfig: View { network.ethEnabled = self.ethEnabled //network.addressMode = Config.NetworkConfig.AddressMode.dhcp - let adminMessageId = bleManager.saveWiFiConfig(config: network, fromUser: node!.user!, toUser: node!.user!) + let adminMessageId = bleManager.saveNetworkConfig(config: network, fromUser: node!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 8420efb6..ea7d6d05 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -18,6 +18,30 @@ struct Settings: View { @State private var connectedNodeNum: Int = 0 @State private var initialLoad: Bool = true + @State private var selection: SettingsSidebar = .about + + enum SettingsSidebar { + case appSettings + case shareChannels + case userConfig + case loraConfig + case channelConfig + case bluetoothConfig + case deviceConfig + case displayConfig + case networkConfig + case positionConfig + case cannedMessagesConfig + case externalNotificationConfig + case mqttConfig + case rangeTestConfig + case serialConfig + case telemetryConfig + case meshLog + case adminMessageLog + case about + } + var body: some View { NavigationSplitView { List { @@ -28,6 +52,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("app.settings") } + .tag(SettingsSidebar.appSettings) let node = nodes.first(where: { $0.num == connectedNodeNum }) if node?.myInfo?.adminIndex ?? 0 > 0 { Section("Configure") { @@ -51,19 +76,19 @@ struct Settings: View { .pickerStyle(.menu) .labelsHidden() .onChange(of: selectedNode) { newValue in - if selectedNode > 0 { - let node = nodes.first(where: { $0.num == newValue }) - let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) - connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) - - if node?.metadata == nil && node!.num != connectedNodeNum { - let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) - - if adminMessageId > 0 { - print("Saved node metadata") - } - } - } +// if selectedNode > 0 { +// let node = nodes.first(where: { $0.num == newValue }) +// let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) +// connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) +// +// if node?.metadata == nil && node!.num != connectedNodeNum { +// let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) +// +// if adminMessageId > 0 { +// print("Saved node metadata") +// } +// } +// } } } } @@ -77,7 +102,9 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("share.channels") } + .tag(SettingsSidebar.shareChannels) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + NavigationLink { UserConfig(node: nodes.first(where: { $0.num == selectedNode }), connectedNode: nodes.first(where: { $0.num == connectedNodeNum })) } label: { @@ -86,6 +113,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("user") } + .tag(SettingsSidebar.userConfig) .disabled(selectedNode == 0) NavigationLink() { @@ -95,7 +123,9 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("lora") } - .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + .tag(SettingsSidebar.loraConfig) + .disabled(selectedNode == 0) + //.disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink() { Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) @@ -104,6 +134,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("channels") } + .tag(SettingsSidebar.channelConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink() { @@ -113,6 +144,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("bluetooth") } + .tag(SettingsSidebar.bluetoothConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { @@ -122,6 +154,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("device") } + .tag(SettingsSidebar.deviceConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { @@ -131,6 +164,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("display") } + .tag(SettingsSidebar.displayConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { @@ -141,6 +175,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("network") } + .tag(SettingsSidebar.networkConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { @@ -151,6 +186,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("position") } + .tag(SettingsSidebar.positionConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) } @@ -165,6 +201,7 @@ struct Settings: View { Text("canned.messages") } + .tag(SettingsSidebar.cannedMessagesConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { @@ -174,6 +211,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("external.notification") } + .tag(SettingsSidebar.externalNotificationConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { @@ -183,6 +221,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("mqtt") } + .tag(SettingsSidebar.mqttConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { @@ -192,6 +231,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("range.test") } + .tag(SettingsSidebar.rangeTestConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { @@ -201,6 +241,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("serial") } + .tag(SettingsSidebar.serialConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink { @@ -210,6 +251,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("telemetry") } + .tag(SettingsSidebar.telemetryConfig) .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) } Section(header: Text("logging")) { @@ -220,6 +262,8 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("mesh.log") } + .tag(SettingsSidebar.meshLog) + NavigationLink { let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) AdminMessageList(user: connectedNode?.user) @@ -228,6 +272,7 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("admin.log") } + .tag(SettingsSidebar.adminMessageLog) } Section(header: Text("about")) { NavigationLink { @@ -238,6 +283,7 @@ struct Settings: View { Text("about.meshtastic") } + .tag(SettingsSidebar.about) } } .onAppear { From 8141a36e06e52517068b71b231bd1f9d38d8828b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 25 Jan 2023 23:01:45 -0800 Subject: [PATCH 55/57] Dont ever save positions at apple park implement display mode --- Meshtastic/Enums/DisplayEnums.swift | 43 ++++++++++ Meshtastic/Helpers/BLEManager.swift | 2 +- Meshtastic/Helpers/MeshPackets.swift | 82 ++++--------------- .../contents | 3 +- Meshtastic/Persistence/UpdateCoreData.swift | 64 ++++++++++++++- .../Views/Settings/Config/DisplayConfig.swift | 47 +++++++---- 6 files changed, 157 insertions(+), 84 deletions(-) diff --git a/Meshtastic/Enums/DisplayEnums.swift b/Meshtastic/Enums/DisplayEnums.swift index 46110532..a65bd94b 100644 --- a/Meshtastic/Enums/DisplayEnums.swift +++ b/Meshtastic/Enums/DisplayEnums.swift @@ -104,6 +104,7 @@ enum OledTypes: Int, CaseIterable, Identifiable { case auto = 0 case ssd1306 = 1 case sh1106 = 2 + case sh1107 = 3 var id: Int { self.rawValue } var description: String { @@ -115,6 +116,8 @@ enum OledTypes: Int, CaseIterable, Identifiable { return "SSD 1306" case .sh1106: return "SH 1106" + case .sh1107: + return "SH 1107" } } } @@ -127,6 +130,46 @@ enum OledTypes: Int, CaseIterable, Identifiable { return Config.DisplayConfig.OledType.oledSsd1306 case .sh1106: return Config.DisplayConfig.OledType.oledSh1106 + case .sh1107: + return Config.DisplayConfig.OledType.oledSh1106 + } + } +} + +// Default of 0 is auto +enum DisplayModes: Int, CaseIterable, Identifiable { + + case defaultMode = 0 + case twoColor = 1 + case inverted = 2 + case color = 3 + + var id: Int { self.rawValue } + var description: String { + get { + switch self { + case .defaultMode: + return "Default 128x64 screen layout" + case .twoColor: + return "Optimized for 2 color displays" + case .inverted: + return "Inverted top bar for 2 Color display" + case .color: + return "TFT Full Color Displays" + } + } + } + func protoEnumValue() -> Config.DisplayConfig.DisplayMode { + + switch self { + case .defaultMode: + return Config.DisplayConfig.DisplayMode.default + case .twoColor: + return Config.DisplayConfig.DisplayMode.twocolor + case .inverted: + return Config.DisplayConfig.DisplayMode.inverted + case .color: + return Config.DisplayConfig.DisplayMode.color } } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 808b9bd7..1ad90961 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -505,7 +505,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .remoteHardwareApp: MeshLogger.log("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .positionApp: - positionPacket(packet: decodedInfo.packet, context: context!) + upsertPositionPacket(packet: decodedInfo.packet, context: context!) case .waypointApp: waypointPacket(packet: decodedInfo.packet, context: context!) case .nodeinfoApp: diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index ae101ad9..cbd9a56c 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -589,7 +589,8 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newNode.user = newUser } - if nodeInfo.position.latitudeI > 0 || nodeInfo.position.longitudeI > 0 { + if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) + { let position = PositionEntity(context: context) position.seqNo = Int32(nodeInfo.position.seqNumber) position.latitudeI = nodeInfo.position.latitudeI @@ -656,14 +657,19 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje if nodeInfo.hasPosition { - let position = PositionEntity(context: context) - position.latitudeI = nodeInfo.position.latitudeI - position.longitudeI = nodeInfo.position.longitudeI - position.altitude = nodeInfo.position.altitude - position.satsInView = Int32(nodeInfo.position.satsInView) - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) - let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet - fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet + + 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 + position.longitudeI = nodeInfo.position.longitudeI + position.altitude = nodeInfo.position.altitude + position.satsInView = Int32(nodeInfo.position.satsInView) + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) + let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet + fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet + } + } // Look for a MyInfo @@ -753,6 +759,8 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { if let adminMessage = try? AdminMessage(serializedData: packet.decoded.payload) { + + if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { if let cmmc = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) { @@ -819,62 +827,6 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } } -func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.received %@", comment: "Position Packet received from node: %@"), String(packet.from)) - MeshLogger.log("📍 \(logString)") - - let fetchNodePositionRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - - do { - - if let positionMessage = try? Position(serializedData: packet.decoded.payload) { - // Don't save empty position packets - if positionMessage.longitudeI > 0 || positionMessage.latitudeI > 0 { - let fetchedNode = try context.fetch(fetchNodePositionRequest) as! [NodeInfoEntity] - if fetchedNode.count == 1 { - - let position = PositionEntity(context: context) - position.snr = packet.rxSnr - position.seqNo = Int32(positionMessage.seqNumber) - position.latitudeI = positionMessage.latitudeI - position.longitudeI = positionMessage.longitudeI - position.altitude = positionMessage.altitude - position.satsInView = Int32(positionMessage.satsInView) - position.speed = Int32(positionMessage.groundSpeed) - position.heading = Int32(positionMessage.groundTrack) - if positionMessage.timestamp != 0 { - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp))) - } else { - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) - } - let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet - mutablePositions.add(position) - fetchedNode[0].id = Int64(packet.from) - fetchedNode[0].num = Int64(packet.from) - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) - fetchedNode[0].snr = packet.rxSnr - fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet - do { - try context.save() - print("💾 Updated Node Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError)") - } - } - } else { - print("💥 Empty POSITION_APP Packet") - print(try! packet.jsonString()) - } - } - } catch { - print("💥 Error Deserializing POSITION_APP packet.") - } -} - func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { if let routingMessage = try? Routing(serializedData: packet.decoded.payload) { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents index 5602f840..6f82e5d0 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV6.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -60,6 +60,7 @@ + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c739ecde..6552e54a 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -62,7 +62,7 @@ public func clearTelemetry(destNum: Int64, metricsType: Int32, context: NSManage public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) { do { - let objects = channel.allPrivateMessages// try context.fetch(fetchChannelMessagesRequest) as! [NSManagedObject] + let objects = channel.allPrivateMessages for object in objects { context.delete(object) } @@ -75,7 +75,7 @@ public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObje public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext) { do { - let objects = user.messageList//try context.fetch(fetchUserMessagesRequest) as! [NSManagedObject] + let objects = user.messageList for object in objects { context.delete(object) } @@ -101,6 +101,64 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext) { } } +func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.received %@", comment: "Position Packet received from node: %@"), String(packet.from)) + MeshLogger.log("📍 \(logString)") + + let fetchNodePositionRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + + if let positionMessage = try? Position(serializedData: packet.decoded.payload) { + + // Don't save empty position packets + if positionMessage.longitudeI > 0 || positionMessage.latitudeI > 0 && (positionMessage.latitudeI != 373346000 && positionMessage.longitudeI != -1220090000) + { + let fetchedNode = try context.fetch(fetchNodePositionRequest) as! [NodeInfoEntity] + if fetchedNode.count == 1 { + + let position = PositionEntity(context: context) + position.snr = packet.rxSnr + position.seqNo = Int32(positionMessage.seqNumber) + position.latitudeI = positionMessage.latitudeI + position.longitudeI = positionMessage.longitudeI + position.altitude = positionMessage.altitude + position.satsInView = Int32(positionMessage.satsInView) + position.speed = Int32(positionMessage.groundSpeed) + position.heading = Int32(positionMessage.groundTrack) + if positionMessage.timestamp != 0 { + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp))) + } else { + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) + } + let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet + mutablePositions.add(position) + fetchedNode[0].id = Int64(packet.from) + fetchedNode[0].num = Int64(packet.from) + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) + fetchedNode[0].snr = packet.rxSnr + fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet + do { + try context.save() + print("💾 Updated Node Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError)") + } + } + } else { + print("💥 Empty POSITION_APP Packet") + print(try! packet.jsonString()) + } + } + } catch { + print("💥 Error Deserializing POSITION_APP packet.") + } +} + func upsertBluetoothConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.bluetooth.config %@", comment: "Bluetooth config received: %@"), String(nodeNum)) @@ -208,6 +266,7 @@ func upsertDisplayConfigPacket(config: Config, nodeNum: Int64, context: NSManage newDisplayConfig.compassNorthTop = config.display.compassNorthTop newDisplayConfig.flipScreen = config.display.flipScreen newDisplayConfig.oledType = Int32(config.display.oled.rawValue) + newDisplayConfig.displayMode = Int32(config.display.displaymode.rawValue) fetchedNode[0].displayConfig = newDisplayConfig } else { @@ -218,6 +277,7 @@ func upsertDisplayConfigPacket(config: Config, nodeNum: Int64, context: NSManage fetchedNode[0].displayConfig?.compassNorthTop = config.display.compassNorthTop fetchedNode[0].displayConfig?.flipScreen = config.display.flipScreen fetchedNode[0].displayConfig?.oledType = Int32(config.display.oled.rawValue) + fetchedNode[0].displayConfig?.displayMode = Int32(config.display.displaymode.rawValue) } do { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 9103c5ce..1554b536 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -24,29 +24,20 @@ struct DisplayConfig: View { @State var compassNorthTop = false @State var flipScreen = false @State var oledType = 0 + @State var displayMode = 0 var body: some View { Form { Section(header: Text("Device Screen")) { - Picker("Screen on for", selection: $screenOnSeconds ) { - ForEach(ScreenOnIntervals.allCases) { soi in - Text(soi.description) + Picker("Display Mode", selection: $displayMode ) { + ForEach(DisplayModes.allCases) { dm in + Text(dm.description) } } .pickerStyle(DefaultPickerStyle()) - - Text("How long the screen remains on after the user button is pressed or messages are received.") - .font(.caption) - - Picker("Carousel Interval", selection: $screenCarouselInterval ) { - ForEach(ScreenCarouselIntervals.allCases) { sci in - Text(sci.description) - } - } - .pickerStyle(DefaultPickerStyle()) - Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.") + Text("Override automatic OLED screen detection.") .font(.caption) Toggle(isOn: $compassNorthTop) { @@ -64,6 +55,7 @@ struct DisplayConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Text("Flip screen vertically") .font(.caption) + Picker("OLED Type", selection: $oledType ) { ForEach(OledTypes.allCases) { ot in Text(ot.description) @@ -74,7 +66,25 @@ struct DisplayConfig: View { .font(.caption) } - Section(header: Text("Format")) { + Section(header: Text("Timing & Format")) { + Picker("Screen on for", selection: $screenOnSeconds ) { + ForEach(ScreenOnIntervals.allCases) { soi in + Text(soi.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("How long the screen remains on after the user button is pressed or messages are received.") + .font(.caption) + + Picker("Carousel Interval", selection: $screenCarouselInterval ) { + ForEach(ScreenCarouselIntervals.allCases) { sci in + Text(sci.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.") + .font(.caption) + Picker("GPS Format", selection: $gpsFormat ) { ForEach(GpsFormats.allCases) { lu in Text(lu.description) @@ -116,6 +126,7 @@ struct DisplayConfig: View { dc.compassNorthTop = compassNorthTop dc.flipScreen = flipScreen dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue() + dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue() let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: node!.user!, toUser: node!.user!) if adminMessageId > 0 { @@ -143,6 +154,7 @@ struct DisplayConfig: View { self.compassNorthTop = node?.displayConfig?.compassNorthTop ?? false self.flipScreen = node?.displayConfig?.flipScreen ?? false self.oledType = Int(node?.displayConfig?.oledType ?? 0) + self.displayMode = Int(node?.displayConfig?.displayMode ?? 0) self.hasChanges = false } .onChange(of: screenOnSeconds) { newScreenSecs in @@ -175,5 +187,10 @@ struct DisplayConfig: View { if newOledType != node!.displayConfig!.oledType { hasChanges = true } } } + .onChange(of: displayMode) { newDisplayMode in + if node != nil && node!.displayConfig != nil { + if newDisplayMode != node!.displayConfig!.displayMode { hasChanges = true } + } + } } } From 090e414439dd1dff5cabcd4146cf8a91edd7f340 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 26 Jan 2023 16:19:51 -0800 Subject: [PATCH 56/57] Assorted updates --- .../Views/Map/Custom/MapViewSwiftUI.swift | 16 +++++---- Meshtastic/Views/Map/WaypointFormView.swift | 33 ++++++++++++------- Meshtastic/Views/Settings/Settings.swift | 3 +- de.lproj/Localizable.strings | 1 + en.lproj/Localizable.strings | 1 + zh-Hans.lproj/Localizable.strings | 1 + 6 files changed, 35 insertions(+), 20 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index caa9f52e..56a0d1cc 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -162,12 +162,16 @@ struct MapViewSwiftUI: UIViewRepresentable { } @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { - // Screen Position - CGPoint - let location = longPressRecognizer.location(in: self.parent.mapView) - // Map Coordinate - CLLocationCoordinate2D - let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - // Add annotation: - if coordinate.longitude != LocationHelper.DefaultLocation.longitude && coordinate.latitude != LocationHelper.DefaultLocation.latitude { + + if gesture.state != UIGestureRecognizer.State.ended { + return + } else if gesture.state != UIGestureRecognizer.State.began { + + // Screen Position - CGPoint + let location = longPressRecognizer.location(in: self.parent.mapView) + + // Map Coordinate - CLLocationCoordinate2D + let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) let annotation = MKPointAnnotation() annotation.title = "Dropped Pin" annotation.coordinate = coordinate diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 201ba6d9..767a9614 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -25,6 +25,7 @@ struct WaypointFormView: View { @State private var expires: Bool = false @State private var expire: Date = Date() // = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours @State private var locked: Bool = false + @State private var lockedTo: Int64 = 0 var body: some View { @@ -108,15 +109,15 @@ struct WaypointFormView: View { } } - Toggle(isOn: $expires) { - Label("Expires", systemImage: "clock.badge.xmark") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if expires { - DatePicker("Expire", selection: $expire, in: Date.now...) - .datePickerStyle(.compact) - .font(.callout) - } +// Toggle(isOn: $expires) { +// Label("Expires", systemImage: "clock.badge.xmark") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// if expires { +// DatePicker("Expire", selection: $expire, in: Date.now...) +// .datePickerStyle(.compact) +// .font(.callout) +// } Toggle(isOn: $locked) { Label("Locked", systemImage: "lock") } @@ -143,7 +144,12 @@ struct WaypointFormView: View { let unicode = unicodeScalers[unicodeScalers.startIndex].value newWaypoint.icon = unicode if locked { - newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) + + if lockedTo == 0 { + newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) + } else { + newWaypoint.lockedTo = UInt32(lockedTo) + } } if expire.timeIntervalSince1970 > 0 { newWaypoint.expire = UInt32(expire.timeIntervalSince1970) @@ -213,6 +219,10 @@ struct WaypointFormView: View { } else { expires = false } + if waypoint.locked > 0 { + locked = true + lockedTo = waypoint.locked + } } else { name = "" description = "" @@ -227,8 +237,7 @@ struct WaypointFormView: View { if coordinate.distance(from: LocationHelper.DefaultLocation) == 0.0 { // Too close to apple park, bail out waypointId = 0 - //dismiss() - //print(coordinate.distance(from: LocationHelper.DefaultLocation)) + dismiss() } } } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index ea7d6d05..87cfd1a8 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -124,8 +124,7 @@ struct Settings: View { Text("lora") } .tag(SettingsSidebar.loraConfig) - .disabled(selectedNode == 0) - //.disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) NavigationLink() { Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 87117b20..7da486b3 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -169,6 +169,7 @@ "mesh.log.traceroute.received.route %@"="Traceroute Ergebnis: %@"; "mesh.log.wantconfig %@"="Issuing Want Config to %@"; "mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; +"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; "message"="Nachricht"; "message.details"="Nachrichtendetails"; "messages"="Nachrichten"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 7f67069a..eb3f3caa 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -169,6 +169,7 @@ "mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; "mesh.log.wantconfig %@"="Issuing Want Config to %@"; "mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; +"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; "message"="Message"; "message.details"="Message Details"; "messages"="Messages"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 946d2694..8a9f0699 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -169,6 +169,7 @@ "mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; "mesh.log.wantconfig %@"="Issuing Want Config to %@"; "mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; +"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; "message"="消息"; "message.details"="消息详情"; "messages"="消息"; From 39be42db5f5dfd87abdc480a47e8357e899cc7bf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 26 Jan 2023 16:29:00 -0800 Subject: [PATCH 57/57] Comment out display type I don't have protos for yet --- Meshtastic/Enums/DisplayEnums.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Enums/DisplayEnums.swift b/Meshtastic/Enums/DisplayEnums.swift index a65bd94b..d124af5d 100644 --- a/Meshtastic/Enums/DisplayEnums.swift +++ b/Meshtastic/Enums/DisplayEnums.swift @@ -104,7 +104,7 @@ enum OledTypes: Int, CaseIterable, Identifiable { case auto = 0 case ssd1306 = 1 case sh1106 = 2 - case sh1107 = 3 + //case sh1107 = 3 var id: Int { self.rawValue } var description: String { @@ -116,8 +116,8 @@ enum OledTypes: Int, CaseIterable, Identifiable { return "SSD 1306" case .sh1106: return "SH 1106" - case .sh1107: - return "SH 1107" + //case .sh1107: + // return "SH 1107" } } } @@ -130,8 +130,8 @@ enum OledTypes: Int, CaseIterable, Identifiable { return Config.DisplayConfig.OledType.oledSsd1306 case .sh1106: return Config.DisplayConfig.OledType.oledSh1106 - case .sh1107: - return Config.DisplayConfig.OledType.oledSh1106 + //case .sh1107: + // return Config.DisplayConfig.OledType.oledSh1107 } } }