From 628f7544bec8b93840c06b4cf8eda45586f2171f Mon Sep 17 00:00:00 2001 From: StevenBuzzi Date: Wed, 4 Mar 2026 17:11:09 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=80=ED=8C=85=20=EC=8B=9C=20serial=20packe?= =?UTF-8?q?t=EC=9D=B4=20=EC=A0=84=EC=86=A1=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++ .../com/laseroptek/raman/ui/MainActivity.kt | 6 ++- .../raman/ui/screens/main/MainViewModel.kt | 49 ++++++++++++----- docs/change_summary_2026-03-04.md | 53 +++++++++++++++++++ docs/serial_tx_startup_race.md | 41 ++++++++++++++ 5 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 docs/change_summary_2026-03-04.md create mode 100644 docs/serial_tx_startup_race.md 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 7a81810..37a16ef 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,47 @@ 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 = getGuideBeamTxValue())) + // 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 Guide Beam Write + txPacket(READ_WRITE.WRITE, CMD.GUIDE_BEAM, GuideBeam(value = getGuideBeamTxValue())) - // tx SPRAY_DCD Write (DEFAULT VALUE) - txPacket(READ_WRITE.WRITE, CMD.SPRAY_DCD, sprayDcd.value) + // 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) diff --git a/docs/change_summary_2026-03-04.md b/docs/change_summary_2026-03-04.md new file mode 100644 index 0000000..a86bd27 --- /dev/null +++ b/docs/change_summary_2026-03-04.md @@ -0,0 +1,53 @@ +# 변경 요약 (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가 호출되어 초기 패킷이 누락되는 문제를 줄임 + +## 2) HandPiece 기본값 변경 + +### 변경 파일 +- `app/src/main/java/com/laseroptek/raman/data/model/serial/HandPiece.kt` + +### 변경 내용 +- `HandPiece.type` 기본값 변경 + - 이전: `1` + - 이후: `0` + +### 영향 포인트 +- 기본 인스턴스 생성 시 handpiece type의 초기 상태가 달라집니다. +- 초기 테이블 선택/상태 표시 로직에서 기본 타입 가정이 있다면 함께 점검 필요합니다. + +## 3) 문서화 + +### 추가/수정 파일 +- `docs/serial_tx_startup_race.md` (신규) +- `README.md` (Troubleshooting 링크 추가) + +### 내용 +- FD(File Descriptor) 개념 +- startup race 원인 및 수정 내역 +- 왜 TX에서 직접 open하지 않았는지 +- 확인 포인트 + +## 4) 참고 +- 현재 요약은 커밋 로그가 아닌 워크트리 `git diff` 기준입니다. +- 빌드 검증은 `gradlew` CRLF 문제로 로컬 셸에서 미실행 상태입니다. 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) 수신 여부 확인