SmartPort 안드로이드 앱

SmartPort — USB ↔ TCP 브릿지 기능 문서

이 문서는 SmartPort 앱에 추가된 로컬 TCP 브릿지 기능의 설계, 구현, 사용법을 정리합니다. 기존 앱은 USB 시리얼 장치를 네이티브 PTY(가상 시리얼 포트) 한 곳에만 노출했는데, 여기에 127.0.0.1:8899(기본값) TCP 소켓으로도 동일한 장치를 노출하는 기능을 병행 추가했습니다.


1. 개요

항목내용
목적연결된 USB 시리얼 장치를 로컬 TCP 소켓으로 노출하여, 같은 기기의 다른 프로세스나 adb forward를 통한 PC에서 시리얼 장치를 사용할 수 있게 함
바인딩 주소127.0.0.1 (루프백 전용, 외부 네트워크에는 노출되지 않음)
기본 포트8899
동시 연결클라이언트 1개만 허용 (물리 시리얼 포트처럼 단일 소유자)
데이터 처리바이트를 그대로 양방향 중계 (프로토콜/프레이밍 없음)
활성화 여부MainPreferences.tcpBridgeEnabled (기본값 true)

업데이트: 기존에 있던 네이티브 PTY 브릿지(/data/data/com.clipman.smartport/files/serialpipe, VSPPty/VSPListener/SerialDataapp/src/main/cpp 네이티브 코드)는 완전히 제거되었습니다. 이제 USB 시리얼 장치를 노출하는 유일한 경로는 이 TCP 브릿지입니다.


