요약
"브라우저로 연결하기"(hardwareLite)에서 연결이 한 번 실패하면, 그 시도가 열어둔 시리얼 포트가 닫히지 않은 채 남습니다. 이후 같은 기기로는 "연결하기"를 몇 번을 눌러도 계속 실패하고, 이에 대한 안내가 없습니다. 특정 1개 회사의 모듈 문제가 아니라 Web Serial을 쓰는 lite 모듈 전체에 해당하는 코어 쪽 문제로 보입니다.
저희(로보메이션) 측으로 "구형 햄스터 연결 → 햄스터S 연결 → 다시 구형 연결이 안 됨" 이런식의 문제가 있다는 문의사항이 들어왔습니다. 보통 학교 수업시간에 신형 BLE 동글에 햄스터 구형과 신형을 번갈아 연결하면서 수업중에 사용합니다. 그런데 로봇 전환 중에 연결이 한 번 실패하면 그 뒤로 계속 안 된다고 합니다. 그래서 이 문제의 원인을 분석하는 중에 'entryjs' 코어에 문제가 있음을 확인하였습니다.
재현 방법
Chrome, FF 핸드셰이크 프로토콜을 쓰는 로보메이션 공용 동글과 로봇 2종(햄스터, 거북이)으로 재현했습니다. 핸드셰이크가 실패하는 조합이면 다른 lite 모듈로도 같은 결과가 나올 것입니다.
- 동글을 햄스터가 아닌 로봇(거북이)과 페어링해 PC에 연결
- 작품 만들기 → 하드웨어 → 브라우저로 연결하기 → 햄스터 선택 → 포트 선택
- 핸드셰이크 실패로 "연결 실패" 표시. 여기까지는 정상 동작
- 동글을 꽂아둔 채 햄스터와 다시 페어링
- "연결하기" 버튼으로 재시도, 포트 선택
- 기대는 정상 연결이지만 실제로는 즉시 실패합니다. 콘솔에
DOMException: InvalidStateError: The port is already open. 가 찍히고, 몇 번을 재시도해도 같습니다
원인 분석
결함 A. connect() 실패 경로에 포트 정리가 없음
src/class/hw_lite.ts connect():
try {
await this.webConnector.connect(); // 포트 open + reader/writer 락 획득
this.setStatus('connected');
...
await this.webConnector.initialDevice(); // 핸드셰이크 실패 시 throw
...
} catch (error) {
console.error(error);
Entry.toast.alert(...);
this.getConnectFailedMenu(); // status 변경뿐 — 포트를 닫지 않음
}
webConnector.connect()(webSerialConnector.ts)가 port.open()과 getReader()/getWriter()까지 성공한 뒤 initialDevice()의 핸드셰이크에서 throw되면, catch는 상태만 connectFailed로 바꾸고 열린 포트와 스트림 락은 그대로 남습니다.
재시도가 실패하는 이유는 Web Serial의 동작 때문입니다. navigator.serial.requestPort()는 같은 기기에 대해 항상 같은 SerialPort 객체를 반환하고, 이미 open 상태인 포트의 open()은 InvalidStateError로 reject됩니다. 그래서 첫 실패가 남긴 포트가 닫히기 전까지, 포트 선택 대화상자에서 같은 기기를 고르는 한 재연결은 시작 단계에서 막힙니다. 다른 포트를 고르면 이전에 발생한 누수가 잠복되게 됩니다.
이 상태의 복구 수단은 "연결 해제" 버튼뿐인데(disconnect()가 포트를 닫아줍니다), 연결 실패 화면에서 해제를 눌러야 한다는 걸 사용자가 알 방법이 없습니다. 실패 사유도 개발자 콘솔에만 남습니다.
결함 B. 모듈 재선택 시 기존 커넥터를 정리 없이 교체
src/command/commands/object.js의 objectAddHardwareLiteBlocks.do()는 기존 연결 여부와 무관하게 setExternalModule()을 호출하고, src/class/hw_lite.ts의 setWebConnector()는 기존 커넥터를 정리 없이 새 인스턴스로 바꿉니다:
setWebConnector() {
...
this.serial = new WebSerialConnector(this.hwModule, this); // 이전 커넥터(열린 포트 보유)는 방치
this.webConnector = this.serial;
}
이전 커넥터가 열린 포트를 쥔 채 참조를 잃으면, 그 뒤에는 "연결 해제" 버튼도 새 커넥터만 닫기 때문에 침조를 잃은 포트를 닫을 방법이 없습니다.
이전 세션의 constantServing() 루프도 문제가 됩니다. 종료 조건이 status === 'disconnected' 하나뿐이라(webSerialConnector.ts), 재선택으로 상태가 connectFailed가 된 뒤에도 참조 없는 커넥터 위에서 계속 돕니다.
제안 수정
A. 실패 시 정리: hw_lite.connect() catch에서 커넥터 정리:
} catch (error) {
console.error(error);
await this.serial?.removeSerialPort().catch((err: Error) => console.error(err));
Entry.toast.alert(...);
this.getConnectFailedMenu();
}
기존 public 메서드 removeSerialPort()와 기존 필드 this.serial만 사용하므로 신규 API가 필요 없습니다. BLE 연결은 this.serial이 undefined라 no-op입니다. BLE 커넥터 자체는 아래 "부수 발견"의 별개 결함을 먼저 정리한 뒤 다루는 편이 낫다고 봅니다.
B. 재선택 방어: setExternalModule() 진입 시 이전 커넥터의 포트 정리:
setExternalModule(moduleObject: EntryHWLiteBaseModule) {
this.serial?.removeSerialPort().catch((err: Error) => console.error(err));
this.hwModule = moduleObject;
...
}
B-2. constantServing()의 중단 조건을 getStatus() !== 'connected'로 변경 (현재는 'disconnected'만 체크).
영향 범위
- Web Serial을 쓰는 모든 hardwareLite 모듈 (develop 기준 30종). 30종 중 연결 실패 시 자체적으로 포트를 정리하는 모듈이 없으므로, 'entryjs' 코어에서 고치는 게 맞다고 판단했습니다
- 무선 동글류처럼 페어링 상태에 따라 핸드셰이크가 실패할 수 있는 교구에서 자주 발생합니다
- 교실 환경 특성상 여러대의 로봇을 사용하고, "실패하면 해제 없이 다시 시도/다시 선택"하는 동선이 일반적이라 수정이 필요하다고 판단하였습니다.
요약
"브라우저로 연결하기"(hardwareLite)에서 연결이 한 번 실패하면, 그 시도가 열어둔 시리얼 포트가 닫히지 않은 채 남습니다. 이후 같은 기기로는 "연결하기"를 몇 번을 눌러도 계속 실패하고, 이에 대한 안내가 없습니다. 특정 1개 회사의 모듈 문제가 아니라 Web Serial을 쓰는 lite 모듈 전체에 해당하는 코어 쪽 문제로 보입니다.
저희(로보메이션) 측으로 "구형 햄스터 연결 → 햄스터S 연결 → 다시 구형 연결이 안 됨" 이런식의 문제가 있다는 문의사항이 들어왔습니다. 보통 학교 수업시간에 신형 BLE 동글에 햄스터 구형과 신형을 번갈아 연결하면서 수업중에 사용합니다. 그런데 로봇 전환 중에 연결이 한 번 실패하면 그 뒤로 계속 안 된다고 합니다. 그래서 이 문제의 원인을 분석하는 중에 'entryjs' 코어에 문제가 있음을 확인하였습니다.
재현 방법
Chrome, FF 핸드셰이크 프로토콜을 쓰는 로보메이션 공용 동글과 로봇 2종(햄스터, 거북이)으로 재현했습니다. 핸드셰이크가 실패하는 조합이면 다른 lite 모듈로도 같은 결과가 나올 것입니다.
DOMException: InvalidStateError: The port is already open.가 찍히고, 몇 번을 재시도해도 같습니다원인 분석
결함 A.
connect()실패 경로에 포트 정리가 없음src/class/hw_lite.tsconnect():webConnector.connect()(webSerialConnector.ts)가port.open()과getReader()/getWriter()까지 성공한 뒤initialDevice()의 핸드셰이크에서 throw되면, catch는 상태만connectFailed로 바꾸고 열린 포트와 스트림 락은 그대로 남습니다.재시도가 실패하는 이유는 Web Serial의 동작 때문입니다.
navigator.serial.requestPort()는 같은 기기에 대해 항상 같은SerialPort객체를 반환하고, 이미 open 상태인 포트의open()은InvalidStateError로 reject됩니다. 그래서 첫 실패가 남긴 포트가 닫히기 전까지, 포트 선택 대화상자에서 같은 기기를 고르는 한 재연결은 시작 단계에서 막힙니다. 다른 포트를 고르면 이전에 발생한 누수가 잠복되게 됩니다.이 상태의 복구 수단은 "연결 해제" 버튼뿐인데(
disconnect()가 포트를 닫아줍니다), 연결 실패 화면에서 해제를 눌러야 한다는 걸 사용자가 알 방법이 없습니다. 실패 사유도 개발자 콘솔에만 남습니다.결함 B. 모듈 재선택 시 기존 커넥터를 정리 없이 교체
src/command/commands/object.js의objectAddHardwareLiteBlocks.do()는 기존 연결 여부와 무관하게setExternalModule()을 호출하고,src/class/hw_lite.ts의setWebConnector()는 기존 커넥터를 정리 없이 새 인스턴스로 바꿉니다:이전 커넥터가 열린 포트를 쥔 채 참조를 잃으면, 그 뒤에는 "연결 해제" 버튼도 새 커넥터만 닫기 때문에 침조를 잃은 포트를 닫을 방법이 없습니다.
이전 세션의
constantServing()루프도 문제가 됩니다. 종료 조건이status === 'disconnected'하나뿐이라(webSerialConnector.ts), 재선택으로 상태가connectFailed가 된 뒤에도 참조 없는 커넥터 위에서 계속 돕니다.제안 수정
A. 실패 시 정리:
hw_lite.connect()catch에서 커넥터 정리:기존 public 메서드
removeSerialPort()와 기존 필드this.serial만 사용하므로 신규 API가 필요 없습니다. BLE 연결은this.serial이 undefined라 no-op입니다. BLE 커넥터 자체는 아래 "부수 발견"의 별개 결함을 먼저 정리한 뒤 다루는 편이 낫다고 봅니다.B. 재선택 방어:
setExternalModule()진입 시 이전 커넥터의 포트 정리:B-2.
constantServing()의 중단 조건을getStatus() !== 'connected'로 변경 (현재는'disconnected'만 체크).영향 범위