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
|
||||
// the retryBlocking loop here is in place because the _Android device_
|
||||
// may not be ready to connect right away.
|
||||
// TODO: Test and define what happens when all attempts failed.
|
||||
// The user needs to be informed and given the choice to try again.
|
||||
// When all attempts fail, retryBlocking() lets the exception pass through.
|
||||
// That exception is wrapped in BluetoothException, which then needs to be
|
||||
// handled by the caller.
|
||||
val totalNumAttempts = 5
|
||||
retryBlocking(numberOfRetries = totalNumAttempts, delayBetweenRetries = 100) { attemptNumber, previousException ->
|
||||
if (abortConnectAttempt)
|
||||
|
|
|
@ -381,13 +381,6 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
|
|||
// instance was already processed. This check here instead
|
||||
// verifies if we have seen the same Bluetooth address on
|
||||
// *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) {
|
||||
previouslyDiscoveredDevices[comboctlBtAddress] = androidBtDevice
|
||||
logger(LogLevel.DEBUG) {
|
||||
|
|
|
@ -2743,8 +2743,6 @@ class Pump(
|
|||
)
|
||||
}
|
||||
|
||||
numObservedScreens++
|
||||
|
||||
val factorIndexOnScreen = parsedScreen.beginTime.hour
|
||||
|
||||
// numUnits null means the basal profile factor
|
||||
|
@ -2752,6 +2750,11 @@ class Pump(
|
|||
if (parsedScreen.numUnits == null)
|
||||
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,
|
||||
// it means it was already read earlier.
|
||||
if (basalProfileFactors[factorIndexOnScreen] >= 0)
|
||||
|
@ -2759,14 +2762,16 @@ class Pump(
|
|||
|
||||
val factor = parsedScreen.numUnits
|
||||
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(
|
||||
RTCommandProgressStage.GettingBasalProfile(numRetrievedFactors)
|
||||
)
|
||||
|
||||
numRetrievedFactors++
|
||||
|
||||
return@longPressRTButtonUntil if (numObservedScreens >= NUM_COMBO_BASAL_PROFILE_FACTORS)
|
||||
LongPressRTButtonsCommand.ReleaseButton
|
||||
else
|
||||
|
@ -2819,21 +2824,19 @@ class Pump(
|
|||
}
|
||||
}
|
||||
|
||||
numRetrievedFactors++
|
||||
getBasalProfileReporter.setCurrentProgressStage(
|
||||
RTCommandProgressStage.GettingBasalProfile(numRetrievedFactors)
|
||||
)
|
||||
numRetrievedFactors++
|
||||
}
|
||||
}
|
||||
|
||||
// All factors retrieved. Press CHECK once to get back to the total
|
||||
// basal rate screen, and then CHECK again to return to the main menu.
|
||||
|
||||
rtNavigationContext.shortPressButton(RTNavigationButton.CHECK)
|
||||
waitUntilScreenAppears(rtNavigationContext, ParsedScreen.BasalRateTotalScreen::class)
|
||||
|
||||
rtNavigationContext.shortPressButton(RTNavigationButton.CHECK)
|
||||
waitUntilScreenAppears(rtNavigationContext, ParsedScreen.MainScreen::class)
|
||||
// All factors retrieved. Press BACK repeatedly until we are back at the main menu.
|
||||
cycleToRTScreen(
|
||||
rtNavigationContext,
|
||||
RTNavigationButton.BACK,
|
||||
ParsedScreen.MainScreen::class
|
||||
)
|
||||
|
||||
getBasalProfileReporter.setCurrentProgressStage(BasicProgressStage.Finished)
|
||||
|
||||
|
|
Loading…
Reference in a new issue