Skip to content

feat(android): 유선·무선 스위치를 자동으로 가려 쓴다 - #6

Open
wingwogus wants to merge 16 commits into
dev-five-git:mainfrom
wingwogus:feat/android-tauri
Open

feat(android): 유선·무선 스위치를 자동으로 가려 쓴다#6
wingwogus wants to merge 16 commits into
dev-five-git:mainfrom
wingwogus:feat/android-tauri

Conversation

@wingwogus

Copy link
Copy Markdown
Contributor

Android에서 스위치를 유선·무선 어느 쪽으로 꽂아도 앱이 알아서 가려 쓴다.
USB가 붙어 있으면 USB를, 없으면 보호자가 고른 XIAO 하나에만 BLE로 붙는다.

지키는 선

  • 앞 4칸의 순서와 자리는 전송이 바뀌어도 그대로다. 사용자는 자리로 동작을 기억한다.
  • 누른 상태에서 전송이 끊기면 실행하지 않고 취소한다. 오선택 비용이 크다(접근성 원칙 3).
  • 임의 기기에 붙지 않는다. 보호자가 고른 기기여야 하고, 연결 후 핸드셰이크가 신원을 다시 가린다.
  • 상태를 화면에 드러낸다. 어느 전송이 살아 있는지 알 수 없으면 통제감을 잃는다(원칙 2).

실기에서 찾아 고친 결함

유닛 테스트만으로는 하나도 잡히지 않았던 것들이다. 각각 실패하는 테스트를 먼저 세웠다.

결함 증상 원인
USB 권한 허용이 거부로 읽힘 허용해도 연결 안 됨 FLAG_IMMUTABLEEXTRA_PERMISSION_GRANTED 채우기를 막음
이 프로젝트의 XIAO를 USB에서 배제 권한 다이얼로그조차 안 뜸 Arduino VID 화이트리스트
연결 208ms 후 끊김 무한 재연결 bulkTransfer는 유휴 타임아웃과 실제 오류를 모두 음수로 주는데 리더가 전부 상실로 읽음
시작 직후 크래시 UnsatisfiedLinkError JNI 심볼 9개 중 5개만 배포 — 아무도 JNI 크레이트를 빌드하지 않았음
앱을 켜면 컨트롤러만 뜸 보호자가 속도·스위치를 정할 수단 없음 설정을 여는 코드가 오버레이 경로에만 있었음
무선 스위치를 등록할 수 없음 스캔 결과 0건 실제 XIAO는 광고에 이름을 싣지 않고 NUS UUID만 보내는데 스캔이 이름으로 걸러냄

마지막 것이 가장 오래 걸렸다. 맥에서 광고 패킷을 직접 뜯어 kCBAdvDataLocalName이 없는 것을 확인했다. 맥에 이름이 보이는 건 GATT 연결로 캐시한 값이라 오히려 진단을 흐렸고, 폰에는 MAC만 뜬다. 데스크톱은 이름 또는 NUS UUID로 판정하는데(||) 안드로이드만 이름을 요구했다.

실기 검증

Samsung SM-A155N, XIAO nRF52840 Sense.

USB

한번 앱이 XIAO nRF52840 Sense에 액세스하도록 허용하시겠습니까?
입력 ready active=usb usb=ready ble=lost      # 45초+ 유지, 상실 0건
모드 dwelling                                  # 실제 스위치 누름이 코어에 도달

BLE (USB 없이)

입력 reconnecting active=none usb=lost ble=starting
입력 ready active=ble usb=lost ble=ready
모드 scanning → 모드 confirm → 두드림 보냄 (509, 1357) clickable=true

시나리오 14개 중 8개 통과: 실제 스위치 입력 도달, BLE 전용 동작, 블루투스 off/on 중 USB 유지, BLE 권한 회수/재부여 중 USB 유지, 오버레이 권한, 단일 OverlayService(포그라운드 1개), 상태 실시간 갱신, 오버레이 터치 통과(FLAG_NOT_FOCUSABLE|NOT_TOUCHABLE 확인).

기록은 .omo/evidence/android-auto-usb-ble/에 있다.

게이트

  • bun test 81 pass / 0 fail
  • Android 유닛 테스트 93 tests BUILD SUCCESSFUL
  • bun run typecheck, bun run lint (clippy -D warnings + fmt + oxlint) 통과
  • Kotlin 컴파일 BUILD SUCCESSFUL

