Skip to content

Commit 459370a

Browse files
authored
feat: Implement acceleration graph and data handling in MainActivity and YureSensorService (#10)
1 parent cf366de commit 459370a

File tree

3 files changed

+200
-1
lines changed

3 files changed

+200
-1
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package com.sasakulab.yure_android_client
2+
3+
import androidx.compose.foundation.Canvas
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.border
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.MaterialTheme
11+
import androidx.compose.material3.Text
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.geometry.Offset
15+
import androidx.compose.ui.graphics.Color
16+
import androidx.compose.ui.graphics.Path
17+
import androidx.compose.ui.graphics.drawscope.Stroke
18+
import androidx.compose.ui.unit.dp
19+
20+
@Composable
21+
fun AccelerationGraph(
22+
sensorData: List<AccelerationData>,
23+
currentTime: Long,
24+
modifier: Modifier = Modifier
25+
) {
26+
Column(modifier = modifier) {
27+
Text(
28+
text = "Sensor Data: ${sensorData.size} points | Last: ${if (sensorData.isNotEmpty()) "%.2f, %.2f, %.2f".format(sensorData[0].x, sensorData[0].y, sensorData[0].z) else "N/A"}",
29+
style = MaterialTheme.typography.bodySmall,
30+
modifier = Modifier.padding(4.dp)
31+
)
32+
Box(
33+
modifier = Modifier
34+
.fillMaxWidth()
35+
.weight(1f)
36+
.background(Color.White)
37+
.border(1.dp, Color.Gray)
38+
) {
39+
Canvas(modifier = Modifier.matchParentSize()) {
40+
val width = size.width
41+
val height = size.height
42+
val centerY = height / 2f
43+
44+
val tDiv = 50f
45+
val xMul = 10f
46+
val xOff = 50f
47+
val yOff = 0f
48+
val zOff = -50f
49+
50+
// Background Grid Lines
51+
drawLine(
52+
color = Color.Gray.copy(alpha = 0.25f),
53+
start = Offset(0f, centerY + xOff),
54+
end = Offset(width, centerY + xOff),
55+
strokeWidth = 1f
56+
)
57+
drawLine(
58+
color = Color.Gray.copy(alpha = 0.25f),
59+
start = Offset(0f, centerY + yOff),
60+
end = Offset(width, centerY + yOff),
61+
strokeWidth = 1f
62+
)
63+
drawLine(
64+
color = Color.Gray.copy(alpha = 0.25f),
65+
start = Offset(0f, centerY + zOff),
66+
end = Offset(width, centerY + zOff),
67+
strokeWidth = 1f
68+
)
69+
70+
// Draw vertical lines (time axis grid)
71+
var i = 0f
72+
while (i < width) {
73+
drawLine(
74+
color = Color.Gray.copy(alpha = 0.25f),
75+
start = Offset(i, 0f),
76+
end = Offset(i, height),
77+
strokeWidth = 1f
78+
)
79+
i += 1000f / tDiv
80+
}
81+
82+
if (sensorData.isEmpty()) return@Canvas
83+
84+
// Draw X-axis data in red
85+
val xPath = Path()
86+
var isFirstX = true
87+
sensorData.asReversed().forEach { data ->
88+
val x = width - ((currentTime - data.t) / tDiv)
89+
if (x >= 0 && x <= width) {
90+
val y = (centerY + (data.x * xMul).toFloat() + xOff).coerceIn(0f, height)
91+
if (isFirstX) {
92+
xPath.moveTo(x, y)
93+
isFirstX = false
94+
} else {
95+
xPath.lineTo(x, y)
96+
}
97+
}
98+
}
99+
drawPath(
100+
path = xPath,
101+
color = Color.Red,
102+
style = Stroke(width = 2f)
103+
)
104+
105+
// Draw Y-axis data in green
106+
val yPath = Path()
107+
var isFirstY = true
108+
sensorData.asReversed().forEach { data ->
109+
val x = width - ((currentTime - data.t) / tDiv)
110+
if (x >= 0 && x <= width) {
111+
val y = (centerY + (data.y * xMul).toFloat() + yOff).coerceIn(0f, height)
112+
if (isFirstY) {
113+
yPath.moveTo(x, y)
114+
isFirstY = false
115+
} else {
116+
yPath.lineTo(x, y)
117+
}
118+
}
119+
}
120+
drawPath(
121+
path = yPath,
122+
color = Color.Green,
123+
style = Stroke(width = 2f)
124+
)
125+
126+
// Draw Z-axis data in blue
127+
val zPath = Path()
128+
var isFirstZ = true
129+
sensorData.asReversed().forEach { data ->
130+
val x = width - ((currentTime - data.t) / tDiv)
131+
if (x >= 0 && x <= width) {
132+
val y = (centerY + (data.z * xMul).toFloat() + zOff).coerceIn(0f, height)
133+
if (isFirstZ) {
134+
zPath.moveTo(x, y)
135+
isFirstZ = false
136+
} else {
137+
zPath.lineTo(x, y)
138+
}
139+
}
140+
}
141+
drawPath(
142+
path = zPath,
143+
color = Color.Blue,
144+
style = Stroke(width = 2f)
145+
)
146+
}
147+
}
148+
}
149+
}

app/src/main/java/com/sasakulab/yure_android_client/MainActivity.kt

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,51 @@ import android.os.Bundle
77
import androidx.activity.ComponentActivity
88
import androidx.activity.compose.setContent
99
import androidx.activity.enableEdgeToEdge
10+
import androidx.compose.foundation.Canvas
1011
import androidx.compose.foundation.layout.*
12+
import androidx.compose.foundation.rememberScrollState
1113
import androidx.compose.foundation.text.KeyboardOptions
14+
import androidx.compose.foundation.verticalScroll
1215
import androidx.compose.material3.*
1316
import androidx.compose.runtime.*
1417
import androidx.compose.ui.Alignment
1518
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.geometry.Offset
20+
import androidx.compose.ui.graphics.Color
21+
import androidx.compose.ui.graphics.Path
22+
import androidx.compose.ui.graphics.drawscope.Stroke
1623
import androidx.compose.ui.text.input.KeyboardType
1724
import androidx.compose.ui.unit.dp
1825
import com.sasakulab.yure_android_client.ui.theme.YureandroidclientTheme
26+
import kotlinx.coroutines.delay
1927
import kotlin.random.Random
2028

2129
class MainActivity : ComponentActivity() {
2230

2331
private lateinit var yureId: String
2432
private var isSharing = mutableStateOf(false)
2533
private var statusText = mutableStateOf("Stop")
34+
private val sensorDataBuffer = mutableStateListOf<AccelerationData>()
35+
private val maxBufferSize = 3000
36+
37+
companion object {
38+
var dataListener: ((AccelerationData) -> Unit)? = null
39+
}
2640

2741
override fun onCreate(savedInstanceState: Bundle?) {
2842
super.onCreate(savedInstanceState)
2943

3044
// Get or Generate ゆれ識別子
3145
yureId = getYureId()
3246

47+
// Set data listener
48+
dataListener = { data ->
49+
sensorDataBuffer.add(0, data)
50+
if (sensorDataBuffer.size > maxBufferSize) {
51+
sensorDataBuffer.removeAt(sensorDataBuffer.size - 1)
52+
}
53+
}
54+
3355
enableEdgeToEdge()
3456
setContent {
3557
YureandroidclientTheme {
@@ -40,6 +62,7 @@ class MainActivity : ComponentActivity() {
4062
statusText = statusText.value,
4163
serverUrl = getServerUrl(),
4264
bufferSize = getBufferSize(),
65+
sensorData = sensorDataBuffer,
4366
onStartClick = { startSharing() },
4467
onStopClick = { stopSharing() },
4568
onServerUrlChanged = { saveServerUrl(it) },
@@ -117,6 +140,7 @@ class MainActivity : ComponentActivity() {
117140

118141
override fun onDestroy() {
119142
super.onDestroy()
143+
dataListener = null
120144
}
121145
}
122146

@@ -136,6 +160,7 @@ fun YureScreen(
136160
statusText: String,
137161
serverUrl: String,
138162
bufferSize: Int,
163+
sensorData: List<AccelerationData>,
139164
onStartClick: () -> Unit,
140165
onStopClick: () -> Unit,
141166
onServerUrlChanged: (String) -> Unit,
@@ -144,10 +169,20 @@ fun YureScreen(
144169
) {
145170
var showServerUrlDialog by remember { mutableStateOf(false) }
146171
var showBufferSizeDialog by remember { mutableStateOf(false) }
172+
var currentTime by remember { mutableStateOf(System.currentTimeMillis()) }
173+
174+
// Reload current time every 50ms
175+
LaunchedEffect(Unit) {
176+
while (true) {
177+
delay(50)
178+
currentTime = System.currentTimeMillis()
179+
}
180+
}
147181

148182
Column(
149183
modifier = modifier
150184
.fillMaxSize()
185+
.verticalScroll(rememberScrollState())
151186
.padding(16.dp),
152187
horizontalAlignment = Alignment.CenterHorizontally,
153188
verticalArrangement = Arrangement.Center
@@ -166,7 +201,19 @@ fun YureScreen(
166201
text = statusText,
167202
style = MaterialTheme.typography.bodyLarge
168203
)
169-
Spacer(modifier = Modifier.height(32.dp))
204+
205+
Spacer(modifier = Modifier.height(16.dp))
206+
207+
// Display graph (always displayed, empty graph if no data)
208+
AccelerationGraph(
209+
sensorData = sensorData,
210+
currentTime = currentTime,
211+
modifier = Modifier
212+
.fillMaxWidth()
213+
.height(250.dp)
214+
)
215+
216+
Spacer(modifier = Modifier.height(16.dp))
170217

171218
Button(
172219
onClick = onStartClick,

app/src/main/java/com/sasakulab/yure_android_client/YureSensorService.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ class YureSensorService : Service(), SensorEventListener {
146146
t = System.currentTimeMillis()
147147
)
148148

149+
// Send data to MainActivity via listener
150+
MainActivity.dataListener?.invoke(data)
151+
149152
synchronized(bufferLock) {
150153
dataBuffer.add(data)
151154
if (dataBuffer.size >= bufferSize) {

0 commit comments

Comments
 (0)