Explorar el Código

first concept

Ivan Valkou hace 5 años
padre
commit
a5208dec3c

+ 12 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -11,6 +11,9 @@
 		388E595E25AD948C0019842D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E595D25AD948C0019842D /* ContentView.swift */; };
 		388E596025AD948E0019842D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 388E595F25AD948E0019842D /* Assets.xcassets */; };
 		388E596325AD948E0019842D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 388E596225AD948E0019842D /* Preview Assets.xcassets */; };
+		388E596C25AD95110019842D /* OpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E596B25AD95110019842D /* OpenAPS.swift */; };
+		388E596F25AD96040019842D /* javascript in Resources */ = {isa = PBXBuildFile; fileRef = 388E596E25AD96040019842D /* javascript */; };
+		388E597225AD9CF10019842D /* json in Resources */ = {isa = PBXBuildFile; fileRef = 388E597125AD9CF10019842D /* json */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
@@ -20,6 +23,9 @@
 		388E595F25AD948E0019842D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		388E596225AD948E0019842D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
 		388E596425AD948E0019842D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		388E596B25AD95110019842D /* OpenAPS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPS.swift; sourceTree = "<group>"; };
+		388E596E25AD96040019842D /* javascript */ = {isa = PBXFileReference; lastKnownFileType = folder; path = javascript; sourceTree = "<group>"; };
+		388E597125AD9CF10019842D /* json */ = {isa = PBXFileReference; lastKnownFileType = folder; path = json; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -37,6 +43,8 @@
 			isa = PBXGroup;
 			children = (
 				388E595A25AD948C0019842D /* FreeAPS */,
+				388E596E25AD96040019842D /* javascript */,
+				388E597125AD9CF10019842D /* json */,
 				388E595925AD948C0019842D /* Products */,
 			);
 			sourceTree = "<group>";
@@ -57,6 +65,7 @@
 				388E595F25AD948E0019842D /* Assets.xcassets */,
 				388E596425AD948E0019842D /* Info.plist */,
 				388E596125AD948E0019842D /* Preview Content */,
+				388E596B25AD95110019842D /* OpenAPS.swift */,
 			);
 			path = FreeAPS;
 			sourceTree = "<group>";
@@ -128,6 +137,8 @@
 			files = (
 				388E596325AD948E0019842D /* Preview Assets.xcassets in Resources */,
 				388E596025AD948E0019842D /* Assets.xcassets in Resources */,
+				388E597225AD9CF10019842D /* json in Resources */,
+				388E596F25AD96040019842D /* javascript in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -138,6 +149,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				388E596C25AD95110019842D /* OpenAPS.swift in Sources */,
 				388E595E25AD948C0019842D /* ContentView.swift in Sources */,
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
 			);

+ 3 - 0
FreeAPS/ContentView.swift

@@ -11,6 +11,9 @@ struct ContentView: View {
     var body: some View {
         Text("Hello, world!")
             .padding()
+            .onAppear {
+                OpenAPS().determineBasal()
+            }
     }
 }
 

+ 53 - 0
FreeAPS/OpenAPS.swift

@@ -0,0 +1,53 @@
+//
+//  OpenAPS.swift
+//  FreeAPS
+//
+//  Created by Ivan Valkou on 12.01.2021.
+//
+
+import Foundation
+import JavaScriptCore
+
+final class OpenAPS {
+    private let vmQueue = DispatchQueue(label: "DispatchQueue.JSVirtualMachine")
+
+    func determineBasal() {
+        let vm = vmQueue.sync { JSVirtualMachine()! }
+        let context = JSContext(virtualMachine: vm)!
+
+        context.exceptionHandler = { context, exception in
+            print(exception!.toString()!)
+        }
+
+        let scripts = [
+            loadScript(name: "prepare"),
+            loadScript(name: "basal-set-temp"),
+            loadScript(name: "determine-basal"),
+            loadScript(name: "glucose-get-last")
+        ]
+
+        scripts.forEach { context.evaluateScript($0) }
+
+        let glucose = loadJSON(name: "glucose")
+        let currentTemp = loadJSON(name: "temp_basal")
+        let iobData = loadJSON(name: "iob")
+        let profile = loadJSON(name: "profile")
+        let autosensData = loadJSON(name: "autosens")
+        let mealData = loadJSON(name: "meal")
+
+        context.evaluateScript("var glucoseStatus = getLastGlucose(\(glucose));")
+        let result = context.evaluateScript("determine_basal(glucoseStatus, \(currentTemp), \(iobData), \(profile), \(autosensData), \(mealData), tempBasalFunctions, true, 100, 1527924300000);")
+        print(result!.toDictionary()!)
+        print(context.objectForKeyedSubscript("logError")!
+               .toString()!)
+
+    }
+
+    private func loadScript(name: String) -> String {
+        try! String(contentsOf: Bundle.main.url(forResource: "javascript/\(name)", withExtension: "js")!)
+    }
+
+    private func loadJSON(name: String) -> String {
+        try! String(contentsOf: Bundle.main.url(forResource: "json/\(name)", withExtension: "json")!)
+    }
+}

+ 60 - 0
javascript/basal-set-temp.js

@@ -0,0 +1,60 @@
+'use strict';
+
+function reason(rT, msg) {
+  rT.reason = (rT.reason ? rT.reason + '. ' : '') + msg;
+  console.error(msg);
+}
+
+var tempBasalFunctions = {};
+
+tempBasalFunctions.getMaxSafeBasal = function getMaxSafeBasal(profile) {
+
+    var max_daily_safety_multiplier = (isNaN(profile.max_daily_safety_multiplier) || profile.max_daily_safety_multiplier === null) ? 3 : profile.max_daily_safety_multiplier;
+    var current_basal_safety_multiplier = (isNaN(profile.current_basal_safety_multiplier) || profile.current_basal_safety_multiplier === null) ? 4 : profile.current_basal_safety_multiplier;
+
+    return Math.min(profile.max_basal, max_daily_safety_multiplier * profile.max_daily_basal, current_basal_safety_multiplier * profile.current_basal);
+};
+
+tempBasalFunctions.setTempBasal = function setTempBasal(rate, duration, profile, rT, currenttemp) {
+    //var maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal);
+
+    var maxSafeBasal = tempBasalFunctions.getMaxSafeBasal(profile);
+    var round_basal = require('./round-basal');
+
+    if (rate < 0) {
+        rate = 0;
+    } else if (rate > maxSafeBasal) {
+        rate = maxSafeBasal;
+    }
+
+    var suggestedRate = round_basal(rate, profile);
+    if (typeof(currenttemp) !== 'undefined' && typeof(currenttemp.duration) !== 'undefined' && typeof(currenttemp.rate) !== 'undefined' && currenttemp.duration > (duration-10) && currenttemp.duration <= 120 && suggestedRate <= currenttemp.rate * 1.2 && suggestedRate >= currenttemp.rate * 0.8 && duration > 0 ) {
+        rT.reason += " "+currenttemp.duration+"m left and " + currenttemp.rate + " ~ req " + suggestedRate + "U/hr: no temp required";
+        return rT;
+    }
+
+    if (suggestedRate === profile.current_basal) {
+      if (profile.skip_neutral_temps === true) {
+        if (typeof(currenttemp) !== 'undefined' && typeof(currenttemp.duration) !== 'undefined' && currenttemp.duration > 0) {
+          reason(rT, 'Suggested rate is same as profile rate, a temp basal is active, canceling current temp');
+          rT.duration = 0;
+          rT.rate = 0;
+          return rT;
+        } else {
+          reason(rT, 'Suggested rate is same as profile rate, no temp basal is active, doing nothing');
+          return rT;
+        }
+      } else {
+        reason(rT, 'Setting neutral temp basal of ' + profile.current_basal + 'U/hr');
+        rT.duration = duration;
+        rT.rate = suggestedRate;
+        return rT;
+      }
+    } else {
+      rT.duration = duration;
+      rT.rate = suggestedRate;
+      return rT;
+    }
+};
+
+module.exports = tempBasalFunctions;

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1162 - 0
javascript/determine-basal.js


+ 82 - 0
javascript/glucose-get-last.js

@@ -0,0 +1,82 @@
+var getLastGlucose = function (data) {
+    data = data.map(function prepGlucose (obj) {
+        //Support the NS sgv field to avoid having to convert in a custom way
+        obj.glucose = obj.glucose || obj.sgv;
+        return obj;
+    });
+
+    var now = data[0];
+    var now_date = now.date || Date.parse(now.display_time) || Date.parse(then.dateString);
+    var change;
+    var last_deltas = [];
+    var short_deltas = [];
+    var long_deltas = [];
+    var last_cal = 0;
+
+    //console.error(now.glucose);
+    for (var i=1; i < data.length; i++) {
+        // if we come across a cal record, don't process any older SGVs
+        if (typeof data[i] !== 'undefined' && data[i].type === "cal") {
+            last_cal = i;
+            break;
+        }
+        // only use data from the same device as the most recent BG data point
+        if (typeof data[i] !== 'undefined' && data[i].glucose > 38 && data[i].device === now.device) {
+            var then = data[i];
+            var then_date = then.date || Date.parse(then.display_time) || Date.parse(then.dateString);
+            var avgdelta = 0;
+            var minutesago;
+            if (typeof then_date !== 'undefined' && typeof now_date !== 'undefined') {
+                minutesago = Math.round( (now_date - then_date) / (1000 * 60) );
+                // multiply by 5 to get the same units as delta, i.e. mg/dL/5m
+                change = now.glucose - then.glucose;
+                avgdelta = change/minutesago * 5;
+            } else { console.error("Error: date field not found: cannot calculate avgdelta"); }
+            //if (i < 5) {
+                //console.error(then.glucose, minutesago, avgdelta);
+            //}
+            // use the average of all data points in the last 2.5m for all further "now" calculations
+            if (-2 < minutesago && minutesago < 2.5) {
+                now.glucose = ( now.glucose + then.glucose ) / 2;
+                now_date = ( now_date + then_date ) / 2;
+                //console.error(then.glucose, now.glucose);
+            // short_deltas are calculated from everything ~5-15 minutes ago
+            } else if (2.5 < minutesago && minutesago < 17.5) {
+                //console.error(minutesago, avgdelta);
+                short_deltas.push(avgdelta);
+                // last_deltas are calculated from everything ~5 minutes ago
+                if (2.5 < minutesago && minutesago < 7.5) {
+                    last_deltas.push(avgdelta);
+                }
+                //console.error(then.glucose, minutesago, avgdelta, last_deltas, short_deltas);
+            // long_deltas are calculated from everything ~20-40 minutes ago
+            } else if (17.5 < minutesago && minutesago < 42.5) {
+                long_deltas.push(avgdelta);
+            }
+        }
+    }
+    var last_delta = 0;
+    var short_avgdelta = 0;
+    var long_avgdelta = 0;
+    if (last_deltas.length > 0) {
+        last_delta = last_deltas.reduce(function(a, b) { return a + b; }) / last_deltas.length;
+    }
+    if (short_deltas.length > 0) {
+        short_avgdelta = short_deltas.reduce(function(a, b) { return a + b; }) / short_deltas.length;
+    }
+    if (long_deltas.length > 0) {
+        long_avgdelta = long_deltas.reduce(function(a, b) { return a + b; }) / long_deltas.length;
+    }
+
+    return {
+        delta: Math.round( last_delta * 100 ) / 100
+        , glucose: Math.round( now.glucose * 100 ) / 100
+        , noise: Math.round(now.noise)
+        , short_avgdelta: Math.round( short_avgdelta * 100 ) / 100
+        , long_avgdelta: Math.round( long_avgdelta * 100 ) / 100
+        , date: now_date
+        , last_cal: last_cal
+    };
+};
+
+module.exports = getLastGlucose;

+ 4 - 0
javascript/prepare.js

@@ -0,0 +1,4 @@
+var require = function(arg) { return function(basal, profile) { return basal; }; };
+var module = {};
+var logError = "";
+var process = { stderr: { write: console.log } };

+ 1 - 0
json/autosens.json

@@ -0,0 +1 @@
+{"ratio":1.0}

+ 8 - 0
json/basal_profile.json

@@ -0,0 +1,8 @@
+[
+  {
+    "minutes": 0,
+    "rate": 1,
+    "start": "00:00:00",
+    "i": 0
+  }
+]

+ 14 - 0
json/carbhistory.json

@@ -0,0 +1,14 @@
+[
+  {
+    "enteredBy": "fakecarbs",
+    "carbs": 5,
+    "created_at": "2018-06-02T07:00:00.000Z",
+    "insulin": null
+  },
+  {
+    "enteredBy": "fakecarbs",
+    "carbs": 15,
+    "created_at": "2018-06-02T07:05:00.000Z",
+    "insulin": null
+  }
+]

+ 1 - 0
json/clock.json

@@ -0,0 +1 @@
+"2018-06-02T00:30:00-07:00"

+ 8 - 0
json/example.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+
+oref0-calculate-iob pumphistory.json profile.json clock.json autosens.json > iob.json
+oref0-meal pumphistory.json profile.json clock.json glucose.json basal_profile.json carbhistory.json > meal.json
+oref0-determine-basal iob.json temp_basal.json glucose.json profile.json --auto-sens autosens.json --meal meal.json --microbolus --currentTime 1527924300000 > suggested.json
+cat suggested.json | jq -C -c '. | del(.predBGs) | del(.reason)'
+cat suggested.json | jq -C -c .reason
+cat suggested.json | jq -C -c .predBGs

+ 50 - 0
json/glucose.json

@@ -0,0 +1,50 @@
+[
+  {
+    "date": 1527924300000,
+    "dateString": "2018-06-02T00:25:00-07:00",
+    "sgv": 101,
+    "device": "fakecgm",
+    "type": "sgv",
+    "glucose": 101
+  },
+  {
+    "date": 1527924000000,
+    "dateString": "2018-06-02T00:20:00-07:00",
+    "sgv": 102,
+    "device": "fakecgm",
+    "type": "sgv",
+    "glucose": 102
+  },
+  {
+    "date": 1527923700000,
+    "dateString": "2018-06-02T00:15:00-07:00",
+    "sgv": 105,
+    "device": "fakecgm",
+    "type": "sgv",
+    "glucose": 105
+  },
+  {
+    "date": 1527923400000,
+    "dateString": "2018-06-02T00:10:00-07:00",
+    "sgv": 105,
+    "device": "fakecgm",
+    "type": "sgv",
+    "glucose": 105
+  },
+  {
+    "date": 1527923100000,
+    "dateString": "2018-06-02T00:05:00-07:00",
+    "sgv": 102,
+    "device": "fakecgm",
+    "type": "sgv",
+    "glucose": 102
+  },
+  {
+    "date": 1527922800000,
+    "dateString": "2018-06-02T00:00:00-07:00",
+    "sgv": 100,
+    "device": "fakecgm",
+    "type": "sgv",
+    "glucose": 100
+  }
+]

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
json/iob.json


+ 1 - 0
json/meal.json

@@ -0,0 +1 @@
+{"carbs":20,"nsCarbs":20,"bwCarbs":0,"journalCarbs":0,"mealCOB":20,"currentDeviation":-1.21,"maxDeviation":1.74,"minDeviation":0.1,"slopeFromMaxDeviation":-0.983,"slopeFromMinDeviation":0,"allDeviations":[-1,0,2],"lastCarbTime":1527923100000,"bwFound":false,"reason":"not enough glucose data to calculate carb absorption"}

+ 82 - 0
json/profile.json

@@ -0,0 +1,82 @@
+{
+  "carb_ratios": {
+    "schedule": [
+      {
+        "x": 0,
+        "i": 0,
+        "offset": 0,
+        "ratio": 10,
+        "r": 10,
+        "start": "00:00:00"
+      }
+    ],
+    "units": "grams"
+  },
+  "carb_ratio": 10,
+  "isfProfile": {
+    "first": 1,
+    "sensitivities": [
+      {
+        "endOffset": 1440,
+        "offset": 0,
+        "x": 0,
+        "sensitivity": 50,
+        "start": "00:00:00",
+        "i": 0
+      }
+    ],
+    "user_preferred_units": "mg/dL",
+    "units": "mg/dL"
+  },
+  "sens": 50,
+  "bg_targets": {
+    "first": 1,
+    "targets": [
+      {
+        "max_bg": 100,
+        "min_bg": 100,
+        "x": 0,
+        "offset": 0,
+        "low": 100,
+        "start": "00:00:00",
+        "high": 100,
+        "i": 0
+      }
+    ],
+    "user_preferred_units": "mg/dL",
+    "units": "mg/dL"
+  },
+  "max_bg": 100,
+  "min_bg": 100,
+  "out_units": "mg/dL",
+  "max_basal": 4,
+  "min_5m_carbimpact": 8,
+  "maxCOB": 120,
+  "max_iob": 6,
+  "max_daily_safety_multiplier": 4,
+  "current_basal_safety_multiplier": 5,
+  "autosens_max": 2,
+  "autosens_min": 0.5,
+  "remainingCarbsCap": 90,
+  "enableUAM": true,
+  "enableSMB_with_bolus": true,
+  "enableSMB_with_COB": true,
+  "enableSMB_with_temptarget": false,
+  "enableSMB_after_carbs": true,
+  "maxSMBBasalMinutes": 75,
+  "curve": "rapid-acting",
+  "useCustomPeakTime": false,
+  "insulinPeakTime": 75,
+  "dia": 6,
+  "bolus_increment": 0.1,
+  "current_basal": 1.0,
+  "basalprofile": [
+    {
+      "minutes": 0,
+      "rate": 1.0,
+      "start": "00:00:00",
+      "i": 0
+    }
+  ],
+  "max_daily_basal": 1.0
+}

+ 19 - 0
json/pumphistory.json

@@ -0,0 +1,19 @@
+[
+  {
+    "timestamp": "2018-06-02T00:05:00-07:00",
+    "_type": "Bolus",
+    "amount": 0.4,
+    "duration": 0
+  },
+  {
+    "timestamp": "2018-06-02T00:00:00-07:00",
+    "_type": "TempBasalDuration",
+    "duration (min)": 14400
+  },
+  {
+    "timestamp": "2018-06-02T00:00:00-07:00",
+    "_type": "TempBasal",
+    "temp": "absolute",
+    "rate": 0
+  }
+]

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
json/suggested.json


+ 5 - 0
json/temp_basal.json

@@ -0,0 +1,5 @@
+{
+  "duration": 30,
+  "temp": "absolute",
+  "rate": 0
+}