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": {
|
"permissions": {
|
||||||
"allow": [
|
"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) }
|
var permissionsGranted by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val filteredDevices = bleManager.discoveredDevices
|
val filteredDevices = bleManager.discoveredDevices
|
||||||
.filter { it.name.isNotBlank() }
|
.filter { it.name.startsWith("2025") || it.name.startsWith("VBT") }
|
||||||
.sortedByDescending { it.rssi }
|
.sortedByDescending { it.rssi }
|
||||||
|
|
||||||
val isConnected by bleManager.isConnected
|
val isConnected by bleManager.isConnected
|
||||||
|
|||||||
+108
-74
@@ -396,8 +396,9 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canvas-based sensor layout visualization.
|
* Canvas-based sensor layout on body — sensors overlaid on abdomen/bladder.
|
||||||
* Draws body outline, bladder outline, and 6 sensor dots with labels.
|
* Actual hardware spec proportions (cross array 4+2).
|
||||||
|
* Scale: 1mm ≈ 8px, centered on body.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun SensorLayoutCanvas(
|
private fun SensorLayoutCanvas(
|
||||||
@@ -409,119 +410,149 @@ private fun SensorLayoutCanvas(
|
|||||||
val redColor = Color.Red.copy(alpha = 0.5f)
|
val redColor = Color.Red.copy(alpha = 0.5f)
|
||||||
val orangeColor = Color(0xFFFF9800).copy(alpha = 0.6f)
|
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(400.dp),
|
.height(420.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(340.dp)
|
.fillMaxWidth()
|
||||||
.height(400.dp)
|
.height(420.dp)
|
||||||
) {
|
) {
|
||||||
val centerX = size.width / 2
|
val cx = size.width / 2
|
||||||
val centerY = size.height / 2
|
val cy = size.height / 2
|
||||||
|
|
||||||
// Body outline (rounded rectangle) — enlarged
|
// Body outline (abdomen area)
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
color = Color.Gray.copy(alpha = 0.2f),
|
color = Color.Gray.copy(alpha = 0.15f),
|
||||||
topLeft = Offset(centerX - 140f, centerY - 185f),
|
topLeft = Offset(cx - 200f, cy - 200f),
|
||||||
size = Size(280f, 370f),
|
size = Size(400f, 400f),
|
||||||
cornerRadius = CornerRadius(40f),
|
cornerRadius = CornerRadius(50f),
|
||||||
style = Stroke(width = 2f)
|
style = Stroke(width = 2f)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bladder outline (dashed ellipse) — enlarged
|
// Bladder outline (behind sensors)
|
||||||
drawOval(
|
drawOval(
|
||||||
color = Color.Cyan.copy(alpha = 0.3f),
|
color = Color.Cyan.copy(alpha = 0.2f),
|
||||||
topLeft = Offset(centerX - 85f, centerY - 80f),
|
topLeft = Offset(cx - 140f, cy - 110f),
|
||||||
size = Size(170f, 140f),
|
size = Size(280f, 220f),
|
||||||
style = Stroke(
|
style = Stroke(
|
||||||
width = 1.5f,
|
width = 2f,
|
||||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 6f))
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 6f))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
drawOval(
|
||||||
// Sensor positions — spread out more
|
color = Color.Cyan.copy(alpha = 0.05f),
|
||||||
data class SensorPos(val ch: Int, val x: Float, val y: Float, val angle: Float)
|
topLeft = Offset(cx - 140f, cy - 110f),
|
||||||
val sensors = listOf(
|
size = Size(280f, 220f)
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for (s in sensors) {
|
// Draw sensors centered on body
|
||||||
val detected = if (s.ch < channelDetected.size) channelDetected[s.ch] else false
|
for (spec in sensors) {
|
||||||
val color = if (detected) greenColor else redColor
|
val detected = if (spec.ch < channelDetected.size) channelDetected[spec.ch] else false
|
||||||
val sx = centerX + s.x
|
val color = if (detected) chColors[spec.ch] else chColors[spec.ch].copy(alpha = 0.25f)
|
||||||
val sy = centerY + s.y
|
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(
|
drawRoundRect(
|
||||||
color = color,
|
color = color.copy(alpha = 0.4f),
|
||||||
topLeft = Offset(sx - 20f, sy - 10f),
|
topLeft = Offset(px - pw / 2, py - ph / 2),
|
||||||
size = Size(40f, 20f),
|
size = Size(pw, ph),
|
||||||
cornerRadius = CornerRadius(4f)
|
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) {
|
if (directionIcon.isNotEmpty() && !isLocked) {
|
||||||
val arrowLength = 40f
|
val al = 45f
|
||||||
when {
|
when {
|
||||||
directionIcon.contains("up") -> {
|
directionIcon.contains("up") -> {
|
||||||
val ax = centerX; val ay = centerY - 175f
|
val ax = cx; val ay = cy - 190f
|
||||||
drawLine(orangeColor, Offset(ax, ay + arrowLength), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax, ay + al), Offset(ax, ay), strokeWidth = 4f)
|
||||||
drawLine(orangeColor, Offset(ax - 12f, ay + 12f), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax - 14f, ay + 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||||
drawLine(orangeColor, Offset(ax + 12f, ay + 12f), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax + 14f, ay + 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||||
}
|
}
|
||||||
directionIcon.contains("down") -> {
|
directionIcon.contains("down") -> {
|
||||||
val ax = centerX; val ay = centerY + 175f
|
val ax = cx; val ay = cy + 190f
|
||||||
drawLine(orangeColor, Offset(ax, ay - arrowLength), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax, ay - al), Offset(ax, ay), strokeWidth = 4f)
|
||||||
drawLine(orangeColor, Offset(ax - 12f, ay - 12f), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax - 14f, ay - 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||||
drawLine(orangeColor, Offset(ax + 12f, ay - 12f), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax + 14f, ay - 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||||
}
|
}
|
||||||
directionIcon.contains("left") -> {
|
directionIcon.contains("left") -> {
|
||||||
val ax = centerX - 170f; val ay = centerY
|
val ax = cx - 190f; val ay = cy
|
||||||
drawLine(orangeColor, Offset(ax + arrowLength, ay), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax + al, ay), Offset(ax, ay), strokeWidth = 4f)
|
||||||
drawLine(orangeColor, Offset(ax + 12f, ay - 12f), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax + 14f, ay - 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||||
drawLine(orangeColor, Offset(ax + 12f, ay + 12f), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax + 14f, ay + 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||||
}
|
}
|
||||||
directionIcon.contains("right") -> {
|
directionIcon.contains("right") -> {
|
||||||
val ax = centerX + 170f; val ay = centerY
|
val ax = cx + 190f; val ay = cy
|
||||||
drawLine(orangeColor, Offset(ax - arrowLength, ay), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax - al, ay), Offset(ax, ay), strokeWidth = 4f)
|
||||||
drawLine(orangeColor, Offset(ax - 12f, ay - 12f), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax - 14f, ay - 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||||
drawLine(orangeColor, Offset(ax - 12f, ay + 12f), Offset(ax, ay), strokeWidth = 4f)
|
drawLine(orangeColor, Offset(ax - 14f, ay + 14f), Offset(ax, ay), strokeWidth = 4f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overlay text labels — repositioned for larger layout
|
// Channel labels next to sensors
|
||||||
data class LabelPos(val ch: Int, val x: Int, val y: Int, val label: String)
|
data class LabelInfo(val ch: Int, val xMm: Float, val yMm: Float, val label: String)
|
||||||
val labels = listOf(
|
val labelSpecs = listOf(
|
||||||
LabelPos(3, 0, -120, "CH3\n-10\u00B0"),
|
LabelInfo(3, 0f, 0f, "CH3 · 30\u00B0"),
|
||||||
LabelPos(2, 0, -40, "CH2\n0\u00B0"),
|
LabelInfo(2, 0f, 7f, "CH2 · 20\u00B0"),
|
||||||
LabelPos(1, 0, 40, "CH1\n+10\u00B0"),
|
LabelInfo(1, 0f, 14f, "CH1 · 10\u00B0"),
|
||||||
LabelPos(0, 0, 120, "CH0\n+20\u00B0"),
|
LabelInfo(0, 0f, 21f, "CH0 · 0\u00B0"),
|
||||||
LabelPos(4, -110, -40, "CH4\nL 5\u00B0"),
|
LabelInfo(4, -10f, 9.85f, "CH4"),
|
||||||
LabelPos(5, 110, -40, "CH5\nR 5\u00B0")
|
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 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(
|
||||||
text = lbl.label,
|
text = lbl.label,
|
||||||
fontSize = 9.sp,
|
fontSize = 11.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = if (detected) Color(0xFF4CAF50) else Color.Gray,
|
color = if (detected) chColors[lbl.ch] else Color.Gray.copy(alpha = 0.6f),
|
||||||
textAlign = TextAlign.Center,
|
modifier = Modifier.offset(x = xOff.dp, y = yOff.dp)
|
||||||
lineHeight = 11.sp,
|
|
||||||
modifier = Modifier.offset(x = lbl.x.dp, y = (lbl.y + 18).dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -544,19 +575,22 @@ private fun updateDirectionGuide(detected: List<Boolean>): Pair<String, String>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vertical guidance
|
// Vertical guidance
|
||||||
|
// CH0=위(배꼽), CH3=아래(치골)
|
||||||
|
// CH0✅ CH3❌ → 방광이 센서보다 위 → 센서를 위로
|
||||||
|
// CH0❌ CH3✅ → 방광이 센서보다 아래 → 센서를 아래로
|
||||||
if (!topDetected && bottomDetected) {
|
if (!topDetected && bottomDetected) {
|
||||||
return "Move sensor DOWN" to "arrow.down"
|
return "Move sensor UP" to "arrow.up"
|
||||||
}
|
}
|
||||||
if (topDetected && !bottomDetected) {
|
if (topDetected && !bottomDetected) {
|
||||||
return "Move sensor UP" to "arrow.up"
|
return "Move sensor DOWN" to "arrow.down"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal guidance
|
// Horizontal guidance
|
||||||
if (!leftDetected && rightDetected) {
|
if (!leftDetected && rightDetected) {
|
||||||
return "Move sensor RIGHT" to "arrow.right"
|
return "Move sensor LEFT" to "arrow.left"
|
||||||
}
|
}
|
||||||
if (leftDetected && !rightDetected) {
|
if (leftDetected && !rightDetected) {
|
||||||
return "Move sensor LEFT" to "arrow.left"
|
return "Move sensor RIGHT" to "arrow.right"
|
||||||
}
|
}
|
||||||
|
|
||||||
// No signal
|
// No signal
|
||||||
|
|||||||
Reference in New Issue
Block a user