Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
753bb25
CareLevo Init
Kisu-Choi Apr 20, 2026
0419896
Rebased onto dev branch
Kisu-Choi Apr 20, 2026
adeb509
Alling logging
MilosKozak Apr 21, 2026
4e0901e
Use kotlin directory
MilosKozak Apr 21, 2026
871555b
Fix threading issues
MilosKozak Apr 21, 2026
3f5465e
Carelevo: PR cleanup
MilosKozak Apr 21, 2026
8554499
Remove excessive logging
MilosKozak Apr 21, 2026
452477d
CareLevo: change package
MilosKozak Apr 21, 2026
b941648
CareLevo: fix flaky tests
MilosKozak Apr 21, 2026
16e6e47
Preparation for new BT stack
MilosKozak Apr 22, 2026
c94c73b
Preparation for new BT stack
MilosKozak Apr 22, 2026
9f4b940
Merge remote-tracking branch 'origin/dev' into carelevo
MilosKozak May 15, 2026
99d8bee
Fix merge
MilosKozak May 15, 2026
05a3433
3.4.2.3
MilosKozak Jun 4, 2026
aeec1aa
Merge branch 'dev' into carelevo
MilosKozak Jul 8, 2026
3c1da92
Carelevo merge changes
MilosKozak Jul 9, 2026
3dcf22c
fix tests threading
MilosKozak Jul 9, 2026
e12d693
Carelevo: new BLE stack
MilosKozak Jul 9, 2026
934d2c8
Carelevo: new BLE stack #2
MilosKozak Jul 9, 2026
10b31a9
Merge branch 'dev' into carelevo
MilosKozak Jul 9, 2026
6ee969f
Carelevo: activation wizard polishing
MilosKozak Jul 10, 2026
b95b6b1
Carelevo: queue lifecycle
MilosKozak Jul 14, 2026
902b71b
Merge remote-tracking branch 'origin/dev' into carelevo
MilosKozak Jul 14, 2026
3fdf695
Carelevo: update set profile
MilosKozak Jul 14, 2026
9f349b2
Carelevo: BLE migration
MilosKozak Jul 14, 2026
00c1346
Carelevo: BLE migration #2
MilosKozak Jul 15, 2026
d105c69
Carelevo: BLE migration #3
MilosKozak Jul 15, 2026
1ae13e1
Merge remote-tracking branch 'origin/dev' into carelevo
MilosKozak Jul 15, 2026
3e10535
Carelevo: BLE migration #4
MilosKozak Jul 15, 2026
e6c5962
Carelevo: BLE migration #5
MilosKozak Jul 15, 2026
10159a3
Carelevo: BLE migration #6
MilosKozak Jul 15, 2026
a1f4853
Carelevo: BLE migration #7
MilosKozak Jul 15, 2026
51bea5f
Carelevo: cleanup
MilosKozak Jul 15, 2026
f807fff
Carelevo: cleanup
MilosKozak Jul 16, 2026
a6d9937
Carelevo: tests
MilosKozak Jul 16, 2026
49c7fbd
Carelevo: tests #2
MilosKozak Jul 16, 2026
c3cd512
Carelevo: tests #3
MilosKozak Jul 16, 2026
c387e16
Carelevo: tests #4
MilosKozak Jul 16, 2026
8c96ca2
Equil: tests
MilosKozak Jul 17, 2026
d888e03
Carelevo: tests #5
MilosKozak Jul 17, 2026
7f0fe28
Merge branch 'dev', remote-tracking branch 'origin' into carelevo
MilosKozak Jul 21, 2026
7cc802b
Merge remote-tracking branch 'origin/dev' into carelevo
MilosKozak Jul 21, 2026
94db69e
Carelevo: fix bonding on A13
MilosKozak Jul 21, 2026
15a8404
Carelevo: onCharacteristicChanged logging
MilosKozak Jul 21, 2026
3f92eb1
CareLevo: prioritize Android 13 BLE handling and scan selection
Kisu-Choi Jul 22, 2026
6b1d7b0
Merge pull request #4994 from Kisu-Choi/fix/carelevo
MilosKozak Jul 22, 2026
d01e7bc
Carelevo: alarms & suspend fix
MilosKozak Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 3 additions & 1 deletion .claude/agent-memory/code-reviewer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@

## See Also