빌드 파이프라인도 함께 고쳤다. Tauri의 rust 플러그인은 src-tauri만 빌드하고 crates/hanbeon-jni는 아무도 빌드하지 않아 손으로 복사해온 상태였다. Gradle이 ABI마다 빌드하게 걸어 심볼 누락이 기기에 도달할 수 없게 했다. 디버그 APK도 333MB → 170MB로 줄였다(워크스페이스에 cargo 프로파일이 없어 전체 디버그 정보가 들어가던 것).

남은 것

  • 컨트롤러 오버레이가 설정 화면 하단을 가린다. 숨기려 시도했다가 설정 화면이 아예 사라져 되돌렸다 — 부분 가림보다 설정을 못 여는 게 훨씬 나쁘다. 실기에서 검증된 복귀 경로를 찾은 뒤 별도로 다룬다.
  • BLE 시나리오 3개(양쪽 연결, 눌린 상태 분리, USB 재연결 선점)는 USB+BLE 동시 연결과 사람 손이 필요하다.
  • PRD 7절이 USB/Bluetooth HID 키보드를 전제하는데 구현은 CDC 시리얼 + BLE NUS다. 갱신 필요.
  • src-tauri/gen/이 gitignore 대상이라 스캐폴드를 재생성하면 이 구현이 지워진다. 추적 방식 결정 필요.

참고

BLE 주변기기는 한 번에 하나의 중앙기기와만 GATT 세션을 유지한다. 폰이 스위치를 쓰는 동안 보드는 계속 광고하지만(맥에서 -52dBm) 맥은 연결할 수 없다. BLE 프로토콜 특성이고 제품상으로도 동시 사용은 의미가 없다.

…id build

- crates/hanbeon-core: platform-free scan/adapt/journal/profile/preset/host (from proven reference)
- crates/hanbeon-jni: JNI bridge for future android host (reference port)
- desktop feature flag: tray/global-shortcut/enigo/rodio/serialport behind --features desktop
- android target compiles with NoopHost placeholder until accessibility plugin lands
- fix @devup-ui broken workspace:^ releases via overrides; real typescript 6.0.3
- tauri android init scaffold in gen/
…ests

- #[cfg_attr(android, tauri::mobile_entry_point)] on run() (JVM entry symbol)
- desktop integration test requires desktop feature
- clippy clean in both feature configurations
…read

- prepare_floating gated behind desktop (webview eval before ready crashes on Android)
- Registry::noop for Android: rustls-platform-verifier needs JNI init that only
  the desktop network stack provides; Hana Cloud preset sync stays desktop-only
- verified on device: SM-A155N (Galaxy A15, Android 16), app alive 15s+ no panic
- port 7 Kotlin files from verified reference (ControllerView, OverlayService,
  HanbeonAccessibilityService, HighlightView, UsbSwitch, Core, DebugActionReceiver)
- android_bridge.rs: JNI start_overlay_service via tao ndk_glue context
- manifest: SYSTEM_ALERT_WINDOW, FGS specialUse, accessibility binding
- device-verified on SM-A155N (Android 16): overlay windows above Chrome,
  accessibility focus move (next -> 됨), gesture select, failure logging
- findBoard: prefer Arduino VID (0x2341) and clones, exclude network adapters
  (Realtek 0x0BDA etc.) that also expose CDC and were mistaken for the board
- dataInterface: read from the active configuration, not device.getInterface
- claim retry with port reset; vendor-interface fallback removed in favor of
  strict CDC matching after Realtek misdetection
- device-verified: switch press/release P/R events flow to core, gesture
  judgement works, interval adaptation kicks in on mistakes
The red FLOATING-INDEX-LOADED banner and manual button were verification
scaffolding. OverlayService auto-start now runs from a useEffect timer on
Android only; the debug button is gone.
OverlayService.openSettings now launches MainActivity with
screen=settings extra; MainActivity navigates the wry WebView to
settings/index.html via onWebViewCreate hook and returns to the
floating controller when relaunched without the extra.
Same flow as desktop: app starts with the floating controller, the
settings cell overlays the settings form on top (no URL navigation),
and closing it keeps the scan state intact. SettingsForm gains an
optional onClose for in-page dismissal.
Default screen is now settings/, but the overlay auto-start lived in
the floating page which never mounts. Move the auto-start effect to
settings/page.tsx so the controller comes up on launch regardless of
which route loads first. Add bridge logging for future tracing.
Relative paths passed to loadUrlMainThread were resolved as
http://index.html/ and failed with ERR_NAME_NOT_RESOLVED. Navigate to
absolute http://tauri.localhost/... URLs instead.
USB가 붙어 있으면 USB를, 없으면 보호자가 고른 XIAO 하나에만 BLE로 붙는다.
전송이 바뀌어도 앞 4칸의 순서와 자리는 그대로다.

