RileyLinkMinimedDeviceTableViewController.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. //
  2. // RileyLinkMinimedDeviceTableViewController.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 3/5/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import UIKit
  9. import CoreBluetooth
  10. import LoopKitUI
  11. import MinimedKit
  12. import RileyLinkBLEKit
  13. import RileyLinkKit
  14. import RileyLinkKitUI
  15. let CellIdentifier = "Cell"
  16. public class RileyLinkMinimedDeviceTableViewController: UITableViewController {
  17. public let device: RileyLinkDevice
  18. private let ops: PumpOps
  19. private var pumpState: PumpState? {
  20. didSet {
  21. // Update the UI if its visible
  22. guard rssiFetchTimer != nil else { return }
  23. if let cell = cellForRow(.awake) {
  24. cell.setAwakeUntil(pumpState?.awakeUntil, formatter: dateFormatter)
  25. }
  26. if let cell = cellForRow(.model) {
  27. cell.setPumpModel(pumpState?.pumpModel)
  28. }
  29. if let cell = cellForRow(.tune) {
  30. cell.setTuneInfo(lastValidFrequency: pumpState?.lastValidFrequency, lastTuned: pumpState?.lastTuned, measurementFormatter: measurementFormatter, dateFormatter: dateFormatter)
  31. }
  32. }
  33. }
  34. private var bleRSSI: Int?
  35. private var firmwareVersion: String? {
  36. didSet {
  37. guard isViewLoaded else {
  38. return
  39. }
  40. cellForRow(.version)?.detailTextLabel?.text = firmwareVersion
  41. }
  42. }
  43. private var uptime: TimeInterval? {
  44. didSet {
  45. guard isViewLoaded else {
  46. return
  47. }
  48. cellForRow(.uptime)?.setDetailAge(uptime)
  49. }
  50. }
  51. private var lastIdle: Date? {
  52. didSet {
  53. guard isViewLoaded else {
  54. return
  55. }
  56. cellForRow(.idleStatus)?.setDetailDate(lastIdle, formatter: dateFormatter)
  57. }
  58. }
  59. private var rssiFetchTimer: Timer? {
  60. willSet {
  61. rssiFetchTimer?.invalidate()
  62. }
  63. }
  64. private var appeared = false
  65. public init(device: RileyLinkDevice, pumpOps: PumpOps) {
  66. self.device = device
  67. self.ops = pumpOps
  68. self.pumpState = pumpOps.pumpState.value
  69. super.init(style: .grouped)
  70. updateDeviceStatus()
  71. }
  72. required public init?(coder aDecoder: NSCoder) {
  73. fatalError("init(coder:) has not been implemented")
  74. }
  75. public override func viewDidLoad() {
  76. super.viewDidLoad()
  77. title = device.name
  78. self.observe()
  79. }
  80. @objc func updateRSSI() {
  81. device.readRSSI()
  82. }
  83. func updateUptime() {
  84. device.runSession(withName: "Get stats for uptime") { (session) in
  85. do {
  86. let statistics = try session.getRileyLinkStatistics()
  87. DispatchQueue.main.async {
  88. self.uptime = statistics.uptime
  89. }
  90. } catch { }
  91. }
  92. }
  93. private func updateDeviceStatus() {
  94. device.getStatus { (status) in
  95. DispatchQueue.main.async {
  96. self.lastIdle = status.lastIdle
  97. self.firmwareVersion = status.version
  98. }
  99. }
  100. }
  101. // References to registered notification center observers
  102. private var notificationObservers: [Any] = []
  103. deinit {
  104. for observer in notificationObservers {
  105. NotificationCenter.default.removeObserver(observer)
  106. }
  107. }
  108. private func observe() {
  109. let center = NotificationCenter.default
  110. let mainQueue = OperationQueue.main
  111. notificationObservers = [
  112. center.addObserver(forName: .DeviceNameDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in
  113. if let cell = self?.cellForRow(.customName) {
  114. cell.detailTextLabel?.text = self?.device.name
  115. }
  116. self?.title = self?.device.name
  117. },
  118. center.addObserver(forName: .DeviceConnectionStateDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in
  119. if let cell = self?.cellForRow(.connection) {
  120. cell.detailTextLabel?.text = self?.device.peripheralState.description
  121. }
  122. },
  123. center.addObserver(forName: .DeviceRSSIDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in
  124. self?.bleRSSI = note.userInfo?[RileyLinkDevice.notificationRSSIKey] as? Int
  125. if let cell = self?.cellForRow(.rssi), let formatter = self?.integerFormatter {
  126. cell.setDetailRSSI(self?.bleRSSI, formatter: formatter)
  127. }
  128. },
  129. center.addObserver(forName: .DeviceDidStartIdle, object: device, queue: mainQueue) { [weak self] (note) in
  130. self?.updateDeviceStatus()
  131. },
  132. center.addObserver(forName: .PumpOpsStateDidChange, object: ops, queue: mainQueue) { [weak self] (note) in
  133. if let state = note.userInfo?[PumpOps.notificationPumpStateKey] as? PumpState {
  134. self?.pumpState = state
  135. }
  136. }
  137. ]
  138. }
  139. public override func viewWillAppear(_ animated: Bool) {
  140. super.viewWillAppear(animated)
  141. if appeared {
  142. tableView.reloadData()
  143. }
  144. rssiFetchTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(updateRSSI), userInfo: nil, repeats: true)
  145. appeared = true
  146. updateRSSI()
  147. updateUptime()
  148. }
  149. public override func viewWillDisappear(_ animated: Bool) {
  150. super.viewWillDisappear(animated)
  151. rssiFetchTimer = nil
  152. }
  153. // MARK: - Formatters
  154. private lazy var dateFormatter: DateFormatter = {
  155. let dateFormatter = DateFormatter()
  156. dateFormatter.dateStyle = .none
  157. dateFormatter.timeStyle = .medium
  158. return dateFormatter
  159. }()
  160. private lazy var integerFormatter = NumberFormatter()
  161. private lazy var measurementFormatter: MeasurementFormatter = {
  162. let formatter = MeasurementFormatter()
  163. formatter.numberFormatter = decimalFormatter
  164. return formatter
  165. }()
  166. private lazy var decimalFormatter: NumberFormatter = {
  167. let decimalFormatter = NumberFormatter()
  168. decimalFormatter.numberStyle = .decimal
  169. decimalFormatter.minimumSignificantDigits = 5
  170. return decimalFormatter
  171. }()
  172. // MARK: - Table view data source
  173. private enum Section: Int, CaseCountable {
  174. case device
  175. case pump
  176. case commands
  177. }
  178. private enum DeviceRow: Int, CaseCountable {
  179. case customName
  180. case version
  181. case rssi
  182. case connection
  183. case uptime
  184. case idleStatus
  185. }
  186. private enum PumpRow: Int, CaseCountable {
  187. case id
  188. case model
  189. case awake
  190. }
  191. private enum CommandRow: Int, CaseCountable {
  192. case tune
  193. case changeTime
  194. case mySentryPair
  195. case dumpHistory
  196. case fetchGlucose
  197. case getPumpModel
  198. case pressDownButton
  199. case readPumpStatus
  200. case readBasalSchedule
  201. case enableLED
  202. case discoverCommands
  203. case getStatistics
  204. }
  205. private func cellForRow(_ row: DeviceRow) -> UITableViewCell? {
  206. return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.device.rawValue))
  207. }
  208. private func cellForRow(_ row: PumpRow) -> UITableViewCell? {
  209. return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.pump.rawValue))
  210. }
  211. private func cellForRow(_ row: CommandRow) -> UITableViewCell? {
  212. return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.commands.rawValue))
  213. }
  214. public override func numberOfSections(in tableView: UITableView) -> Int {
  215. return Section.count
  216. }
  217. public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  218. switch Section(rawValue: section)! {
  219. case .device:
  220. return DeviceRow.count
  221. case .pump:
  222. return PumpRow.count
  223. case .commands:
  224. return CommandRow.count
  225. }
  226. }
  227. public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  228. let cell: UITableViewCell
  229. if let reusableCell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier) {
  230. cell = reusableCell
  231. } else {
  232. cell = UITableViewCell(style: .value1, reuseIdentifier: CellIdentifier)
  233. }
  234. cell.accessoryType = .none
  235. switch Section(rawValue: indexPath.section)! {
  236. case .device:
  237. switch DeviceRow(rawValue: indexPath.row)! {
  238. case .customName:
  239. cell.textLabel?.text = LocalizedString("Name", comment: "The title of the cell showing device name")
  240. cell.detailTextLabel?.text = device.name
  241. cell.accessoryType = .disclosureIndicator
  242. case .version:
  243. cell.textLabel?.text = LocalizedString("Firmware", comment: "The title of the cell showing firmware version")
  244. cell.detailTextLabel?.text = firmwareVersion
  245. case .connection:
  246. cell.textLabel?.text = LocalizedString("Connection State", comment: "The title of the cell showing BLE connection state")
  247. cell.detailTextLabel?.text = device.peripheralState.description
  248. case .rssi:
  249. cell.textLabel?.text = LocalizedString("Signal Strength", comment: "The title of the cell showing BLE signal strength (RSSI)")
  250. cell.setDetailRSSI(bleRSSI, formatter: integerFormatter)
  251. case .uptime:
  252. cell.textLabel?.text = LocalizedString("Uptime", comment: "The title of the cell showing uptime")
  253. cell.setDetailAge(uptime)
  254. case .idleStatus:
  255. cell.textLabel?.text = LocalizedString("On Idle", comment: "The title of the cell showing the last idle")
  256. cell.setDetailDate(lastIdle, formatter: dateFormatter)
  257. }
  258. case .pump:
  259. switch PumpRow(rawValue: indexPath.row)! {
  260. case .id:
  261. cell.textLabel?.text = LocalizedString("Pump ID", comment: "The title of the cell showing pump ID")
  262. cell.detailTextLabel?.text = ops.pumpSettings.pumpID
  263. case .model:
  264. cell.textLabel?.text = LocalizedString("Pump Model", comment: "The title of the cell showing the pump model number")
  265. cell.setPumpModel(pumpState?.pumpModel)
  266. case .awake:
  267. cell.setAwakeUntil(pumpState?.awakeUntil, formatter: dateFormatter)
  268. }
  269. case .commands:
  270. cell.accessoryType = .disclosureIndicator
  271. cell.detailTextLabel?.text = nil
  272. switch CommandRow(rawValue: indexPath.row)! {
  273. case .tune:
  274. cell.setTuneInfo(lastValidFrequency: pumpState?.lastValidFrequency, lastTuned: pumpState?.lastTuned, measurementFormatter: measurementFormatter, dateFormatter: dateFormatter)
  275. case .changeTime:
  276. cell.textLabel?.text = LocalizedString("Change Time", comment: "The title of the command to change pump time")
  277. let localTimeZone = TimeZone.current
  278. let localTimeZoneName = localTimeZone.abbreviation() ?? localTimeZone.identifier
  279. if let pumpTimeZone = pumpState?.timeZone {
  280. let timeZoneDiff = TimeInterval(pumpTimeZone.secondsFromGMT() - localTimeZone.secondsFromGMT())
  281. let formatter = DateComponentsFormatter()
  282. formatter.allowedUnits = [.hour, .minute]
  283. let diffString = timeZoneDiff != 0 ? formatter.string(from: abs(timeZoneDiff)) ?? String(abs(timeZoneDiff)) : ""
  284. cell.detailTextLabel?.text = String(format: LocalizedString("%1$@%2$@%3$@", comment: "The format string for displaying an offset from a time zone: (1: GMT)(2: -)(3: 4:00)"), localTimeZoneName, timeZoneDiff != 0 ? (timeZoneDiff < 0 ? "-" : "+") : "", diffString)
  285. } else {
  286. cell.detailTextLabel?.text = localTimeZoneName
  287. }
  288. case .mySentryPair:
  289. cell.textLabel?.text = LocalizedString("MySentry Pair", comment: "The title of the command to pair with mysentry")
  290. case .dumpHistory:
  291. cell.textLabel?.text = LocalizedString("Fetch Recent History", comment: "The title of the command to fetch recent history")
  292. case .fetchGlucose:
  293. cell.textLabel?.text = LocalizedString("Fetch Enlite Glucose", comment: "The title of the command to fetch recent glucose")
  294. case .getPumpModel:
  295. cell.textLabel?.text = LocalizedString("Get Pump Model", comment: "The title of the command to get pump model")
  296. case .pressDownButton:
  297. cell.textLabel?.text = LocalizedString("Send Button Press", comment: "The title of the command to send a button press")
  298. case .readPumpStatus:
  299. cell.textLabel?.text = LocalizedString("Read Pump Status", comment: "The title of the command to read pump status")
  300. case .readBasalSchedule:
  301. cell.textLabel?.text = LocalizedString("Read Basal Schedule", comment: "The title of the command to read basal schedule")
  302. case .enableLED:
  303. cell.textLabel?.text = LocalizedString("Enable Diagnostic LEDs", comment: "The title of the command to enable diagnostic LEDs")
  304. case .discoverCommands:
  305. cell.textLabel?.text = LocalizedString("Discover Commands", comment: "The title of the command to discover commands")
  306. case .getStatistics:
  307. cell.textLabel?.text = LocalizedString("RileyLink Statistics", comment: "The title of the command to fetch RileyLink statistics")
  308. }
  309. }
  310. return cell
  311. }
  312. public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  313. switch Section(rawValue: section)! {
  314. case .device:
  315. return LocalizedString("Device", comment: "The title of the section describing the device")
  316. case .pump:
  317. return LocalizedString("Pump", comment: "The title of the section describing the pump")
  318. case .commands:
  319. return LocalizedString("Commands", comment: "The title of the section describing commands")
  320. }
  321. }
  322. // MARK: - UITableViewDelegate
  323. public override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  324. switch Section(rawValue: indexPath.section)! {
  325. case .device:
  326. switch DeviceRow(rawValue: indexPath.row)! {
  327. case .customName:
  328. return true
  329. default:
  330. return false
  331. }
  332. case .pump:
  333. return false
  334. case .commands:
  335. return device.peripheralState == .connected
  336. }
  337. }
  338. public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  339. switch Section(rawValue: indexPath.section)! {
  340. case .device:
  341. switch DeviceRow(rawValue: indexPath.row)! {
  342. case .customName:
  343. let vc = TextFieldTableViewController()
  344. if let cell = tableView.cellForRow(at: indexPath) {
  345. vc.title = cell.textLabel?.text
  346. vc.value = device.name
  347. vc.delegate = self
  348. vc.keyboardType = .default
  349. }
  350. show(vc, sender: indexPath)
  351. default:
  352. break
  353. }
  354. case .commands:
  355. let vc: CommandResponseViewController
  356. switch CommandRow(rawValue: indexPath.row)! {
  357. case .tune:
  358. vc = .tuneRadio(ops: ops, device: device, measurementFormatter: measurementFormatter)
  359. case .changeTime:
  360. vc = .changeTime(ops: ops, device: device)
  361. case .mySentryPair:
  362. vc = .mySentryPair(ops: ops, device: device)
  363. case .dumpHistory:
  364. vc = .dumpHistory(ops: ops, device: device)
  365. case .fetchGlucose:
  366. vc = .fetchGlucose(ops: ops, device: device)
  367. case .getPumpModel:
  368. vc = .getPumpModel(ops: ops, device: device)
  369. case .pressDownButton:
  370. vc = .pressDownButton(ops: ops, device: device)
  371. case .readPumpStatus:
  372. vc = .readPumpStatus(ops: ops, device: device, measurementFormatter: measurementFormatter)
  373. case .readBasalSchedule:
  374. vc = .readBasalSchedule(ops: ops, device: device, integerFormatter: integerFormatter)
  375. case .enableLED:
  376. // vc = .enableLEDs(ops: ops, device: device)
  377. vc = .getStatistics(ops: ops, device: device)
  378. case .discoverCommands:
  379. vc = .discoverCommands(ops: ops, device: device)
  380. case .getStatistics:
  381. vc = .getStatistics(ops: ops, device: device)
  382. }
  383. if let cell = tableView.cellForRow(at: indexPath) {
  384. vc.title = cell.textLabel?.text
  385. }
  386. show(vc, sender: indexPath)
  387. case .pump:
  388. break
  389. }
  390. }
  391. }
  392. extension RileyLinkMinimedDeviceTableViewController: TextFieldTableViewControllerDelegate {
  393. public func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) {
  394. _ = navigationController?.popViewController(animated: true)
  395. }
  396. public func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) {
  397. if let indexPath = tableView.indexPathForSelectedRow {
  398. switch Section(rawValue: indexPath.section)! {
  399. case .device:
  400. switch DeviceRow(rawValue: indexPath.row)! {
  401. case .customName:
  402. device.setCustomName(controller.value!)
  403. default:
  404. break
  405. }
  406. default:
  407. break
  408. }
  409. }
  410. }
  411. }
  412. private extension TimeInterval {
  413. func format(using units: NSCalendar.Unit) -> String? {
  414. let formatter = DateComponentsFormatter()
  415. formatter.allowedUnits = units
  416. formatter.unitsStyle = .full
  417. formatter.zeroFormattingBehavior = .dropLeading
  418. formatter.maximumUnitCount = 2
  419. return formatter.string(from: self)
  420. }
  421. }
  422. private extension UITableViewCell {
  423. func setDetailDate(_ date: Date?, formatter: DateFormatter) {
  424. if let date = date {
  425. detailTextLabel?.text = formatter.string(from: date)
  426. } else {
  427. detailTextLabel?.text = "-"
  428. }
  429. }
  430. func setDetailRSSI(_ decibles: Int?, formatter: NumberFormatter) {
  431. detailTextLabel?.text = formatter.decibleString(from: decibles) ?? "-"
  432. }
  433. func setDetailAge(_ age: TimeInterval?) {
  434. if let age = age {
  435. detailTextLabel?.text = age.format(using: [.day, .hour, .minute])
  436. } else {
  437. detailTextLabel?.text = ""
  438. }
  439. }
  440. func setAwakeUntil(_ awakeUntil: Date?, formatter: DateFormatter) {
  441. switch awakeUntil {
  442. case let until? where until.timeIntervalSinceNow < 0:
  443. textLabel?.text = LocalizedString("Last Awake", comment: "The title of the cell describing an awake radio")
  444. setDetailDate(until, formatter: formatter)
  445. case let until?:
  446. textLabel?.text = LocalizedString("Awake Until", comment: "The title of the cell describing an awake radio")
  447. setDetailDate(until, formatter: formatter)
  448. default:
  449. textLabel?.text = LocalizedString("Listening Off", comment: "The title of the cell describing no radio awake data")
  450. detailTextLabel?.text = nil
  451. }
  452. }
  453. func setPumpModel(_ pumpModel: PumpModel?) {
  454. if let pumpModel = pumpModel {
  455. detailTextLabel?.text = String(describing: pumpModel)
  456. } else {
  457. detailTextLabel?.text = LocalizedString("Unknown", comment: "The detail text for an unknown pump model")
  458. }
  459. }
  460. func setTuneInfo(lastValidFrequency: Measurement<UnitFrequency>?, lastTuned: Date?, measurementFormatter: MeasurementFormatter, dateFormatter: DateFormatter) {
  461. if let frequency = lastValidFrequency, let date = lastTuned {
  462. textLabel?.text = measurementFormatter.string(from: frequency)
  463. setDetailDate(date, formatter: dateFormatter)
  464. } else {
  465. textLabel?.text = LocalizedString("Tune Radio Frequency", comment: "The title of the command to re-tune the radio")
  466. }
  467. }
  468. }