Merge branch 'dev' of https://github.com/nightscout/AndroidAPS into dev
This commit is contained in:
commit
621d8b9614
5 changed files with 173 additions and 37 deletions
153
pump/combov2/README.md
Normal file
153
pump/combov2/README.md
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
# Overview over combov2's and ComboCtl's architecture
|
||||||
|
|
||||||
|
ComboCtl is the core driver. It uses Kotlin Multiplatform and is written in a platform agnostic
|
||||||
|
way. The code is located in `comboctl/`, and is also available in [its own separate repository]
|
||||||
|
(https://github.com/dv1/ComboCtl). That separate repository is kept in sync with the ComboCtl
|
||||||
|
copy in AndroidAPS as much as possible, with some notable changes (see below). "combov2" is the
|
||||||
|
name of the AndroidAPS driver. In short: combov2 = ComboCtl + extra AndroidAPS integration code.
|
||||||
|
|
||||||
|
## Directory structure
|
||||||
|
|
||||||
|
The directory structure of the local ComboCtl itself is:
|
||||||
|
|
||||||
|
* `comboctl/src/commonMain/` : The platform agnostic portion of ComboCtl. The vast majority of
|
||||||
|
ComboCtl's logic is contained there.
|
||||||
|
* `comboctl/src/androidMain/` : The Android specific code. This in particular contains
|
||||||
|
implementations of the Bluetooth interfaces that are defined in `commonMain/`.
|
||||||
|
* `comboctl/src/jvmTest/` : Unit tests. This subdirectory is called `jvmTest` because in the
|
||||||
|
ComboCtl repository, there is also a `jvmMain/` subdirectory, and the unit tests are run
|
||||||
|
with the JVM.
|
||||||
|
|
||||||
|
The AndroidAPS specific portion of the driver is located in `src/`. This connects ComboCtl with
|
||||||
|
AndroidAPS. In particular, this is where the `ComboV2Plugin` class is located. That's the main
|
||||||
|
entrypoint for the combov2 driver plugin.
|
||||||
|
|
||||||
|
## Basic description of how ComboCtl communicates with the pump
|
||||||
|
|
||||||
|
ComboCtl uses Kotlin coroutines. It uses [the Default dispatcher](https://kotlinlang.
|
||||||
|
org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html),
|
||||||
|
with [a limitedParallelism](https://kotlinlang.org/api/kotlinx.
|
||||||
|
coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/limited-parallelism.html)
|
||||||
|
constraint to prevent actual parallelism, that is, to not let coroutine jobs run on multiple
|
||||||
|
threads concurrently. Coroutines are used in ComboCtl to greatly simplify the communication steps,
|
||||||
|
which normally require a number of state machines to be implemented manually. Stackless coroutines
|
||||||
|
like Kotlin's essentially are automatically generated state machines under the hood, and this is
|
||||||
|
what they are used for here. Enabling parallelism is not part of such a state machine. Furthermore,
|
||||||
|
communication with the Combo does not benefit from parallelism.
|
||||||
|
|
||||||
|
The communication code in ComboCtl is split in higher level operations (in its `Pump` class) and
|
||||||
|
lower level ones (in its `PumpIO` class). `Pump` instantiates `PumpIO` internally, and focuses on
|
||||||
|
implementing functionality like reading basal profiles, setting TBRs etc. `PumpIO` implements the
|
||||||
|
building blocks for these higher level operations. In particular, `PumpIO` has an internal
|
||||||
|
coroutine scope that is used for sending data to the Combo and for running a "heartbeat" loop.
|
||||||
|
That "heartbeat" is a message that needs to be regularly sent to the Combo (unless other data is
|
||||||
|
sent to the Combo in time, like a command to press a button). If nothing is sent to a Combo for
|
||||||
|
some time, it will eventually disconnect. For this reason, that heartbeat loop is necessary.
|
||||||
|
|
||||||
|
PumpIO also contains the code for performing the pump pairing.
|
||||||
|
|
||||||
|
Going further down a level, `TransportLayer` implements the IO code to generate packets for the
|
||||||
|
Combo and parse packets coming from the Combo. This includes code for authenticating outgoing
|
||||||
|
packets and for checking incoming ones. `TransportLayer` also contains the `IO` subclass, which
|
||||||
|
actually transfers packets to and receives data from the Combo.
|
||||||
|
|
||||||
|
One important detail to keep in mind about the `IO` class is that it enforces a packet send
|
||||||
|
interval of 200 ms. That is: The time between packet transmission is never shorter than 200 ms
|
||||||
|
(it is OK to be longer). The interval is important, because the Combo has a ring buffer for the
|
||||||
|
packet it receives, and transmitting packets to the Combo too quickly causes an overflow and a
|
||||||
|
subsequent error in the Combo, which then terminates the connection.
|
||||||
|
|
||||||
|
The Combo can run in three modes. The first one is the "service" mode, which is only briefly
|
||||||
|
used for setting up the connection. Immediately after the connection is established, the pump
|
||||||
|
continues in the "command" or "remote terminal" (abbr. "RT") mode. The "command" mode is what the
|
||||||
|
remote control of the Combo uses for its direct commands (that is, delivering bolus and retrieving
|
||||||
|
the latest changes / activities from the history). The "remote terminal" mode replicates the LCD
|
||||||
|
on the pump itself along with the 4 Combo buttons.
|
||||||
|
|
||||||
|
Only a few operations are possible in the command mode. In particular, the driver uses the bolus
|
||||||
|
delivery command from the command mode, the command to retrieve a history delta, and the command
|
||||||
|
for getting the pump's current date and time. But everything else (getting basal profile, setting
|
||||||
|
TBR, getting pump status...) is done in the remote terminal mode, by emulating a user pressing
|
||||||
|
buttons. This unfortunately means that these operations are performed slowly, but there is no
|
||||||
|
other choice.
|
||||||
|
|
||||||
|
## Details about long-pressing RT buttons
|
||||||
|
|
||||||
|
As part of operations like reading the pump's profile, an emulated long RT button press is sometimes
|
||||||
|
used. Such long presses cause more rapid changes compared to multiple short button presses. A
|
||||||
|
button press is "long" when the emulated user "holds down" the button, while a short button press
|
||||||
|
equals pressing and immediately releasing the emulated button.
|
||||||
|
|
||||||
|
The greater speed of long button presses comes with a drawback though: "Overshoots" can happen. For
|
||||||
|
example, if long button pressing is used for adjusting a quantity on screen, then the quantity may
|
||||||
|
still get incremented/decremented after the emulated user "released" the button. It is therefore
|
||||||
|
necessary to check the quantity on screen, and finetune it with short button presses afterwards
|
||||||
|
if necessary.
|
||||||
|
|
||||||
|
## Idempotent and non-idempotent high level commands
|
||||||
|
|
||||||
|
A command is _idempotent_ if it can be repeated if the connection to the pump was lost. Most
|
||||||
|
commands are idempotent. For example, reading the basal profile can be repeated if during the
|
||||||
|
initial basal profile retrieval the connection was lost (for example because the user walked away
|
||||||
|
from the pump). After a few attempts to repeat the command, an error is produced (to avoid an
|
||||||
|
infinite loop).
|
||||||
|
|
||||||
|
Currently, there is only one non-idempotent command: Delivering a bolus. This one _cannot_ be
|
||||||
|
repeated, otherwise there is a high risk of infusing too much insulin. Instead, in case of a
|
||||||
|
connection failure, the delivering bolus command fails immediately and is not automatically
|
||||||
|
attempted again.
|
||||||
|
|
||||||
|
## Automatic datetime adjustments and timezone offset handling
|
||||||
|
|
||||||
|
ComboCtl automatically adjusts the date and time of the Combo. This is done through the RT mode,
|
||||||
|
since there is no command-mode command to _set_ the current datetime (but there is one for
|
||||||
|
_getting_ the current datetime). But since the Combo cannot store a timezone offset (it only stores
|
||||||
|
localtime), the timezone offset that has been used so far is stored in a dedicated field in the
|
||||||
|
pump state store that ComboCtl uses. DST changes and timezone changes can be tracked properly with
|
||||||
|
this logic.
|
||||||
|
|
||||||
|
The pump's current datetime is always retrieved (through the command mode) every time a connection
|
||||||
|
is established to it, and compared to the system's current datetime. If these two differ too much,
|
||||||
|
the pump's datetime is automatically adjusted. This keeps the pump's datetime in sync.
|
||||||
|
|
||||||
|
## Notes about how TBRs are set
|
||||||
|
|
||||||
|
TBRs are set through the remote terminal mode. The driver assumes that the Combo is configured
|
||||||
|
to use 15-minute TBR duration steps sizes and a TBR percentage maximum of 500%. There is code
|
||||||
|
in the driver to detect if the maximum is not set to 500%. If AndroidAPS tries to set a percentage
|
||||||
|
that is higher than the actually configured maximum, then eventually, an error is reported.
|
||||||
|
|
||||||
|
:warning: The duration step size cannot be detected by the driver. The user _must_ make sure that
|
||||||
|
the step size is configured to 15 minutes.
|
||||||
|
|
||||||
|
## Pairing with a Combo and the issue with pump queue connection timeouts
|
||||||
|
|
||||||
|
When pairing, the pump queue's internal timeout is likely to be reached. Essentially, the queue
|
||||||
|
tries to connect to the pump right after the driver was selected in the configuration. But
|
||||||
|
a connection cannot be established because the pump is not yet paired.
|
||||||
|
|
||||||
|
When the queue attempts to connect to the pump, it "thinks" that if the connect procedure does not
|
||||||
|
complete after 120 seconds, then the driver must be stuck somehow. The queue then hits a timeout.
|
||||||
|
The assumption about 120s is correct if the Combo is already paired (a connection should be set up
|
||||||
|
in far less time than 120s). But if it is currently being paired, the steps involved can take
|
||||||
|
about 2-3 minutes.
|
||||||
|
|
||||||
|
For this reason, the driver automatically requests a pump update - which connects to the pump -
|
||||||
|
once pairing is done.
|
||||||
|
|
||||||
|
## Changes to ComboCtl in the local copy
|
||||||
|
|
||||||
|
The code in `comboctl/` is ComboCtl minus the `jvmMain/` code, which contains code for the Linux
|
||||||
|
platform. This includes C++ glue code to the BlueZ stack. Since none of this is useful to
|
||||||
|
AndroidAPS, it is better left out, especially since it consists of almost 9000 lines of code.
|
||||||
|
|
||||||
|
Also, the original `comboctl/build.gradle.kts` files is replaced by `comboctl/build.gradle`, which
|
||||||
|
is much simpler, and builds ComboCtl as a kotlin-android project, not a Kotlin Multiplatform one.
|
||||||
|
This simplifies integration into AndroidAPS, and avoids multiplatform problems (after all,
|
||||||
|
Kotlin Multiplatform is still marked as an alpha version feature).
|
||||||
|
|
||||||
|
When updating ComboCtl, it is important to keep these differences in mind.
|
||||||
|
|
||||||
|
Differences between the copy in `comboctl/` and the original ComboCtl code must be kept as little
|
||||||
|
as possible, and preferably be transferred to the main ComboCtl project. This helps with keeping the
|
||||||
|
`comboctl/` copy and the main project in sync since transferring changes then is straightforward.
|
|
@ -1,14 +0,0 @@
|
||||||
The code in comboctl/ is ComboCtl minus the jvmMain/ code, which contains code for the Linux platform.
|
|
||||||
This includes C++ glue code to the BlueZ stack. Since none of this is useful to AndroidAPS, it is better
|
|
||||||
left out, especially since it consists of almost 9000 lines of code.
|
|
||||||
|
|
||||||
Also, the original comboctl/build.gradle.kts files is replaced by comboctl/build.gradle, which is
|
|
||||||
much simpler, and builds ComboCtl as a kotlin-android project, not a kotlin-multiplatform one.
|
|
||||||
This simplifies integration into AndroidAPS, and avoids multiplatform problems (after all,
|
|
||||||
Kotlin Multiplatform is still marked as an alpha version feature).
|
|
||||||
|
|
||||||
When updating ComboCtl, it is important to keep these differences in mind.
|
|
||||||
|
|
||||||
Differences between the copy in comboctl/ and the original ComboCtl code must be kept as little as
|
|
||||||
possible, and preferably be transferred to the main ComboCtl project. This helps keep the comboctl/
|
|
||||||
copy and the main project in sync.
|
|
|
@ -67,8 +67,9 @@ class AndroidBluetoothDevice(
|
||||||
// just yet (for example because the UI is still shown on the LCD), while
|
// just yet (for example because the UI is still shown on the LCD), while
|
||||||
// the retryBlocking loop here is in place because the _Android device_
|
// the retryBlocking loop here is in place because the _Android device_
|
||||||
// may not be ready to connect right away.
|
// may not be ready to connect right away.
|
||||||
// TODO: Test and define what happens when all attempts failed.
|
// When all attempts fail, retryBlocking() lets the exception pass through.
|
||||||
// The user needs to be informed and given the choice to try again.
|
// That exception is wrapped in BluetoothException, which then needs to be
|
||||||
|
// handled by the caller.
|
||||||
val totalNumAttempts = 5
|
val totalNumAttempts = 5
|
||||||
retryBlocking(numberOfRetries = totalNumAttempts, delayBetweenRetries = 100) { attemptNumber, previousException ->
|
retryBlocking(numberOfRetries = totalNumAttempts, delayBetweenRetries = 100) { attemptNumber, previousException ->
|
||||||
if (abortConnectAttempt)
|
if (abortConnectAttempt)
|
||||||
|
|
|
@ -381,13 +381,6 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
|
||||||
// instance was already processed. This check here instead
|
// instance was already processed. This check here instead
|
||||||
// verifies if we have seen the same Bluetooth address on
|
// verifies if we have seen the same Bluetooth address on
|
||||||
// *different* Android Bluetooth device instances.
|
// *different* Android Bluetooth device instances.
|
||||||
// TODO: Test how AndroidBluetoothInterface behaves if the
|
|
||||||
// device is unpaired while discovery is ongoing (manually by
|
|
||||||
// the user for example). In theory, this should be handled
|
|
||||||
// properly by the onBondStateChanged function below.
|
|
||||||
// TODO: This check may not be necessary on all Android
|
|
||||||
// devices. On some, it seems to also work if we use the
|
|
||||||
// first offered BluetoothDevice.
|
|
||||||
if (comboctlBtAddress !in previouslyDiscoveredDevices) {
|
if (comboctlBtAddress !in previouslyDiscoveredDevices) {
|
||||||
previouslyDiscoveredDevices[comboctlBtAddress] = androidBtDevice
|
previouslyDiscoveredDevices[comboctlBtAddress] = androidBtDevice
|
||||||
logger(LogLevel.DEBUG) {
|
logger(LogLevel.DEBUG) {
|
||||||
|
|
|
@ -2743,8 +2743,6 @@ class Pump(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
numObservedScreens++
|
|
||||||
|
|
||||||
val factorIndexOnScreen = parsedScreen.beginTime.hour
|
val factorIndexOnScreen = parsedScreen.beginTime.hour
|
||||||
|
|
||||||
// numUnits null means the basal profile factor
|
// numUnits null means the basal profile factor
|
||||||
|
@ -2752,6 +2750,11 @@ class Pump(
|
||||||
if (parsedScreen.numUnits == null)
|
if (parsedScreen.numUnits == null)
|
||||||
return@longPressRTButtonUntil LongPressRTButtonsCommand.ContinuePressingButton
|
return@longPressRTButtonUntil LongPressRTButtonsCommand.ContinuePressingButton
|
||||||
|
|
||||||
|
// Increase this _after_ checking for a blinking screen
|
||||||
|
// to not accidentally count the blinking and non-blinking
|
||||||
|
// screens as two separate ones.
|
||||||
|
numObservedScreens++
|
||||||
|
|
||||||
// If the factor in the profile is >= 0,
|
// If the factor in the profile is >= 0,
|
||||||
// it means it was already read earlier.
|
// it means it was already read earlier.
|
||||||
if (basalProfileFactors[factorIndexOnScreen] >= 0)
|
if (basalProfileFactors[factorIndexOnScreen] >= 0)
|
||||||
|
@ -2759,14 +2762,16 @@ class Pump(
|
||||||
|
|
||||||
val factor = parsedScreen.numUnits
|
val factor = parsedScreen.numUnits
|
||||||
basalProfileFactors[factorIndexOnScreen] = factor
|
basalProfileFactors[factorIndexOnScreen] = factor
|
||||||
logger(LogLevel.DEBUG) { "Got basal profile factor #$factorIndexOnScreen : $factor" }
|
|
||||||
|
numRetrievedFactors++
|
||||||
|
logger(LogLevel.DEBUG) {
|
||||||
|
"Got basal profile factor #$factorIndexOnScreen : $factor; $numRetrievedFactors factor(s) read and $numObservedScreens screen(s) observed thus far"
|
||||||
|
}
|
||||||
|
|
||||||
getBasalProfileReporter.setCurrentProgressStage(
|
getBasalProfileReporter.setCurrentProgressStage(
|
||||||
RTCommandProgressStage.GettingBasalProfile(numRetrievedFactors)
|
RTCommandProgressStage.GettingBasalProfile(numRetrievedFactors)
|
||||||
)
|
)
|
||||||
|
|
||||||
numRetrievedFactors++
|
|
||||||
|
|
||||||
return@longPressRTButtonUntil if (numObservedScreens >= NUM_COMBO_BASAL_PROFILE_FACTORS)
|
return@longPressRTButtonUntil if (numObservedScreens >= NUM_COMBO_BASAL_PROFILE_FACTORS)
|
||||||
LongPressRTButtonsCommand.ReleaseButton
|
LongPressRTButtonsCommand.ReleaseButton
|
||||||
else
|
else
|
||||||
|
@ -2819,21 +2824,19 @@ class Pump(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
numRetrievedFactors++
|
||||||
getBasalProfileReporter.setCurrentProgressStage(
|
getBasalProfileReporter.setCurrentProgressStage(
|
||||||
RTCommandProgressStage.GettingBasalProfile(numRetrievedFactors)
|
RTCommandProgressStage.GettingBasalProfile(numRetrievedFactors)
|
||||||
)
|
)
|
||||||
numRetrievedFactors++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All factors retrieved. Press CHECK once to get back to the total
|
// All factors retrieved. Press BACK repeatedly until we are back at the main menu.
|
||||||
// basal rate screen, and then CHECK again to return to the main menu.
|
cycleToRTScreen(
|
||||||
|
rtNavigationContext,
|
||||||
rtNavigationContext.shortPressButton(RTNavigationButton.CHECK)
|
RTNavigationButton.BACK,
|
||||||
waitUntilScreenAppears(rtNavigationContext, ParsedScreen.BasalRateTotalScreen::class)
|
ParsedScreen.MainScreen::class
|
||||||
|
)
|
||||||
rtNavigationContext.shortPressButton(RTNavigationButton.CHECK)
|
|
||||||
waitUntilScreenAppears(rtNavigationContext, ParsedScreen.MainScreen::class)
|
|
||||||
|
|
||||||
getBasalProfileReporter.setCurrentProgressStage(BasicProgressStage.Finished)
|
getBasalProfileReporter.setCurrentProgressStage(BasicProgressStage.Finished)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue