diff --git a/README.md b/README.md index e7ccf82..a8c96fb 100644 --- a/README.md +++ b/README.md @@ -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 설치파일 diff --git a/juc500_xfer/usb_link.py b/juc500_xfer/usb_link.py index d1c3dd1..b4b2c78 100644 --- a/juc500_xfer/usb_link.py +++ b/juc500_xfer/usb_link.py @@ -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,12 +554,22 @@ 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 - raise + else: + raise sent += n if n == 0: break @@ -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로 확인하세요." diff --git a/scripts/setup_macos.sh b/scripts/setup_macos.sh index 285a150..30c198e 100755 --- a/scripts/setup_macos.sh +++ b/scripts/setup_macos.sh @@ -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" diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..7c94efb --- /dev/null +++ b/start.sh @@ -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" "$@"