2. 아키텍처

                ┌───────────────────────────┐
                │        USB 시리얼 장치       │
                └──────────────┬────────────┘
                               │
                 UsbManager / UsbSerialPort
                               │
                    ┌──────────▼──────────┐
                    │   TcpBridgeServer    │
                    │   (USB ↔ TCP 중계)     │
                    └──────────┬──────────┘
                               │
                     127.0.0.1:8899 (기본)
                    (TCP 클라이언트가 connect)
  • TcpBridgeServerVirtualSerialDriver.selectedDevice(현재 선택된 USB 드라이버)를
    참조하여, 사용자가 앱 화면에서 고른 드라이버/장치를 그대로 사용합니다.
  • VirtualSerialDriver는 이제 USB 장치 탐색·드라이버 선택만 담당하며, 실제 바이트 입출력은
    전적으로 TcpBridgeServer가 담당합니다.
  • 네이티브 PTY 관련 코드(VSPPty, VSPListener, SerialData, app/src/main/cpp/*,
    externalNativeBuild/CMake 설정)는 모두 제거되어, 더 이상 NDK/네이티브 빌드가 필요 없습니다.

3. 신규/변경/삭제 파일 목록

3.1 신규 파일

app/src/main/java/com/clipman/smartport/serial/TcpBridgeServer.kt

TCP 브릿지의 핵심 로직을 담당하는 클래스입니다.

멤버설명
companion object.DEFAULT_PORT기본 포트, 8899
companion object.DEFAULT_BAUD_RATEUSB 포트를 열 때 기본 보드레이트, 115200
start(listenPort: Int = DEFAULT_PORT)127.0.0.1:listenPortServerSocket을 열고 accept 루프를 별도 스레드에서 시작
stop()서버 소켓·클라이언트 소켓·USB 포트를 모두 정리하고 중단
isRunning현재 리스닝 중인지 여부
tcpPort실제로 바인딩된 포트 (읽기 전용)
setSerialParameters(baudRate, dataBits, stopBits, parity)USB 포트를 열 때 사용할 시리얼 파라미터를 변경

동작 흐름

  1. start() 호출 시 accept 전용 스레드가 ServerSocket(port, 50, InetAddress.getByName("127.0.0.1"))을 생성.
  2. 클라이언트가 접속하면 handleClient(socket) 실행:
    • 이미 다른 클라이언트가 연결되어 있으면 즉시 거부(소켓 닫음).
    • VirtualSerialDriver.selectedDevice가 없으면(=USB 장치 미선택) 접속을 거부.
    • UsbManager.openDevice()UsbSerialPort.open()setParameters(baudRate, dataBits, stopBits, parity)dtr/rts = true 순으로 USB 포트 오픈.
    • USB → TCP: SerialInputOutputManager.Listener.onNewData()에서 수신한 바이트를 그대로 socket.getOutputStream()에 write.
    • TCP → USB: 별도 스레드에서 socket.getInputStream()을 블로킹 read하여 port.write()로 전달.
  3. 클라이언트 연결 종료(EOF/예외)나 onRunError 발생 시 disconnectClient()가 호출되어 소켓과 USB 포트를 모두 정리하고, 다음 클라이언트를 받을 수 있는 상태로 복귀.
  4. stop() 호출 시 서버 소켓을 닫아 accept 루프를 빠져나오게 하고, 열려 있던 클라이언트/USB 포트를 정리.

3.2 수정 파일

파일변경 내용
utils/preferences/MainPreferences.kttcpBridgeEnabled: Boolean(기본 true), tcpBridgePort: Int(기본 TcpBridgeServer.DEFAULT_PORT = 8899) 프리퍼런스 추가
AppModule.ktKoin DI에 single<TcpBridgeServer> 등록 (VirtualSerialDriver, UsbManager, LoggerRepository 주입)
service/UsbBridgeService.ktonCreate()에서 prefs.tcpBridgeEnabled가 true면 tcpBridgeServer.start(prefs.tcpBridgePort) 호출, onDestroy()에서 tcpBridgeServer.stop() 호출. TCP_BRIDGE_HOST = "127.0.0.1" 상수 추가
ui/MainActivity.ktUSB 장치가 선택된 상태일 때 화면에 TCP bridge address: 127.0.0.1:8899 형태로 표시하는 로직 추가
res/layout/activity_main.xmlTCP 주소 표시용 bridgeTcpText TextView 유지 (기존 PTY 경로 표시용 bridgePathText는 제거됨)
res/values/strings.xmlbridge_tcp_label = "TCP bridge address" 문자열 추가
AndroidManifest.xmlandroid.permission.INTERNET 권한 추가 (루프백 소켓이라도 안드로이드에서 필수)

3.3 삭제 파일 (기존 PTY 브릿지 관련, 모두 제거됨)

파일설명
serial/VSPPty.kt네이티브 PTY 라이브러리(JNI) 래퍼
serial/VSPListener.java네이티브 PTY → Kotlin 콜백 인터페이스
serial/SerialData.ktPTY에서 전달되는 termios/보드레이트 패킷 파싱 클래스
app/src/main/cpp/* (vsp-pty.cpp, openpty.c, real_pty.h, util.h, CMakeLists.txt)PTY 생성을 담당하던 네이티브(C/C++) 소스 전체
VirtualSerialDriver.kt 내 PTY 관련 멤버pty, port, connection, serialInputManager, currentBaudrate, ptyThread, initializeVSP(), handlePtyThread(), stopPtyThread(), onDataReceived(), onNewData(), onRunError(), VSPListener/SerialInputOutputManager.Listener 구현부
UsbBridgeService.kt 내 PTY 관련 멤버PTY_BRIDGE_PATH 상수, verifyPtyRunning(), USB 부착 시 PTY 스레드 재시작 로직, initializeVSP()/handlePtyThread()/stopPtyThread() 호출
MainActivity.kt / activity_main.xml / strings.xmlbridgePathText 뷰, bridge_path_label 문자열 등 PTY 경로 표시 UI
app/build.gradle.ktsexternalNativeBuild { cmake { ... } } 블록, ndk { abiFilters ... } 블록 (네이티브 라이브러리가 없어져 더 이상 필요 없음)

이제 앱은 순수 Kotlin/Java + usb-serial-for-android 라이브러리만으로 동작하며, NDK/CMake 빌드 단계가 필요 없습니다.


4. 설정값

MainPreferences (SharedPreferences 기반)에 아래 값이 추가되었습니다.

var tcpBridgeEnabled by booleanPref(defaultValue = true)
var tcpBridgePort by intPref(defaultValue = TcpBridgeServer.DEFAULT_PORT) // 8899
  • tcpBridgeEnabled = false로 두면 UsbBridgeService가 TCP 브릿지를 아예 시작하지 않습니다.
  • tcpBridgePort 값을 바꾸면 다음 서비스 시작 시 해당 포트로 리슨합니다.
  • 현재는 두 값을 코드/설정으로만 바꿀 수 있고, 앱 UI에 별도 설정 화면은 아직 없습니다.
    (필요 시 MainActivity에 설정 다이얼로그를 추가해 tcpBridgeServer.start(newPort)
    다시 호출하도록 확장 가능)

5. 사용 방법

5.1 같은 기기 내 다른 앱/프로세스에서 접근

Socket socket = new Socket("127.0.0.1", 8899);

5.2 PC에서 접근 (adb 포트 포워딩)

adb forward tcp:8899 tcp:8899

이후 PC의 127.0.0.1:8899로 접속하면 안드로이드 기기의 USB 시리얼 장치와 통신할 수 있습니다. (예: 파이썬 pyserial이 아닌 순수 socket 모듈, 또는 nc 127.0.0.1 8899)

5.3 시리얼 파라미터

기본값은 **115200bps, 8 데이터비트, 1 정지비트, 패리티 없음(8N1)**입니다. TCP 자체에는 보드레이트 개념이 없으므로, USB 쪽 포트를 열 때만 이 값이 사용됩니다. 다른 값이 필요하면 TcpBridgeServer.setSerialParameters()를 호출해 변경할 수 있습니다.

tcpBridgeServer.setSerialParameters(
    baudRate = 9600,
    dataBits = UsbSerialPort.DATABITS_8,
    stopBits = UsbSerialPort.STOPBITS_1,
    parity = UsbSerialPort.PARITY_NONE
)

6. 동작/에러 처리 요약

상황동작
USB 장치 미선택 상태에서 TCP 클라이언트 접속접속 즉시 거부(소켓 close), 로그에 기록
이미 다른 TCP 클라이언트가 연결된 상태에서 추가 접속새 접속 즉시 거부
USB 포트 오픈 실패 (권한 없음 등)로그 기록 후 소켓 정리, 클라이언트 접속 실패로 종료
TCP 클라이언트가 연결을 끊음USB 포트를 닫고 다음 클라이언트를 받을 준비 상태로 복귀
USB 장치가 물리적으로 분리됨SerialInputOutputManager.onRunError 발생 → 클라이언트 연결 해제 및 정리
서비스 종료 (onDestroy)서버 소켓, 클라이언트 소켓, USB 포트 모두 정리 후 리슨 중단

7. 보안 관련 참고사항

  • TcpBridgeServer는 항상 127.0.0.1에만 바인딩됩니다. 즉 같은 기기 내부에서만 접속
    가능하며, Wi-Fi/모바일 네트워크를 통한 외부 접속은 불가능합니다.
  • 외부 네트워크에 노출하려면(권장하지 않음) InetAddress.getByName("127.0.0.1") 부분을
    InetAddress.getByName("0.0.0.0") 등으로 바꿔야 하며, 이 경우 인증 없이 USB 장치를 제어할
    수 있게 되므로 별도의 인증/접근 제어 로직이 필요합니다. 현재 구현에는 포함되어 있지 않습니다.

8. 빌드/테스트 관련 참고사항

  • 이번 변경은 Android SDK/Gradle 원격 저장소 접근이 없는 샌드박스 환경에서 작성되어
    실제 컴파일/실행 테스트는 수행하지 못했습니다. Android Studio에서 빌드 후 확인이 필요합니다.
  • 사용 중인 usb-serial-for-android 라이브러리 버전은 3.3.0이며, SerialInputOutputManager
    API(stop(), Listener.onNewData()/onRunError())는 기존 VirtualSerialDriver.kt
    동일한 방식으로 사용했습니다.
  • 빌드 후 확인 포인트
    • USB 장치 연결 → 앱 화면에 TCP bridge address: 127.0.0.1:8899 표시되는지
    • adb forward tcp:8899 tcp:8899 후 PC에서 접속 시 데이터 송수신이 정상인지
    • 클라이언트 연결 해제 후 재연결이 정상 동작하는지
    • 서비스 재시작(onDestroyonCreate) 시 포트 재바인딩이 정상인지 (Address already in use 에러 여부)

9. 향후 개선 아이디어 (선택 사항)

  • 앱 UI에 TCP 브릿지 on/off 스위치 및 포트 입력 필드 추가
  • TcpBridgeServer에 보드레이트 등 시리얼 파라미터를 UI에서 조정할 수 있는 화면 추가
  • 다중 클라이언트 지원이 필요하다면 브로드캐스트 방식(모든 클라이언트에 동일 데이터 전송)으로 확장
  • 외부 네트워크 노출이 필요한 경우를 위한 간단한 토큰 기반 인증 옵션 추가