MySentryPumpStatusMessageBody.swift 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. //
  2. // MySentryPumpStatusMessageBody.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 9/5/15.
  6. // Copyright © 2015 Nathan Racklyeft. All rights reserved.
  7. //
  8. import Foundation
  9. public enum GlucoseTrend {
  10. case flat
  11. case up
  12. case upUp
  13. case down
  14. case downDown
  15. init?(byte: UInt8) {
  16. switch byte & 0b1110 {
  17. case 0b0000:
  18. self = .flat
  19. case 0b0010:
  20. self = .up
  21. case 0b0100:
  22. self = .upUp
  23. case 0b0110:
  24. self = .down
  25. case 0b1000:
  26. self = .downDown
  27. default:
  28. return nil
  29. }
  30. }
  31. }
  32. public enum SensorReading {
  33. case off
  34. case missing
  35. case meterBGNow
  36. case weakSignal
  37. case calError
  38. case warmup
  39. case ended
  40. case highBG // Above 400 mg/dL
  41. case lost
  42. case unknown
  43. case active(glucose: Int)
  44. init(glucose: Int) {
  45. switch glucose {
  46. case 0:
  47. self = .off
  48. case 1:
  49. self = .missing
  50. case 2:
  51. self = .meterBGNow
  52. case 4:
  53. self = .weakSignal
  54. case 6:
  55. self = .calError
  56. case 8:
  57. self = .warmup
  58. case 10:
  59. self = .ended
  60. case 14:
  61. self = .highBG
  62. case 20:
  63. self = .lost
  64. case 0...20:
  65. self = .unknown
  66. default:
  67. self = .active(glucose: glucose)
  68. }
  69. }
  70. }
  71. public enum ClockType {
  72. case twentyFourHour
  73. case twelveHour
  74. }
  75. /**
  76. Describes a status message sent periodically from the pump to any paired MySentry devices
  77. See: [MinimedRF Class](https://github.com/ps2/minimed_rf/blob/master/lib/minimed_rf/messages/pump_status.rb)
  78. ```
  79. -- ------ -- 00 01 020304050607 08 09 10 11 1213 14 15 16 17 18 19 20 21 2223 24 25 26 27 282930313233 3435 --
  80. se tr pump date 01 bh ph resv bt st sr nxcal iob bl sens date 0000
  81. a2 594040 04 c9 51 092c1e0f0904 01 32 33 00 037a 02 02 05 b0 18 30 13 2b 00d1 00 00 00 70 092b000f0904 0000 33
  82. a2 594040 04 fb 51 1205000f0906 01 05 05 02 0000 04 00 00 00 ff 00 ff ff 0040 00 00 00 71 1205000f0906 0000 2b
  83. a2 594040 04 ff 50 1219000f0906 01 00 00 00 0000 04 00 00 00 00 00 00 00 005e 00 00 00 72 000000000000 0000 8b
  84. a2 594040 04 01 50 1223000f0906 01 00 00 00 0000 04 00 00 00 00 00 00 00 0059 00 00 00 72 000000000000 0000 9f
  85. a2 594040 04 2f 51 1727070f0905 01 84 85 00 00cd 01 01 05 b0 3e 0a 0a 1a 009d 03 00 00 71 1726000f0905 0000 d0
  86. a2 594040 04 9c 51 0003310f0905 01 39 37 00 025b 01 01 06 8d 26 22 08 15 0034 00 00 00 70 0003000f0905 0000 67
  87. a2 594040 04 87 51 0f18150f0907 01 03 71 00 045e 04 02 07 2c 04 44 ff ff 005e 02 00 00 73 0f16000f0907 0000 35
  88. ```
  89. */
  90. public struct MySentryPumpStatusMessageBody: MessageBody, DictionaryRepresentable {
  91. private static let reservoirMultiplier: Double = 10
  92. private static let iobMultiplier: Double = 40
  93. public static let length = 36
  94. public let sequence: UInt8
  95. public let pumpDateComponents: DateComponents
  96. public let batteryRemainingPercent: Int
  97. public let iob: Double
  98. public let reservoirRemainingUnits: Double
  99. public let reservoirRemainingPercent: Int
  100. public let reservoirRemainingMinutes: Int
  101. public let glucoseTrend: GlucoseTrend
  102. public let glucoseDateComponents: DateComponents?
  103. public let glucose: SensorReading
  104. public let previousGlucose: SensorReading
  105. public let sensorAgeHours: Int
  106. public let sensorRemainingHours: Int
  107. public let clockType: ClockType
  108. public let nextSensorCalibrationDateComponents: DateComponents?
  109. private let rxData: Data
  110. public init?(rxData: Data) {
  111. guard rxData.count == type(of: self).length, let trend = GlucoseTrend(byte: rxData[1]) else {
  112. return nil
  113. }
  114. self.rxData = rxData
  115. sequence = rxData[0]
  116. let pumpDateComponents = DateComponents(mySentryBytes: rxData.subdata(in: 2..<8))
  117. let hourByte: UInt8 = rxData[2]
  118. clockType = ((hourByte & 0b10000000) > 0) ? .twentyFourHour : .twelveHour
  119. guard let calendar = pumpDateComponents.calendar, pumpDateComponents.isValidDate(in: calendar) else {
  120. return nil
  121. }
  122. self.pumpDateComponents = pumpDateComponents
  123. self.glucoseTrend = trend
  124. reservoirRemainingUnits = Double(Int(bigEndianBytes: rxData.subdata(in: 12..<14))) / type(of: self).reservoirMultiplier
  125. let reservoirRemainingPercent: UInt8 = rxData[15]
  126. self.reservoirRemainingPercent = Int(round(Double(reservoirRemainingPercent) / 4.0 * 100))
  127. reservoirRemainingMinutes = Int(bigEndianBytes: rxData.subdata(in: 16..<18))
  128. iob = Double(Int(bigEndianBytes: rxData.subdata(in: 22..<24))) / type(of: self).iobMultiplier
  129. let batteryRemainingPercent: UInt8 = rxData[14]
  130. self.batteryRemainingPercent = Int(round(Double(batteryRemainingPercent) / 4.0 * 100))
  131. let glucoseValue = Int(bigEndianBytes: Data([rxData[9], rxData[24] << 7])) >> 7
  132. let previousGlucoseValue = Int(bigEndianBytes: Data([rxData[10], rxData[24] << 6])) >> 7
  133. glucose = SensorReading(glucose: glucoseValue)
  134. previousGlucose = SensorReading(glucose: previousGlucoseValue)
  135. switch glucose {
  136. case .off:
  137. glucoseDateComponents = nil
  138. default:
  139. let glucoseDateComponents = DateComponents(mySentryBytes: rxData.subdata(in: 28..<34))
  140. if glucoseDateComponents.isValidDate(in: calendar) {
  141. self.glucoseDateComponents = glucoseDateComponents
  142. } else {
  143. self.glucoseDateComponents = nil
  144. }
  145. }
  146. let sensorAgeHours: UInt8 = rxData[18]
  147. self.sensorAgeHours = Int(sensorAgeHours)
  148. let sensorRemainingHours: UInt8 = rxData[19]
  149. self.sensorRemainingHours = Int(sensorRemainingHours)
  150. let matchingHour: UInt8 = rxData[20]
  151. var nextSensorCalibrationDateComponents = DateComponents()
  152. nextSensorCalibrationDateComponents.hour = Int(matchingHour)
  153. nextSensorCalibrationDateComponents.minute = Int(rxData[21])
  154. nextSensorCalibrationDateComponents.calendar = calendar
  155. self.nextSensorCalibrationDateComponents = nextSensorCalibrationDateComponents
  156. }
  157. public var dictionaryRepresentation: [String: Any] {
  158. let dateComponentsString = { (components: DateComponents) -> String in
  159. String(
  160. format: "%04d-%02d-%02dT%02d:%02d:%02d",
  161. components.year!,
  162. components.month!,
  163. components.day!,
  164. components.hour!,
  165. components.minute!,
  166. components.second!
  167. )
  168. }
  169. var dict: [String: Any] = [
  170. "glucoseTrend": String(describing: glucoseTrend),
  171. "pumpDate": dateComponentsString(pumpDateComponents),
  172. "reservoirRemaining": reservoirRemainingUnits,
  173. "reservoirRemainingPercent": reservoirRemainingPercent,
  174. "reservoirRemainingMinutes": reservoirRemainingMinutes,
  175. "iob": iob
  176. ]
  177. switch glucose {
  178. case .active(glucose: let glucose):
  179. dict["glucose"] = glucose
  180. default:
  181. break
  182. }
  183. if let glucoseDateComponents = glucoseDateComponents {
  184. dict["glucoseDate"] = dateComponentsString(glucoseDateComponents)
  185. }
  186. dict["sensorStatus"] = String(describing: glucose)
  187. switch previousGlucose {
  188. case .active(glucose: let glucose):
  189. dict["lastGlucose"] = glucose
  190. default:
  191. break
  192. }
  193. dict["lastSensorStatus"] = String(describing: previousGlucose)
  194. dict["sensorAgeHours"] = sensorAgeHours
  195. dict["sensorRemainingHours"] = sensorRemainingHours
  196. if let components = nextSensorCalibrationDateComponents {
  197. dict["nextSensorCalibration"] = String(format: "%02d:%02d", components.hour!, components.minute!)
  198. }
  199. dict["batteryRemainingPercent"] = batteryRemainingPercent
  200. dict["byte1"] = rxData.subdata(in: 1..<2).hexadecimalString
  201. // {50}
  202. let byte1: UInt8 = rxData[1]
  203. dict["byte1High"] = String(format: "%02x", byte1 & 0b11110000)
  204. // {1}
  205. dict["byte1Low"] = Int(byte1 & 0b00000001)
  206. // Observed values: 00, 01, 02, 03
  207. // These seem to correspond with carb/bolus activity
  208. dict["byte11"] = rxData.subdata(in: 11..<12).hexadecimalString
  209. // Current alarms?
  210. // 25: {00,52,65} 4:49 AM - 4:59 AM
  211. // 26: 00
  212. dict["byte2526"] = rxData.subdata(in: 25..<27).hexadecimalString
  213. // 27: {73}
  214. dict["byte27"] = rxData.subdata(in: 27..<28).hexadecimalString
  215. return dict
  216. }
  217. public var txData: Data {
  218. return rxData
  219. }
  220. }
  221. extension MySentryPumpStatusMessageBody: Equatable {
  222. }
  223. public func ==(lhs: MySentryPumpStatusMessageBody, rhs: MySentryPumpStatusMessageBody) -> Bool {
  224. return lhs.pumpDateComponents == rhs.pumpDateComponents && lhs.glucoseDateComponents == rhs.glucoseDateComponents
  225. }