Fix Windows WinUSB not-supported error on connect
Skip get_configuration/kernel-driver APIs on Windows, seed pyusb config cache, and require only Vendor interface 5 for transfers. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+101
-27
@@ -9,9 +9,9 @@ The cable enumerates as USB VID:PID 0711:7500 with six interfaces:
|
|||||||
4 HID keyboard (KM switch, claimed by OS)
|
4 HID keyboard (KM switch, claimed by OS)
|
||||||
5 Vendor Specific (bulk 0x08/0x89 + 0x0A/0x8B) – data bridge
|
5 Vendor Specific (bulk 0x08/0x89 + 0x0A/0x8B) – data bridge
|
||||||
|
|
||||||
Official Wormhole software is Intel/x86_64-only and uses the deprecated
|
On Windows, WinUSB does not implement libusb_get_configuration /
|
||||||
IOUSBDevice stack, which does not work reliably on Apple Silicon. This
|
kernel-driver detach. Only Interface 5 (Vendor) is required for our
|
||||||
module opens interfaces 1 and 5 via libusb for a Silicon-native pathway.
|
protocol; CDC interfaces are optional.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -28,25 +28,22 @@ import usb.util
|
|||||||
VID = 0x0711
|
VID = 0x0711
|
||||||
PID = 0x7500
|
PID = 0x7500
|
||||||
|
|
||||||
# Interface numbers
|
|
||||||
IF_CDC_COMM = 0
|
IF_CDC_COMM = 0
|
||||||
IF_CDC_DATA = 1
|
IF_CDC_DATA = 1
|
||||||
IF_VENDOR = 5
|
IF_VENDOR = 5
|
||||||
|
|
||||||
# Vendor interface endpoints (file payload)
|
|
||||||
EP_DATA_OUT = 0x08
|
EP_DATA_OUT = 0x08
|
||||||
EP_DATA_IN = 0x89
|
EP_DATA_IN = 0x89
|
||||||
|
|
||||||
# Vendor interface endpoints (keepalive / control mux)
|
|
||||||
EP_CTRL_OUT = 0x0A
|
EP_CTRL_OUT = 0x0A
|
||||||
EP_CTRL_IN = 0x8B
|
EP_CTRL_IN = 0x8B
|
||||||
|
|
||||||
# CDC data endpoints (alternate / reserved)
|
|
||||||
EP_CDC_OUT = 0x02
|
EP_CDC_OUT = 0x02
|
||||||
EP_CDC_IN = 0x83
|
EP_CDC_IN = 0x83
|
||||||
|
|
||||||
MAX_PACKET = 512
|
MAX_PACKET = 512
|
||||||
|
|
||||||
|
_IS_WIN = sys.platform == "win32"
|
||||||
|
|
||||||
|
|
||||||
def _libusb_backend():
|
def _libusb_backend():
|
||||||
"""Locate libusb-1.0 on macOS / Windows / Linux."""
|
"""Locate libusb-1.0 on macOS / Windows / Linux."""
|
||||||
@@ -72,7 +69,7 @@ def _libusb_backend():
|
|||||||
"/opt/homebrew/lib/libusb-1.0.dylib",
|
"/opt/homebrew/lib/libusb-1.0.dylib",
|
||||||
"/usr/local/lib/libusb-1.0.dylib",
|
"/usr/local/lib/libusb-1.0.dylib",
|
||||||
]
|
]
|
||||||
elif sys.platform == "win32":
|
elif _IS_WIN:
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
root = os.path.dirname(here)
|
root = os.path.dirname(here)
|
||||||
candidates += [
|
candidates += [
|
||||||
@@ -84,10 +81,10 @@ def _libusb_backend():
|
|||||||
candidates += ["libusb-1.0.so", "libusb-1.0.so.0"]
|
candidates += ["libusb-1.0.so", "libusb-1.0.so.0"]
|
||||||
|
|
||||||
for path in candidates:
|
for path in candidates:
|
||||||
if path and (path.endswith((".dll", ".dylib", ".so", ".so.0")) or os.path.exists(path)):
|
if not path:
|
||||||
if path.endswith((".dll", ".dylib", ".so", ".so.0")) and not os.path.exists(path):
|
continue
|
||||||
# bare DLL name: allow loader PATH search via find_library
|
absolute = os.path.sep in path or (len(path) > 1 and path[1] == ":")
|
||||||
if os.path.sep in path or (len(path) > 1 and path[1] == ":"):
|
if absolute and not os.path.exists(path):
|
||||||
continue
|
continue
|
||||||
be = usb.backend.libusb1.get_backend(find_library=lambda _n, p=path: p)
|
be = usb.backend.libusb1.get_backend(find_library=lambda _n, p=path: p)
|
||||||
if be is not None:
|
if be is not None:
|
||||||
@@ -102,6 +99,23 @@ def _libusb_backend():
|
|||||||
return be
|
return be
|
||||||
|
|
||||||
|
|
||||||
|
def _usb_err_text(exc: BaseException) -> str:
|
||||||
|
parts = [str(exc)]
|
||||||
|
strerror = getattr(exc, "strerror", None)
|
||||||
|
if strerror and strerror not in parts[0]:
|
||||||
|
parts.append(str(strerror))
|
||||||
|
return " ".join(parts).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_not_supported(exc: BaseException) -> bool:
|
||||||
|
text = _usb_err_text(exc)
|
||||||
|
return (
|
||||||
|
"not supported" in text
|
||||||
|
or "unimplemented" in text
|
||||||
|
or "enosys" in text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DeviceInfo:
|
class DeviceInfo:
|
||||||
bus: int
|
bus: int
|
||||||
@@ -140,7 +154,7 @@ def list_devices() -> list[DeviceInfo]:
|
|||||||
|
|
||||||
|
|
||||||
class Juc500Link:
|
class Juc500Link:
|
||||||
"""Claim CDC+Vendor interfaces and provide framed bulk I/O."""
|
"""Claim Vendor (+ optional CDC) interfaces and provide framed bulk I/O."""
|
||||||
|
|
||||||
def __init__(self, serial: Optional[str] = None, timeout_ms: int = 2000):
|
def __init__(self, serial: Optional[str] = None, timeout_ms: int = 2000):
|
||||||
self.timeout_ms = timeout_ms
|
self.timeout_ms = timeout_ms
|
||||||
@@ -158,6 +172,11 @@ class Juc500Link:
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"JUC500(0711:7500 Smart Data Link) 를 찾지 못했습니다. "
|
"JUC500(0711:7500 Smart Data Link) 를 찾지 못했습니다. "
|
||||||
"케이블이 연결되어 있는지 확인하세요."
|
"케이블이 연결되어 있는지 확인하세요."
|
||||||
|
+ (
|
||||||
|
" Windows: Zadig로 Interface 5에 WinUSB를 설치했는지 확인하세요."
|
||||||
|
if _IS_WIN
|
||||||
|
else ""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
chosen = None
|
chosen = None
|
||||||
@@ -175,32 +194,69 @@ class Juc500Link:
|
|||||||
raise RuntimeError(f"시리얼 '{self._serial}' 에 해당하는 JUC500 이 없습니다.")
|
raise RuntimeError(f"시리얼 '{self._serial}' 에 해당하는 JUC500 이 없습니다.")
|
||||||
|
|
||||||
self._dev = chosen
|
self._dev = chosen
|
||||||
# Avoid set_configuration() if the OS already activated config 1
|
|
||||||
# (mass-storage CD is mounted). Re-setting can disrupt the volume.
|
if _IS_WIN:
|
||||||
|
self._configure_windows()
|
||||||
|
else:
|
||||||
|
self._configure_posix()
|
||||||
|
|
||||||
|
# Protocol uses Vendor IF5 only. CDC is best-effort.
|
||||||
|
self._claim(IF_VENDOR, required=True)
|
||||||
|
self._claim(IF_CDC_DATA, required=False)
|
||||||
|
self._claim(IF_CDC_COMM, required=False)
|
||||||
|
|
||||||
|
def _configure_windows(self) -> None:
|
||||||
|
"""WinUSB: libusb_get_configuration() is often unimplemented.
|
||||||
|
|
||||||
|
pyusb caches the active config index after set_configuration().
|
||||||
|
If set fails (already configured), we seed the cache so later
|
||||||
|
write()/read() do not call get_configuration() and raise
|
||||||
|
'Operation not supported or unimplemented on this platform'.
|
||||||
|
"""
|
||||||
|
assert self._dev is not None
|
||||||
|
try:
|
||||||
|
self._dev.set_configuration(1)
|
||||||
|
except usb.core.USBError:
|
||||||
|
pass
|
||||||
|
# Ensure pyusb ResourceManager will not call get_configuration().
|
||||||
|
try:
|
||||||
|
ctx = self._dev._ctx
|
||||||
|
if getattr(ctx, "_active_cfg_index", None) is None:
|
||||||
|
ctx._active_cfg_index = 0 # device has a single config
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _configure_posix(self) -> None:
|
||||||
|
assert self._dev is not None
|
||||||
try:
|
try:
|
||||||
cfg = self._dev.get_active_configuration()
|
cfg = self._dev.get_active_configuration()
|
||||||
except usb.core.USBError:
|
except usb.core.USBError:
|
||||||
self._dev.set_configuration()
|
self._dev.set_configuration(1)
|
||||||
cfg = self._dev.get_active_configuration()
|
cfg = self._dev.get_active_configuration()
|
||||||
if cfg.bConfigurationValue != 1:
|
if cfg.bConfigurationValue != 1:
|
||||||
self._dev.set_configuration(1)
|
self._dev.set_configuration(1)
|
||||||
|
|
||||||
for ifnum in (IF_CDC_DATA, IF_VENDOR):
|
def _claim(self, ifnum: int, *, required: bool) -> None:
|
||||||
self._claim(ifnum)
|
|
||||||
# CDC Comm is optional (interrupt notifications)
|
|
||||||
try:
|
|
||||||
self._claim(IF_CDC_COMM)
|
|
||||||
except usb.core.USBError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _claim(self, ifnum: int) -> None:
|
|
||||||
assert self._dev is not None
|
assert self._dev is not None
|
||||||
|
if not _IS_WIN:
|
||||||
try:
|
try:
|
||||||
if self._dev.is_kernel_driver_active(ifnum):
|
if self._dev.is_kernel_driver_active(ifnum):
|
||||||
self._dev.detach_kernel_driver(ifnum)
|
self._dev.detach_kernel_driver(ifnum)
|
||||||
except (NotImplementedError, usb.core.USBError):
|
except (NotImplementedError, usb.core.USBError):
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
usb.util.claim_interface(self._dev, ifnum)
|
usb.util.claim_interface(self._dev, ifnum)
|
||||||
|
except usb.core.USBError as exc:
|
||||||
|
if required:
|
||||||
|
hint = ""
|
||||||
|
if _IS_WIN:
|
||||||
|
hint = (
|
||||||
|
f"\nZadig에서 Smart Data Link → Interface {ifnum} 에 "
|
||||||
|
"WinUSB 드라이버를 설치하세요. "
|
||||||
|
"(데이터 전송은 Interface 5가 필수입니다.)"
|
||||||
|
)
|
||||||
|
raise RuntimeError(f"USB interface {ifnum} 점유 실패: {exc}{hint}") from exc
|
||||||
|
return
|
||||||
self._claimed.append(ifnum)
|
self._claimed.append(ifnum)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -226,12 +282,19 @@ class Juc500Link:
|
|||||||
def _bulk_write(self, ep: int, payload: bytes, timeout_ms: Optional[int]) -> int:
|
def _bulk_write(self, ep: int, payload: bytes, timeout_ms: Optional[int]) -> int:
|
||||||
assert self._dev is not None
|
assert self._dev is not None
|
||||||
to = self.timeout_ms if timeout_ms is None else timeout_ms
|
to = self.timeout_ms if timeout_ms is None else timeout_ms
|
||||||
# Split into max-packet chunks for bridge chips that dislike large URBs
|
|
||||||
sent = 0
|
sent = 0
|
||||||
view = memoryview(payload)
|
view = memoryview(payload)
|
||||||
while sent < len(payload):
|
while sent < len(payload):
|
||||||
chunk = view[sent : sent + MAX_PACKET]
|
chunk = view[sent : sent + MAX_PACKET]
|
||||||
|
try:
|
||||||
n = self._dev.write(ep, chunk.tobytes(), timeout=to)
|
n = self._dev.write(ep, chunk.tobytes(), timeout=to)
|
||||||
|
except usb.core.USBError as exc:
|
||||||
|
if _is_not_supported(exc):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"USB 쓰기 실패(EP 0x{ep:02x}): {exc}\n"
|
||||||
|
"Windows: Interface 5에 WinUSB가 잡혀 있는지 Zadig로 확인하세요."
|
||||||
|
) from exc
|
||||||
|
raise
|
||||||
sent += n
|
sent += n
|
||||||
if n == 0:
|
if n == 0:
|
||||||
break
|
break
|
||||||
@@ -245,13 +308,24 @@ class Juc500Link:
|
|||||||
return bytes(data)
|
return bytes(data)
|
||||||
except usb.core.USBTimeoutError:
|
except usb.core.USBTimeoutError:
|
||||||
return b""
|
return b""
|
||||||
|
except usb.core.USBError as exc:
|
||||||
|
if _is_not_supported(exc):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"USB 읽기 실패(EP 0x{ep:02x}): {exc}\n"
|
||||||
|
"Windows: Interface 5에 WinUSB가 잡혀 있는지 Zadig로 확인하세요."
|
||||||
|
) from exc
|
||||||
|
# Transient pipe errors — treat like empty during poll
|
||||||
|
return b""
|
||||||
|
|
||||||
def drain(self, duration_s: float = 0.2) -> None:
|
def drain(self, duration_s: float = 0.2) -> None:
|
||||||
deadline = time.monotonic() + duration_s
|
deadline = time.monotonic() + duration_s
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
got = False
|
got = False
|
||||||
for reader in (self.read_data, self.read_ctrl):
|
for reader in (self.read_data, self.read_ctrl):
|
||||||
|
try:
|
||||||
chunk = reader(MAX_PACKET, timeout_ms=50)
|
chunk = reader(MAX_PACKET, timeout_ms=50)
|
||||||
|
except RuntimeError:
|
||||||
|
break
|
||||||
if chunk:
|
if chunk:
|
||||||
got = True
|
got = True
|
||||||
if not got:
|
if not got:
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ JUC500 — Windows 11 사용 안내
|
|||||||
2) 공식 j5create Wormhole 소프트웨어가 떠 있으면 종료/제거합니다.
|
2) 공식 j5create Wormhole 소프트웨어가 떠 있으면 종료/제거합니다.
|
||||||
3) Zadig(https://zadig.akeo.ie/) 로 Smart Data Link 장치에 WinUSB 를 설치합니다.
|
3) Zadig(https://zadig.akeo.ie/) 로 Smart Data Link 장치에 WinUSB 를 설치합니다.
|
||||||
- Options → List All Devices
|
- Options → List All Devices
|
||||||
- Smart Data Link → Interface 5 (Vendor) → WinUSB → Replace Driver
|
- Smart Data Link → Interface 5 (Vendor Specific / MI_05) → WinUSB → Replace Driver
|
||||||
- (권장) Interface 1 (CDC Data) 도 WinUSB
|
|
||||||
- Interface 2(Mass Storage)·3·4(HID) 는 그대로 둡니다.
|
- Interface 2(Mass Storage)·3·4(HID) 는 그대로 둡니다.
|
||||||
|
- (선택) Interface 1 도 WinUSB 가능하지만, 필수는 Interface 5 입니다.
|
||||||
4) JUC500.exe 를 실행합니다. 브라우저가 열리면 「연결」을 누릅니다.
|
4) JUC500.exe 를 실행합니다. 브라우저가 열리면 「연결」을 누릅니다.
|
||||||
5) Mac 쪽에서도 같은 프로그램을 실행한 뒤 연결합니다.
|
5) Mac 쪽에서도 같은 프로그램을 실행한 뒤 연결합니다.
|
||||||
|
|
||||||
|
오류 "Operation not supported or unimplemented on this platform"
|
||||||
|
→ 구버전 버그이거나 Interface 5 WinUSB 미설치. 최신 Setup/portable 로 업데이트하고
|
||||||
|
Zadig에서 Interface 5 가 WinUSB 인지 다시 확인하세요.
|
||||||
|
|
||||||
포터블: JUC500-portable.exe 는 설치 없이 실행할 수 있습니다.
|
포터블: JUC500-portable.exe 는 설치 없이 실행할 수 있습니다.
|
||||||
(같은 WinUSB/libusb 설정이 필요합니다.)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user