
Continuous, multi-day periodic background data-collection engine with pluggable sensors, per-source permission metadata, batched bounded persistence, transformable event pipeline, and inversion-of-control sync/export/test tooling.
A Kotlin Multiplatform (Android + iOS) engine for continuous or periodic, multi-day background
data collection from any pluggable DataSource — location, motion, step count, and BLE scanning
ship out of the box. Collected events get batched local persistence and a pluggable sync engine.
The demo app (app/) showcases every feature area behind bottom navigation:
![]() Home |
![]() Sensors |
![]() Bluetooth |
![]() Sync |
![]() Settings |
pulsekit-core: Tracking engine, SQLDelight persistence, and PermissionController abstraction. Includes a pluggable export infrastructure (DataExporter, CsvFormatter, NdjsonFormatter).pulsekit-location: GPS/network location DataSource with GPX and field-based exporters.pulsekit-motion: Accelerometer and step-counter DataSource with field-based exporters.pulsekit-bluetooth: BLE scan DataSource with field-based exporter.pulsekit-device: Battery and device status DataSource (Level, Charging, Thermal).pulsekit-environment: Barometer, Light, Temperature, Humidity, and Altitude DataSource.pulsekit-sync: Network sync engine with retry/backoff logic.pulsekit-ui: Android Services, Receivers, and Compose PermissionGate.pulsekit-geofence: Region monitoring over the location stream.pulsekit-testing: Test artifact with FakeDataSource and MutableTimeProvider.dependencies {
val version = "0.6.0"
implementation("io.github.alirezajavan:pulsekit-core:$version")
implementation("io.github.alirezajavan:pulsekit-location:$version")
implementation("io.github.alirezajavan:pulsekit-motion:$version")
implementation("io.github.alirezajavan:pulsekit-bluetooth:$version")
implementation("io.github.alirezajavan:pulsekit-device:$version")
implementation("io.github.alirezajavan:pulsekit-environment:$version")
implementation("io.github.alirezajavan:pulsekit-sync:$version")
implementation("io.github.alirezajavan:pulsekit-ui:$version") // optional
implementation("io.github.alirezajavan:pulsekit-geofence:$version") // optional
testImplementation("io.github.alirezajavan:pulsekit-testing:$version") // optional
}val database = createPulseKitDatabase(applicationContext)
val pulseKit = PulseKit.builder(database)
.addDataSource(LocationDataSource(applicationContext))
.addDataSource(MotionDataSource(applicationContext))
.addDataSource(BatteryDataSource(applicationContext))
.addDataSource(BarometerDataSource(applicationContext))
.addDataSource(EnvironmentDataSource(applicationContext, sensorTypeId = SensorType.LIGHT))
.addDataSource(
BluetoothDataSource(applicationContext),
mode = CollectionMode.Periodic(intervalMillis = 5.minutes.inWholeMilliseconds,
windowMillis = 30.seconds.inWholeMilliseconds),
)
.addEventProcessor(LocationPrecisionProcessor(decimalPlaces = 3))
.build()Extend BasePulseKitService and use the provided extension functions to start/stop:
// To start:
context.startPulseKitCollection<MyTrackingService>(sourceIds = setOf(LocationDataSource.ID))
// To stop:
context.stopPulseKitCollection<MyTrackingService>()PermissionController handles the OS-mandated staged ordering. In Compose:
val permissions = rememberDataSourcePermissionState(controller, source)
Button(onClick = {
permissions.request { canCollect -> if (canCollect) onStart() }
}) { Text("Collect ${source.displayName}") }// Sync to backend
val syncEngine = SyncEngine(pulseKit.syncSource, MyBackendUploader())
syncEngine.start(applicationScope)
// Query and Export
val events = pulseKit.queryEvents(EventQuery(limit = 100)).asFlow()
// Use a specific data source's exporter
val locationSource = pulseKit.dataSources.find { it.id == LocationDataSource.ID }
val exporter = locationSource?.exporter!!
// Export to GPX
(exporter as LocationExporter).exportToGpx(events, output)
// Export to CSV with custom columns
val csv = CsvFormatter(exporter, columns = listOf("latitude", "longitude", "speed"))
csv.format(events, output)Explicitly declare permissions and components in your AndroidManifest.xml:
FOREGROUND_SERVICE, WAKE_LOCK, RECEIVE_BOOT_COMPLETED.ACCESS_FINE_LOCATION).BasePulseKitService and PulseKitBootReceiver subclasses.Use pulseKit.validateAndroidSetup(...) at startup to verify your manifest.
Add NSLocationAlwaysAndWhenInUseUsageDescription, NSMotionUsageDescription, and location to UIBackgroundModes. Use IosPulseKitHost to keep the process alive.
| Source | Android | iOS | Notes |
|---|---|---|---|
| Location | ✅ | ✅ | iOS requires UIBackgroundModes
|
| Motion | ✅ | ✅ | |
| Steps | ✅ | ✅ | |
| Bluetooth | ✅ | ✅ | iOS background scanning is limited |
| Battery | ✅ | ✅ | |
| Barometer | ✅ | ✅ | iOS uses CMAltimeter
|
| Environment | ✅ | ❌ | Light, Temp, Humidity (Android only) |
| Rel. Altitude | ❌ | ✅ | iOS only (CMAltimeter) |
See CLAUDE.md for conventions and REFACTOR_PLAN.md for the roadmap.
Apache License 2.0.
A Kotlin Multiplatform (Android + iOS) engine for continuous or periodic, multi-day background
data collection from any pluggable DataSource — location, motion, step count, and BLE scanning
ship out of the box. Collected events get batched local persistence and a pluggable sync engine.
The demo app (app/) showcases every feature area behind bottom navigation:
![]() Home |
![]() Sensors |
![]() Bluetooth |
![]() Sync |
![]() Settings |
pulsekit-core: Tracking engine, SQLDelight persistence, and PermissionController abstraction. Includes a pluggable export infrastructure (DataExporter, CsvFormatter, NdjsonFormatter).pulsekit-location: GPS/network location DataSource with GPX and field-based exporters.pulsekit-motion: Accelerometer and step-counter DataSource with field-based exporters.pulsekit-bluetooth: BLE scan DataSource with field-based exporter.pulsekit-device: Battery and device status DataSource (Level, Charging, Thermal).pulsekit-environment: Barometer, Light, Temperature, Humidity, and Altitude DataSource.pulsekit-sync: Network sync engine with retry/backoff logic.pulsekit-ui: Android Services, Receivers, and Compose PermissionGate.pulsekit-geofence: Region monitoring over the location stream.pulsekit-testing: Test artifact with FakeDataSource and MutableTimeProvider.dependencies {
val version = "0.6.0"
implementation("io.github.alirezajavan:pulsekit-core:$version")
implementation("io.github.alirezajavan:pulsekit-location:$version")
implementation("io.github.alirezajavan:pulsekit-motion:$version")
implementation("io.github.alirezajavan:pulsekit-bluetooth:$version")
implementation("io.github.alirezajavan:pulsekit-device:$version")
implementation("io.github.alirezajavan:pulsekit-environment:$version")
implementation("io.github.alirezajavan:pulsekit-sync:$version")
implementation("io.github.alirezajavan:pulsekit-ui:$version") // optional
implementation("io.github.alirezajavan:pulsekit-geofence:$version") // optional
testImplementation("io.github.alirezajavan:pulsekit-testing:$version") // optional
}val database = createPulseKitDatabase(applicationContext)
val pulseKit = PulseKit.builder(database)
.addDataSource(LocationDataSource(applicationContext))
.addDataSource(MotionDataSource(applicationContext))
.addDataSource(BatteryDataSource(applicationContext))
.addDataSource(BarometerDataSource(applicationContext))
.addDataSource(EnvironmentDataSource(applicationContext, sensorTypeId = SensorType.LIGHT))
.addDataSource(
BluetoothDataSource(applicationContext),
mode = CollectionMode.Periodic(intervalMillis = 5.minutes.inWholeMilliseconds,
windowMillis = 30.seconds.inWholeMilliseconds),
)
.addEventProcessor(LocationPrecisionProcessor(decimalPlaces = 3))
.build()Extend BasePulseKitService and use the provided extension functions to start/stop:
// To start:
context.startPulseKitCollection<MyTrackingService>(sourceIds = setOf(LocationDataSource.ID))
// To stop:
context.stopPulseKitCollection<MyTrackingService>()PermissionController handles the OS-mandated staged ordering. In Compose:
val permissions = rememberDataSourcePermissionState(controller, source)
Button(onClick = {
permissions.request { canCollect -> if (canCollect) onStart() }
}) { Text("Collect ${source.displayName}") }// Sync to backend
val syncEngine = SyncEngine(pulseKit.syncSource, MyBackendUploader())
syncEngine.start(applicationScope)
// Query and Export
val events = pulseKit.queryEvents(EventQuery(limit = 100)).asFlow()
// Use a specific data source's exporter
val locationSource = pulseKit.dataSources.find { it.id == LocationDataSource.ID }
val exporter = locationSource?.exporter!!
// Export to GPX
(exporter as LocationExporter).exportToGpx(events, output)
// Export to CSV with custom columns
val csv = CsvFormatter(exporter, columns = listOf("latitude", "longitude", "speed"))
csv.format(events, output)Explicitly declare permissions and components in your AndroidManifest.xml:
FOREGROUND_SERVICE, WAKE_LOCK, RECEIVE_BOOT_COMPLETED.ACCESS_FINE_LOCATION).BasePulseKitService and PulseKitBootReceiver subclasses.Use pulseKit.validateAndroidSetup(...) at startup to verify your manifest.
Add NSLocationAlwaysAndWhenInUseUsageDescription, NSMotionUsageDescription, and location to UIBackgroundModes. Use IosPulseKitHost to keep the process alive.
| Source | Android | iOS | Notes |
|---|---|---|---|
| Location | ✅ | ✅ | iOS requires UIBackgroundModes
|
| Motion | ✅ | ✅ | |
| Steps | ✅ | ✅ | |
| Bluetooth | ✅ | ✅ | iOS background scanning is limited |
| Battery | ✅ | ✅ | |
| Barometer | ✅ | ✅ | iOS uses CMAltimeter
|
| Environment | ✅ | ❌ | Light, Temp, Humidity (Android only) |
| Rel. Altitude | ❌ | ✅ | iOS only (CMAltimeter) |
See CLAUDE.md for conventions and REFACTOR_PLAN.md for the roadmap.
Apache License 2.0.