Skip to content

Commit

Permalink
Merge pull request #14 from atick-faisal/feature/record-data
Browse files Browse the repository at this point in the history
Feature/record data
  • Loading branch information
atick-faisal authored Nov 10, 2022
2 parents 7f2aa3b + 6eea2a1 commit cccb1c9
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 7 deletions.
54 changes: 54 additions & 0 deletions app/src/main/kotlin/dev/atick/compose/ui/BleViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package dev.atick.compose.ui

import android.os.Environment
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.map
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.data.ScatterDataSet
import com.orhanobut.logger.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.atick.core.ui.BaseViewModel
import dev.atick.movesense.data.BtDevice
import dev.atick.movesense.data.EcgSignal
import dev.atick.movesense.data.toCsv
import dev.atick.movesense.repository.Movesense
import dev.atick.movesense.service.MovesenseService
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -51,9 +60,19 @@ class BleViewModel @Inject constructor(
)
}

val ecgSignal = movesense.ecgSignal

val isScanning = mutableStateOf(false)
val devices = mutableStateListOf<BtDevice>()

val recordState = mutableStateOf<RecordState>(RecordState.NotRecording)
private val recording = mutableListOf<EcgSignal>()

sealed class RecordState(val description: String) {
object Recording : RecordState("STOP RECORDING")
object NotRecording : RecordState("RECORD")
}

fun startScan() {
if (!isScanning.value) {
isScanning.value = true
Expand Down Expand Up @@ -89,4 +108,39 @@ class BleViewModel @Inject constructor(
}
super.onCleared()
}

fun updateRecording(ecgSignal: EcgSignal) {
if (recordState.value == RecordState.Recording) {
recording.add(ecgSignal)
}
}

fun record() {
if (recordState.value == RecordState.NotRecording) {
Logger.d("STARTING TO RECORD ... ")
recordState.value = RecordState.Recording
} else {
Logger.d("SAVING DATA ... ")
saveRecording()
recordState.value = RecordState.NotRecording
recording.clear()
}
}

private fun saveRecording() {
val timestamp = SimpleDateFormat("dd_M_yyyy_hh_mm_ss", Locale.US).format(Date())
val csvData = recording.toCsv()
val myExternalFile = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
"${timestamp}.csv"
)

try {
val fos = FileOutputStream(myExternalFile)
fos.write(csvData.toByteArray())
fos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
8 changes: 8 additions & 0 deletions app/src/main/kotlin/dev/atick/compose/ui/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import dev.atick.compose.ui.BleViewModel
import dev.atick.compose.ui.theme.ComposeTheme
import dev.atick.core.service.BaseLifecycleService.Companion.ACTION_STOP_SERVICE
import dev.atick.core.ui.BaseComposeFragment
import dev.atick.core.utils.extensions.observe
import dev.atick.core.utils.extensions.showAlertDialog
import dev.atick.movesense.service.MovesenseService

Expand All @@ -26,6 +27,13 @@ class HomeFragment : BaseComposeFragment() {
}
}

override fun observeStates() {
super.observeStates()
observe(viewModel.ecgSignal) {
viewModel.updateRecording(it)
}
}

private fun onExitClick() {
requireContext().showAlertDialog(
title = getString(R.string.warning),
Expand Down
19 changes: 18 additions & 1 deletion app/src/main/kotlin/dev/atick/compose/ui/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package dev.atick.compose.ui.home

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
Expand Down Expand Up @@ -32,6 +35,8 @@ fun HomeScreen(
ScatterDataSet(listOf(), "R_PEAK")
)

val recordState by viewModel.recordState

return Box(
Modifier
.fillMaxSize()
Expand Down Expand Up @@ -59,7 +64,19 @@ fun HomeScreen(

Spacer(modifier = Modifier.height(16.dp))

EcgCard(ecgDataset = ecgDataset, rPeakDataset=rPeakDataset)
EcgCard(ecgDataset = ecgDataset, rPeakDataset = rPeakDataset)

Spacer(modifier = Modifier.height(16.dp))

Button(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
shape = RoundedCornerShape(16.dp),
onClick = { viewModel.record() }
) {
Text(text = recordState.description)
}
}
}
}
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ buildscript {
compile_sdk_version = 32
min_sdk_version = 24
target_sdk_version = 32
version_code = 19
version_name = "2.1.3"
version_code = 20
version_name = "2.2.0"

agp_version = "7.1.3"
kotlin_version = "1.6.10"
Expand Down
23 changes: 23 additions & 0 deletions movesense/src/main/kotlin/dev/atick/movesense/data/EcgSignal.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.atick.movesense.data

data class EcgSignal(
val timestamp: Long,
val values: List<Int>
) {
override fun toString(): String {
return "$timestamp,${values.joinToString(separator = ",")}"
}
}

fun MutableList<EcgSignal>.toCsv(): String {
val sb = StringBuilder()
sb.append(
"timestamp,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15\n"
)
forEach {
sb.append(it.toString())
sb.append("\n")
}

return sb.toString()
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package dev.atick.movesense.repository

import androidx.lifecycle.LiveData
import dev.atick.movesense.data.BtDevice
import dev.atick.movesense.data.ConnectionStatus
import dev.atick.movesense.data.Ecg
import dev.atick.movesense.data.RPeakData
import dev.atick.movesense.data.*

interface Movesense {
val isConnected: LiveData<Boolean>
Expand All @@ -14,6 +11,7 @@ interface Movesense {
val ecgData: LiveData<List<Int>>
val rPeakData: LiveData<List<RPeakData>>
val ecg: LiveData<Ecg>
val ecgSignal: LiveData<EcgSignal>
fun startScan(onDeviceFound: (BtDevice) -> Unit)
fun connect(address: String, onConnect: () -> Unit)
fun stopScan()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import dev.atick.movesense.config.MovesenseConfig.URI_EVENT_LISTENER
import dev.atick.movesense.config.MovesenseConfig.URI_MEAS_HR
import dev.atick.movesense.data.*
import io.reactivex.disposables.Disposable
import java.util.Date
import javax.inject.Inject

@SuppressLint("MissingPermission")
Expand Down Expand Up @@ -68,6 +69,10 @@ class MovesenseImpl @Inject constructor(
override val ecg: LiveData<Ecg>
get() = _ecg

private val _ecgSignal = MutableLiveData<EcgSignal>()
override val ecgSignal: LiveData<EcgSignal>
get() = _ecgSignal

override fun startScan(onDeviceFound: (BtDevice) -> Unit) {
Logger.i("SCANNING ... ")
scanDisposable = rxBleClient?.scanBleDevices(
Expand Down Expand Up @@ -231,6 +236,12 @@ class MovesenseImpl @Inject constructor(
data, EcgResponse::class.java
)
ecgResponse?.body?.samples?.let { ecgSamples ->
_ecgSignal.postValue(
EcgSignal(
timestamp = Date().time,
values = ecgSamples
)
)
ecgBuffer.subList(0, bufferLen).clear()
ecgBuffer.addAll(ecgSamples)
_ecgData.postValue(ecgBuffer)
Expand Down

0 comments on commit cccb1c9

Please sign in to comment.