PairPodSetupViewController.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. //
  2. // PairPodSetupViewController.swift
  3. // OmniKitUI
  4. //
  5. // Created by Pete Schwamb on 9/18/18.
  6. // Copyright © 2018 Pete Schwamb. All rights reserved.
  7. //
  8. import UIKit
  9. import LoopKit
  10. import LoopKitUI
  11. import RileyLinkKit
  12. import OmniKit
  13. import os.log
  14. class PairPodSetupViewController: SetupTableViewController {
  15. var rileyLinkPumpManager: RileyLinkPumpManager!
  16. var previouslyEncounteredWeakComms: Bool = false
  17. var pumpManager: OmnipodPumpManager! {
  18. didSet {
  19. if oldValue == nil && pumpManager != nil {
  20. pumpManagerWasSet()
  21. }
  22. }
  23. }
  24. private let log = OSLog(category: "PairPodSetupViewController")
  25. // MARK: -
  26. @IBOutlet weak var activityIndicator: SetupIndicatorView!
  27. @IBOutlet weak var loadingLabel: UILabel!
  28. private var loadingText: String? {
  29. didSet {
  30. tableView.beginUpdates()
  31. loadingLabel.text = loadingText
  32. let isHidden = (loadingText == nil)
  33. loadingLabel.isHidden = isHidden
  34. tableView.endUpdates()
  35. }
  36. }
  37. override func viewDidLoad() {
  38. super.viewDidLoad()
  39. continueState = .initial
  40. }
  41. private func pumpManagerWasSet() {
  42. // Still priming?
  43. let primeFinishesAt = pumpManager.state.podState?.primeFinishTime
  44. let currentTime = Date()
  45. if let finishTime = primeFinishesAt, finishTime > currentTime {
  46. self.continueState = .pairing
  47. let delay = finishTime.timeIntervalSince(currentTime)
  48. DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
  49. self.continueState = .ready
  50. }
  51. }
  52. }
  53. // MARK: - UITableViewDelegate
  54. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  55. if case .pairing = continueState {
  56. return
  57. }
  58. tableView.deselectRow(at: indexPath, animated: true)
  59. }
  60. // MARK: - State
  61. private enum State {
  62. case initial
  63. case pairing
  64. case priming(finishTime: TimeInterval)
  65. case fault
  66. case ready
  67. }
  68. private var continueState: State = .initial {
  69. didSet {
  70. log.default("Changed continueState from %{public}@ to %{public}@", String(describing: oldValue), String(describing: continueState))
  71. switch continueState {
  72. case .initial:
  73. activityIndicator.state = .hidden
  74. footerView.primaryButton.isEnabled = true
  75. footerView.primaryButton.setPairTitle()
  76. case .pairing:
  77. activityIndicator.state = .indeterminantProgress
  78. footerView.primaryButton.isEnabled = false
  79. footerView.primaryButton.setPairTitle()
  80. lastError = nil
  81. loadingText = LocalizedString("Pairing…", comment: "The text of the loading label when pairing")
  82. case .priming(let finishTime):
  83. activityIndicator.state = .timedProgress(finishTime: CACurrentMediaTime() + finishTime)
  84. footerView.primaryButton.isEnabled = false
  85. footerView.primaryButton.setPairTitle()
  86. lastError = nil
  87. loadingText = LocalizedString("Priming…", comment: "The text of the loading label when priming")
  88. case .fault:
  89. activityIndicator.state = .hidden
  90. footerView.primaryButton.isEnabled = true
  91. footerView.primaryButton.setDeactivateTitle()
  92. case .ready:
  93. activityIndicator.state = .completed
  94. footerView.primaryButton.isEnabled = true
  95. footerView.primaryButton.resetTitle()
  96. lastError = nil
  97. loadingText = LocalizedString("Primed", comment: "The text of the loading label when pod is primed")
  98. }
  99. }
  100. }
  101. private var lastError: Error? {
  102. didSet {
  103. guard oldValue != nil || lastError != nil else {
  104. return
  105. }
  106. var errorStrings: [String]
  107. var errorText: String
  108. if let error = lastError as? LocalizedError {
  109. errorStrings = [error.errorDescription, error.failureReason, error.recoverySuggestion].compactMap { $0 }
  110. } else {
  111. errorStrings = [lastError?.localizedDescription].compactMap { $0 }
  112. }
  113. let podCommsError: PodCommsError?
  114. if let pumpManagerError = lastError as? PumpManagerError {
  115. switch pumpManagerError {
  116. // Check for a wrapped PodCommsError in the possible PumpManagerError types
  117. case .communication(let error), .configuration(let error), .connection(let error), .deviceState(let error):
  118. podCommsError = error as? PodCommsError
  119. default:
  120. podCommsError = nil
  121. break
  122. }
  123. } else {
  124. // Check for a non PumpManagerError PodCommsError
  125. podCommsError = lastError as? PodCommsError
  126. }
  127. if let podCommsError = podCommsError, podCommsError.possibleWeakCommsCause {
  128. if previouslyEncounteredWeakComms {
  129. errorStrings.append(LocalizedString("If the problem persists, move to a new area and try again", comment: "Additional pairing recovery suggestion on multiple pairing failures"))
  130. } else {
  131. previouslyEncounteredWeakComms = true
  132. }
  133. }
  134. errorText = errorStrings.joined(separator: ". ")
  135. if !errorText.isEmpty {
  136. errorText += "."
  137. } else if let error = lastError {
  138. // We have an error but no error text, generate a string to describe the error
  139. errorText = String(describing: error)
  140. }
  141. loadingText = errorText
  142. // If we have an error, update the continue state appropriately
  143. if let podCommsError = podCommsError {
  144. if podCommsError.isFaulted {
  145. continueState = .fault
  146. } else {
  147. continueState = .initial
  148. }
  149. } else if lastError != nil {
  150. continueState = .initial
  151. }
  152. }
  153. }
  154. // MARK: - Navigation
  155. private func navigateToReplacePod() {
  156. log.default("Navigating to ReplacePod screen")
  157. performSegue(withIdentifier: "ReplacePod", sender: nil)
  158. }
  159. override func continueButtonPressed(_ sender: Any) {
  160. switch continueState {
  161. case .initial:
  162. pair()
  163. case .ready:
  164. super.continueButtonPressed(sender)
  165. case .fault:
  166. navigateToReplacePod()
  167. default:
  168. break
  169. }
  170. }
  171. override func cancelButtonPressed(_ sender: Any) {
  172. let podState = pumpManager.state.podState
  173. if podState != nil {
  174. let confirmVC = UIAlertController(pumpDeletionHandler: {
  175. self.navigateToReplacePod()
  176. })
  177. self.present(confirmVC, animated: true) {}
  178. } else {
  179. super.cancelButtonPressed(sender)
  180. }
  181. }
  182. // MARK: -
  183. private func pair() {
  184. self.continueState = .pairing
  185. pumpManager.pairAndPrime() { (result) in
  186. DispatchQueue.main.async {
  187. switch result {
  188. case .success(let finishTime):
  189. self.log.default("Pairing succeeded, finishing in %{public}@ sec", String(describing: finishTime))
  190. if finishTime > 0 {
  191. self.continueState = .priming(finishTime: finishTime)
  192. DispatchQueue.main.asyncAfter(deadline: .now() + finishTime) {
  193. self.continueState = .ready
  194. }
  195. } else {
  196. self.continueState = .ready
  197. }
  198. case .failure(let error):
  199. self.log.default("Pairing failed with error: %{public}@", String(describing: error))
  200. self.lastError = error
  201. }
  202. }
  203. }
  204. }
  205. }
  206. private extension PodCommsError {
  207. var possibleWeakCommsCause: Bool {
  208. switch self {
  209. case .invalidData, .noResponse, .invalidAddress, .rssiTooLow, .rssiTooHigh, .unexpectedPacketType:
  210. return true
  211. default:
  212. return false
  213. }
  214. }
  215. }
  216. private extension SetupButton {
  217. func setPairTitle() {
  218. setTitle(LocalizedString("Pair", comment: "Button title to pair with pod during setup"), for: .normal)
  219. }
  220. func setDeactivateTitle() {
  221. setTitle(LocalizedString("Deactivate", comment: "Button title to deactivate pod because of fault during setup"), for: .normal)
  222. }
  223. }
  224. private extension UIAlertController {
  225. convenience init(pumpDeletionHandler handler: @escaping () -> Void) {
  226. self.init(
  227. title: nil,
  228. message: LocalizedString("Are you sure you want to shutdown this pod?", comment: "Confirmation message for shutting down a pod"),
  229. preferredStyle: .actionSheet
  230. )
  231. addAction(UIAlertAction(
  232. title: LocalizedString("Deactivate Pod", comment: "Button title to deactivate pod"),
  233. style: .destructive,
  234. handler: { (_) in
  235. handler()
  236. }
  237. ))
  238. let exit = LocalizedString("Continue", comment: "The title of the continue action in an action sheet")
  239. addAction(UIAlertAction(title: exit, style: .default, handler: nil))
  240. }
  241. }