Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EDA data type #405

Merged
merged 3 commits into from
Oct 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ object CarpDataTypes : DataTypeMetaDataMap()
*/
val NON_GRAVITATIONAL_ACCELERATION = add( NON_GRAVITATIONAL_ACCELERATION_TYPE_NAME, "Acceleration without gravity", DataTimeType.POINT )

internal const val EDA_TYPE_NAME = "$CARP_NAMESPACE.eda"
/**
* Single-channel electrodermal activity, represented as skin conductance.
*/
val EDA = add( EDA_TYPE_NAME, "Electrodermal activity", DataTimeType.POINT )

internal const val ACCELERATION_TYPE_NAME = "$CARP_NAMESPACE.acceleration"
/**
* Rate of change in velocity, including gravity, along perpendicular x, y, and z axes in the device's coordinate system.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dk.cachet.carp.common.application.data

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable


/**
* Holds single-channel electrodermal activity (EDA) data, represented as skin conductance.
* Among others, also known as galvanic skin response (GSR) and skin conductance response/level.
*/
@Serializable
@SerialName( CarpDataTypes.EDA_TYPE_NAME )
data class EDA( val microSiemens: Double ) : Data
{
init
{
require( microSiemens >= 0 ) { "EDA conductance in microsiemens needs to be a positive value." }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,20 @@ import kotlinx.serialization.Serializable
@Serializable
@SerialName( CarpDataTypes.GEOLOCATION_TYPE_NAME )
data class Geolocation( val latitude: Double, val longitude: Double ) : Data
{
companion object
{
const val MIN_LATITUDE: Double = -90.0
const val MAX_LATITUDE: Double = 90.0
const val MIN_LONGITUDE: Double = -180.0
const val MAX_LONGITUDE: Double = 180.0
}

init
{
require( latitude in MIN_LATITUDE..MAX_LATITUDE )
{ "Latitude needs to lie between -90 and 90 decimal degrees." }
require( longitude in MIN_LONGITUDE..MAX_LONGITUDE )
{ "Longitude needs to lie between -180 and 180 decimal degrees." }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ import kotlinx.serialization.Serializable
@Serializable
@SerialName( CarpDataTypes.HEART_RATE_TYPE_NAME )
data class HeartRate( val bpm: Int ) : Data
{
init
{
require( bpm >= 0 ) { "Beats per minute needs to be a positive number." }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ val COMMON_SERIAL_MODULE = SerializersModule {
subclass( AngularVelocity::class )
subclass( CompletedTask::class )
subclass( ECG::class )
subclass( EDA::class )
subclass( Geolocation::class )
subclass( HeartRate::class )
subclass( NonGravitationalAcceleration::class )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ val commonInstances = listOf(
AngularVelocity( 42.0, 42.0, 42.0 ),
CompletedTask( "Task", null ),
ECG( 42.0 ),
EDA( 42.0 ),
Geolocation( 42.0, 42.0 ),
HeartRate( 60 ),
NoData,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dk.cachet.carp.common.application.data

import dk.cachet.carp.common.application.concreteDataTypes
import dk.cachet.carp.common.application.data.input.CarpInputDataTypes
import dk.cachet.carp.common.application.data.input.CustomInput
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer
import kotlin.test.*


class CarpDataTypesReflectionTest
{
@OptIn( InternalSerializationApi::class, ExperimentalSerializationApi::class )
@Test
fun all_data_types_included()
{
val inputDataTypes = CarpInputDataTypes.map { it.toString() }.toSet()
val dataTypes = concreteDataTypes
.filter {
it != NoData::class && // Generic type placeholder which shouldn't be included in `CarpDataTypes`.
it != CustomInput::class // Exceptional input type which shouldn't be included in `CarpInputDataTypes`.
}
.map { it.serializer().descriptor.serialName }
.minus( inputDataTypes )

dataTypes.forEach {
val dataType = DataType.fromString( it )
assertTrue(
dataType in CarpDataTypes,
"Data type \"$it\" isn't registered in ${CarpDataTypes::class.simpleName}."
)
}
}
}
27 changes: 14 additions & 13 deletions docs/carp-common.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ When a data type describes data over the course of a time interval, the time int

All the built-in data types belong to the namespace: **dk.cachet.carp**.

| Name | Description |
| --- | --- |
| [geolocation](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/Geolocation.kt) | Geographic location data, representing longitude and latitude. |
| [stepcount](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/StepCount.kt) | The number of steps a participant has taken in a specified time interval. |
| [ecg](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/ECG.kt) | Electrocardiogram data of a single lead. |
| [heartrate](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/HeartRate.kt) | Number of heart contractions (beats) per minute. |
| [interbeatinterval](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/InterbeatInterval.kt) | The time interval between two consecutive heartbeats. |
| [sensorskincontact](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/SensorSkinContact.kt) | Whether a sensor requiring contact with skin is making proper contact at a specific point in time. |
| [nongravitationalacceleration](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/NonGravitationalAcceleration.kt) | Acceleration excluding gravity along perpendicular x, y, and z axes. |
| [angularvelocity](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/AngularVelocity.kt) | Rate of rotation around perpendicular x, y, and z axes. |
| [signalstrength](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/SignalStrength.kt) | The received signal strength of a wireless device. |
| [triggeredtask](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/TriggeredTask.kt) | A task which was started or stopped by a trigger, referring to identifiers in the study protocol. |
| [completedtask](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/CompletedTask.kt) | An interactive task which was completed over the course of a specified time interval. |
| Name | Description |
|---------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|
| [geolocation](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/Geolocation.kt) | Geographic location data, representing longitude and latitude. |
| [stepcount](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/StepCount.kt) | The number of steps a participant has taken in a specified time interval. |
| [ecg](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/ECG.kt) | Electrocardiogram data of a single lead. |
| [heartrate](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/HeartRate.kt) | Number of heart contractions (beats) per minute. |
| [interbeatinterval](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/InterbeatInterval.kt) | The time interval between two consecutive heartbeats. |
| [sensorskincontact](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/SensorSkinContact.kt) | Whether a sensor requiring contact with skin is making proper contact at a specific point in time. |
| [nongravitationalacceleration](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/NonGravitationalAcceleration.kt) | Acceleration excluding gravity along perpendicular x, y, and z axes. |
| [eda](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/EDA.kt) | Single-channel electrodermal activity, represented as skin conductance. |
| [angularvelocity](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/AngularVelocity.kt) | Rate of rotation around perpendicular x, y, and z axes. |
| [signalstrength](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/SignalStrength.kt) | The received signal strength of a wireless device. |
| [triggeredtask](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/TriggeredTask.kt) | A task which was started or stopped by a trigger, referring to identifiers in the study protocol. |
| [completedtask](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/CompletedTask.kt) | An interactive task which was completed over the course of a specified time interval. |

## Device configurations

Expand Down
30 changes: 30 additions & 0 deletions docs/development-checklists.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,33 @@ These generated test resources will be used to verify the migrations (step 3) of
5. Update the affected JSON schemas. At a minimum you will need to change the request object's API version (e.g., `StudyServiceRequest.json`).
These schemas are useful for non-Kotlin clients.
If you forget to do this, `JsonSchemasTest` will fail; this test validates generated JSON output, known to be correct, using the schemas.

## Add a new measure data type

Keep in mind that CARP data types should be device-agnostic.
The goal is that they can be reused for devices by different vendors.
They act as a common data format.
Therefore, don't include device-specific information in new [`Data`](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/data/Data.kt) types.

If device-specific data is needed, infrastructures built using CARP Core can still specify these in their own codebase.
Furthermore, all [extendable domain objects](../docs/carp-protocols.md#extending-domain-objects) can be uploaded to CARP backends that [use the recommended CARP serializers](../docs/serialization.md#unknownpolymorphicserializer-deserializing-unknown-types);
they don't need the types at compile time or runtime, although then the data won't be validated on upload.

Failing tests and static code analysis (`detektPasses`) will guide you to make sure newly introduced data types are immutable, serializable, registered, and tested.
But, below are the necessary steps to follow:

1. Add data type meta data to `CarpDataTypes`, following the template of existing data types.
2. Add a new class extending from `Data` (or object in case the measure contains no data) to the `dk.cachet.carp.common.application.data` namespace in the `carp.common` subsystem (e.g., `AngularVelocity`).
- Make sure to name the class after the collected _data_, and not the _sensor_ which collects the data (e.g., `AngularVelocity` vs `Gyro`).
- Add clear KDoc documentation on how the data should be interpreted.
For data fields, use/document SI units wherever appropriate, and choose sufficiently precise units so that no data is lost when unit conversions from raw data to the `Data` are done.
- Ensure that the class is immutable (contains no mutable fields) and is a `data class` or `object`.
- Make the class serializable using `kotlinx.serialization`.
For basic types, this should be as easy as marking it as `@Serializable`.
- Specify `@SerialName` using the data type specified in step 1.
3. Register the new data type for polymorphic serialization in [`COMMON_SERIAL_MODULE`](../carp.common/src/commonMain/kotlin/dk/cachet/carp/common/infrastructure/serialization/Serialization.kt).
4. Add a test instance of the new `Data` type to [`commonInstances`](../carp.common/src/commonTest/kotlin/dk/cachet/carp/common/application/TestInstances.kt).
5. Include the data type [in the README](../docs/carp-common.md#data-types).
6. Add a JSON schema in [`rpc/schemas/common/data`](../rpc/schemas/common/data) corresponding to the class name (e.g., `AngularVelocity.json`).
- _Warning_: the presence or validity of this schema [is not yet tested](https://github.com/imotions/carp.core-kotlin/issues/404).
It is recommended to serialize an instance of the new data type (e.g., by running a slightly modified polymorphic serialization test in `DataSerializationTest`) and [validate the output manually for now](https://www.jsonschemavalidator.net/).
13 changes: 13 additions & 0 deletions rpc/schemas/common/data/EDA.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"__type": { "const": "dk.cachet.carp.eda" },
"microSiemens": {
"type": "number",
"minimum": 0
}
},
"required": [ "__type", "microSiemens" ],
"additionalProperties": false
}
12 changes: 10 additions & 2 deletions rpc/schemas/common/data/Geolocation.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
"type": "object",
"properties": {
"__type": { "const": "dk.cachet.carp.geolocation" },
"latitude": { "type": "number" },
"longitude": { "type": "number" }
"latitude": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"longitude": {
"type": "number",
"minimum": -180,
"maximum": 180
}
},
"required": [ "__type", "latitude", "longitude" ],
"additionalProperties": false
Expand Down
5 changes: 4 additions & 1 deletion rpc/schemas/common/data/HeartRate.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"type": "object",
"properties": {
"__type": { "const": "dk.cachet.carp.heartrate" },
"bpm": { "type": "integer" }
"bpm": {
"type": "integer",
"minimum": 0
}
},
"required": [ "__type", "bpm" ],
"additionalProperties": false
Expand Down