- `equil-migration.md` — detailed Equil Compose migration review (2026-03-09)
- `carelevo-ble-transport-migration.md` — Carelevo BLE stack unification onto shared `BleTransport`
(Dana/Equil/Medtrum abstraction), Phase 1 adapter (`BleTransportGattConnection`) review (2026-07-08).
Confirms double-`@Singleton` DI pattern is intentional fleet convention (see Equil precedent), not a bug.
- Earlier migration reviews (NSClient, Tidepool, Wear, SMS, Preferences, EOPatch2): see conversation
history from 2026-03-01 and 2026-03-02.
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Carelevo BLE Transport Migration (Phase 1) — review notes (2026-07-08)

## Architecture
- Shared fleet abstraction: `core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/ble/BleTransport.kt`
(`BleTransport`/`BleAdapter`/`BleScanner`/`BleGatt`/`BleTransportListener`). Used by Dana/Equil/Medtrum/Carelevo.
`BleGatt.writeCharacteristic(data)` and `.enableNotifications()` have NO uuid param — the transport
hardcodes a single write char + single notify char internally (one-service/one-write/one-notify patch
pump shape). `BleTransportListener.onCharacteristicWritten()` carries no status (unlike
`BluetoothGattCallback.onCharacteristicWrite`'s status int) — silently-dropped writes degrade to the
caller's own `withTimeout`, they don't fast-fail. Single listener via `setListener()` (last write wins,
no ownership enforcement).
- `pump/carelevo/.../ble/gatt/BleTransportGattConnection.kt` is a NEW adapter bridging `BleTransport` to
carelevo's own pre-existing `GattConnection` contract, so `BleClientImpl` (coroutine correlation layer)
runs unchanged over either the old `AndroidGattConnection` (direct `BluetoothGatt` wrapper) or the new
shared-transport adapter. Mirrors `AndroidGattConnection`'s internals almost line for line (gattMutex +
3x `@Volatile CompletableDeferred` for write/discovery/descriptor acks).
- `pump/equil/.../ble/EquilBleTransportImpl.kt` is the template `CarelevoBleTransportImpl.kt` was cloned
from — near-identical structure (inner Adapter/Scanner/Gatt classes, `@Synchronized` gattCallback for
conn-state/svc-discovery/descriptor-write, unsynchronized for char write/read/changed).

## Confirmed non-issues (checked against Equil precedent, don't re-flag)
- **Double `@Singleton`** (class-level `@Singleton class XxxBleTransportImpl @Inject constructor` PLUS
`@Provides @Singleton fun provideXxxBleTransport(impl: XxxBleTransportImpl): XxxBleTransport = impl`)
is the established fleet pattern, not a bug. See `app/src/withPumps/kotlin/app/aaps/di/EquilModules.kt`
— this indirection exists so the app module can swap in an emulator transport
(`EquilEmulatorBleTransport`) at runtime while the impl module's own `@Module class` (non-abstract, so
no `@Binds`) still exposes the concrete singleton. Carelevo's `CarelevoBleModule.kt` binds it locally
instead (no emulator swap yet — KDoc says "a future emulator impl can be swapped in here"). Functionally
identical either way (Dagger caches per scoped binding, exactly one instance).
- Synchronous listener callback firing INSIDE `gatt.writeCharacteristic()` on the not-connected path
(`listener?.onConnectionStateChanged(false)`) correctly aborts the just-registered `writeAck` deferred
with no deadlock risk — `completeExceptionally` never tries to reacquire `gattMutex`, and it all runs on
the same coroutine/call-stack that already holds the mutex from `writeCharacteristic`'s `withLock` block.
Verified via `BleTransportGattConnectionTest` `writeCharacteristic on a not-connected transport fast-
fails...` test — legit coverage, not trivially green.
- `CarelevoBleTransportImpl.onServicesDiscovered` tightening the success condition to
`service != null && notifyChara != null && writeChara != null` (vs Equil's bare `service != null`) is a
**safety improvement**, not a regression — Equil's version can report false "success" if the service is
found but a characteristic UUID lookup inside it fails (firmware/UUID mismatch), only surfacing the real
failure later on the first write/enableNotifications call.
- UUID role wiring verified correct: `BleEnvConfig`/`CarelevoConfig.kt` — service `e1b40001`, TX(notify)
`e1b40003` → `notifyChara`, RX(write) `e1b40002` → `writeChara`, CCCD `2902`. Matches Equil's analogous
NRF_UART_NOTIFY/NRF_UART_WRITE mapping.

## Real findings (see full review text in conversation 2026-07-08 for details — recap here)
- **High**: `BleTransportGattConnection` has no self-tracked "closed" flag. `close()` (lines ~122-130)
nulls the transport's listener slot (`transport.setListener(null)`) but a *subsequent* call to
`writeCharacteristic`/`discoverServices`/`enableNotifications` on the same (now-closed) adapter
re-acquires `gattMutex` uncontended, registers a fresh deferred, and calls into the transport — whose
not-connected fast-fail path (`listener?.onConnectionStateChanged(false)`) is now a silent no-op because
the listener is null. The deferred is never completed → hangs until the caller's own `withTimeout`
instead of failing fast. `AndroidGattConnection` avoids this by self-guarding with
`val g = gatt ?: throw GattWriteException(...)` at the top of every suspend op (checks its own nulled
`gatt` field, doesn't depend on a callback round-trip). Not yet reachable in production (adapter isn't
wired into any real call site yet — grep confirms only the test file constructs it), but must be fixed
before Phase 2 wiring.
- **Medium-High (latent/Phase-2)**: Listener-slot ownership has zero enforcement. `CarelevoBleTransportImpl`
is `@Singleton`; `BleTransportGattConnection` is meant to be "one adapter instance owns the transport's
single listener slot for one connection lifecycle" (its own KDoc) but nothing asserts this. If a future
reconnect path ever constructs a new adapter without `close()`-ing the previous one first, the new
adapter's `init { transport.setListener(this) }` silently steals the slot; the old adapter's in-flight
deferred(s) hang until timeout with no error surfaced.
- **Medium**: `writeCharacteristic(uuid, payload, withResponse)`'s `uuid` and `withResponse` params, and
`enableNotifications(uuid)`'s `uuid` param, are silently ignored by `BleTransportGattConnection` (the
underlying `BleGatt` interface has no per-uuid/write-type parameters — single hardcoded write/notify
char). No `require()` guard, no doc comment (contrast with the write-ack-status KDoc it does have).
A caller passing the wrong UUID (e.g. copy-paste bug) would silently "succeed" against the transport's
one real characteristic instead of erroring.
- **Medium (pre-existing, not a regression)**: `writeAck`/`discoveryAck`/`descriptorAck` are bare mutable
fields, not per-op correlation tokens. A late/stale native ack for an aborted operation could in theory
complete a *different*, newer operation's deferred after `gattMutex` released and a new op started. This
risk is identical in `AndroidGattConnection` (same design) — not introduced by the new adapter, just
worth a shared follow-up someday.
- **Test gaps**: no test for close()-then-reuse (would have caught the High finding above); no test for
double-close() idempotency (interface explicitly promises it); no test characterizing two adapters over
one transport clobbering the listener slot. Existing tests (register-before-write, synchronous not-
connected fast-fail, disconnect aborting in-flight write, full BleClient-over-adapter correlation
end-to-end via nested `scope.launch` + `runTest` virtual time) are legitimate, not trivially green.

