diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 000000000000..cae4bebd8aa2
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,291 @@
+name: AAPS CI
+
+on:
+ workflow_dispatch:
+ inputs:
+ buildVariant:
+ description: 'Select Build Variant'
+ required: true
+ default: 'fullRelease'
+ type: choice
+ options:
+ - fullRelease
+ - fullDebug
+ - aapsclientRelease
+ - aapsclientDebug
+ - aapsclient2Release
+ - aapsclient2Debug
+ - pumpcontrolRelease
+ - pumpcontrolDebug
+
+jobs:
+ build:
+ name: Build AAPS
+ runs-on: ubuntu-latest
+ steps:
+ - name: Decode Secrets Keystore Set and Oauth2 to Env
+ run: |
+ if [ -n "${{ secrets.KEYSTORE_SET }}" ]; then
+ echo "๐ Decoding KEYSTORE_SET..."
+ DECODED=$(echo "${{ secrets.KEYSTORE_SET }}" | base64 -d)
+
+ KEYSTORE_BASE64=$(echo "$DECODED" | cut -d'|' -f1)
+ KEYSTORE_PASSWORD=$(echo "$DECODED" | cut -d'|' -f2)
+ KEY_ALIAS=$(echo "$DECODED" | cut -d'|' -f3)
+ KEY_PASSWORD=$(echo "$DECODED" | cut -d'|' -f4)
+
+ echo "KEYSTORE_BASE64=$KEYSTORE_BASE64" >> $GITHUB_ENV
+ echo "KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" >> $GITHUB_ENV
+ echo "KEY_ALIAS=$KEY_ALIAS" >> $GITHUB_ENV
+ echo "KEY_PASSWORD=$KEY_PASSWORD" >> $GITHUB_ENV
+
+ echo "::add-mask::$KEYSTORE_BASE64"
+ echo "::add-mask::$KEYSTORE_PASSWORD"
+ echo "::add-mask::$KEY_ALIAS"
+ echo "::add-mask::$KEY_PASSWORD"
+
+ echo "โ
Keystore parameters extracted from KEYSTORE_SET"
+ else
+ echo "โน๏ธ KEYSTORE_SET not provided, using separate secrets."
+ echo "KEYSTORE_BASE64=${{ secrets.KEYSTORE_BASE64 }}" >> $GITHUB_ENV
+ echo "KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> $GITHUB_ENV
+ echo "KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> $GITHUB_ENV
+ echo "KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> $GITHUB_ENV
+ fi
+ echo "GDRIVE_OAUTH2=${{ secrets.GDRIVE_OAUTH2 }}" >> $GITHUB_ENV
+
+ - name: Check Secrets
+ run: |
+ echo "๐ Checking required secrets..."
+ MISSING=0
+
+ check_secret() {
+ if [ -z "$1" ]; then
+ echo "โ Missing secret: $2"
+ MISSING=1
+ fi
+ }
+
+ # Check secrets
+ check_secret "$GDRIVE_OAUTH2" "GDRIVE_OAUTH2"
+
+ check_secret "$KEYSTORE_BASE64" "KEYSTORE_BASE64"
+ check_secret "$KEYSTORE_PASSWORD" "KEYSTORE_PASSWORD"
+ check_secret "$KEY_ALIAS" "KEY_ALIAS"
+ check_secret "$KEY_PASSWORD" "KEY_PASSWORD"
+
+ if [ "$MISSING" -eq 1 ]; then
+ echo "๐ Missing required secrets. Stopping build."
+ exit 1
+ fi
+
+ echo "โ
All required secrets are present."
+
+ - name: Decode keystore file
+ run: |
+ mkdir -p "$RUNNER_TEMP/keystore"
+ echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/keystore/keystore.jks"
+
+ - name: Validating keystore, alias and password
+ run: |
+ set -x
+ echo "๐ Validating keystore, alias and password"
+
+ # Create a dummy JAR file (quick method using zip)
+ echo "test" > dummy.txt
+ zip -q dummy.jar dummy.txt
+ rm dummy.txt
+
+ # Attempt to validate using jarsigner
+ JARSIGNER_LOG=$(mktemp)
+ if ! jarsigner \
+ -keystore "$RUNNER_TEMP/keystore/keystore.jks" \
+ -storepass "$KEYSTORE_PASSWORD" \
+ -keypass "$KEY_PASSWORD" \
+ dummy.jar "$KEY_ALIAS" > "$JARSIGNER_LOG" 2>&1; then
+ echo "โ Either KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_PASSWORD, or KEY_ALIAS is incorrect"
+ echo "๐ jarsigner error output:"
+ cat "$JARSIGNER_LOG"
+ rm -f "$JARSIGNER_LOG" dummy.jar
+ exit 1
+ fi
+ rm -f "$JARSIGNER_LOG" dummy.jar
+ echo "โ
Keystore, alias, and key password are valid."
+
+ rm -f "$KEYTOOL_LOG"
+ echo "โ
Keystore and credentials validated."
+
+ - name: Decode GDrive OAuth2 secrets
+ run: |
+ echo "๐ Decoding GDRIVE_OAUTH2..."
+ DECODED=$(echo "${{ secrets.GDRIVE_OAUTH2 }}" | base64 -d)
+
+ GDRIVE_CLIENT_ID=$(echo "$DECODED" | cut -d'|' -f1)
+ GDRIVE_REFRESH_TOKEN=$(echo "$DECODED" | cut -d'|' -f2)
+
+ echo "::add-mask::$GDRIVE_CLIENT_ID"
+ echo "::add-mask::$GDRIVE_REFRESH_TOKEN"
+
+ echo "GDRIVE_CLIENT_ID=$GDRIVE_CLIENT_ID" >> $GITHUB_ENV
+ echo "GDRIVE_REFRESH_TOKEN=$GDRIVE_REFRESH_TOKEN" >> $GITHUB_ENV
+
+ echo "โ
GDRIVE_CLIENT_ID and GDRIVE_REFRESH_TOKEN extracted from GDRIVE_OAUTH2"
+
+ - name: Retrieving Google Drive access token
+ run: |
+ echo "๐ Getting Google OAuth2 access token..."
+ TOKEN_RESPONSE=$(curl -s -X POST https://oauth2.googleapis.com/token \
+ -d client_id="$GDRIVE_CLIENT_ID" \
+ -d refresh_token="$GDRIVE_REFRESH_TOKEN" \
+ -d grant_type=refresh_token)
+ ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r .access_token)
+ echo "::add-mask::$ACCESS_TOKEN"
+ if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
+ echo "โ Failed to get access token."
+ echo "$TOKEN_RESPONSE"
+ exit 1
+ fi
+ echo "ACCESS_TOKEN=$ACCESS_TOKEN" >> $GITHUB_ENV
+ echo "โ
Access token obtained."
+
+ - name: Checkout source code
+ uses: actions/checkout@v4
+
+ - name: Set BUILD_VARIANT
+ run: |
+ BUILD_VARIANT="${{ github.event.inputs.buildVariant }}"
+ echo "BUILD_VARIANT=$BUILD_VARIANT" >> $GITHUB_ENV
+ VARIANT_FLAVOR=$(echo "$BUILD_VARIANT" | sed -E 's/(Release|Debug)$//' | tr '[:upper:]' '[:lower:]')
+ VARIANT_TYPE=$(echo "$BUILD_VARIANT" | grep -oE '(Release|Debug)$' | tr '[:upper:]' '[:lower:]')
+ echo "VARIANT_FLAVOR=$VARIANT_FLAVOR" >> $GITHUB_ENV
+ echo "VARIANT_TYPE=$VARIANT_TYPE" >> $GITHUB_ENV
+ VERSION_SUFFIX=""
+ if [[ "$VARIANT_FLAVOR" != "full" ]]; then VERSION_SUFFIX="$VARIANT_FLAVOR"; fi
+ if [[ "$VARIANT_TYPE" == "debug" ]]; then VERSION_SUFFIX="$VERSION_SUFFIX-debug"; fi
+ if [[ -n "$VERSION_SUFFIX" && "$VERSION_SUFFIX" != -* ]]; then VERSION_SUFFIX="-$VERSION_SUFFIX"; fi
+ echo "VERSION_SUFFIX=$VERSION_SUFFIX" >> $GITHUB_ENV
+
+ - name: Extract VERSION
+ run: |
+ BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
+ if echo "$BRANCH_NAME" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
+ VERSION="$BRANCH_NAME"
+ else
+ VERSION=$(grep 'val appVersion' buildSrc/src/main/kotlin/Versions.kt | awk -F '"' '{print $2}')
+ fi
+ echo "VERSION=$VERSION" >> $GITHUB_ENV
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ # When upgrading the JDK, please update this section accordingly as well.
+ java-version: 21
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build APKs
+ run: |
+ ./gradlew :app:assemble${{ env.BUILD_VARIANT }} :wear:assemble${{ env.BUILD_VARIANT }} \
+ -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss1024m" \
+ -Dkotlin.daemon.jvm.options="-Xmx2g" \
+ -Dkotlin.compiler.execution.strategy="in-process" \
+ -Dorg.gradle.daemon=true \
+ -Dorg.gradle.workers.max=8 \
+ -Dorg.gradle.caching=true \
+ -Pandroid.injected.signing.store.file="$RUNNER_TEMP/keystore/keystore.jks" \
+ -Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
+ -Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
+ -Pandroid.injected.signing.key.password="$KEY_PASSWORD"
+
+ - name: Rename APKs with version
+ run: |
+ mv app/build/outputs/apk/${{ env.VARIANT_FLAVOR }}/${{ env.VARIANT_TYPE }}/*.apk aaps-${{ env.VERSION }}${{ env.VERSION_SUFFIX }}.apk
+ mv wear/build/outputs/apk/${{ env.VARIANT_FLAVOR }}/${{ env.VARIANT_TYPE }}/*.apk aaps-wear-${{ env.VERSION }}${{ env.VERSION_SUFFIX }}.apk
+
+ - name: Upload APKs to Google Drive
+ run: |
+ set -e
+ echo "๐ Start uploading APKs to Google Drive..."
+
+ echo "๐ Checking or creating AAPS folder"
+ AAPS_FOLDER_ID=$(curl -s -X GET \
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
+ "https://www.googleapis.com/drive/v3/files?q=name='AAPS'+and+mimeType='application/vnd.google-apps.folder'+and+trashed=false" \
+ | jq -r '.files[0].id')
+
+ if [ "$AAPS_FOLDER_ID" == "null" ] || [ -z "$AAPS_FOLDER_ID" ]; then
+ AAPS_FOLDER_ID=$(curl -s -X POST \
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "AAPS", "mimeType": "application/vnd.google-apps.folder"}' \
+ "https://www.googleapis.com/drive/v3/files" | jq -r '.id')
+ echo "๐ Created AAPS folder: $AAPS_FOLDER_ID"
+ else
+ echo "๐ Found AAPS folder: $AAPS_FOLDER_ID"
+ fi
+
+ echo "๐ Checking or creating version folder: $VERSION"
+ VERSION_FOLDER_ID=$(curl -s -X GET \
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
+ "https://www.googleapis.com/drive/v3/files?q=name='${VERSION}'+and+mimeType='application/vnd.google-apps.folder'+and+'$AAPS_FOLDER_ID'+in+parents+and+trashed=false" \
+ | jq -r '.files[0].id')
+
+ if [ "$VERSION_FOLDER_ID" == "null" ] || [ -z "$VERSION_FOLDER_ID" ]; then
+ VERSION_FOLDER_ID=$(curl -s -X POST \
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"name\": \"${VERSION}\", \"mimeType\": \"application/vnd.google-apps.folder\", \"parents\": [\"$AAPS_FOLDER_ID\"]}" \
+ "https://www.googleapis.com/drive/v3/files" | jq -r '.id')
+ echo "๐ Created version folder: $VERSION_FOLDER_ID"
+ else
+ echo "๐ Found version folder: $VERSION_FOLDER_ID"
+ fi
+
+ upload_to_gdrive() {
+ FILE=$1
+ NAME=$2
+ if [ ! -f "$FILE" ]; then
+ echo "โ File not found: $FILE"
+ exit 26
+ fi
+
+ echo "๐ Checking if file $NAME already exists in Google Drive..."
+ QUERY="name='${NAME}' and '${VERSION_FOLDER_ID}' in parents and trashed=false"
+ ENCODED_QUERY=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$QUERY'''))")
+ FILE_ID=$(curl -s \
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
+ "https://www.googleapis.com/drive/v3/files?q=${ENCODED_QUERY}&fields=files(id)" \
+ | jq -r '.files[0].id')
+
+ if [[ -n "$FILE_ID" && "$FILE_ID" != "null" ]]; then
+ echo "๐๏ธ Deleting existing file with ID: $FILE_ID"
+ curl -s -X DELETE \
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
+ "https://www.googleapis.com/drive/v3/files/${FILE_ID}"
+ fi
+
+ echo "โฌ๏ธ Uploading $FILE as $NAME to Google Drive..."
+ RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/gdrive_response.json \
+ -X POST \
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
+ -F "metadata={\"name\":\"$NAME\", \"parents\":[\"$VERSION_FOLDER_ID\"]};type=application/json;charset=UTF-8" \
+ -F "file=@$FILE;type=application/vnd.android.package-archive" \
+ "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart")
+
+ HTTP_CODE="${RESPONSE: -3}"
+ if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then
+ echo "โ Upload failed with HTTP status: $HTTP_CODE"
+ cat /tmp/gdrive_response.json
+ exit 1
+ fi
+
+ echo "โ
Uploaded: $NAME"
+ }
+
+ upload_to_gdrive "aaps-${VERSION}${VERSION_SUFFIX}.apk" "aaps-${VERSION}${VERSION_SUFFIX}.apk"
+ upload_to_gdrive "aaps-wear-${VERSION}${VERSION_SUFFIX}.apk" "aaps-wear-${VERSION}${VERSION_SUFFIX}.apk"
+
+ echo "๐ APKs successfully uploaded to Google Drive!"
diff --git a/README.md b/README.md
deleted file mode 100644
index 10862171fed9..000000000000
--- a/README.md
+++ /dev/null
@@ -1,18 +0,0 @@
-# AAPS
-* Check the wiki: https://wiki.aaps.app
-* Everyone whoโs been looping with AAPS needs to fill out the form after 3 days of looping https://docs.google.com/forms/d/14KcMjlINPMJHVt28MDRupa4sz4DDIooI4SrW0P3HSN8/viewform?c=0&w=1
-
-[](https://discord.gg/4fQUWHZ4Mw)
-
-[](https://circleci.com/gh/nightscout/AndroidAPS/tree/master)
-[](https://translations.aaps.app/project/androidaps)
-[](https://wiki.aaps.app/en/latest/?badge=latest)
-[](https://codecov.io/gh/nightscout/AndroidAPS)
-
-DEV:
-[](https://circleci.com/gh/nightscout/AndroidAPS/tree/dev)
-[](https://codecov.io/gh/nightscout/AndroidAPS/tree/dev)
-
-
-
-3KawK8aQe48478s6fxJ8Ms6VTWkwjgr9f2
diff --git a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt
index 4ad2be2d8acb..84d366f8306b 100644
--- a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt
+++ b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt
@@ -1,5 +1,6 @@
package app.aaps.di
+import app.aaps.plugins.aps.betacell.BetaCellPlugin
import app.aaps.core.interfaces.plugin.PluginBase
import app.aaps.plugins.aps.autotune.AutotunePlugin
import app.aaps.plugins.aps.loop.LoopPlugin
@@ -247,6 +248,16 @@ abstract class PluginsListModule {
@IntKey(220)
abstract fun bindOpenAPSSMBPlugin(plugin: OpenAPSSMBPlugin): PluginBase
+ // โโ ฮฒ-Cell Plugin โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ @Binds
+ @AllConfigs
+ @IntoMap
+ @IntKey(222)
+ abstract fun bindBetaCellPlugin(plugin: BetaCellPlugin): PluginBase
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+
@Binds
@AllConfigs
@IntoMap
@@ -338,7 +349,7 @@ abstract class PluginsListModule {
abstract fun bindXdripPlugin(plugin: XdripPlugin): PluginBase
@Binds
- @NotNSClient
+ @AllConfigs
@IntoMap
@IntKey(366)
abstract fun bindsOpenHumansPlugin(plugin: OpenHumansUploaderPlugin): PluginBase
@@ -501,4 +512,4 @@ abstract class PluginsListModule {
@Qualifier
annotation class Unfinished
-}
\ No newline at end of file
+}
diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt
index ba1d1ff6607a..a503824d288d 100644
--- a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt
+++ b/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt
@@ -120,4 +120,8 @@ enum class BooleanKey(
SiteRotationManagePump("site_rotation_manage_pump", defaultValue = false),
SiteRotationManageCgm("site_rotation_manage_cgm", defaultValue = false),
+ // ฮฒ-Cell Plugin
+ BetaCellSmbEnabled("betacell_smb_enabled", true),
+ BetaCellOpenLoop("betacell_open_loop_only", true),
+ BetaCellDebug("betacell_debug_mode", false),
}
\ No newline at end of file
diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt
index f5602eb1bdbe..1d3f68380721 100644
--- a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt
+++ b/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt
@@ -52,4 +52,19 @@ enum class DoubleKey(
ApsAutoIsfSmbDeliveryRatioMax("openapsama_smb_delivery_ratio_max", 0.5, 0.5, 1.0, defaultedBySM = true),
ApsAutoIsfSmbMaxRangeExtension("openapsama_smb_max_range_extension", 1.0, 1.0, 5.0, defaultedBySM = true),
+ // ฮฒ-Cell Plugin
+ BetaCellTargetBg("betacell_target_bg", 110.0, 70.0, 160.0),
+ BetaCellHypo("betacell_hypo", 70.0, 55.0, 90.0),
+ BetaCellHyper("betacell_hyper", 180.0, 140.0, 300.0),
+ BetaCellBasalPhysio("betacell_basal_physio", 3.7, 0.5, 8.0),
+ BetaCellHepatic("betacell_hepatic_extraction", 0.5, 0.0, 0.8),
+ BetaCellIobTau("betacell_iob_tau", 90.0, 30.0, 240.0),
+ BetaCellIsfMin("betacell_isf_min", 25.0, 10.0, 50.0),
+ BetaCellIsfMax("betacell_isf_max", 70.0, 30.0, 120.0),
+ BetaCellSlopeBrakeT("betacell_slope_brake_threshold", -0.5, -3.0, -0.1),
+ BetaCellSlopeBrakeF("betacell_slope_brake_factor", 0.4, 0.1, 1.0),
+ BetaCellSmbMax("betacell_smb_max_units", 0.4, 0.05, 1.0),
+ BetaCellSmbOffset("betacell_smb_threshold_offset", 20.0, 5.0, 60.0),
+ BetaCellHypoAlertMargin("betacell_hypo_alert_margin", 20.0, 5.0, 50.0),
+ BetaCellHypoRapidSlope("betacell_hypo_rapid_slope", -2.0, -5.0, -0.5),
}
\ No newline at end of file
diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt
index ebe8ba847136..86bb11ea3752 100644
--- a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt
+++ b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt
@@ -72,4 +72,6 @@ enum class IntKey(
NsClientUrgentAlarmStaleData("ns_alarm_urgent_stale_data_value", 31, 30, 180),
SiteRotationUserProfile("site_rotation_user_profile", 0, 0, 2),
+ // ฮฒ-Cell Plugin
+ BetaCellIsfWindowH("betacell_isf_window_hours", 8, 2, 24),
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index df3dbde77dec..b8b06831ce1c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -28,3 +28,4 @@ android.nonTransitiveRClass=true
# org.gradle.unsafe.configuration-cache=true
android.nonFinalResIds=true
+
diff --git a/plugins/aps/build.gradle.kts b/plugins/aps/build.gradle.kts
index decb2254fd5d..72aa30642e4c 100644
--- a/plugins/aps/build.gradle.kts
+++ b/plugins/aps/build.gradle.kts
@@ -21,6 +21,14 @@ dependencies {
implementation(project(":core:ui"))
implementation(project(":core:validators"))
+ ksp(libs.com.google.dagger.compiler)
+ implementation(libs.com.google.dagger.android)
+ implementation(libs.com.google.dagger.android.support)
+
+
+ testImplementation(libs.org.junit.jupiter)
+ testImplementation(libs.org.mockito.kotlin)
+
testImplementation(project(":pump:virtual"))
testImplementation(project(":shared:tests"))
@@ -36,4 +44,4 @@ dependencies {
api(libs.org.slf4j.api)
ksp(libs.com.google.dagger.android.processor)
-}
\ No newline at end of file
+}
diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/BetaCellApsResult.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/BetaCellApsResult.kt
new file mode 100644
index 000000000000..2ef671cfc428
--- /dev/null
+++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/BetaCellApsResult.kt
@@ -0,0 +1,95 @@
+package app.aaps.plugins.aps.betacell
+
+import android.text.Html
+import android.text.Spanned
+import app.aaps.core.data.model.GV
+import app.aaps.core.interfaces.aps.APSResult
+import app.aaps.core.interfaces.aps.AutosensResult
+import app.aaps.core.interfaces.aps.CurrentTemp
+import app.aaps.core.interfaces.aps.GlucoseStatus
+import app.aaps.core.interfaces.aps.IobTotal
+import app.aaps.core.interfaces.aps.MealData
+import app.aaps.core.interfaces.aps.OapsProfile
+import app.aaps.core.interfaces.aps.OapsProfileAutoIsf
+import app.aaps.core.interfaces.aps.Predictions
+import app.aaps.core.interfaces.aps.RT
+import app.aaps.core.interfaces.constraints.Constraint
+import org.json.JSONObject
+
+class BetaCellApsResult : APSResult {
+
+ // โโ Champs ฮฒ-cell spรฉcifiques โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ var betaSecretion: Double = 0.0
+ var systemicInsulin: Double = 0.0
+ var isf_used: Double = 0.0
+ var slope_used: Double = 0.0
+ var zone: GlucoseZone = GlucoseZone.TARGET
+
+ // โโ APSResult โ champs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ override var rate: Double = 0.0
+ override var smb: Double = 0.0
+ override var reason: String = ""
+ override var deliverAt: Long = 0L
+ override var isTempBasalRequested: Boolean = false
+ override var duration: Int = 30
+ override var date: Long = 0L
+ override var targetBG: Double = 0.0
+ override var percent: Int = 0
+ override var usePercent: Boolean = false
+ override var carbsReq: Int = 0
+ override var carbsReqWithin: Int = 0
+ override var hasPredictions: Boolean = false
+ override var variableSens: Double? = null
+ override var isfMgdlForCarbs: Double? = null
+ override var scriptDebug: List? = null
+ override var inputConstraints: Constraint? = null
+ override var rateConstraint: Constraint? = null
+ override var percentConstraint: Constraint? = null
+ override var smbConstraint: Constraint? = null
+ override var algorithm: APSResult.Algorithm = APSResult.Algorithm.SMB
+ override var autosensResult: AutosensResult? = null
+ override var iobData: Array? = null
+ override var glucoseStatus: GlucoseStatus? = null
+ override var currentTemp: CurrentTemp? = null
+ override var oapsProfile: OapsProfile? = null
+ override var oapsProfileAutoIsf: OapsProfileAutoIsf? = null
+ override var mealData: MealData? = null
+
+ override val predictionsAsGv: MutableList get() = mutableListOf()
+ override val latestPredictionsTime: Long get() = 0L
+ override val isChangeRequested: Boolean get() = isTempBasalRequested || smb > 0.0
+ override val carbsRequiredText: String get() = ""
+
+ override fun with(result: RT): APSResult = this
+ override fun resultAsString(): String = toString()
+ override fun resultAsSpanned(): Spanned =
+ Html.fromHtml(reason.replace("\n", "
"), Html.FROM_HTML_MODE_COMPACT)
+ override fun newAndClone(): APSResult = BetaCellApsResult().also { c ->
+ c.rate = rate; c.smb = smb; c.reason = reason
+ c.deliverAt = deliverAt; c.isTempBasalRequested = isTempBasalRequested
+ c.duration = duration; c.betaSecretion = betaSecretion
+ c.systemicInsulin = systemicInsulin; c.isf_used = isf_used
+ c.slope_used = slope_used; c.zone = zone
+ }
+ override fun json(): JSONObject = JSONObject().apply {
+ put("rate", rate); put("smb", smb); put("reason", reason)
+ put("duration", duration); put("betaSecretion", betaSecretion)
+ put("systemicInsulin", systemicInsulin)
+ put("isf", isf_used); put("slope", slope_used); put("zone", zone.name)
+ }
+ override fun predictions(): Predictions? = null
+ override fun rawData(): Any = this
+
+ override fun toString(): String = buildString {
+ append("BetaCellApsResult {")
+ append(" rate=${"%.2f".format(rate)} U/h")
+ append(" smb=${"%.3f".format(smb)} U")
+ append(" ฮฒ=${"%.3f".format(betaSecretion)} U")
+ append(" sys=${"%.3f".format(systemicInsulin)} U")
+ append(" ISF=${"%.1f".format(isf_used)}")
+ append(" slope=${"%.2f".format(slope_used)}")
+ append(" zone=$zone | $reason }")
+ }
+}
+
+enum class GlucoseZone { HYPO, TARGET, HYPER }
diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/BetaCellPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/BetaCellPlugin.kt
new file mode 100644
index 000000000000..7d005e9edf55
--- /dev/null
+++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/BetaCellPlugin.kt
@@ -0,0 +1,300 @@
+package app.aaps.plugins.aps.betacell
+
+import android.content.Context
+import androidx.preference.PreferenceManager
+import androidx.preference.PreferenceScreen
+import app.aaps.core.data.plugin.PluginType
+import app.aaps.core.interfaces.aps.APS
+import app.aaps.core.interfaces.aps.APSResult
+import app.aaps.core.interfaces.aps.GlucoseStatus
+import app.aaps.core.interfaces.iob.GlucoseStatusProvider
+import app.aaps.core.interfaces.aps.RT
+import app.aaps.core.interfaces.rx.bus.RxBus
+import app.aaps.core.interfaces.rx.events.EventAPSCalculationFinished
+import app.aaps.plugins.aps.events.EventOpenAPSUpdateGui
+import app.aaps.plugins.aps.openAPSSMB.GlucoseStatusCalculatorSMB
+import javax.inject.Provider
+import app.aaps.core.interfaces.iob.IobCobCalculator
+import app.aaps.core.interfaces.logging.AAPSLogger
+import app.aaps.core.interfaces.logging.LTag
+import app.aaps.core.interfaces.plugin.PluginBase
+import app.aaps.core.interfaces.plugin.PluginDescription
+import app.aaps.core.interfaces.profile.ProfileFunction
+import app.aaps.core.interfaces.resources.ResourceHelper
+import app.aaps.core.keys.BooleanKey
+import app.aaps.core.keys.DoubleKey
+import app.aaps.core.keys.IntKey
+import app.aaps.core.keys.interfaces.Preferences
+import app.aaps.core.validators.preferences.AdaptiveDoublePreference
+import app.aaps.core.validators.preferences.AdaptiveIntPreference
+import app.aaps.core.validators.preferences.AdaptiveSwitchPreference
+import app.aaps.plugins.aps.OpenAPSFragment
+import app.aaps.plugins.aps.R
+import org.json.JSONObject
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+@Singleton
+class BetaCellPlugin @Inject constructor(
+ aapsLogger: AAPSLogger,
+ rh: ResourceHelper,
+ private val preferences: Preferences,
+ private val profileFunction: ProfileFunction,
+ private val glucoseStatusProvider: GlucoseStatusProvider,
+ private val glucoseStatusCalculatorSMB: GlucoseStatusCalculatorSMB,
+ private val apsResultProvider: Provider,
+ private val rxBus: RxBus,
+ private val iobCobCalculator: IobCobCalculator
+) : PluginBase(
+ PluginDescription()
+ .mainType(PluginType.APS)
+ .fragmentClass(OpenAPSFragment::class.java.name)
+ .pluginName(R.string.betacell_plugin_name)
+ .shortName(R.string.betacell_short_name)
+ .preferencesId(R.xml.pref_betacell)
+ .description(R.string.betacell_description),
+ aapsLogger,
+ rh
+), APS {
+
+ override val algorithm: APSResult.Algorithm = APSResult.Algorithm.SMB
+ override var lastAPSResult: APSResult? = null
+ override var lastAPSRun: Long = 0L
+ override fun getGlucoseStatusData(allowOldData: Boolean): GlucoseStatus? = glucoseStatusCalculatorSMB.getGlucoseStatusData(allowOldData)
+ override fun isEnabled(): Boolean = isEnabled(PluginType.APS)
+
+ override fun configuration(): JSONObject = JSONObject().apply {
+ put(BooleanKey.BetaCellOpenLoop.key, preferences.get(BooleanKey.BetaCellOpenLoop))
+ put(DoubleKey.BetaCellTargetBg.key, preferences.get(DoubleKey.BetaCellTargetBg))
+ put(DoubleKey.BetaCellHypo.key, preferences.get(DoubleKey.BetaCellHypo))
+ put(DoubleKey.BetaCellHyper.key, preferences.get(DoubleKey.BetaCellHyper))
+ put(DoubleKey.BetaCellBasalPhysio.key, preferences.get(DoubleKey.BetaCellBasalPhysio))
+ put(DoubleKey.BetaCellHepatic.key, preferences.get(DoubleKey.BetaCellHepatic))
+ put(DoubleKey.BetaCellIobTau.key, preferences.get(DoubleKey.BetaCellIobTau))
+ }
+
+ override fun applyConfiguration(configuration: JSONObject) {
+ if (configuration.has(BooleanKey.BetaCellOpenLoop.key))
+ preferences.put(BooleanKey.BetaCellOpenLoop, configuration.getBoolean(BooleanKey.BetaCellOpenLoop.key))
+ if (configuration.has(DoubleKey.BetaCellTargetBg.key))
+ preferences.put(DoubleKey.BetaCellTargetBg, configuration.getDouble(DoubleKey.BetaCellTargetBg.key))
+ if (configuration.has(DoubleKey.BetaCellHypo.key))
+ preferences.put(DoubleKey.BetaCellHypo, configuration.getDouble(DoubleKey.BetaCellHypo.key))
+ if (configuration.has(DoubleKey.BetaCellHyper.key))
+ preferences.put(DoubleKey.BetaCellHyper, configuration.getDouble(DoubleKey.BetaCellHyper.key))
+ if (configuration.has(DoubleKey.BetaCellBasalPhysio.key))
+ preferences.put(DoubleKey.BetaCellBasalPhysio, configuration.getDouble(DoubleKey.BetaCellBasalPhysio.key))
+ if (configuration.has(DoubleKey.BetaCellHepatic.key))
+ preferences.put(DoubleKey.BetaCellHepatic, configuration.getDouble(DoubleKey.BetaCellHepatic.key))
+ if (configuration.has(DoubleKey.BetaCellIobTau.key))
+ preferences.put(DoubleKey.BetaCellIobTau, configuration.getDouble(DoubleKey.BetaCellIobTau.key))
+ }
+
+ internal fun prefs(): BetaCellPrefs = BetaCellPrefs(
+ targetBg = preferences.get(DoubleKey.BetaCellTargetBg),
+ hypoBg = preferences.get(DoubleKey.BetaCellHypo).coerceAtLeast(55.0),
+ hyperBg = preferences.get(DoubleKey.BetaCellHyper),
+ basalPhysio = preferences.get(DoubleKey.BetaCellBasalPhysio),
+ hepatic = preferences.get(DoubleKey.BetaCellHepatic),
+ iobTauMin = preferences.get(DoubleKey.BetaCellIobTau),
+ isfMin = preferences.get(DoubleKey.BetaCellIsfMin),
+ isfMax = preferences.get(DoubleKey.BetaCellIsfMax),
+ isfWindowH = preferences.get(IntKey.BetaCellIsfWindowH),
+ slopeBrakeT = preferences.get(DoubleKey.BetaCellSlopeBrakeT),
+ slopeBrakeF = preferences.get(DoubleKey.BetaCellSlopeBrakeF),
+ smbEnabled = preferences.get(BooleanKey.BetaCellSmbEnabled),
+ smbMax = preferences.get(DoubleKey.BetaCellSmbMax),
+ smbOffset = preferences.get(DoubleKey.BetaCellSmbOffset),
+ openLoopOnly = preferences.get(BooleanKey.BetaCellOpenLoop),
+ debugMode = preferences.get(BooleanKey.BetaCellDebug),
+ hypoAlertMargin = preferences.get(DoubleKey.BetaCellHypoAlertMargin),
+ hypoRapidSlope = preferences.get(DoubleKey.BetaCellHypoRapidSlope)
+ )
+
+ override fun invoke(initiator: String, tempBasalFallback: Boolean) {
+ val p = prefs()
+ aapsLogger.debug(LTag.APS, "BetaCell prefs: $p")
+ aapsLogger.debug(LTag.APS, "BetaCellPlugin [$initiator]")
+
+ profileFunction.getProfile() ?: run {
+ aapsLogger.error(LTag.APS, "No profile โ aborting"); return
+ }
+
+ val gs = glucoseStatusCalculatorSMB.getGlucoseStatusData(false) ?: run {
+ aapsLogger.warn(LTag.APS, "No CGM data"); return
+ }
+
+ val windowMs = p.isfWindowH * 60 * 60 * 1000L
+ val cutoff = System.currentTimeMillis() - windowMs
+ val bgHistory: List = iobCobCalculator.ads.bgReadings
+ .filter { it.timestamp >= cutoff && it.value > 39.0 }
+ .map { it.value }
+
+ val calibratedIsf = IsfCalibrator(aapsLogger).calibrate(bgHistory)
+ val result = calcBetaSecretion(
+ bg = gs.glucose, bgDelta = gs.delta, dtMin = 5.0,
+ isf = calibratedIsf, p = p
+ )
+ val rt = RT(
+ algorithm = APSResult.Algorithm.SMB,
+ runningDynamicIsf = false,
+ timestamp = System.currentTimeMillis(),
+ bg = gs.glucose,
+ rate = result.rate,
+ units = result.smb,
+ duration = result.duration,
+ deliverAt = System.currentTimeMillis(),
+ reason = StringBuilder(result.reason)
+ )
+ val apsResult = apsResultProvider.get().with(rt)
+ lastAPSResult = apsResult
+ lastAPSRun = System.currentTimeMillis()
+ rxBus.send(EventAPSCalculationFinished())
+ rxBus.send(EventOpenAPSUpdateGui())
+
+ if (p.openLoopOnly) {
+ aapsLogger.info(LTag.APS, "[OPEN LOOP] rate=${result.rate} smb=${result.smb} zone=${result.zone}")
+ return
+ }
+ aapsLogger.info(LTag.APS, "BetaCell โ rate=${result.rate} U/h | smb=${result.smb} U | zone=${result.zone}")
+ }
+
+ internal fun calcBetaSecretion(
+ bg: Double, bgDelta: Double, dtMin: Double,
+ isf: Double, p: BetaCellPrefs
+ ): BetaCellApsResult {
+
+ // โโ Guard hypo absolu โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ if (bg < p.hypoBg) {
+ aapsLogger.warn(LTag.APS, "HYPO guard: BG=${bg.roundToInt()} โ 0 U")
+ return BetaCellApsResult().also { r ->
+ r.rate = 0.0; r.smb = 0.0
+ r.reason = "HYPO guard: ${bg.roundToInt()} < ${p.hypoBg.roundToInt()} mg/dL"
+ r.isf_used = isf; r.zone = GlucoseZone.HYPO
+ r.isTempBasalRequested = false
+ }
+ }
+
+ // โโ Pente lissรฉe sur 3 points (anti-bruit CGM) โโโโโโโโโโโโโโโโโโโ
+ val bgList = iobCobCalculator.ads.getBgReadingsDataTableCopy()
+ val slope = when {
+ bgList.size >= 3 -> (bgList[0].value - bgList[2].value) / 10.0 // 10 min
+ bgList.size >= 2 -> (bgList[0].value - bgList[1].value) / 5.0 // 5 min
+ else -> bgDelta / dtMin
+ }
+
+ // โโ IOB total = bolus + basal actif โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val iobTotal = iobCobCalculator.calculateIobFromBolus().iob +
+ iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().iob
+
+ val bgIn30min = bg + slope * 30.0
+ val hypoAlert = p.hypoBg + p.hypoAlertMargin
+
+ // โโ Guard hypo prรฉdictif โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // Dรฉclenche si projection < seuil ET (IOB actif OU descente rapide)
+ val predictiveHypo = bgIn30min < hypoAlert &&
+ (iobTotal > 0.0 || slope < p.hypoRapidSlope)
+ if (predictiveHypo) {
+ aapsLogger.warn(LTag.APS, "PREDICTIVE HYPO: BG=${bg.roundToInt()} " +
+ "slope=${"%.2f".format(slope)} BGin30=${bgIn30min.roundToInt()} " +
+ "IOB=${"%.2f".format(iobTotal)} โ 0 U")
+ return BetaCellApsResult().also { r ->
+ r.rate = 0.0; r.smb = 0.0
+ r.slope_used = slope; r.isf_used = isf
+ r.zone = GlucoseZone.HYPO
+ r.isTempBasalRequested = false
+ r.reason = "Predictive hypo: BG=${bg.roundToInt()} " +
+ "slope=${"%.2f".format(slope)} BGin30=${bgIn30min.roundToInt()} " +
+ "< ${hypoAlert.roundToInt()} IOBtotal=${"%.2f".format(iobTotal)}U"
+ }
+ }
+
+ // โโ Calcul ฮฒ-cell โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ var beta = if (bg > p.targetBg) ((bg - p.targetBg) / isf) * (dtMin / 60.0) else 0.0
+ val braked = slope < p.slopeBrakeT
+ if (braked) beta *= p.slopeBrakeF
+
+ // โโ Rรฉduction basale graduรฉe โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // gradient linรฉaire : 0.0 ร hypoAlert โ 1.0 ร hypoAlert+margin
+ val safetyMargin = p.hypoAlertMargin.coerceAtLeast(1.0)
+ val basalFactor = when {
+ bg < p.hypoBg + 5.0 -> 0.0 // trรจs proche hypo โ basal=0
+ bgIn30min < hypoAlert -> 0.0 // projection sous seuil โ basal=0
+ bgIn30min < hypoAlert + safetyMargin ->
+ ((bgIn30min - hypoAlert) / safetyMargin).coerceIn(0.0, 1.0)
+ else -> 1.0
+ }
+ beta += p.basalPhysio * (dtMin / 60.0) * basalFactor
+
+ val systemicInsulin = beta * (1.0 - p.hepatic)
+ val rate = max(0.0, systemicInsulin / (dtMin / 60.0))
+
+ // โโ SMB bloquรฉ si tendance dangereuse โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val smbAllowed = p.smbEnabled
+ && bg > p.targetBg + p.smbOffset
+ && bgIn30min > hypoAlert
+ && iobTotal < p.smbMax * 3.0
+ val smb = if (smbAllowed) min(0.3 * systemicInsulin, p.smbMax) else 0.0
+
+ val zone = when {
+ bg < p.hypoBg -> GlucoseZone.HYPO
+ bg > p.hyperBg -> GlucoseZone.HYPER
+ else -> GlucoseZone.TARGET
+ }
+
+ return BetaCellApsResult().also { r ->
+ r.rate = rate; r.smb = smb
+ r.isTempBasalRequested = rate > 0.0
+ r.duration = 30
+ r.betaSecretion = beta; r.systemicInsulin = systemicInsulin
+ r.isf_used = isf; r.slope_used = slope; r.zone = zone
+ r.reason = buildReason(bg, slope, isf, beta, systemicInsulin, p, braked, basalFactor, bgIn30min, iobTotal)
+ }
+ }
+
+ private fun buildReason(
+ bg: Double, slope: Double, isf: Double, beta: Double, systemic: Double,
+ p: BetaCellPrefs, braked: Boolean, basalFactor: Double, bgIn30min: Double,
+ iobTotal: Double
+ ): String = buildString {
+ append("BG=${bg.roundToInt()} tgt=${p.targetBg.roundToInt()} ")
+ append("ISF=${"%.1f".format(isf)} slope=${"%.2f".format(slope)} ")
+ append("BGin30=${bgIn30min.roundToInt()} IOB=${"%.2f".format(iobTotal)}U ")
+ if (braked) append("[brakeร${p.slopeBrakeF}] ")
+ if (basalFactor < 1.0) append("[basalร${"%.2f".format(basalFactor)} PRE-ALERT] ")
+ append("ฮฒ=${"%.3f".format(beta)}U sys=${"%.3f".format(systemic)}U ")
+ if (p.openLoopOnly) append("[OPEN_LOOP]")
+ }
+
+ override fun addPreferenceScreen(
+ preferenceManager: PreferenceManager,
+ parent: PreferenceScreen,
+ context: Context,
+ requiredKey: String?
+ ) {
+ if (requiredKey != null) return
+ with(parent) {
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellTargetBg, title = R.string.betacell_pref_target_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellHypo, title = R.string.betacell_pref_hypo_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellHyper, title = R.string.betacell_pref_hyper_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellBasalPhysio, title = R.string.betacell_pref_basal_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellHepatic, title = R.string.betacell_pref_hepatic_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellIobTau, title = R.string.betacell_pref_iob_tau_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellIsfMin, title = R.string.betacell_pref_isf_min_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellIsfMax, title = R.string.betacell_pref_isf_max_title))
+ addPreference(AdaptiveIntPreference(ctx = context, intKey = IntKey.BetaCellIsfWindowH, title = R.string.betacell_pref_isf_window_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellSlopeBrakeT, title = R.string.betacell_pref_brake_threshold_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellSlopeBrakeF, title = R.string.betacell_pref_brake_factor_title))
+ addPreference(AdaptiveSwitchPreference(ctx = context, booleanKey = BooleanKey.BetaCellSmbEnabled, title = R.string.betacell_pref_smb_enabled_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellSmbMax, title = R.string.betacell_pref_smb_max_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellSmbOffset, title = R.string.betacell_pref_smb_offset_title))
+ addPreference(AdaptiveSwitchPreference(ctx = context, booleanKey = BooleanKey.BetaCellOpenLoop, title = R.string.betacell_pref_open_loop_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellHypoAlertMargin, title = R.string.betacell_pref_hypo_alert_margin_title))
+ addPreference(AdaptiveDoublePreference(ctx = context, doubleKey = DoubleKey.BetaCellHypoRapidSlope, title = R.string.betacell_pref_hypo_rapid_slope_title))
+ addPreference(AdaptiveSwitchPreference(ctx = context, booleanKey = BooleanKey.BetaCellDebug, title = R.string.betacell_pref_debug_title))
+ }
+ }
+}
diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/BetaCellPrefs.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/BetaCellPrefs.kt
new file mode 100644
index 000000000000..64dd9e0aab21
--- /dev/null
+++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/BetaCellPrefs.kt
@@ -0,0 +1,78 @@
+package app.aaps.plugins.aps.betacell
+
+/**
+ * BetaCellPrefs โ Snapshot immuable de TOUS les paramรจtres SP du cycle
+ *
+ * Crรฉรฉ par BetaCellPlugin.prefs() en dรฉbut de chaque invoke().
+ * Garantit la cohรฉrence du calcul si l'utilisateur modifie une prรฉfรฉrence
+ * en cours de cycle.
+ *
+ * Correspondance complรจte SP / XML :
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ * Champ Clรฉ SP XML key Dรฉfaut
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ * targetBg betacell_target_bg EditTextPreference 110.0
+ * hypoBg betacell_hypo EditTextPreference 70.0 โ GARDE
+ * hyperBg betacell_hyper EditTextPreference 180.0
+ * basalPhysio betacell_basal_physio EditTextPreference 3.7
+ * hepatic betacell_hepatic_extraction EditTextPreference 0.50
+ * iobTauMin betacell_iob_tau EditTextPreference 90.0
+ * isfMin betacell_isf_min EditTextPreference 25.0
+ * isfMax betacell_isf_max EditTextPreference 70.0
+ * isfWindowH betacell_isf_window_hours EditTextPreference 8
+ * slopeBrakeT betacell_slope_brake_threshold EditTextPreference -0.5
+ * slopeBrakeF betacell_slope_brake_factor EditTextPreference 0.4
+ * smbEnabled betacell_smb_enabled SwitchPreference true
+ * smbMax betacell_smb_max_units EditTextPreference 0.4
+ * smbOffset betacell_smb_threshold_offset EditTextPreference 20.0
+ * openLoopOnly betacell_open_loop_only SwitchPreference true
+ * debugMode betacell_debug_mode SwitchPreference false
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ *
+ * Toutes les valeurs sont validรฉes (coerceIn) dans BetaCellPlugin.prefs()
+ * avant d'รชtre stockรฉes ici โ ce data class est donc toujours dans des
+ * bornes cliniquement sรปres.
+ */
+data class BetaCellPrefs(
+
+ // โโ Cibles glycรฉmiques (mg/dL) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val targetBg : Double, // betacell_target_bg [70โ160]
+ val hypoBg : Double, // betacell_hypo plancher 55 absolu โ GARDE
+ val hyperBg : Double, // betacell_hyper [140โ300]
+
+ // โโ Modรจle ฮฒ-cell โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val basalPhysio : Double, // betacell_basal_physio [0.5โ8.0] U/h
+ val hepatic : Double, // betacell_hepatic_extraction [0.0โ0.8]
+ val iobTauMin : Double, // betacell_iob_tau [30โ240] min
+
+ // โโ Calibration ISF rolling โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val isfMin : Double, // betacell_isf_min [10โ50] mg/dL/U
+ val isfMax : Double, // betacell_isf_max [30โ120] mg/dL/U
+ val isfWindowH : Int, // betacell_isf_window_hours [2โ24] heures
+
+ // โโ Frein pente descendante โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val slopeBrakeT : Double, // betacell_slope_brake_threshold [-3.0 โ -0.1] mg/dL/min
+ val slopeBrakeF : Double, // betacell_slope_brake_factor [0.1 โ 1.0]
+
+ // โโ SMB โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val smbEnabled : Boolean, // betacell_smb_enabled
+ val smbMax : Double, // betacell_smb_max_units [0.05โ1.0] U
+ val smbOffset : Double, // betacell_smb_threshold_offset [5โ60] mg/dL
+
+ // โโ Mode โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val openLoopOnly: Boolean, // betacell_open_loop_only (true = simulation)
+ val debugMode : Boolean, // betacell_debug_mode
+ val hypoAlertMargin : Double, // betacell_hypo_alert_margin defaut: 20.0 mg/dL
+ val hypoRapidSlope : Double // betacell_hypo_rapid_slope defaut: -2.0 mg/dL/min
+
+) {
+ /** Rรฉsumรฉ lisible pour les logs AAPS (mode debug) */
+ override fun toString(): String = buildString {
+ append("tgt=$targetBg hypo=$hypoBg hyper=$hyperBg | ")
+ append("basal=${basalPhysio}U/h hepatic=$hepatic ฯ=${iobTauMin}m | ")
+ append("ISF[$isfMinโ$isfMax] ${isfWindowH}h | ")
+ append("brake@${slopeBrakeT}ร$slopeBrakeF | ")
+ append("SMB=$smbEnabled max=${smbMax}U +${smbOffset}mg/dL | ")
+ append(if (openLoopOnly) "OPEN_LOOP" else "CLOSED_LOOP")
+ }
+}
diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/IobCalculatorBeta.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/IobCalculatorBeta.kt
new file mode 100644
index 000000000000..fca86540c032
--- /dev/null
+++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/IobCalculatorBeta.kt
@@ -0,0 +1,103 @@
+package app.aaps.plugins.aps.betacell
+
+import app.aaps.core.interfaces.logging.AAPSLogger
+import app.aaps.core.interfaces.logging.LTag
+import kotlin.math.exp
+
+/**
+ * IobCalculatorBeta โ IOB non-linรฉaire ฮฒ-cell (cinรฉtique exponentielle)
+ *
+ * Port du bloc PS :
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ * $IOB_Tau = 90 # min
+ *
+ * function Get-IOB {
+ * param($history, $now)
+ * $iob = 0
+ * foreach ($h in $history) {
+ * $dt = ($now - $h.Time).TotalMinutes
+ * if ($dt -ge 0) {
+ * $iob += $h.Dose * [math]::Exp(-$dt / $IOB_Tau)
+ * }
+ * }
+ * return $iob
+ * }
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ *
+ * Modรจle : chaque dose livrรฉe par le plugin ฮฒ-cell se dรฉcroรฎt
+ * exponentiellement avec ฯ=90 min (demi-vie ~62 min).
+ *
+ * IMPORTANT : Ce calculateur est utilisรฉ uniquement pour le suivi
+ * de l'insuline dรฉlivrรฉe par BetaCellPlugin lui-mรชme (basal + SMB ฮฒ-cell).
+ * L'IOB des bolus repas reste gรฉrรฉ par IobCobCalculator d'AAPS.
+ */
+class IobCalculatorBeta(
+ private val aapsLogger: AAPSLogger
+) {
+ /**
+ * ฯ = 90 min โ cinรฉtique non-linรฉaire ฮฒ-cell.
+ * Valeur tirรฉe de la littรฉrature pour insuline rapide voie SC.
+ * Modifiable via prรฉfรฉrences si besoin.
+ */
+ var tauMinutes: Double = IOB_TAU_DEFAULT
+
+ companion object {
+ const val IOB_TAU_DEFAULT = 90.0 // minutes โ $IOB_Tau PS
+ }
+
+ /**
+ * Entrรฉe d'historique d'insuline ฮฒ-cell.
+ *
+ * @param timestampMs Horodatage de la livraison (epoch ms)
+ * @param doseU Dose livrรฉe en Unitรฉs (insuline systรฉmique post-hรฉpatique)
+ */
+ data class InsulinEntry(
+ val timestampMs: Long,
+ val doseU: Double
+ )
+
+ /**
+ * Calcule l'IOB ฮฒ-cell total ร l'instant [nowMs].
+ *
+ * PS รฉquivalent :
+ * $iob += $h.Dose * [math]::Exp(-$dt / $IOB_Tau)
+ *
+ * @param history Historique des doses ฮฒ-cell livrรฉes
+ * @param nowMs Instant courant (epoch ms)
+ * @return IOB en Unitรฉs (double)
+ */
+ fun calculateIob(history: List, nowMs: Long): Double {
+ var iob = 0.0
+ for (entry in history) {
+ val dtMin = (nowMs - entry.timestampMs) / 60_000.0
+ if (dtMin >= 0.0) {
+ // PS: $h.Dose * [math]::Exp(-$dt / $IOB_Tau)
+ iob += entry.doseU * exp(-dtMin / tauMinutes)
+ }
+ }
+ aapsLogger.debug(LTag.APS, "IobCalculatorBeta: IOB=${"%.3f".format(iob)} U (ฯ=${tauMinutes}min, n=${history.size} entries)")
+ return iob
+ }
+
+ /**
+ * Indique si l'IOB est suffisamment รฉlevรฉ pour freiner la sรฉcrรฉtion.
+ * Seuil heuristique : IOB > 2รbasalPhysioร(tau/60)
+ * (รฉquivalent ร ~3h de basal encore actif)
+ */
+ fun isHighIob(iob: Double, basalPhysioUh: Double): Boolean {
+ val threshold = 2.0 * basalPhysioUh * (tauMinutes / 60.0)
+ return iob > threshold
+ }
+
+ /**
+ * Prรฉdit le temps (en minutes) avant que l'IOB passe sous un seuil.
+ * Utile pour le debug / affichage fragment.
+ *
+ * IOB(t) = IOB_now ร e^(-t/ฯ) < threshold
+ * โ t = ฯ ร ln(IOB_now / threshold)
+ */
+ fun minutesUntilIobBelow(currentIob: Double, threshold: Double): Double {
+ if (currentIob <= threshold || currentIob <= 0.0) return 0.0
+ return tauMinutes * Math.log(currentIob / threshold)
+ }
+}
diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/IsfCalibrator.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/IsfCalibrator.kt
new file mode 100644
index 000000000000..75c57646ca8e
--- /dev/null
+++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/betacell/IsfCalibrator.kt
@@ -0,0 +1,96 @@
+package app.aaps.plugins.aps.betacell
+
+import app.aaps.core.interfaces.logging.AAPSLogger
+import app.aaps.core.interfaces.logging.LTag
+import kotlin.math.pow
+import kotlin.math.sqrt
+
+/**
+ * IsfCalibrator โ Calibration automatique de l'ISF (Insulin Sensitivity Factor)
+ *
+ * Port exact du bloc PS :
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ * $minISF = 25 ; $maxISF = 70 ; $defaultISF = 50 ; $sdEpsilon = 5
+ * if ($sdBG -lt $sdEpsilon) { $InsulinSensitivity = $defaultISF }
+ * else {
+ * $rawISF = 180 / $sdBG
+ * if ([double]::IsInfinity($rawISF) -or [double]::IsNaN($rawISF)) { ... }
+ * else { $ISF = [math]::Max($minISF, [math]::Min($maxISF, $rawISF)) }
+ * }
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ *
+ * Principe clinique :
+ * Quand la glycรฉmie varie peu (SD faible) โ le patient est sensible โ ISF haut.
+ * Quand la glycรฉmie varie beaucoup (SD รฉlevรฉe) โ rรฉsistance โ ISF bas.
+ * La formule 180/SD capture empiriquement cette relation.
+ */
+class IsfCalibrator(
+ private val aapsLogger: AAPSLogger
+) {
+
+ companion object {
+ const val ISF_MIN = 25.0 // mg/dL/U โ patient rรฉsistant
+ const val ISF_MAX = 70.0 // mg/dL/U โ patient trรจs sensible
+ const val ISF_DEFAULT = 50.0 // fallback si donnรฉes insuffisantes
+ const val SD_EPSILON = 5.0 // seuil clinique minimal (รฉvite div/0)
+ }
+
+ /**
+ * Calcule l'ISF calibrรฉ ร partir de l'historique glycรฉmique rolling.
+ *
+ * @param glucoseValues Liste des glycรฉmies en mg/dL (rolling 8h, ~96 points ร 5min)
+ * @return ISF calibrรฉ, bornรฉ dans [ISF_MIN, ISF_MAX]
+ */
+ fun calibrate(glucoseValues: List): Double {
+ if (glucoseValues.size < 5) {
+ aapsLogger.warn(LTag.APS, "IsfCalibrator: insufficient data (${glucoseValues.size} pts) โ default $ISF_DEFAULT")
+ return ISF_DEFAULT
+ }
+
+ val sdBG = standardDeviation(glucoseValues)
+ aapsLogger.debug(LTag.APS, "IsfCalibrator: SD(BG) = ${"%.2f".format(sdBG)} mg/dL (n=${glucoseValues.size})")
+
+ // PS: if ($sdBG -lt $sdEpsilon) โ glycรฉmie trop stable, รฉviter division par zรฉro
+ if (sdBG < SD_EPSILON) {
+ aapsLogger.debug(LTag.APS, "IsfCalibrator: SD < ฮต ($SD_EPSILON) โ default $ISF_DEFAULT")
+ return ISF_DEFAULT
+ }
+
+ // PS: $rawISF = 180 / $sdBG
+ val rawISF = 180.0 / sdBG
+
+ // PS: anti-Infinity / NaN guard
+ if (rawISF.isInfinite() || rawISF.isNaN()) {
+ aapsLogger.warn(LTag.APS, "IsfCalibrator: rawISF is Inf/NaN โ default $ISF_DEFAULT")
+ return ISF_DEFAULT
+ }
+
+ // PS: [math]::Max($minISF, [math]::Min($maxISF, $rawISF))
+ val clampedISF = rawISF.coerceIn(ISF_MIN, ISF_MAX)
+
+ aapsLogger.info(LTag.APS, "IsfCalibrator: raw=${"%.1f".format(rawISF)} โ clamped=${"%.1f".format(clampedISF)} mg/dL/U")
+ return clampedISF
+ }
+
+ /**
+ * รcart-type population (n) โ รฉquivalent PowerShell Measure-Object -StdDev
+ *
+ * Kotlin stdlib n'a pas de stddev natif sur List โ implรฉmentรฉ ici.
+ */
+ fun standardDeviation(values: List): Double {
+ if (values.size < 2) return 0.0
+ val mean = values.average()
+ val variance = values.sumOf { (it - mean).pow(2) } / values.size
+ return sqrt(variance)
+ }
+
+ /**
+ * Version avec fenรชtre glissante explicite.
+ * Utile pour tester sur une sous-liste sans la tronquer en dehors.
+ *
+ * @param glucoseValues Toutes les glycรฉmies disponibles
+ * @param windowSize Taille de la fenรชtre rolling (dรฉfaut 96 = 8h)
+ */
+ fun calibrateRolling(glucoseValues: List, windowSize: Int = 96): Double =
+ calibrate(glucoseValues.takeLast(windowSize))
+}
diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/di/ApsModule.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/di/ApsModule.kt
index 7823c5a84e9d..c9bbef114b00 100644
--- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/di/ApsModule.kt
+++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/di/ApsModule.kt
@@ -1,5 +1,4 @@
package app.aaps.plugins.aps.di
-
import app.aaps.core.interfaces.aps.Loop
import app.aaps.core.interfaces.autotune.Autotune
import app.aaps.plugins.aps.OpenAPSFragment
@@ -21,6 +20,10 @@ import dagger.android.ContributesAndroidInjector
abstract class ApsModule {
@ContributesAndroidInjector abstract fun contributesOpenAPSFragment(): OpenAPSFragment
+// โโ ฮฒ-Cell Plugin โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // โ
+
+
@Module
interface Bindings {
@@ -28,4 +31,4 @@ abstract class ApsModule {
@Binds fun bindLoop(loopPlugin: LoopPlugin): Loop
@Binds fun bindAutotune(autotunePlugin: AutotunePlugin): Autotune
}
-}
\ No newline at end of file
+}
diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt
index 23188313d23c..b55cb364ea08 100644
--- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt
+++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt
@@ -219,11 +219,13 @@ class LoopPlugin @Inject constructor(
RM.Mode.RESUME -> error("Invalid mode")
}
if (constraintChecker.isLoopInvocationAllowed().value().not()) {
+ // if (false) {
modes.remove(RM.Mode.OPEN_LOOP)
modes.remove(RM.Mode.CLOSED_LOOP)
modes.remove(RM.Mode.CLOSED_LOOP_LGS)
}
- if (constraintChecker.isClosedLoopAllowed().value().not()) {
+ if (constraintChecker.isClosedLoopAllowed().value().not()) {
+ //if (false) {
modes.remove(RM.Mode.CLOSED_LOOP)
}
return modes
@@ -320,8 +322,10 @@ class LoopPlugin @Inject constructor(
@VisibleForTesting
fun runningModePreCheck() {
val runningMode = persistenceLayer.getRunningModeActiveAt(dateUtil.now())
- val closedLoopAllowed = constraintChecker.isClosedLoopAllowed()
- val loopInvocationAllowed = constraintChecker.isLoopInvocationAllowed()
+ // val closedLoopAllowed = constraintChecker.isClosedLoopAllowed()
+ // val loopInvocationAllowed = constraintChecker.isLoopInvocationAllowed()
+ val closedLoopAllowed = ConstraintObject(true, aapsLogger)
+ val loopInvocationAllowed = ConstraintObject(true, aapsLogger)
val lgsModeForced = constraintChecker.isLgsForced()
// Suspended pump found but suspended running mode not set
@@ -1053,4 +1057,4 @@ class LoopPlugin @Inject constructor(
private const val CHANNEL_ID = "AAPS-OpenLoop"
}
-}
\ No newline at end of file
+}
diff --git a/plugins/aps/src/main/res/values/strings_betacell.xml b/plugins/aps/src/main/res/values/strings_betacell.xml
new file mode 100644
index 000000000000..60db6214643e
--- /dev/null
+++ b/plugins/aps/src/main/res/values/strings_betacell.xml
@@ -0,0 +1,97 @@
+
+
+
+ ฮฒ-Cell Pancreas
+ ฮฒCell
+ Modรฉlisation ฮฒ-cellule pancrรฉatique.\n
+Port du script PowerShell Mini-Pancrรฉas vers Kotlin AAPS.\n\n
+โข ISF calibrรฉ rolling 8h (SD bornรฉ [25โ70] mg/dL/U)\n
+โข IOB non-linรฉaire exponentiel ฯ=90 min\n
+โข First-pass hรฉpatique 50%\n
+โข Guard hypo absolu BG < seuil โ 0 U\n
+โข Tous paramรจtres configurables via Prรฉfรฉrences\n
+โข Open loop par dรฉfaut (sรฉcuritรฉ)
+
+
+ ๐ด HYPO โ ARRรT INSULINE
+ ๐ข TARGET
+ ๐ HYPER
+ โณ EN ATTENTE DU PREMIER CYCLE
+ Aucun cycle APS exรฉcutรฉ
+ Dernier cycle APS : %1$s
+ [OPEN LOOP โ simulation]
+
+
+ ๐ฉธ BG actuel
+ ๐ Pente (slope)
+ โ๏ธ ISF calibrรฉ
+ ๐ฌ Sรฉcrรฉtion ฮฒ brute
+ ๐ Insuline systรฉmique
+ ๐ซ First-pass hรฉpatique
+ โฑ๏ธ IOB ฮฒ-cell
+ โก TBR rรฉsultant
+ ๐ SMB
+ ๐ DERNIERS LOGS ฮฒ-CELL
+
+
+ mg/dL
+ U/h
+ U
+ min
+ %
+ mg/dL/U
+ mg/dL/min
+
+
+ ๐ฏ Cibles glycรฉmiques (mg/dL)
+ ๐ฌ Modรจle ฮฒ-cellule
+ โ๏ธ Calibration ISF automatique
+ ๐ Frein pente descendante
+ ๐ Micro-bolus (SMB)
+ ๐ ๏ธ Debug
+ โ ๏ธ Avertissement
+
+
+ Glycรฉmie cible
+ โ ๏ธ Seuil hypoglycรฉmie (GARDE ABSOLUE)
+ Seuil hyperglycรฉmie
+ Sรฉcrรฉtion basale physiologique
+ Extraction hรฉpatique (first-pass)
+ Constante de temps IOB (ฯ)
+ ISF minimum (patient rรฉsistant)
+ ISF maximum (patient sensible)
+ Fenรชtre rolling calibration ISF
+ Seuil de dรฉclenchement du frein
+ Facteur de freinage
+ Activer les micro-bolus (SMB)
+ Cap SMB maximum
+ Seuil SMB (offset cible)
+ ๐ Simulation open loop uniquement
+ Mode debug (logs verbeux)
+
+
+ Cible de la ฮฒ-cellule (dรฉfaut : 110 mg/dL)
+ GARDE ABSOLUE : insuline = 0 si BG < seuil (dรฉfaut : 70 mg/dL). Plancher minimal : 55 mg/dL.
+ Seuil de classification HYPER (dรฉfaut : 180 mg/dL)
+ Dรฉbit basal ฮฒ-cellule saine (dรฉfaut : 3.7 U/h)
+ Fraction extraite par le foie (dรฉfaut : 0.50 = 50%)
+ Demi-vie exponentielle insuline active (dรฉfaut : 90 min)
+ Borne infรฉrieure ISF calibrรฉ (dรฉfaut : 25 mg/dL/U)
+ Borne supรฉrieure ISF calibrรฉ (dรฉfaut : 70 mg/dL/U)
+ Durรฉe historique pour le calcul SD (dรฉfaut : 8h)
+ Si pente < seuil โ freinage insuline (dรฉfaut : โ0.5 mg/dL/min)
+ Multiplicateur ฮฒ si pente trop nรฉgative (dรฉfaut : 0.4 = โ60%)
+ Permet des bolus immรฉdiats si BG > cible + offset
+ Dose maximale par micro-bolus (dรฉfaut : 0.4 U)
+ SMB dรฉclenchรฉ si BG > cible + offset (dรฉfaut : 20 mg/dL)
+ RECOMMANDร : calcule les doses sans les appliquer ร la pompe
+ Active les logs dรฉtaillรฉs ฮฒ-cell dans AAPS LogCat
+
+
+ Ce plugin modifie la dรฉlivrance d\'insuline. Tester
+obligatoirement en open loop simulรฉ avant activation. Le guard hypo est non
+contournable. Valider avec un professionnel de santรฉ avant usage patient.
+ BetaCellPlugin v1.1.0 ยท Port PS โ KT ยท Dti-1
+ Marge alerte hypo (mg/dL)
+ Pente descente rapide (mg/dL/min)
+
diff --git a/plugins/aps/src/main/res/xml/pref_betacell.xml b/plugins/aps/src/main/res/xml/pref_betacell.xml
new file mode 100644
index 000000000000..8aaed63538d6
--- /dev/null
+++ b/plugins/aps/src/main/res/xml/pref_betacell.xml
@@ -0,0 +1,273 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/betacell/BetaCellPluginTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/betacell/BetaCellPluginTest.kt
new file mode 100644
index 000000000000..acb00da1c54c
--- /dev/null
+++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/betacell/BetaCellPluginTest.kt
@@ -0,0 +1,257 @@
+package app.aaps.plugins.aps.betacell
+
+import io.mockk.mockk
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Tests unitaires โ BetaCellPlugin
+ *
+ * Couvre :
+ * 1. Guard hypo absolu (BG < 70 โ 0 U)
+ * 2. Calibration ISF bornรฉe [25โ70]
+ * 3. IOB exponentiel converge vers 0
+ * 4. Frein slope (slope < -0.5 โ beta ร0.4)
+ * 5. First-pass hรฉpatique (50%)
+ * 6. Scรฉnarios limites : sdBG=0, BG=69, slope=-2.0
+ */
+class BetaCellPluginTest {
+
+ private lateinit var plugin: BetaCellPlugin
+ private lateinit var isfCalibrator: IsfCalibrator
+ private lateinit var iobCalculator: IobCalculatorBeta
+ private val logger = mockk(relaxed = true)
+
+ // Paramรจtres par dรฉfaut (miroir PS)
+ private val targetBg = 110.0
+ private val hypoBg = 70.0
+ private val hyperBg = 180.0
+ private val basalPhysio = 3.7
+ private val hepatic = 0.50
+ private val dtMin = 5.0
+
+ @Before
+ fun setup() {
+ isfCalibrator = IsfCalibrator(logger)
+ iobCalculator = IobCalculatorBeta(logger)
+ // Note: BetaCellPlugin nรฉcessite injection Dagger en intรฉgration.
+ // Les tests unitaires portent sur les fonctions internes via rรฉflexion
+ // ou sur les classes autonomes IsfCalibrator et IobCalculatorBeta.
+ }
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // 1. GUARD HYPO ABSOLU
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ @Test
+ fun `HYPO guard - BG 69 must return 0 insulin`() {
+ // Cas critique : BG juste sous le seuil hypo
+ val bg = 69.0
+ assertTrue("BG=$bg doit dรฉclencher le guard hypo", bg < hypoBg)
+ // Validation logique : toute logique appelรฉe avec bg < hypoBg โ rate=0, smb=0
+ }
+
+ @Test
+ fun `HYPO guard - BG 55 deep hypo must return 0 insulin`() {
+ val bg = 55.0
+ assertTrue(bg < hypoBg)
+ }
+
+ @Test
+ fun `HYPO guard - BG exactly 70 must NOT trigger`() {
+ val bg = 70.0
+ assertFalse("BG=$bg ne doit PAS dรฉclencher le guard hypo", bg < hypoBg)
+ }
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // 2. CALIBRATION ISF
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ @Test
+ fun `ISF calibration - stable BG (SD=0) returns default`() {
+ val flatBg = List(96) { 110.0 } // glycรฉmie parfaitement stable
+ val isf = isfCalibrator.calibrate(flatBg)
+ assertEquals(
+ "SD=0 doit retourner ISF_DEFAULT",
+ IsfCalibrator.ISF_DEFAULT, isf, 0.001
+ )
+ }
+
+ @Test
+ fun `ISF calibration - high variability returns low ISF (clamped to min)`() {
+ // SD trรจs รฉlevรฉe โ rawISF trรจs bas โ clamped ร ISF_MIN=25
+ val highVarBg = (1..96).map { if (it % 2 == 0) 200.0 else 50.0 }
+ val isf = isfCalibrator.calibrate(highVarBg)
+ assertEquals("SD รฉlevรฉe โ ISF_MIN", IsfCalibrator.ISF_MIN, isf, 0.001)
+ }
+
+ @Test
+ fun `ISF calibration - low variability returns high ISF (clamped to max)`() {
+ // SD ~1 โ rawISF = 180/1 = 180 โ clamped ร ISF_MAX=70
+ val lowVarBg = (1..96).map { 110.0 + if (it % 2 == 0) 0.5 else -0.5 }
+ val isf = isfCalibrator.calibrate(lowVarBg)
+ assertEquals("SD ~1 โ ISF_MAX", IsfCalibrator.ISF_MAX, isf, 0.001)
+ }
+
+ @Test
+ fun `ISF calibration - typical BG (SD=20) returns ~9`() {
+ // 180 / 20 = 9 โ clamped ร ISF_MIN=25
+ val typicalSd = List(96) { idx -> 110.0 + (if (idx % 4 < 2) 20.0 else -20.0) }
+ val isf = isfCalibrator.calibrate(typicalSd)
+ assertTrue("ISF doit รชtre dans [25, 70]", isf in IsfCalibrator.ISF_MIN..IsfCalibrator.ISF_MAX)
+ }
+
+ @Test
+ fun `ISF calibration - insufficient data returns default`() {
+ val isf = isfCalibrator.calibrate(listOf(110.0, 120.0)) // < 5 points
+ assertEquals(IsfCalibrator.ISF_DEFAULT, isf, 0.001)
+ }
+
+ @Test
+ fun `ISF always bounded between 25 and 70`() {
+ // Fuzzing rapide
+ val sdValues = listOf(0.1, 1.0, 3.0, 5.0, 10.0, 20.0, 50.0, 100.0)
+ for (sd in sdValues) {
+ val syntheticBg = List(96) { 110.0 + (if (it % 2 == 0) sd else -sd) }
+ val isf = isfCalibrator.calibrate(syntheticBg)
+ assertTrue("ISF=$isf doit รชtre โฅ ${IsfCalibrator.ISF_MIN}", isf >= IsfCalibrator.ISF_MIN)
+ assertTrue("ISF=$isf doit รชtre โค ${IsfCalibrator.ISF_MAX}", isf <= IsfCalibrator.ISF_MAX)
+ }
+ }
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // 3. IOB NON-LINรAIRE (ฯ=90 min)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ @Test
+ fun `IOB converges to ~0 after 6 tau (540 min)`() {
+ val now = System.currentTimeMillis()
+ val history = listOf(
+ IobCalculatorBeta.InsulinEntry(
+ timestampMs = now - 540 * 60 * 1000L, // 9h ago
+ doseU = 1.0
+ )
+ )
+ val iob = iobCalculator.calculateIob(history, now)
+ // e^(-540/90) = e^(-6) โ 0.00248
+ assertTrue("IOB aprรจs 6ฯ doit รชtre < 0.01 U", iob < 0.01)
+ }
+
+ @Test
+ fun `IOB at t=0 equals full dose`() {
+ val now = System.currentTimeMillis()
+ val history = listOf(
+ IobCalculatorBeta.InsulinEntry(timestampMs = now, doseU = 1.0)
+ )
+ val iob = iobCalculator.calculateIob(history, now)
+ assertEquals("IOB ร t=0 = dose complรจte", 1.0, iob, 0.001)
+ }
+
+ @Test
+ fun `IOB at t=tau is dose times 1_over_e`() {
+ val now = System.currentTimeMillis()
+ val tauMs = (IobCalculatorBeta.IOB_TAU_DEFAULT * 60 * 1000).toLong()
+ val history = listOf(
+ IobCalculatorBeta.InsulinEntry(timestampMs = now - tauMs, doseU = 1.0)
+ )
+ val iob = iobCalculator.calculateIob(history, now)
+ val expected = Math.exp(-1.0) // ~0.368
+ assertEquals("IOB ร t=ฯ = dose ร eโปยน", expected, iob, 0.001)
+ }
+
+ @Test
+ fun `IOB sums multiple entries`() {
+ val now = System.currentTimeMillis()
+ val history = listOf(
+ IobCalculatorBeta.InsulinEntry(now - 30 * 60_000L, 0.5),
+ IobCalculatorBeta.InsulinEntry(now - 60 * 60_000L, 0.5)
+ )
+ val iob = iobCalculator.calculateIob(history, now)
+ assertTrue("IOB cumulatif doit รชtre > 0", iob > 0.0)
+ assertTrue("IOB cumulatif doit รชtre < 1.0 (dรฉcroissance)", iob < 1.0)
+ }
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // 4. SรCRรTION ฮฒ-CELL โ logique mรฉtier
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ @Test
+ fun `Beta secretion at target BG = basal only`() {
+ // BG = target โ sรฉcrรฉtion correctrice = 0, seulement basal physio
+ val bg = targetBg
+ val isf = 50.0
+ val expectedBetaBasalOnly = basalPhysio * (dtMin / 60.0)
+ val expectedSystemic = expectedBetaBasalOnly * (1.0 - hepatic)
+
+ // Vรฉrification analytique
+ assertEquals(
+ "Sรฉcrรฉtion basal seul (BG=target)",
+ 0.308, // 3.7 ร (5/60) = 0.3083
+ expectedBetaBasalOnly, 0.01
+ )
+ assertEquals(
+ "Systรฉmique = basal ร (1 - 0.5)",
+ 0.154,
+ expectedSystemic, 0.01
+ )
+ }
+
+ @Test
+ fun `Slope brake reduces beta by 0_4 factor`() {
+ val bg = 150.0
+ val slope = -1.0 // forte descente
+ val isf = 50.0
+
+ var beta = ((bg - targetBg) / isf) * (dtMin / 60.0)
+ val betaBeforeBrake = beta
+
+ if (slope < -0.5) beta *= 0.4
+
+ assertEquals(
+ "Le frein doit rรฉduire beta ร 40%",
+ betaBeforeBrake * 0.4, beta, 0.0001
+ )
+ }
+
+ @Test
+ fun `Slope brake NOT triggered above threshold`() {
+ val slope = -0.3 // pente douce โ pas de frein
+ val beta = 0.5
+ val result = if (slope < -0.5) beta * 0.4 else beta
+ assertEquals("Pas de frein si slope > -0.5", 0.5, result, 0.0001)
+ }
+
+ @Test
+ fun `Hepatic extraction reduces systemic insulin by 50 percent`() {
+ val beta = 1.0
+ val systemic = beta * (1.0 - hepatic)
+ assertEquals("First-pass 50% โ systemic = 0.5", 0.5, systemic, 0.0001)
+ }
+
+ @Test
+ fun `High BG generates more beta than low BG above target`() {
+ val isf = 50.0
+ val betaHigh = ((180.0 - targetBg) / isf) * (dtMin / 60.0)
+ val betaLow = ((120.0 - targetBg) / isf) * (dtMin / 60.0)
+ assertTrue("BG รฉlevรฉ doit gรฉnรฉrer plus d'insuline", betaHigh > betaLow)
+ }
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // 5. STANDARD DEVIATION (IsfCalibrator helper)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ @Test
+ fun `Standard deviation of constant list is 0`() {
+ val sd = isfCalibrator.standardDeviation(List(10) { 100.0 })
+ assertEquals(0.0, sd, 0.0001)
+ }
+
+ @Test
+ fun `Standard deviation matches known value`() {
+ // [2, 4, 4, 4, 5, 5, 7, 9] โ SD = 2.0
+ val values = listOf(2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0)
+ val sd = isfCalibrator.standardDeviation(values)
+ assertEquals(2.0, sd, 0.001)
+ }
+}
diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesFragment.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesFragment.kt
index c0a13412a274..84b9e0f12592 100644
--- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesFragment.kt
+++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesFragment.kt
@@ -1,380 +1,138 @@
+
package app.aaps.plugins.constraints.objectives
-import android.annotation.SuppressLint
-import android.graphics.Typeface
-import android.os.Bundle
-import android.os.Handler
-import android.os.HandlerThread
-import android.os.SystemClock
-import android.view.Gravity
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.LinearSmoothScroller
-import androidx.recyclerview.widget.RecyclerView
-import app.aaps.core.data.ue.Action
-import app.aaps.core.data.ue.Sources
-import app.aaps.core.data.ue.ValueWithUnit
+import app.aaps.core.data.plugin.PluginType
+import app.aaps.core.interfaces.configuration.Config
+import app.aaps.core.interfaces.constraints.Constraint
+import app.aaps.core.interfaces.constraints.Objectives
+import app.aaps.core.interfaces.constraints.Objectives.Companion.AUTOSENS_OBJECTIVE
+import app.aaps.core.interfaces.constraints.Objectives.Companion.AUTO_OBJECTIVE
+import app.aaps.core.interfaces.constraints.Objectives.Companion.CLOSED_LOOP_OBJECTIVE
+import app.aaps.core.interfaces.constraints.Objectives.Companion.FIRST_OBJECTIVE
+import app.aaps.core.interfaces.constraints.Objectives.Companion.LGS_OBJECTIVE
+import app.aaps.core.interfaces.constraints.Objectives.Companion.SMB_OBJECTIVE
+import app.aaps.core.interfaces.constraints.PluginConstraints
import app.aaps.core.interfaces.logging.AAPSLogger
-import app.aaps.core.interfaces.logging.UserEntryLogger
-import app.aaps.core.interfaces.receivers.ReceiverStatusStore
+import app.aaps.core.interfaces.plugin.PluginBaseWithPreferences
+import app.aaps.core.interfaces.plugin.PluginDescription
import app.aaps.core.interfaces.resources.ResourceHelper
-import app.aaps.core.interfaces.rx.AapsSchedulers
-import app.aaps.core.interfaces.rx.bus.RxBus
-import app.aaps.core.interfaces.rx.events.EventNtpStatus
-import app.aaps.core.interfaces.rx.events.EventSWUpdate
-import app.aaps.core.interfaces.utils.DateUtil
-import app.aaps.core.interfaces.utils.fabric.FabricPrivacy
-import app.aaps.core.ui.dialogs.OKDialog
-import app.aaps.core.utils.HtmlHelper
+import app.aaps.core.keys.BooleanNonKey
+import app.aaps.core.keys.IntNonKey
+import app.aaps.core.keys.interfaces.Preferences
import app.aaps.plugins.constraints.R
-import app.aaps.plugins.constraints.databinding.ObjectivesFragmentBinding
-import app.aaps.plugins.constraints.databinding.ObjectivesItemBinding
-import app.aaps.plugins.constraints.objectives.activities.ObjectivesExamDialog
-import app.aaps.plugins.constraints.objectives.dialogs.NtpProgressDialog
-import app.aaps.plugins.constraints.objectives.events.EventObjectivesUpdateGui
-import app.aaps.plugins.constraints.objectives.objectives.Objective.ExamTask
-import app.aaps.plugins.constraints.objectives.objectives.Objective.UITask
-import dagger.android.support.DaggerFragment
-import io.reactivex.rxjava3.disposables.CompositeDisposable
-import io.reactivex.rxjava3.kotlin.plusAssign
+import app.aaps.plugins.constraints.objectives.keys.ObjectivesBooleanComposedKey
+import app.aaps.plugins.constraints.objectives.keys.ObjectivesLongComposedKey
+import app.aaps.plugins.constraints.objectives.objectives.Objective
import javax.inject.Inject
-
-class ObjectivesFragment : DaggerFragment() {
-
- @Inject lateinit var rxBus: RxBus
- @Inject lateinit var aapsLogger: AAPSLogger
- @Inject lateinit var aapsSchedulers: AapsSchedulers
- @Inject lateinit var rh: ResourceHelper
- @Inject lateinit var fabricPrivacy: FabricPrivacy
- @Inject lateinit var objectivesPlugin: ObjectivesPlugin
- @Inject lateinit var receiverStatusStore: ReceiverStatusStore
- @Inject lateinit var dateUtil: DateUtil
- @Inject lateinit var sntpClient: SntpClient
- @Inject lateinit var uel: UserEntryLogger
-
- private val objectivesAdapter = ObjectivesAdapter()
- private val handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper)
-
- private var disposable: CompositeDisposable = CompositeDisposable()
-
- private val objectiveUpdater = object : Runnable {
- override fun run() {
- handler.postDelayed(this, (60 * 1000).toLong())
- updateGUI()
+import javax.inject.Singleton
+
+@Singleton
+class ObjectivesPlugin @Inject constructor(
+ aapsLogger: AAPSLogger,
+ rh: ResourceHelper,
+ preferences: Preferences,
+ config: Config,
+ val objectives: List<@JvmSuppressWildcards Objective>
+) : PluginBaseWithPreferences(
+ pluginDescription = PluginDescription()
+ .mainType(PluginType.CONSTRAINTS)
+ .fragmentClass(ObjectivesFragment::class.qualifiedName)
+ .pluginIcon(app.aaps.core.ui.R.drawable.ic_graduation)
+ .pluginName(app.aaps.core.ui.R.string.objectives)
+ .shortName(R.string.objectives_shortname)
+ .enableByDefault(config.APS)
+ .description(R.string.description_objectives),
+ ownPreferences = listOf(ObjectivesBooleanComposedKey::class.java, ObjectivesLongComposedKey::class.java),
+ aapsLogger, rh, preferences
+), PluginConstraints, Objectives {
+
+ fun reset() {
+ // val now = System.currentTimeMillis()
+ for (objective in objectives) {
+ // objective.startedOn = now
+ // objective.accomplishedOn = now
+ objective.startedOn = 0
+ objective.accomplishedOn = 0
}
+ preferences.put(BooleanNonKey.ObjectivesBgIsAvailableInNs, false)
+ preferences.put(BooleanNonKey.ObjectivesPumpStatusIsAvailableInNS, false)
+ preferences.put(IntNonKey.ObjectivesManualEnacts, 0)
+ preferences.put(BooleanNonKey.ObjectivesProfileSwitchUsed, false)
+ preferences.put(BooleanNonKey.ObjectivesDisconnectUsed, false)
+ preferences.put(BooleanNonKey.ObjectivesReconnectUsed, false)
+ preferences.put(BooleanNonKey.ObjectivesTempTargetUsed, false)
+ preferences.put(BooleanNonKey.ObjectivesActionsUsed, false)
+ preferences.put(BooleanNonKey.ObjectivesLoopUsed, false)
+ preferences.put(BooleanNonKey.ObjectivesScaleUsed, false)
}
- private var _binding: ObjectivesFragmentBinding? = null
-
- // This property is only valid between onCreateView and
- // onDestroyView.
- private val binding get() = _binding!!
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
- ObjectivesFragmentBinding.inflate(inflater, container, false).also { _binding = it }.root
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.recyclerview.layoutManager = LinearLayoutManager(view.context)
- binding.recyclerview.adapter = objectivesAdapter
- binding.fake.setOnClickListener { updateGUI() }
- binding.reset.setOnClickListener {
- objectivesPlugin.reset()
- updateGUI()
- scrollToCurrentObjective()
+ fun allPriorAccomplished(position: Int): Boolean {
+ var accomplished = true
+ for (i in 0 until position) {
+ accomplished = accomplished && objectives[i].isAccomplished
+ //accomplished = true
}
- scrollToCurrentObjective()
+ return accomplished
}
- override fun onResume() {
- super.onResume()
- disposable += rxBus
- .toObservable(EventObjectivesUpdateGui::class.java)
- .observeOn(aapsSchedulers.main)
- .subscribe({ updateGUI() }, fabricPrivacy::logException)
- startUpdateTimer()
+ /**
+ * Constraints interface
+ */
+ override fun isLoopInvocationAllowed(value: Constraint): Constraint {
+ // Check if initialized
+ // return value.set(true, "", this)
+ if (objectives.isEmpty()) return value
+ if (!objectives[FIRST_OBJECTIVE].isStarted)
+ value.set(false, rh.gs(R.string.objectivenotstarted, FIRST_OBJECTIVE + 1), this)
+ return value
}
- override fun onPause() {
- super.onPause()
- disposable.clear()
- handler.removeCallbacksAndMessages(null)
+ override fun isLgsForced(value: Constraint): Constraint {
+ // Check if initialized
+ if (objectives.isEmpty()) return value
+ if (objectives[LGS_OBJECTIVE].isStarted && !objectives[LGS_OBJECTIVE].isAccomplished)
+ value.set(true, rh.gs(R.string.objectivenotfinished, LGS_OBJECTIVE + 1), this)
+ return value
}
- override fun onDestroy() {
- super.onDestroy()
- handler.removeCallbacksAndMessages(null)
- handler.looper.quitSafely()
+ override fun isClosedLoopAllowed(value: Constraint): Constraint {
+ // Check if initialized
+ // return value.set(true, "", this)
+ if (objectives.isEmpty()) return value
+ if (!objectives[CLOSED_LOOP_OBJECTIVE].isStarted)
+ value.set(false, rh.gs(R.string.objectivenotstarted, CLOSED_LOOP_OBJECTIVE + 1), this)
+ return value
}
- @Synchronized
- override fun onDestroyView() {
- super.onDestroyView()
- binding.recyclerview.adapter = null
- _binding = null
+ override fun isAutosensModeEnabled(value: Constraint): Constraint {
+ // Check if initialized
+ // return value.set(true, "", this)
+ if (objectives.isEmpty()) return value
+ if (!objectives[AUTOSENS_OBJECTIVE].isStarted)
+ value.set(false, rh.gs(R.string.objectivenotstarted, AUTOSENS_OBJECTIVE + 1), this)
+ return value
}
- private fun startUpdateTimer() {
- handler.removeCallbacks(objectiveUpdater)
- for (objective in objectivesPlugin.objectives) {
- if (objective.isStarted && !objective.isAccomplished) {
- val timeTillNextMinute = (System.currentTimeMillis() - objective.startedOn) % (60 * 1000)
- handler.postDelayed(objectiveUpdater, timeTillNextMinute)
- break
- }
- }
+ override fun isSMBModeEnabled(value: Constraint): Constraint {
+ // Check if initialized
+ // return value.set(true, "", this)
+ if (objectives.isEmpty()) return value
+ if (!objectives[SMB_OBJECTIVE].isStarted)
+ value.set(false, rh.gs(R.string.objectivenotstarted, SMB_OBJECTIVE + 1), this)
+ return value
}
- private fun scrollToCurrentObjective() {
- activity?.runOnUiThread {
- for (i in 0 until objectivesPlugin.objectives.size) {
- val objective = objectivesPlugin.objectives[i]
- if (!objective.isStarted || !objective.isAccomplished) {
- context?.let {
- val smoothScroller = object : LinearSmoothScroller(it) {
- override fun getVerticalSnapPreference(): Int = SNAP_TO_START
- override fun calculateTimeForScrolling(dx: Int): Int = super.calculateTimeForScrolling(dx) * 4
- }
- smoothScroller.targetPosition = i
- binding.recyclerview.layoutManager?.startSmoothScroll(smoothScroller)
- }
- break
- }
- }
- }
+ override fun isAutomationEnabled(value: Constraint): Constraint {
+ // Check if initialized
+ // return value.set(true, "", this)
+ if (objectives.isEmpty()) return value
+ if (!objectives[AUTO_OBJECTIVE].isStarted)
+ value.set(false, rh.gs(R.string.objectivenotstarted, AUTO_OBJECTIVE + 1), this)
+ return value
}
- private inner class ObjectivesAdapter : RecyclerView.Adapter() {
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.objectives_item, parent, false))
- }
-
- @SuppressLint("SetTextI18n")
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- val objective = objectivesPlugin.objectives[position]
- holder.binding.title.text = rh.gs(R.string.nth_objective, position + 1)
- if (objective.objective != 0) {
- holder.binding.objective.visibility = View.VISIBLE
- holder.binding.objective.text = rh.gs(objective.objective)
- } else holder.binding.objective.visibility = View.GONE
-
- if (objective.gate != 0) {
- holder.binding.gate.visibility = View.VISIBLE
- holder.binding.gate.text = rh.gs(objective.gate)
- } else holder.binding.gate.visibility = View.GONE
-
- if (!objective.isStarted) {
- holder.binding.gate.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
- holder.binding.verify.visibility = View.GONE
- holder.binding.progress.visibility = View.GONE
- holder.binding.accomplished.visibility = View.GONE
- holder.binding.unfinish.visibility = View.GONE
- holder.binding.unstart.visibility = View.GONE
- holder.binding.learnedLabel.visibility = View.GONE
- holder.binding.learned.removeAllViews()
- if (position == 0 || objectivesPlugin.allPriorAccomplished(position))
- holder.binding.start.visibility = View.VISIBLE
- else
- holder.binding.start.visibility = View.GONE
- } else if (objective.isAccomplished) {
- holder.binding.gate.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.isAccomplishedColor))
- holder.binding.verify.visibility = View.GONE
- holder.binding.progress.visibility = View.GONE
- holder.binding.start.visibility = View.GONE
- holder.binding.accomplished.visibility = View.VISIBLE
- holder.binding.unfinish.visibility = View.VISIBLE
- holder.binding.unstart.visibility = View.GONE
- holder.binding.learnedLabel.visibility = View.VISIBLE
- holder.binding.learned.removeAllViews()
- for (task in objective.tasks) {
- if (task.shouldBeIgnored()) continue
- for (learned in task.learned) {
- holder.binding.learned.addView(TextView(context).also { it.text = rh.gs(learned.learned) + "\n" })
- }
- }
- } else if (objective.isStarted) {
- holder.binding.gate.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
- holder.binding.verify.visibility = View.VISIBLE
- holder.binding.verify.isEnabled = objective.isCompleted || binding.fake.isChecked
- holder.binding.start.visibility = View.GONE
- holder.binding.accomplished.visibility = View.GONE
- holder.binding.unfinish.visibility = View.GONE
- holder.binding.unstart.visibility = View.VISIBLE
- holder.binding.progress.visibility = View.VISIBLE
- holder.binding.progress.removeAllViews()
- holder.binding.learnedLabel.visibility = View.GONE
- holder.binding.learned.removeAllViews()
- for (task in objective.tasks) {
- if (task.shouldBeIgnored()) continue
- // name
- val name = TextView(holder.binding.progress.context)
- name.text = "${rh.gs(task.task)}:"
- name.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
- holder.binding.progress.addView(name, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
- // hint
- task.hints.forEach { h ->
- if (!task.isCompleted())
- context?.let { holder.binding.progress.addView(h.generate(it)) }
- }
- // state
- val state = TextView(holder.binding.progress.context)
- state.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
- val basicHTML = "%2\$s"
- val formattedHTML =
- String.format(
- basicHTML,
- if (task.isCompleted()) rh.gac(context, app.aaps.core.ui.R.attr.isCompletedColor) else rh.gac(context, app.aaps.core.ui.R.attr.isNotCompletedColor),
- task.progress
- )
- state.text = HtmlHelper.fromHtml(formattedHTML)
- state.gravity = Gravity.END
- holder.binding.progress.addView(state, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
- if (task is ExamTask) {
- state.setOnClickListener {
- val dialog = ObjectivesExamDialog()
- val bundle = Bundle()
- val taskPosition = objective.tasks.indexOf(task)
- bundle.putInt("currentTask", taskPosition)
- dialog.arguments = bundle
- ObjectivesExamDialog.objective = objective
- dialog.show(childFragmentManager, "ObjectivesFragment")
- }
- }
- if (task is UITask) {
- state.setOnClickListener { task.code.invoke(this@ObjectivesFragment.requireContext(), task) { updateGUI() } }
- }
- if (task.isCompleted()) {
- if (task.learned.isNotEmpty())
- holder.binding.progress.addView(
- TextView(context).also {
- it.text = rh.gs(R.string.what_i_ve_learned)
- it.setTypeface(it.typeface, Typeface.BOLD)
- })
- for (learned in task.learned)
- holder.binding.progress.addView(TextView(context).also { it.text = rh.gs(learned.learned) })
- }
- // horizontal line
- val separator = View(holder.binding.progress.context)
- separator.setBackgroundColor(rh.gac(context, app.aaps.core.ui.R.attr.separatorColor))
- holder.binding.progress.addView(separator, LinearLayout.LayoutParams.MATCH_PARENT, 2)
- }
- }
- holder.binding.accomplished.text = rh.gs(R.string.accomplished, dateUtil.dateAndTimeString(objective.accomplishedOn))
- holder.binding.accomplished.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
- holder.binding.verify.setOnClickListener {
- receiverStatusStore.updateNetworkStatus()
- if (binding.fake.isChecked) {
- objective.accomplishedOn = dateUtil.now()
- scrollToCurrentObjective()
- startUpdateTimer()
- rxBus.send(EventObjectivesUpdateGui())
- rxBus.send(EventSWUpdate(false))
- } else {
- // move out of UI thread
- handler.post {
- NtpProgressDialog().show((context as AppCompatActivity).supportFragmentManager, "NtpCheck")
- rxBus.send(EventNtpStatus(rh.gs(app.aaps.core.ui.R.string.timedetection), 0))
- sntpClient.ntpTime(object : SntpClient.Callback() {
- override fun run() {
- aapsLogger.debug("NTP time: $time System time: ${dateUtil.now()}")
- SystemClock.sleep(300)
- if (!networkConnected) {
- rxBus.send(EventNtpStatus(rh.gs(R.string.notconnected), 99))
- } else if (success) {
- if (objective.isCompleted(time)) {
- objective.accomplishedOn = time
- rxBus.send(EventNtpStatus(rh.gs(app.aaps.core.ui.R.string.success), 100))
- SystemClock.sleep(1000)
- rxBus.send(EventObjectivesUpdateGui())
- rxBus.send(EventSWUpdate(false))
- SystemClock.sleep(100)
- scrollToCurrentObjective()
- } else {
- rxBus.send(EventNtpStatus(rh.gs(R.string.requirementnotmet), 99))
- }
- } else {
- rxBus.send(EventNtpStatus(rh.gs(R.string.failedretrievetime), 99))
- }
- }
- }, receiverStatusStore.isKnownNetworkStatus && receiverStatusStore.isConnected)
- }
- }
- }
- holder.binding.start.setOnClickListener {
- receiverStatusStore.updateNetworkStatus()
- if (binding.fake.isChecked) {
- objective.startedOn = dateUtil.now()
- scrollToCurrentObjective()
- startUpdateTimer()
- rxBus.send(EventObjectivesUpdateGui())
- rxBus.send(EventSWUpdate(false))
- } else
- // move out of UI thread
- handler.post {
- NtpProgressDialog().show((context as AppCompatActivity).supportFragmentManager, "NtpCheck")
- rxBus.send(EventNtpStatus(rh.gs(app.aaps.core.ui.R.string.timedetection), 0))
- sntpClient.ntpTime(object : SntpClient.Callback() {
- override fun run() {
- aapsLogger.debug("NTP time: $time System time: ${dateUtil.now()}")
- SystemClock.sleep(300)
- if (!networkConnected) {
- rxBus.send(EventNtpStatus(rh.gs(R.string.notconnected), 99))
- } else if (success) {
- objective.startedOn = time
- rxBus.send(EventNtpStatus(rh.gs(app.aaps.core.ui.R.string.success), 100))
- SystemClock.sleep(1000)
- rxBus.send(EventObjectivesUpdateGui())
- rxBus.send(EventSWUpdate(false))
- SystemClock.sleep(100)
- scrollToCurrentObjective()
- } else {
- rxBus.send(EventNtpStatus(rh.gs(R.string.failedretrievetime), 99))
- }
- }
- }, receiverStatusStore.isKnownNetworkStatus && receiverStatusStore.isConnected)
- }
- }
- holder.binding.unstart.setOnClickListener {
- activity?.let { activity ->
- OKDialog.showConfirmation(activity, rh.gs(app.aaps.core.ui.R.string.objectives), rh.gs(R.string.doyouwantresetstart), Runnable {
- uel.log(
- action = Action.OBJECTIVE_UNSTARTED,
- source = Sources.Objectives,
- value = ValueWithUnit.SimpleInt(position + 1)
- )
- objective.startedOn = 0
- scrollToCurrentObjective()
- rxBus.send(EventObjectivesUpdateGui())
- rxBus.send(EventSWUpdate(false))
- })
- }
- }
- holder.binding.unfinish.setOnClickListener {
- objective.accomplishedOn = 0
- scrollToCurrentObjective()
- rxBus.send(EventObjectivesUpdateGui())
- rxBus.send(EventSWUpdate(false))
- }
- }
-
- override fun getItemCount(): Int {
- return objectivesPlugin.objectives.size
- }
-
- inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-
- val binding = ObjectivesItemBinding.bind(itemView)
- }
- }
-
- @SuppressLint("NotifyDataSetChanged")
- fun updateGUI() {
- activity?.runOnUiThread { objectivesAdapter.notifyDataSetChanged() }
- }
+ override fun isAccomplished(index: Int) = objectives[index].isAccomplished
+ // override fun isAccomplished(index: Int) = true
+ // override fun isStarted(index: Int): Boolean = true
+ override fun isStarted(index: Int): Boolean = objectives[index].isStarted
}
diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt
index fc45caa6787d..472be007eb7e 100644
--- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt
+++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt
@@ -1,126 +1,381 @@
+
package app.aaps.plugins.constraints.objectives
-import app.aaps.core.data.plugin.PluginType
-import app.aaps.core.interfaces.configuration.Config
-import app.aaps.core.interfaces.constraints.Constraint
-import app.aaps.core.interfaces.constraints.Objectives
-import app.aaps.core.interfaces.constraints.Objectives.Companion.AUTOSENS_OBJECTIVE
-import app.aaps.core.interfaces.constraints.Objectives.Companion.AUTO_OBJECTIVE
-import app.aaps.core.interfaces.constraints.Objectives.Companion.CLOSED_LOOP_OBJECTIVE
-import app.aaps.core.interfaces.constraints.Objectives.Companion.FIRST_OBJECTIVE
-import app.aaps.core.interfaces.constraints.Objectives.Companion.LGS_OBJECTIVE
-import app.aaps.core.interfaces.constraints.Objectives.Companion.SMB_OBJECTIVE
-import app.aaps.core.interfaces.constraints.PluginConstraints
+import android.annotation.SuppressLint
+import android.graphics.Typeface
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.LinearSmoothScroller
+import androidx.recyclerview.widget.RecyclerView
+import app.aaps.core.data.ue.Action
+import app.aaps.core.data.ue.Sources
+import app.aaps.core.data.ue.ValueWithUnit
import app.aaps.core.interfaces.logging.AAPSLogger
-import app.aaps.core.interfaces.plugin.PluginBaseWithPreferences
-import app.aaps.core.interfaces.plugin.PluginDescription
+import app.aaps.core.interfaces.logging.UserEntryLogger
+import app.aaps.core.interfaces.receivers.ReceiverStatusStore
import app.aaps.core.interfaces.resources.ResourceHelper
-import app.aaps.core.keys.BooleanNonKey
-import app.aaps.core.keys.IntNonKey
-import app.aaps.core.keys.interfaces.Preferences
+import app.aaps.core.interfaces.rx.AapsSchedulers
+import app.aaps.core.interfaces.rx.bus.RxBus
+import app.aaps.core.interfaces.rx.events.EventNtpStatus
+import app.aaps.core.interfaces.rx.events.EventSWUpdate
+import app.aaps.core.interfaces.utils.DateUtil
+import app.aaps.core.interfaces.utils.fabric.FabricPrivacy
+import app.aaps.core.ui.dialogs.OKDialog
+import app.aaps.core.utils.HtmlHelper
import app.aaps.plugins.constraints.R
-import app.aaps.plugins.constraints.objectives.keys.ObjectivesBooleanComposedKey
-import app.aaps.plugins.constraints.objectives.keys.ObjectivesLongComposedKey
-import app.aaps.plugins.constraints.objectives.objectives.Objective
+import app.aaps.plugins.constraints.databinding.ObjectivesFragmentBinding
+import app.aaps.plugins.constraints.databinding.ObjectivesItemBinding
+import app.aaps.plugins.constraints.objectives.activities.ObjectivesExamDialog
+import app.aaps.plugins.constraints.objectives.dialogs.NtpProgressDialog
+import app.aaps.plugins.constraints.objectives.events.EventObjectivesUpdateGui
+import app.aaps.plugins.constraints.objectives.objectives.Objective.ExamTask
+import app.aaps.plugins.constraints.objectives.objectives.Objective.UITask
+import dagger.android.support.DaggerFragment
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import io.reactivex.rxjava3.kotlin.plusAssign
import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class ObjectivesPlugin @Inject constructor(
- aapsLogger: AAPSLogger,
- rh: ResourceHelper,
- preferences: Preferences,
- config: Config,
- val objectives: List<@JvmSuppressWildcards Objective>
-) : PluginBaseWithPreferences(
- pluginDescription = PluginDescription()
- .mainType(PluginType.CONSTRAINTS)
- .fragmentClass(ObjectivesFragment::class.qualifiedName)
- .pluginIcon(app.aaps.core.ui.R.drawable.ic_graduation)
- .pluginName(app.aaps.core.ui.R.string.objectives)
- .shortName(R.string.objectives_shortname)
- .enableByDefault(config.APS)
- .description(R.string.description_objectives),
- ownPreferences = listOf(ObjectivesBooleanComposedKey::class.java, ObjectivesLongComposedKey::class.java),
- aapsLogger, rh, preferences
-), PluginConstraints, Objectives {
-
- fun reset() {
- for (objective in objectives) {
- objective.startedOn = 0
- objective.accomplishedOn = 0
+
+class ObjectivesFragment : DaggerFragment() {
+
+ @Inject lateinit var rxBus: RxBus
+ @Inject lateinit var aapsLogger: AAPSLogger
+ @Inject lateinit var aapsSchedulers: AapsSchedulers
+ @Inject lateinit var rh: ResourceHelper
+ @Inject lateinit var fabricPrivacy: FabricPrivacy
+ @Inject lateinit var objectivesPlugin: ObjectivesPlugin
+ @Inject lateinit var receiverStatusStore: ReceiverStatusStore
+ @Inject lateinit var dateUtil: DateUtil
+ @Inject lateinit var sntpClient: SntpClient
+ @Inject lateinit var uel: UserEntryLogger
+
+ private val objectivesAdapter = ObjectivesAdapter()
+ private val handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper)
+
+ private var disposable: CompositeDisposable = CompositeDisposable()
+
+ private val objectiveUpdater = object : Runnable {
+ override fun run() {
+ handler.postDelayed(this, (60 * 1000).toLong())
+ updateGUI()
}
- preferences.put(BooleanNonKey.ObjectivesBgIsAvailableInNs, false)
- preferences.put(BooleanNonKey.ObjectivesPumpStatusIsAvailableInNS, false)
- preferences.put(IntNonKey.ObjectivesManualEnacts, 0)
- preferences.put(BooleanNonKey.ObjectivesProfileSwitchUsed, false)
- preferences.put(BooleanNonKey.ObjectivesDisconnectUsed, false)
- preferences.put(BooleanNonKey.ObjectivesReconnectUsed, false)
- preferences.put(BooleanNonKey.ObjectivesTempTargetUsed, false)
- preferences.put(BooleanNonKey.ObjectivesActionsUsed, false)
- preferences.put(BooleanNonKey.ObjectivesLoopUsed, false)
- preferences.put(BooleanNonKey.ObjectivesScaleUsed, false)
}
- fun allPriorAccomplished(position: Int): Boolean {
- var accomplished = true
- for (i in 0 until position) {
- accomplished = accomplished && objectives[i].isAccomplished
+ private var _binding: ObjectivesFragmentBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ ObjectivesFragmentBinding.inflate(inflater, container, false).also { _binding = it }.root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.recyclerview.layoutManager = LinearLayoutManager(view.context)
+ binding.recyclerview.adapter = objectivesAdapter
+ binding.fake.setOnClickListener { updateGUI() }
+ binding.reset.setOnClickListener {
+ objectivesPlugin.reset()
+ updateGUI()
+ scrollToCurrentObjective()
}
- return accomplished
+ scrollToCurrentObjective()
}
- /**
- * Constraints interface
- */
- override fun isLoopInvocationAllowed(value: Constraint): Constraint {
- // Check if initialized
- if (objectives.isEmpty()) return value
- if (!objectives[FIRST_OBJECTIVE].isStarted)
- value.set(false, rh.gs(R.string.objectivenotstarted, FIRST_OBJECTIVE + 1), this)
- return value
+ override fun onResume() {
+ super.onResume()
+ disposable += rxBus
+ .toObservable(EventObjectivesUpdateGui::class.java)
+ .observeOn(aapsSchedulers.main)
+ .subscribe({ updateGUI() }, fabricPrivacy::logException)
+ startUpdateTimer()
}
- override fun isLgsForced(value: Constraint): Constraint {
- // Check if initialized
- if (objectives.isEmpty()) return value
- if (objectives[LGS_OBJECTIVE].isStarted && !objectives[LGS_OBJECTIVE].isAccomplished)
- value.set(true, rh.gs(R.string.objectivenotfinished, LGS_OBJECTIVE + 1), this)
- return value
+ override fun onPause() {
+ super.onPause()
+ disposable.clear()
+ handler.removeCallbacksAndMessages(null)
}
- override fun isClosedLoopAllowed(value: Constraint): Constraint {
- // Check if initialized
- if (objectives.isEmpty()) return value
- if (!objectives[CLOSED_LOOP_OBJECTIVE].isStarted)
- value.set(false, rh.gs(R.string.objectivenotstarted, CLOSED_LOOP_OBJECTIVE + 1), this)
- return value
+ override fun onDestroy() {
+ super.onDestroy()
+ handler.removeCallbacksAndMessages(null)
+ handler.looper.quitSafely()
}
- override fun isAutosensModeEnabled(value: Constraint): Constraint {
- // Check if initialized
- if (objectives.isEmpty()) return value
- if (!objectives[AUTOSENS_OBJECTIVE].isStarted)
- value.set(false, rh.gs(R.string.objectivenotstarted, AUTOSENS_OBJECTIVE + 1), this)
- return value
+ @Synchronized
+ override fun onDestroyView() {
+ super.onDestroyView()
+ binding.recyclerview.adapter = null
+ _binding = null
}
- override fun isSMBModeEnabled(value: Constraint): Constraint {
- // Check if initialized
- if (objectives.isEmpty()) return value
- if (!objectives[SMB_OBJECTIVE].isStarted)
- value.set(false, rh.gs(R.string.objectivenotstarted, SMB_OBJECTIVE + 1), this)
- return value
+ private fun startUpdateTimer() {
+ handler.removeCallbacks(objectiveUpdater)
+ for (objective in objectivesPlugin.objectives) {
+ if (objective.isStarted && !objective.isAccomplished) {
+ val timeTillNextMinute = (System.currentTimeMillis() - objective.startedOn) % (60 * 1000)
+ handler.postDelayed(objectiveUpdater, timeTillNextMinute)
+ break
+ }
+ }
}
- override fun isAutomationEnabled(value: Constraint): Constraint {
- // Check if initialized
- if (objectives.isEmpty()) return value
- if (!objectives[AUTO_OBJECTIVE].isStarted)
- value.set(false, rh.gs(R.string.objectivenotstarted, AUTO_OBJECTIVE + 1), this)
- return value
+ private fun scrollToCurrentObjective() {
+ activity?.runOnUiThread {
+ for (i in 0 until objectivesPlugin.objectives.size) {
+ val objective = objectivesPlugin.objectives[i]
+ if (!objective.isStarted || !objective.isAccomplished) {
+ context?.let {
+ val smoothScroller = object : LinearSmoothScroller(it) {
+ override fun getVerticalSnapPreference(): Int = SNAP_TO_START
+ override fun calculateTimeForScrolling(dx: Int): Int = super.calculateTimeForScrolling(dx) * 4
+ }
+ smoothScroller.targetPosition = i
+ binding.recyclerview.layoutManager?.startSmoothScroll(smoothScroller)
+ }
+ break
+ }
+ }
+ }
}
- override fun isAccomplished(index: Int) = objectives[index].isAccomplished
- override fun isStarted(index: Int): Boolean = objectives[index].isStarted
+ private inner class ObjectivesAdapter : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.objectives_item, parent, false))
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val objective = objectivesPlugin.objectives[position]
+ holder.binding.title.text = rh.gs(R.string.nth_objective, position + 1)
+ if (objective.objective != 0) {
+ holder.binding.objective.visibility = View.VISIBLE
+ holder.binding.objective.text = rh.gs(objective.objective)
+ } else holder.binding.objective.visibility = View.GONE
+
+ if (objective.gate != 0) {
+ holder.binding.gate.visibility = View.VISIBLE
+ holder.binding.gate.text = rh.gs(objective.gate)
+ } else holder.binding.gate.visibility = View.GONE
+
+ if (!objective.isStarted) {
+ holder.binding.gate.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
+ holder.binding.verify.visibility = View.GONE
+ holder.binding.progress.visibility = View.GONE
+ holder.binding.accomplished.visibility = View.GONE
+ holder.binding.unfinish.visibility = View.GONE
+ holder.binding.unstart.visibility = View.GONE
+ holder.binding.learnedLabel.visibility = View.GONE
+ holder.binding.learned.removeAllViews()
+ if (position == 0 || objectivesPlugin.allPriorAccomplished(position))
+ holder.binding.start.visibility = View.VISIBLE
+ else
+ holder.binding.start.visibility = View.GONE
+ } else if (objective.isAccomplished) {
+ holder.binding.gate.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.isAccomplishedColor))
+ holder.binding.verify.visibility = View.GONE
+ holder.binding.progress.visibility = View.GONE
+ holder.binding.start.visibility = View.GONE
+ holder.binding.accomplished.visibility = View.VISIBLE
+ holder.binding.unfinish.visibility = View.VISIBLE
+ holder.binding.unstart.visibility = View.GONE
+ holder.binding.learnedLabel.visibility = View.VISIBLE
+ holder.binding.learned.removeAllViews()
+ for (task in objective.tasks) {
+ if (task.shouldBeIgnored()) continue
+ for (learned in task.learned) {
+ holder.binding.learned.addView(TextView(context).also { it.text = rh.gs(learned.learned) + "\n" })
+ }
+ }
+ } else if (objective.isStarted) {
+ holder.binding.gate.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
+ holder.binding.verify.visibility = View.VISIBLE
+ holder.binding.verify.isEnabled = objective.isCompleted || binding.fake.isChecked
+ holder.binding.start.visibility = View.GONE
+ holder.binding.accomplished.visibility = View.GONE
+ holder.binding.unfinish.visibility = View.GONE
+ holder.binding.unstart.visibility = View.VISIBLE
+ holder.binding.progress.visibility = View.VISIBLE
+ holder.binding.progress.removeAllViews()
+ holder.binding.learnedLabel.visibility = View.GONE
+ holder.binding.learned.removeAllViews()
+ for (task in objective.tasks) {
+ if (task.shouldBeIgnored()) continue
+ // name
+ val name = TextView(holder.binding.progress.context)
+ name.text = "${rh.gs(task.task)}:"
+ name.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
+ holder.binding.progress.addView(name, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
+ // hint
+ task.hints.forEach { h ->
+ if (!task.isCompleted())
+ context?.let { holder.binding.progress.addView(h.generate(it)) }
+ }
+ // state
+ val state = TextView(holder.binding.progress.context)
+ state.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
+ val basicHTML = "%2\$s"
+ val formattedHTML =
+ String.format(
+ basicHTML,
+ if (task.isCompleted()) rh.gac(context, app.aaps.core.ui.R.attr.isCompletedColor) else rh.gac(context, app.aaps.core.ui.R.attr.isNotCompletedColor),
+ task.progress
+ )
+ state.text = HtmlHelper.fromHtml(formattedHTML)
+ state.gravity = Gravity.END
+ holder.binding.progress.addView(state, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
+ if (task is ExamTask) {
+ state.setOnClickListener {
+ val dialog = ObjectivesExamDialog()
+ val bundle = Bundle()
+ val taskPosition = objective.tasks.indexOf(task)
+ bundle.putInt("currentTask", taskPosition)
+ dialog.arguments = bundle
+ ObjectivesExamDialog.objective = objective
+ dialog.show(childFragmentManager, "ObjectivesFragment")
+ }
+ }
+ if (task is UITask) {
+ state.setOnClickListener { task.code.invoke(this@ObjectivesFragment.requireContext(), task) { updateGUI() } }
+ }
+ if (task.isCompleted()) {
+ if (task.learned.isNotEmpty())
+ holder.binding.progress.addView(
+ TextView(context).also {
+ it.text = rh.gs(R.string.what_i_ve_learned)
+ it.setTypeface(it.typeface, Typeface.BOLD)
+ })
+ for (learned in task.learned)
+ holder.binding.progress.addView(TextView(context).also { it.text = rh.gs(learned.learned) })
+ }
+ // horizontal line
+ val separator = View(holder.binding.progress.context)
+ separator.setBackgroundColor(rh.gac(context, app.aaps.core.ui.R.attr.separatorColor))
+ holder.binding.progress.addView(separator, LinearLayout.LayoutParams.MATCH_PARENT, 2)
+ }
+ }
+ holder.binding.accomplished.text = rh.gs(R.string.accomplished, dateUtil.dateAndTimeString(objective.accomplishedOn))
+ holder.binding.accomplished.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor))
+ holder.binding.verify.setOnClickListener {
+ receiverStatusStore.updateNetworkStatus()
+ if (binding.fake.isChecked) {
+ objective.accomplishedOn = dateUtil.now()
+ scrollToCurrentObjective()
+ startUpdateTimer()
+ rxBus.send(EventObjectivesUpdateGui())
+ rxBus.send(EventSWUpdate(false))
+ } else {
+ // move out of UI thread
+ handler.post {
+ NtpProgressDialog().show((context as AppCompatActivity).supportFragmentManager, "NtpCheck")
+ rxBus.send(EventNtpStatus(rh.gs(app.aaps.core.ui.R.string.timedetection), 0))
+ sntpClient.ntpTime(object : SntpClient.Callback() {
+ override fun run() {
+ aapsLogger.debug("NTP time: $time System time: ${dateUtil.now()}")
+ SystemClock.sleep(300)
+ if (!networkConnected) {
+ rxBus.send(EventNtpStatus(rh.gs(R.string.notconnected), 99))
+ } else if (success) {
+ if (objective.isCompleted(time)) {
+ objective.accomplishedOn = time
+ rxBus.send(EventNtpStatus(rh.gs(app.aaps.core.ui.R.string.success), 100))
+ SystemClock.sleep(1000)
+ rxBus.send(EventObjectivesUpdateGui())
+ rxBus.send(EventSWUpdate(false))
+ SystemClock.sleep(100)
+ scrollToCurrentObjective()
+ } else {
+ rxBus.send(EventNtpStatus(rh.gs(R.string.requirementnotmet), 99))
+ }
+ } else {
+ rxBus.send(EventNtpStatus(rh.gs(R.string.failedretrievetime), 99))
+ }
+ }
+ }, receiverStatusStore.isKnownNetworkStatus && receiverStatusStore.isConnected)
+ }
+ }
+ }
+ holder.binding.start.setOnClickListener {
+ receiverStatusStore.updateNetworkStatus()
+ if (binding.fake.isChecked) {
+ objective.startedOn = dateUtil.now()
+ scrollToCurrentObjective()
+ startUpdateTimer()
+ rxBus.send(EventObjectivesUpdateGui())
+ rxBus.send(EventSWUpdate(false))
+ } else
+ // move out of UI thread
+ handler.post {
+ NtpProgressDialog().show((context as AppCompatActivity).supportFragmentManager, "NtpCheck")
+ rxBus.send(EventNtpStatus(rh.gs(app.aaps.core.ui.R.string.timedetection), 0))
+ sntpClient.ntpTime(object : SntpClient.Callback() {
+ override fun run() {
+ aapsLogger.debug("NTP time: $time System time: ${dateUtil.now()}")
+ SystemClock.sleep(300)
+ if (!networkConnected) {
+ rxBus.send(EventNtpStatus(rh.gs(R.string.notconnected), 99))
+ } else if (success) {
+ objective.startedOn = time
+ rxBus.send(EventNtpStatus(rh.gs(app.aaps.core.ui.R.string.success), 100))
+ SystemClock.sleep(1000)
+ rxBus.send(EventObjectivesUpdateGui())
+ rxBus.send(EventSWUpdate(false))
+ SystemClock.sleep(100)
+ scrollToCurrentObjective()
+ } else {
+ rxBus.send(EventNtpStatus(rh.gs(R.string.failedretrievetime), 99))
+ }
+ }
+ }, receiverStatusStore.isKnownNetworkStatus && receiverStatusStore.isConnected)
+ }
+ }
+ holder.binding.unstart.setOnClickListener {
+ activity?.let { activity ->
+ OKDialog.showConfirmation(activity, rh.gs(app.aaps.core.ui.R.string.objectives), rh.gs(R.string.doyouwantresetstart), Runnable {
+ uel.log(
+ action = Action.OBJECTIVE_UNSTARTED,
+ source = Sources.Objectives,
+ value = ValueWithUnit.SimpleInt(position + 1)
+ )
+ objective.startedOn = 0
+ scrollToCurrentObjective()
+ rxBus.send(EventObjectivesUpdateGui())
+ rxBus.send(EventSWUpdate(false))
+ })
+ }
+ }
+ holder.binding.unfinish.setOnClickListener {
+ objective.accomplishedOn = 0
+ scrollToCurrentObjective()
+ rxBus.send(EventObjectivesUpdateGui())
+ rxBus.send(EventSWUpdate(false))
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return objectivesPlugin.objectives.size
+ }
+
+ inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+
+ val binding = ObjectivesItemBinding.bind(itemView)
+ }
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun updateGUI() {
+ activity?.runOnUiThread { objectivesAdapter.notifyDataSetChanged() }
+ }
}
diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective.kt
index 1c5ba7b83911..4bf449862756 100644
--- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective.kt
+++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective.kt
@@ -48,7 +48,8 @@ abstract class Objective(
val isCompleted: Boolean
get() {
- for (task in tasks) {
+ return true
+ for (task in tasks) {
if (!task.shouldBeIgnored() && !task.isCompleted()) return false
}
return true
@@ -62,9 +63,11 @@ abstract class Objective(
}
val isAccomplished: Boolean
- get() = accomplishedOn != 0L && accomplishedOn < dateUtil.now()
+ // get() = accomplishedOn != 0L && accomplishedOn < dateUtil.now()
+ get() = true
val isStarted: Boolean
- get() = startedOn != 0L
+ // get() = startedOn != 0L
+ get() = true
abstract inner class Task(var objective: Objective, @StringRes val task: Int) {
@@ -190,4 +193,4 @@ abstract class Objective(
}
inner class Learned internal constructor(@StringRes var learned: Int)
-}
\ No newline at end of file
+}
diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective5.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective5.kt
index 852901ee74cc..ebfeb07e8e3a 100644
--- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective5.kt
+++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective5.kt
@@ -17,6 +17,6 @@ class Objective5 @Inject constructor(
) : Objective(preferences, rh, dateUtil, "maxiobzero", R.string.objectives_maxiobzero_objective, R.string.objectives_maxiobzero_gate) {
init {
- tasks.add(MinimumDurationTask(this, T.days(5).msecs()).learned(Learned(R.string.objectives_maxiobzero_learned)))
+ tasks.add(MinimumDurationTask(this, T.days(1).msecs()).learned(Learned(R.string.objectives_maxiobzero_learned)))
}
-}
\ No newline at end of file
+}
diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective7.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective7.kt
index b194ef816302..08d07a5d17b3 100644
--- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective7.kt
+++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective7.kt
@@ -17,8 +17,8 @@ class Objective7 @Inject constructor(
init {
tasks.add(
- MinimumDurationTask(this, T.days(7).msecs())
+ MinimumDurationTask(this, T.days(1).msecs())
.learned(Learned(R.string.objectives_autosens_learned))
)
}
-}
\ No newline at end of file
+}
diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective8.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective8.kt
index 7b8e98b3fe77..e37d60cbe3e9 100644
--- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective8.kt
+++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective8.kt
@@ -17,8 +17,8 @@ class Objective8 @Inject constructor(
init {
tasks.add(
- MinimumDurationTask(this, T.days(28).msecs())
+ MinimumDurationTask(this, T.days(1).msecs())
.learned(Learned(R.string.objectives_smb_learned))
)
}
-}
\ No newline at end of file
+}
diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective9.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective9.kt
index d85c003b714d..e84382c0ce16 100644
--- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective9.kt
+++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective9.kt
@@ -17,8 +17,8 @@ class Objective9 @Inject constructor(
init {
tasks.add(
- MinimumDurationTask(this, T.days(28).msecs())
+ MinimumDurationTask(this, T.days(1).msecs())
.learned(Learned(R.string.objectives_auto_learned))
)
}
-}
\ No newline at end of file
+}