OpenAPS.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import Combine
  2. import Foundation
  3. import JavaScriptCore
  4. final class OpenAPS {
  5. private let jsWorker = JavaScriptWorker()
  6. private let processQueue = DispatchQueue(label: "OpenAPS.processQueue", qos: .utility)
  7. private let storage: FileStorage
  8. init(storage: FileStorage) {
  9. self.storage = storage
  10. }
  11. func determineBasal(currentTemp: TempBasal, clock: Date = Date()) -> Future<Suggestion?, Never> {
  12. Future { promise in
  13. self.processQueue.async {
  14. debug(.openAPS, "Start determineBasal")
  15. // clock
  16. self.storage.save(clock, as: Monitor.clock)
  17. // temp_basal
  18. let tempBasal = currentTemp.rawJSON
  19. self.storage.save(tempBasal, as: Monitor.tempBasal)
  20. // meal
  21. let pumpHistory = self.loadFileFromStorage(name: OpenAPS.Monitor.pumpHistory)
  22. let carbs = self.loadFileFromStorage(name: Monitor.carbHistory)
  23. let glucose = self.loadFileFromStorage(name: Monitor.glucose)
  24. let profile = self.loadFileFromStorage(name: Settings.profile)
  25. let basalProfile = self.loadFileFromStorage(name: Settings.basalProfile)
  26. let meal = self.meal(
  27. pumphistory: pumpHistory,
  28. profile: profile,
  29. basalProfile: basalProfile,
  30. clock: clock,
  31. carbs: carbs,
  32. glucose: glucose
  33. )
  34. self.storage.save(meal, as: Monitor.meal)
  35. // iob
  36. let autosens = self.loadFileFromStorage(name: Settings.autosense)
  37. let iob = self.iob(
  38. pumphistory: pumpHistory,
  39. profile: profile,
  40. clock: clock,
  41. autosens: autosens.isEmpty ? .null : autosens
  42. )
  43. self.storage.save(iob, as: Monitor.iob)
  44. // determine-basal
  45. let reservoir = self.loadFileFromStorage(name: Monitor.reservoir)
  46. let suggested = self.determineBasal(
  47. glucose: glucose,
  48. currentTemp: tempBasal,
  49. iob: iob,
  50. profile: profile,
  51. autosens: autosens.isEmpty ? .null : autosens,
  52. meal: meal,
  53. microBolusAllowed: true,
  54. reservoir: reservoir
  55. )
  56. debug(.openAPS, "SUGGESTED: \(suggested)")
  57. if var suggestion = Suggestion(from: suggested) {
  58. suggestion.timestamp = suggestion.deliverAt ?? clock
  59. self.storage.save(suggestion, as: Enact.suggested)
  60. promise(.success(suggestion))
  61. } else {
  62. promise(.success(nil))
  63. }
  64. }
  65. }
  66. }
  67. func autosense() -> Future<Autosens?, Never> {
  68. Future { promise in
  69. self.processQueue.async {
  70. debug(.openAPS, "Start autosens")
  71. let pumpHistory = self.loadFileFromStorage(name: OpenAPS.Monitor.pumpHistory)
  72. let carbs = self.loadFileFromStorage(name: Monitor.carbHistory)
  73. let glucose = self.loadFileFromStorage(name: Monitor.glucose)
  74. let profile = self.loadFileFromStorage(name: Settings.profile)
  75. let basalProfile = self.loadFileFromStorage(name: Settings.basalProfile)
  76. let tempTargets = self.loadFileFromStorage(name: Settings.tempTargets)
  77. let autosensResult = self.autosense(
  78. glucose: glucose,
  79. pumpHistory: pumpHistory,
  80. basalprofile: basalProfile,
  81. profile: profile,
  82. carbs: carbs,
  83. temptargets: tempTargets
  84. )
  85. debug(.openAPS, "AUTOSENS: \(autosensResult)")
  86. if var autosens = Autosens(from: autosensResult) {
  87. autosens.timestamp = Date()
  88. self.storage.save(autosens, as: Settings.autosense)
  89. promise(.success(autosens))
  90. } else {
  91. promise(.success(nil))
  92. }
  93. }
  94. }
  95. }
  96. func autotune(categorizeUamAsBasal: Bool = false, tuneInsulinCurve: Bool = false) -> Future<Autotune?, Never> {
  97. Future { promise in
  98. self.processQueue.async {
  99. debug(.openAPS, "Start autotune")
  100. let pumpHistory = self.loadFileFromStorage(name: OpenAPS.Monitor.pumpHistory)
  101. let glucose = self.loadFileFromStorage(name: Monitor.glucose)
  102. let profile = self.loadFileFromStorage(name: Settings.profile)
  103. let pumpProfile = self.loadFileFromStorage(name: Settings.pumpProfile)
  104. let carbs = self.loadFileFromStorage(name: Monitor.carbHistory)
  105. let autotunePreppedGlucose = self.autotunePrepare(
  106. pumphistory: pumpHistory,
  107. profile: profile,
  108. glucose: glucose,
  109. pumpprofile: pumpProfile,
  110. carbs: carbs,
  111. categorizeUamAsBasal: categorizeUamAsBasal,
  112. tuneInsulinCurve: tuneInsulinCurve
  113. )
  114. debug(.openAPS, "AUTOTUNE PREP: \(autotunePreppedGlucose)")
  115. let previousAutotune = self.storage.retrieve(Settings.autotune, as: RawJSON.self)
  116. let autotuneResult = self.autotuneRun(
  117. autotunePreparedData: autotunePreppedGlucose,
  118. previousAutotuneResult: previousAutotune ?? profile,
  119. pumpProfile: pumpProfile
  120. )
  121. debug(.openAPS, "AUTOTUNE RESULT: \(autotuneResult)")
  122. if let autotune = Autotune(from: autotuneResult) {
  123. self.storage.save(autotuneResult, as: Settings.autotune)
  124. promise(.success(autotune))
  125. } else {
  126. promise(.success(nil))
  127. }
  128. }
  129. }
  130. }
  131. func makeProfiles(useAutotune: Bool) -> Future<Autotune?, Never> {
  132. Future { promise in
  133. debug(.openAPS, "Start makeProfiles")
  134. self.processQueue.async {
  135. var preferences = self.loadFileFromStorage(name: Settings.preferences)
  136. if preferences.isEmpty {
  137. preferences = Preferences().rawJSON
  138. }
  139. let pumpSettings = self.loadFileFromStorage(name: Settings.settings)
  140. let bgTargets = self.loadFileFromStorage(name: Settings.bgTargets)
  141. let basalProfile = self.loadFileFromStorage(name: Settings.basalProfile)
  142. let isf = self.loadFileFromStorage(name: Settings.insulinSensitivities)
  143. let cr = self.loadFileFromStorage(name: Settings.carbRatios)
  144. let tempTargets = self.loadFileFromStorage(name: Settings.tempTargets)
  145. let model = self.loadFileFromStorage(name: Settings.model)
  146. let autotune = useAutotune ? self.loadFileFromStorage(name: Settings.autotune) : .empty
  147. let pumpProfile = self.makeProfile(
  148. preferences: preferences,
  149. pumpSettings: pumpSettings,
  150. bgTargets: bgTargets,
  151. basalProfile: basalProfile,
  152. isf: isf,
  153. carbRatio: cr,
  154. tempTargets: tempTargets,
  155. model: model,
  156. autotune: RawJSON.null
  157. )
  158. let profile = self.makeProfile(
  159. preferences: preferences,
  160. pumpSettings: pumpSettings,
  161. bgTargets: bgTargets,
  162. basalProfile: basalProfile,
  163. isf: isf,
  164. carbRatio: cr,
  165. tempTargets: tempTargets,
  166. model: model,
  167. autotune: autotune.isEmpty ? .null : autotune
  168. )
  169. self.storage.save(pumpProfile, as: Settings.pumpProfile)
  170. self.storage.save(profile, as: Settings.profile)
  171. if let tunedProfile = Autotune(from: profile) {
  172. promise(.success(tunedProfile))
  173. return
  174. }
  175. promise(.success(nil))
  176. }
  177. }
  178. }
  179. // MARK: - Private
  180. private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) -> RawJSON {
  181. dispatchPrecondition(condition: .onQueue(processQueue))
  182. return jsWorker.inCommonContext { worker in
  183. worker.evaluate(script: Script(name: Prepare.log))
  184. worker.evaluate(script: Script(name: Bundle.iob))
  185. worker.evaluate(script: Script(name: Prepare.iob))
  186. return worker.call(function: Function.generate, with: [
  187. pumphistory,
  188. profile,
  189. clock,
  190. autosens
  191. ])
  192. }
  193. }
  194. private func meal(pumphistory: JSON, profile: JSON, basalProfile: JSON, clock: JSON, carbs: JSON, glucose: JSON) -> RawJSON {
  195. dispatchPrecondition(condition: .onQueue(processQueue))
  196. return jsWorker.inCommonContext { worker in
  197. worker.evaluate(script: Script(name: Prepare.log))
  198. worker.evaluate(script: Script(name: Bundle.meal))
  199. worker.evaluate(script: Script(name: Prepare.meal))
  200. return worker.call(function: Function.generate, with: [
  201. pumphistory,
  202. profile,
  203. clock,
  204. glucose,
  205. basalProfile,
  206. carbs
  207. ])
  208. }
  209. }
  210. private func autotunePrepare(
  211. pumphistory: JSON,
  212. profile: JSON,
  213. glucose: JSON,
  214. pumpprofile: JSON,
  215. carbs: JSON,
  216. categorizeUamAsBasal: Bool,
  217. tuneInsulinCurve: Bool
  218. ) -> RawJSON {
  219. dispatchPrecondition(condition: .onQueue(processQueue))
  220. return jsWorker.inCommonContext { worker in
  221. worker.evaluate(script: Script(name: Prepare.log))
  222. worker.evaluate(script: Script(name: Bundle.autotunePrep))
  223. worker.evaluate(script: Script(name: Prepare.autotunePrep))
  224. return worker.call(function: Function.generate, with: [
  225. pumphistory,
  226. profile,
  227. glucose,
  228. pumpprofile,
  229. carbs,
  230. categorizeUamAsBasal,
  231. tuneInsulinCurve
  232. ])
  233. }
  234. }
  235. private func autotuneRun(
  236. autotunePreparedData: JSON,
  237. previousAutotuneResult: JSON,
  238. pumpProfile: JSON
  239. ) -> RawJSON {
  240. dispatchPrecondition(condition: .onQueue(processQueue))
  241. return jsWorker.inCommonContext { worker in
  242. worker.evaluate(script: Script(name: Prepare.log))
  243. worker.evaluate(script: Script(name: Bundle.autotuneCore))
  244. worker.evaluate(script: Script(name: Prepare.autotuneCore))
  245. return worker.call(function: Function.generate, with: [
  246. autotunePreparedData,
  247. previousAutotuneResult,
  248. pumpProfile
  249. ])
  250. }
  251. }
  252. private func determineBasal(
  253. glucose: JSON,
  254. currentTemp: JSON,
  255. iob: JSON,
  256. profile: JSON,
  257. autosens: JSON,
  258. meal: JSON,
  259. microBolusAllowed: Bool,
  260. reservoir: JSON
  261. ) -> RawJSON {
  262. dispatchPrecondition(condition: .onQueue(processQueue))
  263. return jsWorker.inCommonContext { worker in
  264. worker.evaluate(script: Script(name: Prepare.log))
  265. worker.evaluate(script: Script(name: Prepare.determineBasal))
  266. worker.evaluate(script: Script(name: Bundle.basalSetTemp))
  267. worker.evaluate(script: Script(name: Bundle.getLastGlucose))
  268. worker.evaluate(script: Script(name: Bundle.determineBasal))
  269. if let middleware = self.middlewareScript(name: OpenAPS.Middleware.determineBasal) {
  270. worker.evaluate(script: middleware)
  271. }
  272. return worker.call(
  273. function: Function.generate,
  274. with: [
  275. iob,
  276. currentTemp,
  277. glucose,
  278. profile,
  279. autosens,
  280. meal,
  281. microBolusAllowed,
  282. reservoir
  283. ]
  284. )
  285. }
  286. }
  287. private func autosense(
  288. glucose: JSON,
  289. pumpHistory: JSON,
  290. basalprofile: JSON,
  291. profile: JSON,
  292. carbs: JSON,
  293. temptargets: JSON
  294. ) -> RawJSON {
  295. dispatchPrecondition(condition: .onQueue(processQueue))
  296. return jsWorker.inCommonContext { worker in
  297. worker.evaluate(script: Script(name: Prepare.log))
  298. worker.evaluate(script: Script(name: Bundle.autosens))
  299. worker.evaluate(script: Script(name: Prepare.autosens))
  300. return worker.call(
  301. function: Function.generate,
  302. with: [
  303. glucose,
  304. pumpHistory,
  305. basalprofile,
  306. profile,
  307. carbs,
  308. temptargets
  309. ]
  310. )
  311. }
  312. }
  313. private func exportDefaultPreferences() -> RawJSON {
  314. dispatchPrecondition(condition: .onQueue(processQueue))
  315. return jsWorker.inCommonContext { worker in
  316. worker.evaluate(script: Script(name: Prepare.log))
  317. worker.evaluate(script: Script(name: Bundle.profile))
  318. worker.evaluate(script: Script(name: Prepare.profile))
  319. return worker.call(function: Function.exportDefaults, with: [])
  320. }
  321. }
  322. private func makeProfile(
  323. preferences: JSON,
  324. pumpSettings: JSON,
  325. bgTargets: JSON,
  326. basalProfile: JSON,
  327. isf: JSON,
  328. carbRatio: JSON,
  329. tempTargets: JSON,
  330. model: JSON,
  331. autotune: JSON
  332. ) -> RawJSON {
  333. dispatchPrecondition(condition: .onQueue(processQueue))
  334. return jsWorker.inCommonContext { worker in
  335. worker.evaluate(script: Script(name: Prepare.log))
  336. worker.evaluate(script: Script(name: Bundle.profile))
  337. worker.evaluate(script: Script(name: Prepare.profile))
  338. return worker.call(
  339. function: Function.generate,
  340. with: [
  341. pumpSettings,
  342. bgTargets,
  343. isf,
  344. basalProfile,
  345. preferences,
  346. carbRatio,
  347. tempTargets,
  348. model,
  349. autotune
  350. ]
  351. )
  352. }
  353. }
  354. private func loadJSON(name: String) -> String {
  355. try! String(contentsOf: Foundation.Bundle.main.url(forResource: "json/\(name)", withExtension: "json")!)
  356. }
  357. private func loadFileFromStorage(name: String) -> RawJSON {
  358. storage.retrieveRaw(name) ?? OpenAPS.defaults(for: name)
  359. }
  360. private func middlewareScript(name: String) -> Script? {
  361. if let body = storage.retrieveRaw(name) {
  362. return Script(name: "Middleware", body: body)
  363. }
  364. if let url = Foundation.Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
  365. return Script(name: "Middleware", body: try! String(contentsOf: url))
  366. }
  367. return nil
  368. }
  369. static func defaults(for file: String) -> RawJSON {
  370. let prefix = file.hasSuffix(".json") ? "json/defaults" : "javascript"
  371. guard let url = Foundation.Bundle.main.url(forResource: "\(prefix)/\(file)", withExtension: "") else {
  372. return ""
  373. }
  374. return (try? String(contentsOf: url)) ?? ""
  375. }
  376. }