## See also
- [MEMORY.md](MEMORY.md) main index
1 change: 1 addition & 0 deletions .idea/dictionaries/project_dictionary.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ dependencies {
// - :pump:combov2:comboctl is a support lib pulled in transitively by :pump:combov2
// buildFile.exists() skips the phantom :pump:omnipod container Gradle auto-creates from the
// nested :pump:omnipod:* includes (it has no build script / no consumable variant).
// :pump:carelevo is included automatically via this filter (present in settings.gradle).
val pumpExclusions = setOf(":pump:virtual", ":pump:combov2:comboctl")
rootProject.subprojects
.filter { it.path.startsWith(":pump:") && it.path !in pumpExclusions && it.buildFile.exists() }
Expand Down Expand Up @@ -259,4 +260,3 @@ if (!gitAvailable()) {
if (isMaster() && !allCommitted()) {
throw GradleException("There are uncommitted changes. Clone sources again as described in wiki and do not allow gradle update")
}

41 changes: 41 additions & 0 deletions app/src/withPumps/kotlin/app/aaps/di/CarelevoModules.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package app.aaps.di

import app.aaps.core.interfaces.configuration.Config
import app.aaps.core.interfaces.configuration.ExternalOptions
import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag
import app.aaps.pump.carelevo.ble.CarelevoBleTransport
import app.aaps.pump.carelevo.ble.CarelevoBleTransportImpl
import app.aaps.pump.carelevo.emulator.CarelevoEmulatorBleTransport
import app.aaps.pump.carelevo.emulator.CarelevoPumpEmulator
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
class CarelevoModules {

/**
* Selects the emulated patch over the real Bluetooth stack when the `emulate_carelevo` marker
* file is present, so the driver can be exercised end-to-end without patch hardware.
*/
@Provides
@Singleton
fun provideCarelevoBleTransport(
config: Config,
carelevoBleTransportImpl: CarelevoBleTransportImpl,
aapsLogger: AAPSLogger
): CarelevoBleTransport =
if (config.isEnabled(ExternalOptions.EMULATE_CARELEVO)) {
aapsLogger.debug(LTag.PUMPEMULATOR, "CareLevo emulator active — real Bluetooth is not used")
CarelevoEmulatorBleTransport(
emulator = CarelevoPumpEmulator(aapsLogger = aapsLogger),
aapsLogger = aapsLogger
)
} else {
carelevoBleTransportImpl
}
}
3 changes: 3 additions & 0 deletions app/src/withPumps/kotlin/app/aaps/di/PumpDriversModule.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.aaps.di

import app.aaps.pump.carelevo.di.CarelevoModule
import app.aaps.pump.common.di.PumpCommonModule
import app.aaps.pump.common.di.RileyLinkModule
import app.aaps.pump.diaconn.di.DiaconnG8Module
Expand Down Expand Up @@ -30,6 +31,8 @@ import info.nightscout.pump.combov2.di.ComboV2Module
MedtrumModule::class,
EquilModule::class,
EquilModules::class,
CarelevoModule::class,
CarelevoModules::class
]
)
@InstallIn(SingletonComponent::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum class ManufacturerType(val description: String) {
Ypsomed("Ypsomed"),
G2e("G2e"),
Eoflow("Eoflow"),
CareMedi("CareMedi"),
Equil("Equil");

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ enum class PumpCapability {
DiaconnCapabilities(arrayOf(Capability.Bolus, Capability.ExtendedBolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.Refill, Capability.ReplaceBattery, Capability.TDD, Capability.ManualTDDLoad)), //
EopatchCapabilities(arrayOf(Capability.Bolus, Capability.ExtendedBolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min)),
MedtrumCapabilities(arrayOf(Capability.Bolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min, Capability.TDD)), // Technically the pump supports ExtendedBolus, but not implemented (yet)
;
CarelevoCapabilities(arrayOf(Capability.Bolus, Capability.ExtendedBolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min));

var children: ArrayList<Capability> = ArrayList()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,26 @@ enum class PumpType(
model = "untested",
parent = MEDTRUM_NANO
),

CAREMEDI_CARELEVO(
description = "Carelevo",
manufacturer = ManufacturerType.CareMedi,
model = "Carelevo",
bolusSize = 0.05,
specialBolusSize = null,
extendedBolusSettings = DoseSettings(0.05, 30, 8 * 60, 0.05, 25.0),
pumpTempBasalType = PumpTempBasalType.Absolute,
tbrSettings = DoseSettings(0.05, 30, 12 * 60, 0.0, 15.0),
specialBasalDurations = arrayOf(Capability.BasalRate_Duration30minAllowed),
baseBasalMinValue = 0.05,
baseBasalMaxValue = 15.0,
baseBasalStep = 0.05,
baseBasalSpecialSteps = null,
pumpCapability = PumpCapability.CarelevoCapabilities,
isPatchPump = true,
maxReservoirReading = 300,
source = Source.Carelevo
),
EQUIL(
description = "Equil",
manufacturer = ManufacturerType.Equil,
Expand Down Expand Up @@ -511,7 +531,8 @@ enum class PumpType(
MDI,
VirtualPump,
Unknown,
EQuil
EQuil,
Carelevo
}

companion object {
Expand Down
2 changes: 2 additions & 0 deletions core/data/src/main/kotlin/app/aaps/core/data/ue/Sources.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ enum class Sources {
Garmin,
Scene, //From Scene activation
Database, // for PersistenceLayer

Carelevo,
Unknown //if necessary
;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ enum class ExternalOptions(val filename: String) {
EMULATE_DANA_R("emulate_dana_r"),
EMULATE_DANA_R_KOREAN("emulate_dana_r_korean"),
EMULATE_DANA_R_V2("emulate_dana_r_v2"),
EMULATE_CARELEVO("emulate_carelevo"),
ENABLE_OMNIPOD_DRIFT_COMPENSATION("omnipod_drift_compensation"),
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ enum class NotificationId(
// Pump — Dana emulator
PUMP_EMULATOR_DISPLAY(INFO, PUMP),

// Pump — Carelevo
CARELEVO_PATCH_ALERT(URGENT, PUMP, allowMultiple = true),

// CGM
BG_READINGS_MISSED(URGENT, CGM),
SENSOR_CHANGE_DETECTED(NORMAL, CGM),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ interface BleAdapter {
data class ScannedDevice(
val name: String,
val address: String,
val scanRecordBytes: ByteArray? = null
val scanRecordBytes: ByteArray? = null,
val rssi: Int = Int.MIN_VALUE
)

interface BleScanner {
Expand Down
Loading