SmartPort(All) — USB ↔ TCP 브릿지 기능 문서
이 문서는 SmartPort 앱에 추가된 로컬 TCP 브릿지 기능의 설계, 구현, 사용법을 정리합니다.
기존 앱은 USB 시리얼 장치를 네이티브 PTY(가상 시리얼 포트) 한 곳에만 노출했는데, 여기에
127.0.0.1:8899(기본값) TCP 소켓으로도 동일한 장치를 노출하는 기능을 병행 추가했습니다.
1. 개요
| 항목 | 내용 |
|---|---|
| 목적 | 연결된 USB 시리얼 장치를 로컬 TCP 소켓으로 변환하여, PTY를 열 수 없는 앱/프로세스(예: PC에서 adb forward로 접근)도 시리얼 장치를 사용할 수 있게 함 |
| 바인딩 주소 | 127.0.0.1 (루프백 전용, 외부 네트워크에는 노출되지 않음) |
| 기본 포트 | 8899 |
| 동시 연결 | 클라이언트 1개만 허용 (물리 시리얼 포트처럼 단일 소유자) |
| 데이터 처리 | 바이트를 그대로 양방향 중계 (프로토콜/프레이밍 없음) |
| 활성화 여부 | MainPreferences.tcpBridgeEnabled (기본값 true) |
기존 PTY 브릿지(/data/data/com.clipman.smartport/files/serialpipe)는 그대로 유지되며,
TCP 브릿지는 완전히 독립적으로 병행 동작합니다. 둘 중 하나만 써도 되고 동시에 켜둬도 됩니다.
2. 아키텍처
┌───────────────────────────┐
│ USB 시리얼 장치 │
└──────────────┬────────────┘
│
UsbManager / UsbSerialPort
│
┌──────────────────────┴──────────────────────┐
│ │
┌───────▼────────┐ ┌──────────▼─────────┐
│ VirtualSerialDriver │ │ TcpBridgeServer │
│ (기존, PTY 담당) │ │ (신규, TCP 담당) │
│ │ │ │
│ native PTY ↔ USB │ │ TCP 소켓 ↔ USB │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
/data/.../serialpipe 127.0.0.1:8899 (기본)
(다른 프로세스가 open) (TCP 클라이언트가 connect)
VirtualSerialDriver와TcpBridgeServer는 서로 다른UsbDeviceConnection/UsbSerialPort
인스턴스를 각자 열어서 사용합니다 (독립적인 세션). 즉 PTY 클라이언트와 TCP 클라이언트가
동시에 USB 장치에 접근하면 두 세션이 번갈아 포트를 열고 닫는 형태로 동작하므로,
운영 환경에서는 되도록 둘 중 하나만 실제로 사용하는 것을 권장합니다.TcpBridgeServer는VirtualSerialDriver.selectedDevice(현재 선택된 USB 드라이버)를
그대로 참조하여, 사용자가 앱 화면에서 고른 드라이버/장치를 그대로 사용합니다.
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 | PTY 경로 표시용 bridgePathText 아래에 TCP 주소 표시용 bridgeTcpText TextView 추가 |
res/values/strings.xml | bridge_tcp_label = "TCP bridge address" 문자열 추가 |
AndroidManifest.xml | android.permission.INTERNET 권한 추가 (루프백 소켓이라도 안드로이드에서 필수) |
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에서 조정할 수 있는 화면 추가- 다중 클라이언트 지원이 필요하다면 브로드캐스트 방식(모든 클라이언트에 동일 데이터 전송)으로 확장
- 외부 네트워크 노출이 필요한 경우를 위한 간단한 토큰 기반 인증 옵션 추가