443 lines
19 KiB
Swift
443 lines
19 KiB
Swift
//
|
|
// ScannerViewController.swift
|
|
// BluefruitPlayground
|
|
//
|
|
// Created by Antonio García on 11/10/2019.
|
|
// Copyright © 2019 Adafruit. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import CoreBluetooth
|
|
|
|
class ScannerViewController: UIViewController {
|
|
// Constants
|
|
static let kNavigationControllerIdentifier = "ScannerNavigationController"
|
|
|
|
// Config
|
|
private static let kDelayToShowWait: TimeInterval = 1.0
|
|
private static let kShowRssiValue = Config.isDebugEnabled && false
|
|
|
|
// UI
|
|
@IBOutlet weak var baseTableView: UITableView!
|
|
@IBOutlet weak var waitView: UIView!
|
|
@IBOutlet weak var waitLabel: UILabel!
|
|
@IBOutlet weak var problemsButton: UIButton!
|
|
@IBOutlet weak var scanAutomaticallyButton: CornerShadowButton!
|
|
@IBOutlet weak var actionsContainerView: UIStackView!
|
|
|
|
// Data
|
|
private let refreshControl = UIRefreshControl()
|
|
private let bleManager = Config.bleManager
|
|
private var peripheralList = PeripheralList(bleManager: Config.bleManager)
|
|
|
|
private var selectedPeripheral: BlePeripheral? {
|
|
didSet {
|
|
if isViewLoaded {
|
|
UIView.animate(withDuration: 0.3) {
|
|
self.actionsContainerView.alpha = self.selectedPeripheral == nil ? 1:0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private var infoAlertController: UIAlertController?
|
|
|
|
private var isBaseTableScrolling = false
|
|
private var isScannerTableWaitingForReload = false
|
|
|
|
private let navigationButton = UIButton(type: .custom)
|
|
|
|
// MARK: - Lifecycle
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
// Setup UI
|
|
waitView.alpha = 0
|
|
let topContentInsetForDetails: CGFloat = 20
|
|
baseTableView.contentInset = UIEdgeInsets(top: topContentInsetForDetails, left: 0, bottom: 0, right: 0)
|
|
|
|
// Setup table refresh
|
|
refreshControl.addTarget(self, action: #selector(tableRefresh), for: UIControl.Event.valueChanged)
|
|
baseTableView.addSubview(refreshControl)
|
|
refreshControl.tintColor = waitLabel.textColor
|
|
|
|
// Hide automatic scanning if needed
|
|
scanAutomaticallyButton.isHidden = !Config.isAutomaticConnectionEnabled
|
|
|
|
// Localization
|
|
let localizationManager = LocalizationManager.shared
|
|
self.title = localizationManager.localizedString("scanner_title")
|
|
|
|
waitLabel.text = localizationManager.localizedString("scanner_searching")
|
|
problemsButton.setTitle(localizationManager.localizedString("scanner_problems_action").uppercased(), for: .normal)
|
|
scanAutomaticallyButton.setTitle(localizationManager.localizedString("scanner_automatic_action").uppercased(), for: .normal)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
self.navigationItem.backBarButtonItem = nil // Clear any custom back button
|
|
|
|
if let customNavigationBar = navigationController?.navigationBar as? NavigationBarWithScrollAwareRightButton {
|
|
customNavigationBar.setRightButton(topViewController: self, image: UIImage(named: "info"), target: self, action: #selector(about(_:)))
|
|
}
|
|
|
|
// Ble Notifications
|
|
registerNotifications(enabled: true)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
// Disconnect if needed
|
|
let connectedPeripherals = bleManager.connectedPeripherals()
|
|
if connectedPeripherals.count == 1, let peripheral = connectedPeripherals.first {
|
|
DLog("Disconnect from previously connected peripheral")
|
|
// Disconnect from peripheral
|
|
disconnect(peripheral: peripheral)
|
|
}
|
|
|
|
// Update UI
|
|
updateScannedPeripherals()
|
|
|
|
// Start scannning
|
|
if !bleManager.isScanning {
|
|
bleManager.refreshPeripherals()
|
|
|
|
}
|
|
// Remove saved peripheral for autoconnect
|
|
Settings.clearAutoconnectPeripheral()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
// Ble Notifications
|
|
registerNotifications(enabled: false)
|
|
|
|
// Stop scanning
|
|
bleManager.stopScan()
|
|
|
|
// Clear peripherals
|
|
peripheralList.clear()
|
|
}
|
|
|
|
// MARK: - BLE Notifications
|
|
private weak var didDiscoverPeripheralObserver: NSObjectProtocol?
|
|
private weak var didUnDiscoverPeripheralObserver: NSObjectProtocol?
|
|
private weak var willConnectToPeripheralObserver: NSObjectProtocol?
|
|
private weak var didConnectToPeripheralObserver: NSObjectProtocol?
|
|
private weak var didDisconnectFromPeripheralObserver: NSObjectProtocol?
|
|
private weak var peripheralDidUpdateNameObserver: NSObjectProtocol?
|
|
private weak var willDiscoverServicesObserver: NSObjectProtocol?
|
|
|
|
private func registerNotifications(enabled: Bool) {
|
|
let notificationCenter = NotificationCenter.default
|
|
if enabled {
|
|
didDiscoverPeripheralObserver = notificationCenter.addObserver(forName: .didDiscoverPeripheral, object: nil, queue: .main, using: {[weak self] _ in self?.updateScannedPeripherals()})
|
|
didUnDiscoverPeripheralObserver = notificationCenter.addObserver(forName: .didUnDiscoverPeripheral, object: nil, queue: .main, using: {[weak self] _ in self?.updateScannedPeripherals()})
|
|
willConnectToPeripheralObserver = notificationCenter.addObserver(forName: .willConnectToPeripheral, object: nil, queue: .main, using: {[weak self] notification in self?.willConnectToPeripheral(notification: notification)})
|
|
didConnectToPeripheralObserver = notificationCenter.addObserver(forName: .didConnectToPeripheral, object: nil, queue: .main, using: {[weak self] notification in self?.didConnectToPeripheral(notification: notification)})
|
|
didDisconnectFromPeripheralObserver = notificationCenter.addObserver(forName: .didDisconnectFromPeripheral, object: nil, queue: .main, using: {[weak self] notification in self?.didDisconnectFromPeripheral(notification: notification)})
|
|
peripheralDidUpdateNameObserver = notificationCenter.addObserver(forName: .peripheralDidUpdateName, object: nil, queue: .main, using: {[weak self] notification in self?.peripheralDidUpdateName(notification: notification)})
|
|
willDiscoverServicesObserver = notificationCenter.addObserver(forName: .willDiscoverServices, object: nil, queue: .main, using: {[weak self] notification in self?.willDiscoverServices(notification: notification)})
|
|
|
|
} else {
|
|
if let didDiscoverPeripheralObserver = didDiscoverPeripheralObserver {notificationCenter.removeObserver(didDiscoverPeripheralObserver)}
|
|
if let didUnDiscoverPeripheralObserver = didUnDiscoverPeripheralObserver {notificationCenter.removeObserver(didUnDiscoverPeripheralObserver)}
|
|
if let willConnectToPeripheralObserver = willConnectToPeripheralObserver {notificationCenter.removeObserver(willConnectToPeripheralObserver)}
|
|
if let didConnectToPeripheralObserver = didConnectToPeripheralObserver {notificationCenter.removeObserver(didConnectToPeripheralObserver)}
|
|
if let didDisconnectFromPeripheralObserver = didDisconnectFromPeripheralObserver {notificationCenter.removeObserver(didDisconnectFromPeripheralObserver)}
|
|
if let peripheralDidUpdateNameObserver = peripheralDidUpdateNameObserver {notificationCenter.removeObserver(peripheralDidUpdateNameObserver)}
|
|
if let willDiscoverServicesObserver = willDiscoverServicesObserver {notificationCenter.removeObserver(willDiscoverServicesObserver)}
|
|
}
|
|
}
|
|
|
|
private func willConnectToPeripheral(notification: Notification) {
|
|
guard let peripheral = bleManager.peripheral(from: notification) else { return }
|
|
presentInfoDialog(title: LocalizationManager.shared.localizedString("scanner_connecting"), peripheral: peripheral)
|
|
}
|
|
|
|
private func didConnectToPeripheral(notification: Notification) {
|
|
guard let selectedPeripheral = selectedPeripheral, let identifier = notification.userInfo?[BleManager.NotificationUserInfoKey.uuid.rawValue] as? UUID, selectedPeripheral.identifier == identifier else {
|
|
DLog("Connected to an unexpected peripheral")
|
|
return
|
|
}
|
|
|
|
// Setup peripheral
|
|
AdafruitBoardsManager.shared.startBoard(connectedBlePeripheral: selectedPeripheral) { [weak self] result in
|
|
guard let self = self else { return }
|
|
|
|
switch result {
|
|
case .success:
|
|
DLog("setupPeripheral success")
|
|
|
|
// Finished setup
|
|
self.dismissInfoDialog {
|
|
self.showPeripheralDetails()
|
|
}
|
|
|
|
case .failure(let error):
|
|
DLog("setupPeripheral error: \(error.localizedDescription)")
|
|
let localizationManager = LocalizationManager.shared
|
|
|
|
let alertController = UIAlertController(title: localizationManager.localizedString("dialog_error"), message: localizationManager.localizedString("scanner_error_startboard"), preferredStyle: .alert)
|
|
let okAction = UIAlertAction(title: localizationManager.localizedString("dialog_ok"), style: .default, handler: nil)
|
|
alertController.addAction(okAction)
|
|
self.present(alertController, animated: true, completion: nil)
|
|
|
|
self.disconnect(peripheral: selectedPeripheral)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func willDiscoverServices(notification: Notification) {
|
|
infoAlertController?.message = LocalizationManager.shared.localizedString("scanner_discoveringservices")
|
|
}
|
|
|
|
private func didDisconnectFromPeripheral(notification: Notification) {
|
|
let peripheral = bleManager.peripheral(from: notification)
|
|
let currentlyConnectedPeripheralsCount = bleManager.connectedPeripherals().count
|
|
|
|
guard let selectedPeripheral = selectedPeripheral, selectedPeripheral.identifier == peripheral?.identifier || currentlyConnectedPeripheralsCount == 0 else { // If selected peripheral is disconnected or if there are no peripherals connected (after a failed dfu update)
|
|
return
|
|
}
|
|
|
|
// Clear selected peripheral
|
|
self.selectedPeripheral = nil
|
|
|
|
// Dismiss any info open dialogs
|
|
infoAlertController?.dismiss(animated: true, completion: nil)
|
|
infoAlertController = nil
|
|
|
|
// Reload table
|
|
reloadBaseTable()
|
|
}
|
|
|
|
private func peripheralDidUpdateName(notification: Notification) {
|
|
let name = notification.userInfo?[BlePeripheral.NotificationUserInfoKey.name.rawValue] as? String
|
|
DLog("centralManager peripheralDidUpdateName: \(name ?? "<unknown>")")
|
|
|
|
DispatchQueue.main.async {
|
|
// Reload table
|
|
self.reloadBaseTable()
|
|
}
|
|
}
|
|
|
|
// MARK: - Connections
|
|
private func connect(peripheral: BlePeripheral) {
|
|
// Connect to selected peripheral
|
|
selectedPeripheral = peripheral
|
|
bleManager.connect(to: peripheral)
|
|
reloadBaseTable()
|
|
}
|
|
|
|
private func disconnect(peripheral: BlePeripheral) {
|
|
selectedPeripheral = nil
|
|
bleManager.disconnect(from: peripheral)
|
|
reloadBaseTable()
|
|
}
|
|
|
|
// MARK: - Actions
|
|
@objc func tableRefresh() {
|
|
refreshPeripherals()
|
|
refreshControl.endRefreshing()
|
|
}
|
|
|
|
@IBAction func about(_ sender: Any) {
|
|
guard let viewController = storyboard?.instantiateViewController(withIdentifier: AboutViewController.kIdentifier) else { return }
|
|
|
|
self.present(viewController, animated: true, completion: nil)
|
|
}
|
|
|
|
@IBAction func scanAutomatically(_ sender: Any) {
|
|
ScreenFlowManager.gotoAutoconnect()
|
|
}
|
|
|
|
private func showPeripheralDetails() {
|
|
// Save selected peripheral for autoconnect
|
|
if let autoconnectPeripheral = selectedPeripheral {
|
|
Settings.autoconnectPeripheral = (autoconnectPeripheral.identifier, autoconnectPeripheral.advertisement.advertisementData)
|
|
}
|
|
|
|
// Go to home screen
|
|
ScreenFlowManager.gotoCPBModules()
|
|
}
|
|
|
|
// MARK: - UI
|
|
private func refreshPeripherals() {
|
|
bleManager.refreshPeripherals()
|
|
reloadBaseTable()
|
|
}
|
|
|
|
private func updateScannedPeripherals() {
|
|
|
|
// Reload table
|
|
if isBaseTableScrolling {
|
|
isScannerTableWaitingForReload = true
|
|
} else {
|
|
reloadBaseTable()
|
|
}
|
|
}
|
|
|
|
private func reloadBaseTable() {
|
|
isBaseTableScrolling = false
|
|
isScannerTableWaitingForReload = false
|
|
let filteredPeripherals = peripheralList.filteredPeripherals(forceUpdate: true) // Refresh the peripherals
|
|
|
|
baseTableView.reloadData()
|
|
|
|
// Select the previously selected row
|
|
if let selectedPeripheral = selectedPeripheral, let selectedRow = filteredPeripherals.firstIndex(of: selectedPeripheral) {
|
|
baseTableView.selectRow(at: IndexPath(row: selectedRow + 1, section: 0), animated: false, scrollPosition: .none)
|
|
}
|
|
|
|
//
|
|
updateDetailsCellOpacity()
|
|
}
|
|
|
|
private func presentInfoDialog(title: String, peripheral: BlePeripheral) {
|
|
if infoAlertController != nil {
|
|
infoAlertController?.dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
infoAlertController = UIAlertController(title: nil, message: title, preferredStyle: .alert)
|
|
infoAlertController!.addAction(UIAlertAction(title: LocalizationManager.shared.localizedString("dialog_cancel"), style: .cancel, handler: { [weak self] _ in
|
|
self?.bleManager.disconnect(from: peripheral)
|
|
}))
|
|
present(infoAlertController!, animated: true, completion: nil)
|
|
}
|
|
|
|
private func dismissInfoDialog(completion: (() -> Void)? = nil) {
|
|
guard infoAlertController != nil else {
|
|
completion?()
|
|
return
|
|
}
|
|
|
|
infoAlertController?.dismiss(animated: true, completion: completion)
|
|
infoAlertController = nil
|
|
}
|
|
|
|
private var wasWaitVisible = false
|
|
private func updateWaitView() {
|
|
let numPeripherals = peripheralList.filteredPeripherals(forceUpdate: false).count
|
|
let isWaitVisible = numPeripherals == 0
|
|
if wasWaitVisible != isWaitVisible {
|
|
self.wasWaitVisible = isWaitVisible
|
|
waitView.layer.removeAllAnimations()
|
|
if isWaitVisible {
|
|
UIView.animate(withDuration: 0.2, delay: ScannerViewController.kDelayToShowWait, options: [], animations: {
|
|
self.waitView.alpha = 1
|
|
}, completion: nil)
|
|
} else {
|
|
self.waitView.alpha = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateDetailsCellOpacity() {
|
|
guard let detailsCell = baseTableView.visibleCells.first(where: { $0 is TitleTableViewCell }) else { return }
|
|
|
|
guard let customNavigationBar = navigationController?.navigationBar as? NavigationBarWithScrollAwareRightButton else { return }
|
|
|
|
let titleTableViewCell = detailsCell as! TitleTableViewCell
|
|
titleTableViewCell.alpha = max(0, 2 - customNavigationBar.navigationBarScrollViewProgress)
|
|
}
|
|
}
|
|
|
|
// MARK: - UITableViewDataSource
|
|
extension ScannerViewController: UITableViewDataSource {
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
// Update wait prompt (wait 1 second to make it visible)
|
|
updateWaitView()
|
|
|
|
// Calculate num cells
|
|
// (1 detail cell + num peripherals) if at least 1 peripheral is found
|
|
let numPeripherals = peripheralList.filteredPeripherals(forceUpdate: false).count
|
|
return numPeripherals > 0 ? 1 + numPeripherals : 0
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let isDetails = indexPath.row == 0
|
|
|
|
let reuseIdentifier = isDetails ? "DetailsCell" : "PeripheralCell"
|
|
return tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
|
|
}
|
|
}
|
|
|
|
// MARK: UITableViewDelegate
|
|
extension ScannerViewController: UITableViewDelegate {
|
|
|
|
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
|
|
let localizationManager = LocalizationManager.shared
|
|
let isDetails = indexPath.row == 0
|
|
|
|
if isDetails {
|
|
let detailsCell = cell as! TitleTableViewCell
|
|
|
|
detailsCell.titleLabel.text = localizationManager.localizedString("scanner_subtitle")
|
|
} else { // Is a peripheral
|
|
let peripheralCell = cell as! CommonTableViewCell
|
|
let peripheralIndex = indexPath.row - 1
|
|
let peripheral = peripheralList.filteredPeripherals(forceUpdate: false)[peripheralIndex]
|
|
|
|
// Board Image
|
|
var boardImage: UIImage?
|
|
if let adafruitData = peripheral.adafruitManufacturerData(), let boardModel = adafruitData.boardModel {
|
|
switch boardModel {
|
|
case .circuitPlaygroundBluefruit: boardImage = UIImage(named: "scan_cpb")
|
|
case .clue_nRF52840: boardImage = UIImage(named: "scan_clue")
|
|
default: break
|
|
}
|
|
}
|
|
|
|
// Fill peripheral data
|
|
let name = peripheral.name ?? localizationManager.localizedString("scanner_unnamed")
|
|
peripheralCell.titleLabel.text = ScannerViewController.kShowRssiValue ? "\(peripheral.rssi ?? -127)dBm \(name)" : name
|
|
peripheralCell.iconImageView.image = RssiUI.signalImage(for: peripheral.rssi)
|
|
peripheralCell.rightIconImageView?.image = boardImage
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
let isDetails = indexPath.row == 0
|
|
guard !isDetails else { return }
|
|
|
|
let peripheralIndex = indexPath.row - 1
|
|
let peripheral = peripheralList.filteredPeripherals(forceUpdate: false)[peripheralIndex]
|
|
|
|
connect(peripheral: peripheral)
|
|
}
|
|
}
|
|
|
|
// MARK: UIScrollViewDelegate
|
|
extension ScannerViewController {
|
|
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
isBaseTableScrolling = true
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
isBaseTableScrolling = false
|
|
|
|
if isScannerTableWaitingForReload {
|
|
reloadBaseTable()
|
|
}
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
|
// NavigationBar Button Custom Animation
|
|
if let customNavigationBar = navigationController?.navigationBar as? NavigationBarWithScrollAwareRightButton {
|
|
customNavigationBar.updateRightButtonPosition()
|
|
}
|
|
|
|
// Move Refresh control when a large title is used
|
|
if let height = navigationController?.navigationBar.frame.height {
|
|
refreshControl.bounds = CGRect(x: refreshControl.bounds.origin.x, y: NavigationBarWithScrollAwareRightButton.navBarHeightLargeState - height, width: refreshControl.bounds.size.width, height: refreshControl.bounds.size.height)
|
|
}
|
|
|
|
// Hide details opacity when showing the refresh control
|
|
updateDetailsCellOpacity()
|
|
}
|
|
}
|