PairPodSetupViewController.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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. var podCommsError: PodCommsError? = nil
  114. if let pumpManagerError = lastError as? PumpManagerError {
  115. switch pumpManagerError {
  116. case .communication(let error):
  117. podCommsError = error as? PodCommsError
  118. default:
  119. break
  120. }
  121. }
  122. if let podCommsError = podCommsError, podCommsError.possibleWeakCommsCause {
  123. if previouslyEncounteredWeakComms {
  124. errorStrings.append(LocalizedString("If the problem persists, move to a new area and try again", comment: "Additional pairing recovery suggestion on multiple pairing failures"))
  125. } else {
  126. previouslyEncounteredWeakComms = true
  127. }
  128. }
  129. errorText = errorStrings.joined(separator: ". ")
  130. if !errorText.isEmpty {
  131. errorText += "."
  132. } else if let error = lastError {
  133. // We have an error but no error text, generate a string to describe the error
  134. errorText = String(describing: error)
  135. }
  136. loadingText = errorText
  137. // If we have an error, update the continue state appropriately
  138. if let podCommsError = podCommsError {
  139. if podCommsError.isFaulted {
  140. continueState = .fault
  141. } else {
  142. continueState = .initial
  143. }
  144. } else if lastError != nil {
  145. continueState = .initial
  146. }
  147. }
  148. }
  149. // MARK: - Navigation
  150. private func navigateToReplacePod() {
  151. log.default("Navigating to ReplacePod screen")
  152. performSegue(withIdentifier: "ReplacePod", sender: nil)
  153. }
  154. override func continueButtonPressed(_ sender: Any) {
  155. switch continueState {
  156. case .initial:
  157. pair()
  158. case .ready:
  159. super.continueButtonPressed(sender)
  160. case .fault:
  161. navigateToReplacePod()
  162. default:
  163. break
  164. }
  165. }
  166. override func cancelButtonPressed(_ sender: Any) {
  167. let podState = pumpManager.state.podState
  168. if podState != nil {
  169. let confirmVC = UIAlertController(pumpDeletionHandler: {
  170. self.navigateToReplacePod()
  171. })
  172. self.present(confirmVC, animated: true) {}
  173. } else {
  174. super.cancelButtonPressed(sender)
  175. }
  176. }
  177. // MARK: -
  178. private func pair() {
  179. self.continueState = .pairing
  180. pumpManager.pairAndPrime() { (result) in
  181. DispatchQueue.main.async {
  182. switch result {
  183. case .success(let finishTime):
  184. self.log.default("Pairing succeeded, finishing in %{public}@ sec", String(describing: finishTime))
  185. if finishTime > 0 {
  186. self.continueState = .priming(finishTime: finishTime)
  187. DispatchQueue.main.asyncAfter(deadline: .now() + finishTime) {
  188. self.continueState = .ready
  189. }
  190. } else {
  191. self.continueState = .ready
  192. }
  193. case .failure(let error):
  194. self.log.default("Pairing failed with error: %{public}@", String(describing: error))
  195. self.lastError = error
  196. }
  197. }
  198. }
  199. }
  200. }
  201. private extension PodCommsError {
  202. var possibleWeakCommsCause: Bool {
  203. switch self {
  204. case .invalidData, .noResponse, .invalidAddress, .rssiTooLow, .rssiTooHigh, .unexpectedPacketType:
  205. return true
  206. default:
  207. return false
  208. }
  209. }
  210. }
  211. private extension SetupButton {
  212. func setPairTitle() {
  213. setTitle(LocalizedString("Pair", comment: "Button title to pair with pod during setup"), for: .normal)
  214. }
  215. func setDeactivateTitle() {
  216. setTitle(LocalizedString("Deactivate", comment: "Button title to deactivate pod because of fault during setup"), for: .normal)
  217. }
  218. }
  219. private extension UIAlertController {
  220. convenience init(pumpDeletionHandler handler: @escaping () -> Void) {
  221. self.init(
  222. title: nil,
  223. message: LocalizedString("Are you sure you want to shutdown this pod?", comment: "Confirmation message for shutting down a pod"),
  224. preferredStyle: .actionSheet
  225. )
  226. addAction(UIAlertAction(
  227. title: LocalizedString("Deactivate Pod", comment: "Button title to deactivate pod"),
  228. style: .destructive,
  229. handler: { (_) in
  230. handler()
  231. }
  232. ))
  233. let exit = LocalizedString("Continue", comment: "The title of the continue action in an action sheet")
  234. addAction(UIAlertAction(title: exit, style: .default, handler: nil))
  235. }
  236. }