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