Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
551365c
feat: dexcom connector
Chin-eng Aug 3, 2026
e9774bd
feat: dexcom initial files
Chin-eng Aug 5, 2026
c38aa09
feat: dexcom topicdata
Chin-eng Aug 6, 2026
48f9da5
feat: offsets
Chin-eng Aug 6, 2026
9fb32ae
feat: dexcom EGV route
Chin-eng Aug 6, 2026
afdd0e6
feat: dexcom convertor: EGV
Chin-eng Aug 6, 2026
0807c02
feat: offsets
Chin-eng Aug 6, 2026
07ac68d
feat: dexcom route
Chin-eng Aug 6, 2026
2199d25
feat: dexcom requests
Chin-eng Aug 6, 2026
6d7b786
feat: gradle change
Chin-eng Aug 6, 2026
be4e226
feat: dexcom route
Chin-eng Aug 7, 2026
44b9c4d
feat: kafka dexcom connect files
Chin-eng Aug 8, 2026
ee29e02
feat: connector gradle kts
Chin-eng Aug 9, 2026
aa2e003
feat: dexcom dockerfile
Chin-eng Aug 9, 2026
18a3bdf
dexcom docker service
Chin-eng Aug 9, 2026
2616b55
feat: dexcom properties template
Chin-eng Aug 9, 2026
d7837fc
feat: dexcom egv import
Chin-eng Aug 9, 2026
2d8d28a
feat: dexcom egv
Chin-eng Aug 9, 2026
ed2c665
feat: dexcom user repo addition
Chin-eng Aug 9, 2026
59c8db1
feat: dexcomUser and User not found file
Chin-eng Aug 10, 2026
26cb46d
feat: git user files
Chin-eng Aug 10, 2026
d91ce32
feat:dexcom more user files
Chin-eng Aug 10, 2026
80c82cd
feat: dexcom config files
Chin-eng Aug 10, 2026
111ce26
feat: sequence extension
Chin-eng Aug 13, 2026
037ca17
feat: dexcom egv route update to mapctaching
Chin-eng Aug 14, 2026
fd1e110
feat: added sequence extension
Chin-eng Aug 14, 2026
f45ecc0
feat: dexcom events route
Chin-eng Aug 14, 2026
1149c6e
feat: dexcom calibrations
Chin-eng Aug 14, 2026
1cb11ba
feat: dexcom alerts
Chin-eng Aug 14, 2026
9141190
feat: dexcom devices
Chin-eng Aug 14, 2026
06edfc0
feat: dexcom daterange
Chin-eng Aug 14, 2026
341d826
feat: kafka offset manager
Chin-eng Aug 16, 2026
60f4f3d
feat: dexcom source task
Chin-eng Aug 16, 2026
bff9544
feat: dexcom abstartc rest source connector
Chin-eng Aug 17, 2026
8120e41
feat: dexcom source connector
Chin-eng Aug 17, 2026
e1c8484
feat: jackson empty consturctor
Chin-eng Aug 20, 2026
6196cb2
feat: dexcom caliberation endpoint
Chin-eng Aug 20, 2026
fd2afb1
feat: dexcom events connctor route
Chin-eng Aug 20, 2026
b914b9b
feat: alerts dexcom connector
Chin-eng Aug 20, 2026
53564f4
feat: dexcom data range connctor
Chin-eng Aug 20, 2026
ce83721
feat: dexcom device connector
Chin-eng Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions dexcom-library/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

group = 'org.radarbase'
version = '0.0.1'

apply plugin: 'maven-publish'

repositories {
// Use jcenter for resolving dependencies.
// You can declare any Maven/Ivy/file repository here.
mavenCentral()
}

dependencies {
// Use the Kotlin JDK 8 standard library.
implementation libs.kotlin.stdlib

implementation libs.okhttp

implementation libs.radar.schemas.commons

implementation libs.jackson.annotations

implementation libs.jackson.databind

implementation libs.avro

implementation libs.jackson.datatype.jsr310

implementation libs.slf4j.api

// Use the Kotlin test library.
testImplementation libs.kotlin.test

// Use the Kotlin JUnit integration.
testImplementation libs.kotlin.test.junit
}

