BasalDeliveryTable.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. //
  2. // BasalDeliveryTable.swift
  3. // OmniKit
  4. //
  5. // Created by Pete Schwamb on 4/4/18.
  6. // Copyright © 2018 Pete Schwamb. All rights reserved.
  7. //
  8. import Foundation
  9. // Max time between pulses for scheduled basal and temp basal extra timing command
  10. let maxTimeBetweenPulses = TimeInterval(hours: 5)
  11. // Near zero basal rate used for non-Eros pods for zero scheduled basal rates and temp basals
  12. let nearZeroBasalRate = 0.01
  13. // Special flag used for non-Eros pods for near zero basal rates pulse timing for $13 & $16 extra commands
  14. let nearZeroBasalRateFlag: UInt32 = 0x80000000
  15. public struct BasalDeliveryTable {
  16. static let segmentDuration: TimeInterval = .minutes(30)
  17. let entries: [InsulinTableEntry]
  18. public init(entries: [InsulinTableEntry]) {
  19. self.entries = entries
  20. }
  21. public init(schedule: BasalSchedule) {
  22. struct TempSegment {
  23. let pulses: Int
  24. }
  25. let numSegments = 48
  26. let maxSegmentsPerEntry = 16
  27. var halfPulseRemainder = false
  28. let expandedSegments = stride(from: 0, to: numSegments, by: 1).map { (index) -> TempSegment in
  29. let rate = schedule.rateAt(offset: Double(index) * .minutes(30))
  30. let pulsesPerHour = Int(round(rate / Pod.pulseSize))
  31. let pulsesPerSegment = pulsesPerHour >> 1
  32. let halfPulse = pulsesPerHour & 0b1 != 0
  33. let segment = TempSegment(pulses: pulsesPerSegment + ((halfPulseRemainder && halfPulse) ? 1 : 0))
  34. halfPulseRemainder = halfPulseRemainder != halfPulse
  35. return segment
  36. }
  37. var tableEntries = [InsulinTableEntry]()
  38. let addEntry = { (segments: [TempSegment], alternateSegmentPulse: Bool) in
  39. tableEntries.append(InsulinTableEntry(
  40. segments: segments.count,
  41. pulses: segments.first!.pulses,
  42. alternateSegmentPulse: alternateSegmentPulse
  43. ))
  44. }
  45. var altSegmentPulse = false
  46. var segmentsToMerge = [TempSegment]()
  47. for segment in expandedSegments {
  48. guard let firstSegment = segmentsToMerge.first else {
  49. segmentsToMerge.append(segment)
  50. continue
  51. }
  52. let delta = segment.pulses - firstSegment.pulses
  53. if segmentsToMerge.count == 1 {
  54. altSegmentPulse = delta == 1
  55. }
  56. let expectedDelta: Int
  57. if !altSegmentPulse {
  58. expectedDelta = 0
  59. } else {
  60. expectedDelta = segmentsToMerge.count % 2
  61. }
  62. if expectedDelta != delta || segmentsToMerge.count == maxSegmentsPerEntry {
  63. addEntry(segmentsToMerge, altSegmentPulse)
  64. segmentsToMerge.removeAll()
  65. }
  66. segmentsToMerge.append(segment)
  67. }
  68. addEntry(segmentsToMerge, altSegmentPulse)
  69. self.entries = tableEntries
  70. }
  71. public init(tempBasalRate: Double, duration: TimeInterval) {
  72. self.entries = BasalDeliveryTable.rateToTableEntries(rate: tempBasalRate, duration: duration)
  73. }
  74. private static func rateToTableEntries(rate: Double, duration: TimeInterval) -> [InsulinTableEntry] {
  75. var tableEntries = [InsulinTableEntry]()
  76. let pulsesPerHour = Int(round(rate / Pod.pulseSize))
  77. let pulsesPerSegment = pulsesPerHour >> 1
  78. let alternateSegmentPulse = pulsesPerHour & 0b1 != 0
  79. var remaining = Int(round(duration / BasalDeliveryTable.segmentDuration))
  80. while remaining > 0 {
  81. let segments = min(remaining, 16)
  82. let tableEntry = InsulinTableEntry(segments: segments, pulses: Int(pulsesPerSegment), alternateSegmentPulse: segments > 1 ? alternateSegmentPulse : false)
  83. tableEntries.append(tableEntry)
  84. remaining -= segments
  85. }
  86. return tableEntries
  87. }
  88. public func numSegments() -> Int {
  89. return entries.reduce(0) { $0 + $1.segments }
  90. }
  91. }
  92. extension BasalDeliveryTable: CustomDebugStringConvertible {
  93. public var debugDescription: String {
  94. return "BasalDeliveryTable(\(entries))"
  95. }
  96. }
  97. // Round basal rate by rounding down to pulse size boundary,
  98. // but basal rates within a small delta will be rounded up.
  99. // Rounds down to 0 for both non-Eros and Eros (temp basals).
  100. func roundToSupportedBasalRate(rate: Double) -> Double
  101. {
  102. let delta = 0.01
  103. let supportedBasalRates: [Double] = (0...600).map { Double($0) / Double(Pod.pulsesPerUnit) }
  104. return supportedBasalRates.last(where: { $0 <= rate + delta }) ?? 0
  105. }
  106. // Return rounded basal rate for pulse timing purposes.
  107. // For non-Eros, returns nearZeroBasalRate (0.01) for a zero basal rate.
  108. func roundToSupportedBasalTimingRate(rate: Double) -> Double {
  109. var rrate = roundToSupportedBasalRate(rate: rate)
  110. if rrate == 0.0 {
  111. rrate = Pod.zeroBasalRate // will be an adjusted value for non-Eros cases
  112. }
  113. return rrate
  114. }
  115. public struct RateEntry {
  116. let totalPulses: Double
  117. let delayBetweenPulses: TimeInterval
  118. public init(totalPulses: Double, delayBetweenPulses: TimeInterval) {
  119. self.totalPulses = totalPulses
  120. self.delayBetweenPulses = delayBetweenPulses
  121. }
  122. public var rate: Double {
  123. if totalPulses == 0 {
  124. // Eros zero TB is the only case not using pulses
  125. return 0
  126. } else {
  127. // Use delayBetweenPulses to compute rate which will also work for non-Eros near zero rates.
  128. // Round the rate calculation to a two digit value to avoid slightly off values for some cases.
  129. return round(((.hours(1) / delayBetweenPulses) / Pod.pulsesPerUnit) * 100) / 100.0
  130. }
  131. }
  132. public var duration: TimeInterval {
  133. if totalPulses == 0 {
  134. // Eros zero TB case uses fixed 30 minute rate entries
  135. return TimeInterval(minutes: 30)
  136. } else {
  137. // Use delayBetweenPulses to compute duration which will also work for non-Eros near zero rates.
  138. // Round to nearest second to not be slightly off of the 30 minute rate entry boundary for some cases.
  139. return round(delayBetweenPulses * totalPulses)
  140. }
  141. }
  142. public var data: Data {
  143. var delayBetweenPulsesInHundredthsOfMillisecondsWithFlag = UInt32(delayBetweenPulses.hundredthsOfMilliseconds)
  144. // non-Eros near zero basal rates use the nearZeroBasalRateFlag
  145. if delayBetweenPulses == maxTimeBetweenPulses && totalPulses != 0 {
  146. delayBetweenPulsesInHundredthsOfMillisecondsWithFlag |= nearZeroBasalRateFlag
  147. }
  148. var data = Data()
  149. data.appendBigEndian(UInt16(round(totalPulses * 10)))
  150. data.appendBigEndian(delayBetweenPulsesInHundredthsOfMillisecondsWithFlag)
  151. return data
  152. }
  153. public static func makeEntries(rate: Double, duration: TimeInterval) -> [RateEntry] {
  154. let maxPulsesPerEntry: Double = 0xffff / 10 // max # of 1/10th pulses encoded in a 2-byte value
  155. var entries = [RateEntry]()
  156. let rrate = roundToSupportedBasalTimingRate(rate: rate)
  157. var remainingSegments = Int(round(duration.minutes / 30))
  158. let pulsesPerSegment = round(rrate / Pod.pulseSize) / 2
  159. let maxSegmentsPerEntry = pulsesPerSegment > 0 ? Int(maxPulsesPerEntry / pulsesPerSegment) : 1
  160. var remainingPulses = rrate * duration.hours / Pod.pulseSize
  161. while (remainingSegments > 0) {
  162. let entry: RateEntry
  163. if rrate == 0 {
  164. // Eros zero TBR only, one rate entry per segment with no pulses
  165. entry = RateEntry(totalPulses: 0, delayBetweenPulses: maxTimeBetweenPulses)
  166. remainingSegments -= 1 // one rate entry per half hour
  167. } else if rrate == nearZeroBasalRate {
  168. // Non-Eros near zero value temp or scheduled basal, one entry with 1/10 pulse per 1/2 hour of duration
  169. entry = RateEntry(totalPulses: Double(remainingSegments) / 10, delayBetweenPulses: maxTimeBetweenPulses)
  170. remainingSegments = 0 // just a single entry
  171. } else {
  172. let numSegments = min(maxSegmentsPerEntry, Int(round(remainingPulses / pulsesPerSegment)))
  173. remainingSegments -= numSegments
  174. let pulseCount = pulsesPerSegment * Double(numSegments)
  175. let delayBetweenPulses = .hours(1) / rrate * Pod.pulseSize
  176. entry = RateEntry(totalPulses: pulseCount, delayBetweenPulses: delayBetweenPulses)
  177. remainingPulses -= pulseCount
  178. }
  179. entries.append(entry)
  180. }
  181. return entries
  182. }
  183. }
  184. extension RateEntry: CustomDebugStringConvertible {
  185. public var debugDescription: String {
  186. return "RateEntry(rate:\(rate), duration:\(duration.timeIntervalStr))"
  187. }
  188. }