Fix missing _is_not_supported and harden WinUSB config cache

Restores the helper accidentally removed when adding access-denied
handling, retries I/O after seeding pyusb config index on Windows, and
handles USB overflow reads without crashing the HELLO loop.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
joyharam
2026-07-15 11:20:59 +09:00
parent d9c0b8078c
commit 7627c17653
4 changed files with 132 additions and 5 deletions
+14
View File
@@ -81,6 +81,18 @@ GUI **Wormhole 종료** 버튼 또는 `python -m juc500_xfer kill-wormhole` 로
## 사용법
### GUI (권장)
```bash
# 최초 1회
./scripts/setup_macos.sh
# 이후
./start.sh
```
인자를 넘기면 CLI로 동작합니다: `./start.sh doctor`, `./start.sh kill-wormhole` 등.
양쪽 PC에 케이블을 꽂고, **먼저 수신 쪽**, 이어서 송신 쪽을 실행합니다.
**Windows (수신 예)**
@@ -93,6 +105,8 @@ python -m juc500_xfer recv --dest Downloads -y
```bash
python -m juc500_xfer send ~/Desktop/archive.zip
# 또는
./start.sh send ~/Desktop/archive.zip
```
## Windows 설치파일
+84 -3
View File
@@ -194,6 +194,24 @@ def _is_access_denied(exc: BaseException) -> bool:
)
def _is_not_supported(exc: BaseException) -> bool:
"""WinUSB often returns LIBUSB_ERROR_NOT_SUPPORTED for get_configuration etc."""
text = _usb_err_text(exc)
errno = getattr(exc, "errno", None)
return (
errno in (95, 40) # ENOTSUP / Windows ERROR_NOT_SUPPORTED-ish
or "not supported" in text
or "unimplemented" in text
or "enosys" in text
)
def _is_overflow(exc: BaseException) -> bool:
text = _usb_err_text(exc)
errno = getattr(exc, "errno", None)
return errno == 84 or "overflow" in text
def _claim_failure_message(ifnum: int, exc: BaseException) -> str:
base = f"USB interface {ifnum} 점유 실패: {exc}"
if not _IS_WIN:
@@ -211,6 +229,12 @@ def _claim_failure_message(ifnum: int, exc: BaseException) -> str:
"6) 케이블 뽑았다가 다시 꽂고 JUC500 재실행 → 연결\n"
"장치 관리자에서 Interface 5가 'WinUSB' / 'libusb-winusb' 인지도 확인하세요."
)
if _is_not_supported(exc):
return (
f"{base}\n\n"
"WinUSB가 해당 libusb API를 지원하지 않습니다. Interface 5(MI_05)에 "
"WinUSB가 맞는지 Zadig로 확인하고, 최신 JUC500 빌드를 사용하세요."
)
return (
f"{base}\n"
f"Zadig에서 Smart Data Link → Interface {ifnum} (MI_0{ifnum}) 에 WinUSB를 설치하세요."
@@ -431,14 +455,28 @@ class Juc500Link:
self._claim(IF_CDC_COMM, required=False)
def _configure_windows(self) -> None:
"""Avoid libusb_get_configuration — unimplemented on WinUSB.
Every pyusb read/write resolves endpoints via get_active_configuration(),
which calls backend.get_configuration() when the cache is empty and then
raises 'Operation not supported or unimplemented on this platform'.
Always seed _active_cfg_index after open.
"""
assert self._dev is not None
try:
self._dev.set_configuration(1)
except usb.core.USBError:
except (usb.core.USBError, NotImplementedError):
pass
self._ensure_cfg_cache(force=True)
def _ensure_cfg_cache(self, *, force: bool = False) -> None:
"""Seed pyusb config index so I/O never calls get_configuration on WinUSB."""
if self._dev is None:
return
try:
ctx = self._dev._ctx
if getattr(ctx, "_active_cfg_index", None) is None:
if force or getattr(ctx, "_active_cfg_index", None) is None:
# Composite JUC500 exposes a single configuration at index 0.
ctx._active_cfg_index = 0
except Exception:
pass
@@ -506,6 +544,8 @@ class Juc500Link:
def _bulk_write(self, ep: int, payload: bytes, timeout_ms: Optional[int]) -> int:
assert self._dev is not None
if _IS_WIN:
self._ensure_cfg_cache()
to = self.timeout_ms if timeout_ms is None else timeout_ms
sent = 0
view = memoryview(payload)
@@ -514,11 +554,21 @@ class Juc500Link:
try:
n = self._dev.write(ep, chunk.tobytes(), timeout=to)
except usb.core.USBError as exc:
if _is_not_supported(exc):
if _IS_WIN and _is_not_supported(exc):
self._ensure_cfg_cache(force=True)
try:
n = self._dev.write(ep, chunk.tobytes(), timeout=to)
except usb.core.USBError as exc2:
raise RuntimeError(
f"USB 쓰기 실패(EP 0x{ep:02x}): {exc2}\n"
"Windows: Interface 5에 WinUSB가 잡혀 있는지 Zadig로 확인하세요."
) from exc2
elif _is_not_supported(exc):
raise RuntimeError(
f"USB 쓰기 실패(EP 0x{ep:02x}): {exc}\n"
"Windows: Interface 5에 WinUSB가 잡혀 있는지 Zadig로 확인하세요."
) from exc
else:
raise
sent += n
if n == 0:
@@ -527,6 +577,8 @@ class Juc500Link:
def _bulk_read(self, ep: int, size: int, timeout_ms: Optional[int]) -> bytes:
assert self._dev is not None
if _IS_WIN:
self._ensure_cfg_cache()
to = self.timeout_ms if timeout_ms is None else timeout_ms
try:
data = self._dev.read(ep, size, timeout=to)
@@ -534,7 +586,36 @@ class Juc500Link:
except usb.core.USBTimeoutError:
return b""
except usb.core.USBError as exc:
if _is_overflow(exc):
for bigger in (max(size, 512) * 2, 4096, 16384, 65536):
if bigger <= size:
continue
try:
data = self._dev.read(ep, bigger, timeout=to)
return bytes(data)
except usb.core.USBTimeoutError:
return b""
except usb.core.USBError as exc2:
if _is_overflow(exc2):
size = bigger
continue
if _is_not_supported(exc2):
break
return b""
return b""
if _is_not_supported(exc):
if _IS_WIN:
self._ensure_cfg_cache(force=True)
try:
data = self._dev.read(ep, size, timeout=to)
return bytes(data)
except usb.core.USBTimeoutError:
return b""
except usb.core.USBError as exc2:
raise RuntimeError(
f"USB 읽기 실패(EP 0x{ep:02x}): {exc2}\n"
"Windows: Interface 5에 WinUSB가 잡혀 있는지 Zadig로 확인하세요."
) from exc2
raise RuntimeError(
f"USB 읽기 실패(EP 0x{ep:02x}): {exc}\n"
"Windows: Interface 5에 WinUSB가 잡혀 있는지 Zadig로 확인하세요."
+3 -1
View File
@@ -20,7 +20,9 @@ pip install --no-index --find-links=vendor/wheels -r requirements.txt \
pip install -e .
echo
echo "설치 완료. 예:"
echo "설치 완료. 실행:"
echo " ./start.sh"
echo "예:"
echo " source $ROOT/.venv/bin/activate"
echo " juc500 info"
echo " juc500 gui"
Executable
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# JUC500 GUI 실행 (인자 없으면 GUI, 있으면 CLI로 전달)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
if [[ -x "$ROOT/.venv/bin/python" ]]; then
PY="$ROOT/.venv/bin/python"
elif command -v python3 >/dev/null 2>&1; then
PY="$(command -v python3)"
else
echo "Python3 가 없습니다. 먼저 설치하세요:" >&2
echo " ./scripts/setup_macos.sh" >&2
exit 1
fi
if [[ ! -x "$ROOT/.venv/bin/python" ]]; then
echo "경고: .venv 없음 — 시스템 Python 사용. 문제가 있으면 ./scripts/setup_macos.sh" >&2
fi
export PYTHONPATH="$ROOT${PYTHONPATH:+:$PYTHONPATH}"
# 의존성 빠른 확인
if ! "$PY" -c "import usb" 2>/dev/null; then
echo "pyusb 가 없습니다. 먼저 설치하세요:" >&2
echo " ./scripts/setup_macos.sh" >&2
exit 1
fi
exec "$PY" "$ROOT/juc500_app.py" "$@"