- 전송 계약과 중재기를 분리해 USB 우선·BLE 대기를 한 곳에서 판정한다
- 누른 상태에서 전송이 끊기면 실행하지 않고 취소한다. 오선택 비용이 크다
- 임의 기기에 붙지 않는다. 보호자가 고른 이름과 신원이 정확히 맞아야 붙는다
- 상태를 화면에 드러낸다. 어느 전송이 살아 있는지 알 수 없으면 통제감을 잃는다

실기에서 찾아 고친 것:
- USB 권한 허용이 거부로 읽혔다. FLAG_IMMUTABLE이 결과 값 채우기를 막았다
- Arduino VID만 허용해 이 프로젝트의 XIAO를 배제했다. CDC면 후보로 받고
  신원은 핸드셰이크로 가린다
- bulkTransfer는 유휴 타임아웃과 실제 오류를 모두 음수로 준다. 리더가 이를
  전부 연결 상실로 읽어, 스위치를 누르지 않는 것이 '빠짐'으로 판정됐다
- JNI 크레이트를 아무도 빌드하지 않아 오래된 .so가 배포됐고 앱이 시작하다
  죽었다. Gradle이 ABI마다 빌드하게 해 다시 새지 않게 했다
- 디버그 정보로 APK가 333MB였다. line-tables만 남겨 170MB로 줄였다
컨트롤러만 떠서 보호자가 주사 속도나 스위치를 정할 수단이 없었다.
설정을 여는 코드가 오버레이의 설정 칸을 고르는 경로에만 있었기 때문이다.

시작할 때 프로필을 읽어 온보딩 전이면 온보딩을, 마쳤으면 설정을 띄운다.
보호자가 무선 스위치를 등록할 수 없었다. 실제 XIAO는 광고 패킷에 이름을
싣지 않고 NUS 서비스 UUID만 보내는데, 스캔이 이름으로 걸러냈기 때문이다.

맥에서 이름이 보이는 것은 GATT 연결로 캐시한 값이라 착각을 부른다.
광고에는 kCBAdvDataLocalName이 없고 폰에는 MAC만 뜬다.

데스크톱은 이름 또는 NUS UUID로 판정하는데 안드로이드만 이름을 요구했다.
같은 기준으로 맞춘다 — 이름이 없으면 NUS 광고로 받고, 이름이 있으면
여전히 정확히 일치해야 한다. 신원은 연결 후 핸드셰이크가 다시 가린다.
main이 펌웨어 설치기를 들여오면서 같은 파일을 건드렸다. 충돌 3곳을 풀고
안드로이드 빌드가 깨지지 않게 모듈 경계를 다시 맞췄다.

- serialport는 main이 무조건 의존으로 바꿨다. 안드로이드도 쓰므로 그대로
  두고 desktop 피처의 dep: 참조만 뺐다
- arduino·firmware·flasher와 show_floating은 데스크톱 전용이다. 안드로이드는
  보드를 굽지 않고 floating 창도 없다 — 컨트롤러는 OverlayService가 올린다
- profile 모듈은 이 브랜치에서 hanbeon-core로 옮겼다. main이 들고 온 사본은
  중복이라 지웠다
- 무선 스위치 등록은 온보딩 첫 칸에서 두 번째 칸으로 옮겼다. main이 펌웨어
  안내를 맨 앞에 두었고, 유선을 마친 뒤 무선을 고르는 순서가 자연스럽다
- 재탐지 문구를 connectionCopy로 옮겼다. 조용히 두면 사용자 일시정지로 읽혀
  길게 눌러 다시 시작하려 한다

알려진 문제: page 경계 테스트가 Onboarding 모듈을 프로세스 전역으로 모킹해
온보딩 단계 순서를 검증하는 테스트 2개가 전체 실행에서 실패한다. page에
온보딩 주입점을 만들어야 제대로 풀리므로 따로 다룬다.
병합 뒤 온보딩 단계 순서 테스트 2개가 전체 실행에서만 깨졌다. 파일을 따로
돌리면 통과해서 배치 문제로 보였지만 원인은 모킹이었다.

bun은 모듈을 통째로 갈아끼운다. 두 스위트가 각자 @/lib/profile의 일부만
모킹하자 나중에 로드된 쪽이 상대가 쓰는 export를 지웠고, 실제 Onboarding이
closeSettings를 찾지 못해 터졌다. page 경계 스위트의 Onboarding 스텁이 그
오류를 가리고 있었을 뿐이다.

profile과 invoke에 각각 하나의 이중체를 두고 스위트가 동작만 덮어쓰게 했다.
가리개였던 Onboarding 스텁은 지웠다 — 이제 실제 컴포넌트가 로드된다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant