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/SerialData및app/src/main/cpp네이티브 코드)는 완전히 제거되었습니다. 이제 USB 시리얼 장치를 노출하는 유일한 경로는 이 TCP 브릿지입니다.
2. 아키텍처
┌───────────────────────────┐
│ USB 시리얼 장치 │
└──────────────┬────────────┘
│
UsbManager / UsbSerialPort
│
┌──────────▼──────────┐
│ TcpBridgeServer │
│ (USB ↔ TCP 중계) │
└──────────┬──────────┘
│
127.0.0.1:8899 (기본)
(TCP 클라이언트가 connect)
TcpBridgeServer는VirtualSerialDriver.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_RATE | USB 포트를 열 때 기본 보드레이트, 115200 |
start(listenPort: Int = DEFAULT_PORT) | 127.0.0.1:listenPort에 ServerSocket을 열고 accept 루프를 별도 스레드에서 시작 |
stop() | 서버 소켓·클라이언트 소켓·USB 포트를 모두 정리하고 중단 |
isRunning | 현재 리스닝 중인지 여부 |
tcpPort | 실제로 바인딩된 포트 (읽기 전용) |
setSerialParameters(baudRate, dataBits, stopBits, parity) | USB 포트를 열 때 사용할 시리얼 파라미터를 변경 |
동작 흐름
start()호출 시 accept 전용 스레드가ServerSocket(port, 50, InetAddress.getByName("127.0.0.1"))을 생성.- 클라이언트가 접속하면
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()로 전달.
- 클라이언트 연결 종료(EOF/예외)나
onRunError발생 시disconnectClient()가 호출되어 소켓과 USB 포트를 모두 정리하고, 다음 클라이언트를 받을 수 있는 상태로 복귀. stop()호출 시 서버 소켓을 닫아 accept 루프를 빠져나오게 하고, 열려 있던 클라이언트/USB 포트를 정리.
3.2 수정 파일
| 파일 | 변경 내용 |
|---|---|
utils/preferences/MainPreferences.kt | tcpBridgeEnabled: Boolean(기본 true), tcpBridgePort: Int(기본 TcpBridgeServer.DEFAULT_PORT = 8899) 프리퍼런스 추가 |
AppModule.kt | Koin DI에 single<TcpBridgeServer> 등록 (VirtualSerialDriver, UsbManager, LoggerRepository 주입) |
service/UsbBridgeService.kt | onCreate()에서 prefs.tcpBridgeEnabled가 true면 tcpBridgeServer.start(prefs.tcpBridgePort) 호출, onDestroy()에서 tcpBridgeServer.stop() 호출. TCP_BRIDGE_HOST = "127.0.0.1" 상수 추가 |
ui/MainActivity.kt | USB 장치가 선택된 상태일 때 화면에 TCP bridge address: 127.0.0.1:8899 형태로 표시하는 로직 추가 |
res/layout/activity_main.xml | TCP 주소 표시용 bridgeTcpText TextView 유지 (기존 PTY 경로 표시용 bridgePathText는 제거됨) |
res/values/strings.xml | bridge_tcp_label = "TCP bridge address" 문자열 추가 |
AndroidManifest.xml | android.permission.INTERNET 권한 추가 (루프백 소켓이라도 안드로이드에서 필수) |
3.3 삭제 파일 (기존 PTY 브릿지 관련, 모두 제거됨)
| 파일 | 설명 |
|---|---|
serial/VSPPty.kt | 네이티브 PTY 라이브러리(JNI) 래퍼 |
serial/VSPListener.java | 네이티브 PTY → Kotlin 콜백 인터페이스 |
serial/SerialData.kt | PTY에서 전달되는 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.xml | bridgePathText 뷰, bridge_path_label 문자열 등 PTY 경로 표시 UI |
app/build.gradle.kts | externalNativeBuild { 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에서 접속 시 데이터 송수신이 정상인지- 클라이언트 연결 해제 후 재연결이 정상 동작하는지
- 서비스 재시작(
onDestroy→onCreate) 시 포트 재바인딩이 정상인지 (Address already in use에러 여부)
- USB 장치 연결 → 앱 화면에
9. 향후 개선 아이디어 (선택 사항)
- 앱 UI에 TCP 브릿지 on/off 스위치 및 포트 입력 필드 추가
TcpBridgeServer에 보드레이트 등 시리얼 파라미터를 UI에서 조정할 수 있는 화면 추가- 다중 클라이언트 지원이 필요하다면 브로드캐스트 방식(모든 클라이언트에 동일 데이터 전송)으로 확장
- 외부 네트워크 노출이 필요한 경우를 위한 간단한 토큰 기반 인증 옵션 추가