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:
@@ -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 설치파일
|
||||
|
||||
+85
-4
@@ -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로 확인하세요."
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" "$@"
|
||||
Reference in New Issue
Block a user