fix: placement direction guide 수정 + sensor layout 확대 + BLE prefix 필터(2025/VBT)
- PlacementGuideView: 방향 가이드 로직 수정 (CH0=배꼽, CH3=치골 기준) CH0✅CH3❌→UP, CH3✅CH0❌→DOWN, 좌우도 동일하게 보정 - SensorLayoutCanvas: 스케일 16px/mm, 방광 280×220, body 400×400 - DeviceScanView: BLE prefix 필터 "2025" | "VBT" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git push -u origin master)"
|
||||
"Bash(git push -u origin master)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(./gradlew assembleDebug)",
|
||||
"Bash(xargs grep:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ fun DeviceScanView(appState: AppState) {
|
||||
var permissionsGranted by remember { mutableStateOf(false) }
|
||||
|
||||
val filteredDevices = bleManager.discoveredDevices
|
||||
.filter { it.name.isNotBlank() }
|
||||
.filter { it.name.startsWith("2025") || it.name.startsWith("VBT") }
|
||||
.sortedByDescending { it.rssi }
|
||||
|
||||
val isConnected by bleManager.isConnected
|
||||
|
||||
+108
-74
@@ -396,8 +396,9 @@ fun PlacementGuideView(appState: AppState) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Canvas-based sensor layout visualization.
|
||||
* Draws body outline, bladder outline, and 6 sensor dots with labels.
|
||||
* Canvas-based sensor layout on body — sensors overlaid on abdomen/bladder.
|
||||
* Actual hardware spec proportions (cross array 4+2).
|
||||
* Scale: 1mm ≈ 8px, centered on body.
|
||||
*/
|
||||
@Composable
|
||||
private fun SensorLayoutCanvas(
|
||||
@@ -409,119 +410,149 @@ private fun SensorLayoutCanvas(
|
||||
val redColor = Color.Red.copy(alpha = 0.5f)
|
||||
val orangeColor = Color(0xFFFF9800).copy(alpha = 0.6f)
|
||||
|
||||
val chColors = listOf(
|
||||
Color(0xFFFF6B6B), // CH0
|
||||
Color(0xFFFFB347), // CH1
|
||||
Color(0xFF87CF3E), // CH2
|
||||
Color(0xFF4FC3F7), // CH3
|
||||
Color(0xFF64B5F6), // CH4
|
||||
Color(0xFFCE93D8) // CH5
|
||||
)
|
||||
|
||||
val s = 16f // scale: 1mm = 16px
|
||||
|
||||
data class SensorSpec(val ch: Int, val xMm: Float, val yMm: Float, val tilt: Float, val wMm: Float, val hMm: Float)
|
||||
val sensors = listOf(
|
||||
SensorSpec(3, 0f, 0f, 0f, 12f, 6f),
|
||||
SensorSpec(2, 0f, 7f, 0f, 12f, 6f),
|
||||
SensorSpec(1, 0f, 14f, 0f, 12f, 6f),
|
||||
SensorSpec(0, 0f, 21f, 0f, 12f, 6f),
|
||||
SensorSpec(4, -10f, 9.85f, -5f, 6f, 12f),
|
||||
SensorSpec(5, 10f, 9.85f, 5f, 6f, 12f)
|
||||
)
|
||||
|
||||
val arrayMidMm = 10.5f
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp),
|
||||
.height(420.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.width(340.dp)
|
||||
.height(400.dp)
|
||||
.fillMaxWidth()
|
||||
.height(420.dp)
|
||||
) {
|
||||
val centerX = size.width / 2
|
||||
val centerY = size.height / 2
|
||||
val cx = size.width / 2
|
||||
val cy = size.height / 2
|
||||
|
||||
// Body outline (rounded rectangle) — enlarged
|
||||
// Body outline (abdomen area)
|
||||
drawRoundRect(
|
||||
color = Color.Gray.copy(alpha = 0.2f),
|
||||
topLeft = Offset(centerX - 140f, centerY - 185f),
|
||||
size = Size(280f, 370f),
|
||||
cornerRadius = CornerRadius(40f),
|
||||
color = Color.Gray.copy(alpha = 0.15f),
|
||||
topLeft = Offset(cx - 200f, cy - 200f),
|
||||
size = Size(400f, 400f),
|
||||
cornerRadius = CornerRadius(50f),
|
||||
style = Stroke(width = 2f)
|
||||
)
|
||||
|
||||
// Bladder outline (dashed ellipse) — enlarged
|
||||
// Bladder outline (behind sensors)
|
||||
drawOval(
|
||||
color = Color.Cyan.copy(alpha = 0.3f),
|
||||
topLeft = Offset(centerX - 85f, centerY - 80f),
|
||||
size = Size(170f, 140f),
|
||||
color = Color.Cyan.copy(alpha = 0.2f),
|
||||
topLeft = Offset(cx - 140f, cy - 110f),
|
||||
size = Size(280f, 220f),
|
||||
style = Stroke(
|
||||
width = 1.5f,
|
||||
width = 2f,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 6f))
|
||||
)
|
||||
)
|
||||
|
||||
// Sensor positions — spread out more
|
||||
data class SensorPos(val ch: Int, val x: Float, val y: Float, val angle: Float)
|
||||
val sensors = listOf(
|
||||
SensorPos(3, 0f, -120f, -10f),
|
||||
SensorPos(2, 0f, -40f, 0f),
|
||||
SensorPos(1, 0f, 40f, 10f),
|
||||
SensorPos(0, 0f, 120f, 20f),
|
||||
SensorPos(4, -110f, -40f, 10f),
|
||||
SensorPos(5, 110f, -40f, 10f)
|
||||
drawOval(
|
||||
color = Color.Cyan.copy(alpha = 0.05f),
|
||||
topLeft = Offset(cx - 140f, cy - 110f),
|
||||
size = Size(280f, 220f)
|
||||
)
|
||||
|
||||
for (s in sensors) {
|
||||
val detected = if (s.ch < channelDetected.size) channelDetected[s.ch] else false
|
||||
val color = if (detected) greenColor else redColor
|
||||
val sx = centerX + s.x
|
||||
val sy = centerY + s.y
|
||||
// Draw sensors centered on body
|
||||
for (spec in sensors) {
|
||||
val detected = if (spec.ch < channelDetected.size) channelDetected[spec.ch] else false
|
||||
val color = if (detected) chColors[spec.ch] else chColors[spec.ch].copy(alpha = 0.25f)
|
||||
val border = if (detected) chColors[spec.ch] else Color.Gray.copy(alpha = 0.4f)
|
||||
val px = cx + spec.xMm * s
|
||||
val py = cy + (spec.yMm - arrayMidMm) * s
|
||||
val pw = spec.wMm * s
|
||||
val ph = spec.hMm * s
|
||||
|
||||
rotate(degrees = s.angle, pivot = Offset(sx, sy)) {
|
||||
rotate(degrees = spec.tilt, pivot = Offset(px, py)) {
|
||||
drawRoundRect(
|
||||
color = color,
|
||||
topLeft = Offset(sx - 20f, sy - 10f),
|
||||
size = Size(40f, 20f),
|
||||
cornerRadius = CornerRadius(4f)
|
||||
color = color.copy(alpha = 0.4f),
|
||||
topLeft = Offset(px - pw / 2, py - ph / 2),
|
||||
size = Size(pw, ph),
|
||||
cornerRadius = CornerRadius(6f)
|
||||
)
|
||||
drawRoundRect(
|
||||
color = border,
|
||||
topLeft = Offset(px - pw / 2, py - ph / 2),
|
||||
size = Size(pw, ph),
|
||||
cornerRadius = CornerRadius(6f),
|
||||
style = Stroke(width = 3f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Direction arrow overlay
|
||||
// Direction arrows
|
||||
if (directionIcon.isNotEmpty() && !isLocked) {
|
||||
val arrowLength = 40f
|
||||
val al = 45f
|
||||
when {
|
||||
directionIcon.contains("up") -> {
|
||||
val ax = centerX; val ay = centerY - 175f
|
||||
drawLine(orangeColor, Offset(ax, ay + arrowLength), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax - 12f, ay + 12f), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax + 12f, ay + 12f), Offset(ax, ay), strokeWidth = 4f)
|
||||
val ax = cx; val ay = cy - 190f
|
||||
drawLine(orangeColor, Offset(ax, ay + al), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax - 14f, ay + 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax + 14f, ay + 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||
}
|
||||
directionIcon.contains("down") -> {
|
||||
val ax = centerX; val ay = centerY + 175f
|
||||
drawLine(orangeColor, Offset(ax, ay - arrowLength), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax - 12f, ay - 12f), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax + 12f, ay - 12f), Offset(ax, ay), strokeWidth = 4f)
|
||||
val ax = cx; val ay = cy + 190f
|
||||
drawLine(orangeColor, Offset(ax, ay - al), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax - 14f, ay - 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax + 14f, ay - 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||
}
|
||||
directionIcon.contains("left") -> {
|
||||
val ax = centerX - 170f; val ay = centerY
|
||||
drawLine(orangeColor, Offset(ax + arrowLength, ay), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax + 12f, ay - 12f), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax + 12f, ay + 12f), Offset(ax, ay), strokeWidth = 4f)
|
||||
val ax = cx - 190f; val ay = cy
|
||||
drawLine(orangeColor, Offset(ax + al, ay), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax + 14f, ay - 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax + 14f, ay + 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||
}
|
||||
directionIcon.contains("right") -> {
|
||||
val ax = centerX + 170f; val ay = centerY
|
||||
drawLine(orangeColor, Offset(ax - arrowLength, ay), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax - 12f, ay - 12f), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax - 12f, ay + 12f), Offset(ax, ay), strokeWidth = 4f)
|
||||
val ax = cx + 190f; val ay = cy
|
||||
drawLine(orangeColor, Offset(ax - al, ay), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax - 14f, ay - 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||
drawLine(orangeColor, Offset(ax - 14f, ay + 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay text labels — repositioned for larger layout
|
||||
data class LabelPos(val ch: Int, val x: Int, val y: Int, val label: String)
|
||||
val labels = listOf(
|
||||
LabelPos(3, 0, -120, "CH3\n-10\u00B0"),
|
||||
LabelPos(2, 0, -40, "CH2\n0\u00B0"),
|
||||
LabelPos(1, 0, 40, "CH1\n+10\u00B0"),
|
||||
LabelPos(0, 0, 120, "CH0\n+20\u00B0"),
|
||||
LabelPos(4, -110, -40, "CH4\nL 5\u00B0"),
|
||||
LabelPos(5, 110, -40, "CH5\nR 5\u00B0")
|
||||
// Channel labels next to sensors
|
||||
data class LabelInfo(val ch: Int, val xMm: Float, val yMm: Float, val label: String)
|
||||
val labelSpecs = listOf(
|
||||
LabelInfo(3, 0f, 0f, "CH3 · 30\u00B0"),
|
||||
LabelInfo(2, 0f, 7f, "CH2 · 20\u00B0"),
|
||||
LabelInfo(1, 0f, 14f, "CH1 · 10\u00B0"),
|
||||
LabelInfo(0, 0f, 21f, "CH0 · 0\u00B0"),
|
||||
LabelInfo(4, -10f, 9.85f, "CH4"),
|
||||
LabelInfo(5, 10f, 9.85f, "CH5")
|
||||
)
|
||||
for (lbl in labels) {
|
||||
for (lbl in labelSpecs) {
|
||||
val detected = if (lbl.ch < channelDetected.size) channelDetected[lbl.ch] else false
|
||||
val yOff = (lbl.yMm - arrayMidMm) * s
|
||||
val xOff = if (lbl.ch == 4) (lbl.xMm * s - 60f)
|
||||
else if (lbl.ch == 5) (lbl.xMm * s + 60f)
|
||||
else (12f * s / 2 + 12f)
|
||||
Text(
|
||||
text = lbl.label,
|
||||
fontSize = 9.sp,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (detected) Color(0xFF4CAF50) else Color.Gray,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 11.sp,
|
||||
modifier = Modifier.offset(x = lbl.x.dp, y = (lbl.y + 18).dp)
|
||||
color = if (detected) chColors[lbl.ch] else Color.Gray.copy(alpha = 0.6f),
|
||||
modifier = Modifier.offset(x = xOff.dp, y = yOff.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -544,19 +575,22 @@ private fun updateDirectionGuide(detected: List<Boolean>): Pair<String, String>
|
||||
}
|
||||
|
||||
// Vertical guidance
|
||||
// CH0=위(배꼽), CH3=아래(치골)
|
||||
// CH0✅ CH3❌ → 방광이 센서보다 위 → 센서를 위로
|
||||
// CH0❌ CH3✅ → 방광이 센서보다 아래 → 센서를 아래로
|
||||
if (!topDetected && bottomDetected) {
|
||||
return "Move sensor DOWN" to "arrow.down"
|
||||
return "Move sensor UP" to "arrow.up"
|
||||
}
|
||||
if (topDetected && !bottomDetected) {
|
||||
return "Move sensor UP" to "arrow.up"
|
||||
return "Move sensor DOWN" to "arrow.down"
|
||||
}
|
||||
|
||||
// Horizontal guidance
|
||||
if (!leftDetected && rightDetected) {
|
||||
return "Move sensor RIGHT" to "arrow.right"
|
||||
return "Move sensor LEFT" to "arrow.left"
|
||||
}
|
||||
if (leftDetected && !rightDetected) {
|
||||
return "Move sensor LEFT" to "arrow.left"
|
||||
return "Move sensor RIGHT" to "arrow.right"
|
||||
}
|
||||
|
||||
// No signal
|
||||
|
||||
Reference in New Issue
Block a user