diff --git a/README.md b/README.md index 2b67ac7..a462888 100644 --- a/README.md +++ b/README.md @@ -214,3 +214,10 @@ - `loadFluenceTable()`, `loadHzTable()`, `calculateInterpolatedC()` 영향 확인 - 성능/안정성 이슈 시 - `txPacketLoop()` 주기, `RX_TIMEOUT_THRESHOLD`, DB 로그 적재량 점검 + +## 10) Troubleshooting + +- 시리얼 초기 TX 누락(Startup race, FD 설명, 수정 내역): + - [`docs/serial_tx_startup_race.md`](docs/serial_tx_startup_race.md) +- 변경 요약 문서(`git diff` 기준): + - [`docs/change_summary_2026-03-04.md`](docs/change_summary_2026-03-04.md) diff --git a/app/src/main/java/com/laseroptek/raman/ui/MainActivity.kt b/app/src/main/java/com/laseroptek/raman/ui/MainActivity.kt index 65c7d8c..70d3fbc 100644 --- a/app/src/main/java/com/laseroptek/raman/ui/MainActivity.kt +++ b/app/src/main/java/com/laseroptek/raman/ui/MainActivity.kt @@ -260,9 +260,11 @@ class MainActivity : ComponentActivity() { // This prevents serial interrupts from stealing CPU during the first frame. delay(200) - vm.txPacketOnce() - + // IMPORTANT: + // rxPacketLoop() starts serial open() asynchronously. + // Start RX first so txPacketOnce() is less likely to run before FD is ready. vm.rxPacketLoop() + vm.txPacketOnce() vm.txPacketLoop() Timber.d("System fully operational.") diff --git a/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt index b5a5063..41401c9 100644 --- a/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt @@ -814,24 +814,62 @@ class MainViewModel @Inject constructor( } fun txPacketOnce() { - // viewModel init 으로 이동. 필요. - // 경고 정보 조회 (주기적 heart beat) - // repeatOnLifecycle은 Activity가 포그라운드에 있을 때로 한정지어, 특정 Lifecycle이 Trigger 되었을 때 동작하도록 만드는 block 임. + viewModelScope.launch(dispatcherProvider.io) { + // txPacketOnce is called during app startup. + // Because serial open() is started in rxPacketLoop() asynchronously, + // FD can still be -1 here (startup race). Wait briefly before first TX burst. + if (!waitUntilSerialReady()) { + Timber.e("txPacketOnce skipped: serial port is not ready (fd=%d)", serialPortRepository.getFD()) + return@launch + } - // tx Version Read - txPacket(READ_WRITE.READ, CMD.VERSION, byteArrayOf(0x41.toByte())) + // viewModel init 으로 이동. 필요. + // 경고 정보 조회 (주기적 heart beat) + // repeatOnLifecycle은 Activity가 포그라운드에 있을 때로 한정지어, 특정 Lifecycle이 Trigger 되었을 때 동작하도록 만드는 block 임. - // tx Q-Switch Write - txPacket(READ_WRITE.WRITE, CMD.Q_SWITCH, qSwitch.value) + // tx Version Read + txPacket(READ_WRITE.READ, CMD.VERSION, byteArrayOf(0x41.toByte())) - // tx Guide Beam Write - txPacket(READ_WRITE.WRITE, CMD.GUIDE_BEAM, GuideBeam(value = guideBeam.value.toInt())) + // tx Q-Switch Write + txPacket(READ_WRITE.WRITE, CMD.Q_SWITCH, qSwitch.value) - // tx DCD_GAS Write (DEFAULT VALUE) - txPacket(READ_WRITE.WRITE, CMD.DCD_GAS, dcdGas.value.copy(status = 0x50)) + // tx Oven Write (align with Engineer KTP write source) + txPacket(READ_WRITE.WRITE, CMD.OVEN, Oven(ktp = temperature_write.value.ktp)) - // tx SPRAY_DCD Write (DEFAULT VALUE) - txPacket(READ_WRITE.WRITE, CMD.SPRAY_DCD, sprayDcd.value) + // tx Guide Beam Write + txPacket(READ_WRITE.WRITE, CMD.GUIDE_BEAM, GuideBeam(value = getGuideBeamTxValue())) + + // tx DCD_GAS Write (DEFAULT VALUE) + txPacket(READ_WRITE.WRITE, CMD.DCD_GAS, dcdGas.value.copy(status = 0x50)) + + // tx SPRAY_DCD Write (DEFAULT VALUE) + txPacket(READ_WRITE.WRITE, CMD.SPRAY_DCD, sprayDcd.value) + } + } + + private suspend fun waitUntilSerialReady( + timeoutMillis: Long = 2000L, + pollIntervalMillis: Long = 20L + ): Boolean { + // Poll FD until open() completes, with a bounded timeout to avoid blocking forever. + val start = System.currentTimeMillis() + while (System.currentTimeMillis() - start < timeoutMillis) { + if (serialPortRepository.getFD() != -1) return true + delay(pollIntervalMillis) + } + return serialPortRepository.getFD() != -1 + } + + // Guide Beam step mapping (0~10) + // 0 -> fixed 0 + // 1~10 -> min~max range in 10 steps (10 -> max) + private fun getGuideBeamTxValue(): Int { + val step = guideBeam.value.toInt().coerceIn(0, 10) + return if (step == 0) { + 0 + } else { + guideBeamMin.value + ((step - 1) * (guideBeamMax.value - guideBeamMin.value) / 9) + } } // Example: Emitting an event after a delay diff --git a/app/src/main/java/com/laseroptek/raman/utils/ext/PacketExtension.kt b/app/src/main/java/com/laseroptek/raman/utils/ext/PacketExtension.kt index 38696a9..92a7312 100644 --- a/app/src/main/java/com/laseroptek/raman/utils/ext/PacketExtension.kt +++ b/app/src/main/java/com/laseroptek/raman/utils/ext/PacketExtension.kt @@ -487,7 +487,7 @@ fun QSwitch.toByteArray(): ByteArray { val delayTimeIntegerPart = this.delayTime.toInt() val delayTimeFractionPart = ((this.delayTime - delayTimeIntegerPart) * 10).toInt() val intervalTimeIntegerPart = this.intervalTime.toInt() - val intervalTimeFractionPart = ((this.intervalTime - delayTimeIntegerPart) * 10).toInt() + val intervalTimeFractionPart = ((this.intervalTime - intervalTimeIntegerPart) * 10).toInt() val delayTimeArray = byteArrayOf( ((delayTimeIntegerPart.getNthDigit(2) + 0x30) and 0xFF).toByte(), diff --git a/docs/change_summary_2026-03-04.md b/docs/change_summary_2026-03-04.md new file mode 100644 index 0000000..98f2980 --- /dev/null +++ b/docs/change_summary_2026-03-04.md @@ -0,0 +1,23 @@ +# 변경 요약 (2026-03-04) + +이 문서는 현재 `git diff` 기준으로 반영된 변경을 정리합니다. + +## 1) 시리얼 초기 전송 누락(Startup race) 대응 + +### 변경 파일 +- `app/src/main/java/com/laseroptek/raman/ui/MainActivity.kt` +- `app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt` + +### 변경 내용 +- `MainActivity.initialize()`의 시리얼 시작 순서를 조정 + - 이전: `txPacketOnce()` -> `rxPacketLoop()` -> `txPacketLoop()` + - 이후: `rxPacketLoop()` -> `txPacketOnce()` -> `txPacketLoop()` +- `MainViewModel.txPacketOnce()`를 코루틴(IO)에서 실행하도록 변경 +- `waitUntilSerialReady()` 추가 + - 최대 2초 동안 20ms 간격으로 `FD != -1` 확인 + - 준비 실패 시 에러 로그 후 초기 TX 중단 +- 관련 설명 주석 추가 + +### 의도/효과 +- 앱 시작 직후 `open()` 완료 전 TX가 먼저 발생하는 레이스를 완화/방어 +- 포트 미준비 상태(`FD == -1`)에서 write가 호출되어 초기 패킷이 누락되는 문제를 줄임 diff --git a/docs/serial_tx_startup_race.md b/docs/serial_tx_startup_race.md new file mode 100644 index 0000000..fd0aef4 --- /dev/null +++ b/docs/serial_tx_startup_race.md @@ -0,0 +1,41 @@ +# Serial TX Startup Race 정리 + +## 1) FD(File Descriptor)란? +- `FD`는 리눅스/안드로이드에서 열린 리소스(파일/소켓/시리얼 포트)를 가리키는 정수 핸들입니다. +- 이 프로젝트에서 시리얼 포트 상태는 다음처럼 판단합니다. + - `FD == -1`: 포트 미오픈(유효하지 않음) + - `FD >= 0`: 포트 오픈 완료(유효) +- 따라서 `FD == -1` 상태에서 `write()`를 호출하면 실제 시리얼 전송이 되지 않습니다. + +## 2) 문제 원인 +- `txPacketOnce()`가 앱 시작 직후 실행됩니다. +- 시리얼 포트 `open()`은 `rxPacketLoop()` 내부에서 코루틴으로 비동기 시작됩니다. +- 기존 순서에서 `txPacketOnce()`가 먼저 호출되면, 포트 오픈 완료 전(`FD == -1`)에 TX가 시도되어 초기 패킷 전송이 누락될 수 있습니다. + +## 3) 적용한 수정 + +### A. 초기 호출 순서 조정 +- 파일: `app/src/main/java/com/laseroptek/raman/ui/MainActivity.kt` +- 변경: + - 이전: `txPacketOnce()` -> `rxPacketLoop()` -> `txPacketLoop()` + - 이후: `rxPacketLoop()` -> `txPacketOnce()` -> `txPacketLoop()` +- 목적: RX 루프가 먼저 포트 오픈을 시작하도록 해서 초기 TX 레이스 확률을 줄임 + +### B. `txPacketOnce()`에 포트 준비 대기 추가 +- 파일: `app/src/main/java/com/laseroptek/raman/ui/screens/main/MainViewModel.kt` +- 변경: + - `txPacketOnce()`를 IO 코루틴에서 실행 + - `waitUntilSerialReady()`(최대 2초, 20ms 폴링)로 `FD != -1` 확인 후 TX 진행 + - 시간 내 준비 실패 시 로그를 남기고 전송 중단 +- 목적: 순서만으로 보장되지 않는 코루틴 스케줄링 레이스를 방어 + +## 4) 왜 TX에서 직접 open()하지 않았는가? +- 현재 구조에서 `open()`의 데이터 콜백은 `rxPacketLoop()`의 `callbackFlow`와 연결됩니다. +- TX 경로에서 별도 `open()`을 하면 중복 오픈/콜백 소유권/FD 교체 타이밍 이슈가 생길 수 있습니다. +- 안정적인 패턴은: + - 포트 오픈 책임: RX(단일 지점) + - 포트 사용(TX): ready 확인 후 write + +## 5) 확인 포인트 +- 앱 시작 직후 로그에서 `FD`가 유효해진 뒤 `txPacketOnce()`의 TX 로그가 출력되는지 확인 +- 장비 측 시리얼 모니터에서 초기 패킷(Version/Q-Switch/GuideBeam/DCD/SprayDCD) 수신 여부 확인