import UIKit import MapboxCoreNavigation import MapboxNavigation import MapboxDirections import Mapbox import CoreBluetooth class ViewController: UIViewController, MGLMapViewDelegate, CLLocationManagerDelegate, NavigationMapViewDelegate, NavigationViewControllerDelegate, CBCentralManagerDelegate, CBPeripheralDelegate { var mapView: NavigationMapView? var currentRoute: Route? { get { return routes?.first } set { guard let selected = newValue else { routes?.remove(at: 0); return } guard let routes = routes else { self.routes = [selected]; return } self.routes = [selected] + routes.filter { $0 != selected } } } var routes: [Route]? { didSet { guard let routes = routes, let current = routes.first else { mapView?.removeRoutes(); return } mapView?.showRoutes(routes) mapView?.showWaypoints(current) } } var startButton: UIButton? var locationManager = CLLocationManager() private typealias RouteRequestSuccess = (([Route]) -> Void) private typealias RouteRequestFailure = ((NSError) -> Void) //MARK: - Lifecycle Methods override func viewDidLoad() { super.viewDidLoad() let centralQueue: DispatchQueue = DispatchQueue(label: "com.gusabr.centralQueueName", attributes: .concurrent) centralManager = CBCentralManager(delegate: self, queue: centralQueue) NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .routeControllerProgressDidChange, object: nil) locationManager.delegate = self locationManager.requestWhenInUseAuthorization() mapView = NavigationMapView(frame: view.bounds) mapView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] mapView?.userTrackingMode = .follow mapView?.delegate = self mapView?.navigationMapViewDelegate = self let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) mapView?.addGestureRecognizer(gesture) view.addSubview(mapView!) startButton = UIButton() startButton?.setTitle("Start Navigation", for: .normal) startButton?.translatesAutoresizingMaskIntoConstraints = false startButton?.backgroundColor = .blue startButton?.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20) startButton?.addTarget(self, action: #selector(tappedButton(sender:)), for: .touchUpInside) startButton?.isHidden = true view.addSubview(startButton!) startButton?.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true startButton?.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true view.setNeedsLayout() } // MARK: - Core Bluetooth service and characteristic IDs // HC-08 Service FFE0 Char FFE1 // RN4871 Device Service 49535343-FE7D-4AE5-8FA9-9FAFD205E455 Char 49535343-8841-43F4-A8D4-ECBE34729BB3 let Service_CBUUID = CBUUID(string: "FFE0") let Characteristic_CBUUID = CBUUID(string: "FFE1") var centralManager: CBCentralManager? var peripheralHRM: CBPeripheral? var myCharacteristic: CBCharacteristic? override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } // MARK: - CBCentralManagerDelegate methods // STEP 3.1: this method is called based on // the device's Bluetooth state; we can ONLY // scan for peripherals if Bluetooth is .poweredOn func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .unknown: print("Bluetooth status is UNKNOWN") case .resetting: print("Bluetooth status is RESETTING") case .unsupported: print("Bluetooth status is UNSUPPORTED") case .unauthorized: print("Bluetooth status is UNAUTHORIZED") case .poweredOff: print("Bluetooth status is POWERED OFF") case .poweredOn: print("Bluetooth status is POWERED ON") // STEP 3.2: scan for peripherals that we're interested in centralManager?.scanForPeripherals(withServices: [Service_CBUUID]) } // END switch } // END func centralManagerDidUpdateState // STEP 4.1: discover what peripheral devices OF INTEREST // are available for this app to connect to func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { print(peripheral.name!) // STEP 4.2: MUST store a reference to the peripheral in // class instance variable peripheralHRM = peripheral // STEP 4.3: since HeartRateMonitorViewController // adopts the CBPeripheralDelegate protocol, // the peripheralHRM must set its // delegate property to HeartRateMonitorViewController // (self) peripheralHRM?.delegate = self // STEP 5: stop scanning to preserve battery life; // re-scan if disconnected centralManager?.stopScan() // STEP 6: connect to the discovered peripheral of interest centralManager?.connect(peripheralHRM!) } // END func centralManager(... didDiscover peripheral // STEP 7: "Invoked when a connection is successfully created with a peripheral." // we can only move forwards when we know the connection // to the peripheral succeeded func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { // STEP 8: look for services of interest on peripheral peripheralHRM?.discoverServices([Service_CBUUID]) } // END func centralManager(... didConnect peripheral // STEP 15: when a peripheral disconnects, func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { centralManager?.scanForPeripherals(withServices: [Service_CBUUID]) } // END func centralManager(... didDisconnectPeripheral peripheral func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { for service in peripheral.services! { if service.uuid == Service_CBUUID { peripheral.discoverCharacteristics(nil, for: service) } } } // END func peripheral(... didDiscoverServices // STEP 10: confirm we've discovered characteristics // of interest within services of interest public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { for characteristic in service.characteristics! { myCharacteristic = characteristic } // END for } // END func peripheral(... didDiscoverCharacteristicsFor service private var routeProgress: RouteProgress? let distanceFormatter = DistanceFormatter(approximate: true) @objc func progressDidChange(_ notification: NSNotification) { let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress let legProgress = routeProgress.currentLegProgress let stepProgress = legProgress.currentStepProgress // let stepDistance = distanceFormatter.measurement(of: stepProgress.distanceRemaining) let stepDistance = distanceFormatter.string(from: stepProgress.distanceRemaining) let visualInstruction = routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction var string = "" if let unwrapped = visualInstruction?.primaryInstruction.maneuverDirection.description { string += unwrapped string += "\t" } string += stepDistance string += "\t" if let unwrapped = visualInstruction?.primaryInstruction.text?.description { string += unwrapped } print(string) peripheralHRM?.writeValue(string.data(using: String.Encoding.utf8)!, for: myCharacteristic!, type: CBCharacteristicWriteType.withoutResponse) } //overriding layout lifecycle callback so we can style the start button override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() startButton?.layer.cornerRadius = startButton!.bounds.midY startButton?.clipsToBounds = true startButton?.setNeedsDisplay() } @objc func tappedButton(sender: UIButton) { guard let route = currentRoute else { return } // For demonstration purposes, simulate locations if the Simulate Navigation option is on. let navigationService = MapboxNavigationService(route: route, simulating: .always) let navigationOptions = NavigationOptions(navigationService: navigationService) let navigationViewController = NavigationViewController(for: route, options: navigationOptions) navigationViewController.delegate = self present(navigationViewController, animated: true, completion: nil) } @objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) { guard gesture.state == .ended else { return } let spot = gesture.location(in: mapView) guard let location = mapView?.convert(spot, toCoordinateFrom: mapView) else { return } requestRoute(destination: location) } func requestRoute(destination: CLLocationCoordinate2D) { guard let userLocation = mapView?.userLocation!.location else { return } let userWaypoint = Waypoint(location: userLocation, heading: mapView?.userLocation?.heading, name: "user") let destinationWaypoint = Waypoint(coordinate: destination) let options = NavigationRouteOptions(waypoints: [userWaypoint, destinationWaypoint]) options.includesSteps = true Directions.shared.calculate(options) { (waypoints, routes, error) in guard let routes = routes else { return } self.routes = routes self.startButton?.isHidden = false self.mapView?.showRoutes(routes) self.mapView?.showWaypoints(self.currentRoute!) } } // Delegate method called when the user selects a route func navigationMapView(_ mapView: NavigationMapView, didSelect route: Route) { self.currentRoute = route } }