PairPodSetupViewController.swift 9.0 KB

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