project.afterEvaluate {
publishing {
publications {
library(MavenPublication) {
setGroupId "$group"
setArtifactId "dexcom-library"
version "$version"
from components.java
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.radarbase.dexcom.converter

import com.fasterxml.jackson.databind.JsonNode
import org.radarbase.dexcom.user.User
import org.radarcns.connector.dexcom.DexcomAlert
import java.time.Instant

class DexcomAlertsConverter(
private val topic: String = "connect_dexcom_alert",
) : DexcomDataConverter {
override fun processRecords(
root: JsonNode,
user: User,
): Sequence<Result<TopicData>> {
val array = root.get("records")
?: return emptySequence()
return array.asSequence()
.mapCatching {
val systemTimeInstant = DexcomEGVConverter.parseDexcomTime(it.get("systemTime").asText())
TopicData(
key = user.observationKey,
topic = topic,
offset = systemTimeInstant.epochSecond,
value = it.toDexcomAlert(systemTimeInstant),
)
}
}

private fun JsonNode.toDexcomAlert(systemTimeInstant: Instant): DexcomAlert =
DexcomAlert.newBuilder().apply {
recordId = get("recordId").asText()
systemTime = systemTimeInstant.epochSecond.toDouble()
displayTime = textOrNull("displayTime")
alertName = get("alertName").asText()
alertState = get("alertState").asText()
displayDevice = textOrNull("displayDevice")
transmitterGeneration = textOrNull("transmitterGeneration")
transmitterGenerationVariant = textOrNull("transmitterGenerationVariant")
transmitterId = textOrNull("transmitterId")
displayApp = textOrNull("displayApp")
timeReceived = System.currentTimeMillis() / 1000.0
}.build()

private fun JsonNode.textOrNull(field: String): String? =
get(field)?.takeIf { !it.isNull }?.asText()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.radarbase.dexcom.converter

import com.fasterxml.jackson.databind.JsonNode
import org.radarbase.dexcom.user.User
import org.radarcns.connector.dexcom.DexcomCalibration
import java.time.Instant

class DexcomCalibrationsConverter(
private val topic: String = "connect_dexcom_calibration",
) : DexcomDataConverter {
override fun processRecords(
root: JsonNode,
user: User,
): Sequence<Result<TopicData>> {
val array = root.get("records")
?: return emptySequence()
return array.asSequence()
.mapCatching {
val systemTimeInstant = DexcomEGVConverter.parseDexcomTime(it.get("systemTime").asText())
TopicData(
key = user.observationKey,
topic = topic,
offset = systemTimeInstant.epochSecond,
value = it.toDexcomCalibration(systemTimeInstant),
)
}
}

private fun JsonNode.toDexcomCalibration(systemTimeInstant: Instant): DexcomCalibration =
DexcomCalibration.newBuilder().apply {
recordId = get("recordId").asText()
systemTime = systemTimeInstant.epochSecond.toDouble()
displayTime = textOrNull("displayTime")
unit = textOrNull("unit")
value = intOrNull("value")
displayDevice = textOrNull("displayDevice")
transmitterId = textOrNull("transmitterId")
transmitterTicks = longOrNull("transmitterTicks")
transmitterGeneration = textOrNull("transmitterGeneration")
transmitterGenerationVariant = textOrNull("transmitterGenerationVariant")
timeReceived = System.currentTimeMillis() / 1000.0
}.build()

private fun JsonNode.textOrNull(field: String): String? =
get(field)?.takeIf { !it.isNull }?.asText()

private fun JsonNode.intOrNull(field: String): Int? =
get(field)?.takeIf { !it.isNull }?.asInt()

private fun JsonNode.longOrNull(field: String): Long? =
get(field)?.takeIf { !it.isNull }?.asLong()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.radarbase.dexcom.converter

import com.fasterxml.jackson.databind.JsonNode
import okhttp3.Headers
import org.radarbase.dexcom.request.DexcomRequestGenerator.Companion.JSON_READER
import org.radarbase.dexcom.request.RestRequest
import org.radarbase.dexcom.user.User
import java.time.Instant

interface DexcomDataConverter : RecordConverter {
fun processRecords(
root: JsonNode,
user: User,
): Sequence<Result<TopicData>>

override fun convert(
request: RestRequest,
headers: Headers,
data: ByteArray,
): List<TopicData> {
val node = JSON_READER.readTree(data)

return processRecords(node, request.user)
.mapNotNull { result ->
result.fold(
{ it },
{
RecordConverter.logger.error("Data conversion failed: ${it.message}")
null
},
)
}
.toList()
}

fun Instant.toEpoch(): Long = toEpochMilli() / 1000
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package org.radarbase.dexcom.converter

import com.fasterxml.jackson.databind.JsonNode
import org.radarbase.dexcom.user.User
import org.radarcns.connector.dexcom.DexcomDataRange
import org.radarcns.connector.dexcom.DexcomDataRangeMoment
import org.radarcns.connector.dexcom.DexcomDataRangeMomentEnd
import org.radarcns.connector.dexcom.DexcomDataRangeMomentEgvsEnd
import org.radarcns.connector.dexcom.DexcomDataRangeMomentEgvsStart
import org.radarcns.connector.dexcom.DexcomDataRangeMomentEventsEnd
import org.radarcns.connector.dexcom.DexcomDataRangeMomentEventsStart
import org.radarcns.connector.dexcom.DexcomDataRangeWindow
import org.radarcns.connector.dexcom.DexcomDataRangeWindowEgvs
import org.radarcns.connector.dexcom.DexcomDataRangeWindowEvents
import java.time.Instant

/**
* /dataRange returns a single object (not records[]).
*/
class DexcomDataRangeConverter(
private val topic: String = "connect_dexcom_data_range",
) : DexcomDataConverter {
override fun processRecords(
root: JsonNode,
user: User,
): Sequence<Result<TopicData>> {
return sequenceOf(root).mapCatching {
val value = it.toDexcomDataRange()
val offset = it.offsetFromResponse()
TopicData(
key = user.observationKey,
topic = topic,
offset = offset,
value = value,
)
}
}

private fun JsonNode.offsetFromResponse(): Long {
val candidates = listOfNotNull(
get("egvs")?.get("end")?.get("systemTime")?.asText(),
get("events")?.get("end")?.get("systemTime")?.asText(),
get("calibrations")?.get("end")?.get("systemTime")?.asText(),
)
return candidates.firstNotNullOfOrNull { text ->
runCatching { DexcomEGVConverter.parseDexcomTime(text).epochSecond }.getOrNull()
} ?: Instant.now().epochSecond
}

private fun JsonNode.toDexcomDataRange(): DexcomDataRange =
DexcomDataRange.newBuilder().apply {
calibrations = get("calibrations")?.takeIf { !it.isNull }?.toCalibrationsWindow()
egvs = get("egvs")?.takeIf { !it.isNull }?.toEgvsWindow()
events = get("events")?.takeIf { !it.isNull }?.toEventsWindow()
timeReceived = System.currentTimeMillis() / 1000.0
}.build()

private fun JsonNode.toCalibrationsWindow(): DexcomDataRangeWindow =
DexcomDataRangeWindow.newBuilder().apply {
start = get("start")?.toMoment() ?: emptyMoment()
end = get("end")?.toMomentEnd() ?: emptyMomentEnd()
}.build()

private fun JsonNode.toEgvsWindow(): DexcomDataRangeWindowEgvs =
DexcomDataRangeWindowEgvs.newBuilder().apply {
start = get("start")?.toEgvsStart() ?: emptyEgvsStart()
end = get("end")?.toEgvsEnd() ?: emptyEgvsEnd()
}.build()

private fun JsonNode.toEventsWindow(): DexcomDataRangeWindowEvents =
DexcomDataRangeWindowEvents.newBuilder().apply {
start = get("start")?.toEventsStart() ?: emptyEventsStart()
end = get("end")?.toEventsEnd() ?: emptyEventsEnd()
}.build()

private fun JsonNode.toMoment(): DexcomDataRangeMoment =
DexcomDataRangeMoment.newBuilder().apply {
systemTime = systemTimeEpochOrNull()
displayTime = textOrNull("displayTime")
}.build()

private fun JsonNode.toMomentEnd(): DexcomDataRangeMomentEnd =
DexcomDataRangeMomentEnd.newBuilder().apply {
systemTime = systemTimeEpochOrNull()
displayTime = textOrNull("displayTime")
}.build()

private fun JsonNode.toEgvsStart(): DexcomDataRangeMomentEgvsStart =
DexcomDataRangeMomentEgvsStart.newBuilder().apply {
systemTime = systemTimeEpochOrNull()
displayTime = textOrNull("displayTime")
}.build()

private fun JsonNode.toEgvsEnd(): DexcomDataRangeMomentEgvsEnd =
DexcomDataRangeMomentEgvsEnd.newBuilder().apply {
systemTime = systemTimeEpochOrNull()
displayTime = textOrNull("displayTime")
}.build()

private fun JsonNode.toEventsStart(): DexcomDataRangeMomentEventsStart =
DexcomDataRangeMomentEventsStart.newBuilder().apply {
systemTime = systemTimeEpochOrNull()
displayTime = textOrNull("displayTime")
}.build()

private fun JsonNode.toEventsEnd(): DexcomDataRangeMomentEventsEnd =
DexcomDataRangeMomentEventsEnd.newBuilder().apply {
systemTime = systemTimeEpochOrNull()
displayTime = textOrNull("displayTime")
}.build()

private fun JsonNode.systemTimeEpochOrNull(): Double? =
get("systemTime")
?.takeIf { !it.isNull }
?.asText()
?.let { text ->
runCatching {
DexcomEGVConverter.parseDexcomTime(text).epochSecond.toDouble()
}.getOrNull()
}

private fun JsonNode.textOrNull(field: String): String? =
get(field)?.takeIf { !it.isNull }?.asText()

private fun emptyMoment() =
DexcomDataRangeMoment.newBuilder().build()

private fun emptyMomentEnd() =
DexcomDataRangeMomentEnd.newBuilder().build()

private fun emptyEgvsStart() =
DexcomDataRangeMomentEgvsStart.newBuilder().build()

private fun emptyEgvsEnd() =
DexcomDataRangeMomentEgvsEnd.newBuilder().build()

private fun emptyEventsStart() =
DexcomDataRangeMomentEventsStart.newBuilder().build()

private fun emptyEventsEnd() =
DexcomDataRangeMomentEventsEnd.newBuilder().build()
}
Loading