Improve JUC500 device discovery and Windows doctor diagnostics
Widen libusb DLL search, copy DLL into venv on setup, match sibling PIDs, and add doctor command with PnP VID_0711 hints for WinUSB. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -67,6 +67,16 @@ python -m juc500_xfer info
|
||||
|
||||
또는 `scripts\setup_windows.bat` 실행.
|
||||
|
||||
**장치가 안 보이면:**
|
||||
|
||||
```powershell
|
||||
# libusb DLL 복사 (필수)
|
||||
copy vendor\libusb-1.0.dll .venv\Scripts\libusb-1.0.dll
|
||||
python -m juc500_xfer doctor
|
||||
```
|
||||
|
||||
Zadig로 **Interface 5 (MI_05)** 에 WinUSB 설치 여부를 확인하세요.
|
||||
|
||||
## 사용법
|
||||
|
||||
양쪽 PC에 케이블을 꽂고, **먼저 수신 쪽**, 이어서 송신 쪽을 실행합니다.
|
||||
|
||||
+32
-3
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
|
||||
from . import __version__
|
||||
from .protocol import Session
|
||||
from .usb_link import list_devices, Juc500Link
|
||||
from .usb_link import list_devices, Juc500Link, doctor
|
||||
|
||||
|
||||
def _progress(prefix: str):
|
||||
@@ -34,13 +34,39 @@ def _progress(prefix: str):
|
||||
def cmd_info(_args: argparse.Namespace) -> int:
|
||||
devices = list_devices()
|
||||
if not devices:
|
||||
print("연결된 JUC500 장치가 없습니다.")
|
||||
print("연결된 JUC500 장치가 없습니다. 진단: python -m juc500_xfer doctor")
|
||||
return 1
|
||||
for d in devices:
|
||||
print(f"bus={d.bus} addr={d.address} serial={d.serial!r} product={d.product!r}")
|
||||
print(
|
||||
f"{d.vid:04x}:{d.pid:04x} bus={d.bus} addr={d.address} "
|
||||
f"serial={d.serial!r} product={d.product!r}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_doctor(_args: argparse.Namespace) -> int:
|
||||
rep = doctor()
|
||||
print(f"libusb: {rep.libusb_path or '(없음)'}")
|
||||
if rep.libusb_error:
|
||||
print(f"libusb 오류: {rep.libusb_error}")
|
||||
print(f"USB 장치 수(libusb): {rep.usb_device_count}")
|
||||
print(f"JUC500 후보: {len(rep.juc500)}")
|
||||
for j in rep.juc500:
|
||||
print(f" - {j}")
|
||||
if rep.sample_devices:
|
||||
print("USB 샘플:")
|
||||
for line in rep.sample_devices:
|
||||
print(f" {line}")
|
||||
if rep.pnp_vid0711:
|
||||
print("Windows PnP VID_0711:")
|
||||
for row in rep.pnp_vid0711:
|
||||
print(f" - {row}")
|
||||
print("안내:")
|
||||
for h in rep.hints:
|
||||
print(f" * {h}")
|
||||
return 0 if rep.ok else 1
|
||||
|
||||
|
||||
def cmd_probe(args: argparse.Namespace) -> int:
|
||||
with Juc500Link(serial=args.serial) as link:
|
||||
print(f"열림: serial={link.serial!r}")
|
||||
@@ -114,6 +140,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
s = sub.add_parser("info", help="연결된 JUC500 장치 목록")
|
||||
s.set_defaults(func=cmd_info)
|
||||
|
||||
s = sub.add_parser("doctor", help="USB/libusb/WinUSB 진단")
|
||||
s.set_defaults(func=cmd_doctor)
|
||||
|
||||
s = sub.add_parser("probe", help="USB 인터페이스 점유 및 송수신 프로브")
|
||||
s.add_argument("--serial", default=None)
|
||||
s.set_defaults(func=cmd_probe)
|
||||
|
||||
@@ -16,7 +16,7 @@ from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from .protocol import Session
|
||||
from .usb_link import Juc500Link, list_devices
|
||||
from .usb_link import Juc500Link, list_devices, doctor
|
||||
|
||||
DEFAULT_GUI_PORT = 8765
|
||||
|
||||
@@ -398,8 +398,23 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
def _api_get(self, path: str, qs: dict[str, list[str]]) -> None:
|
||||
if path == "/api/status":
|
||||
devices = list_devices()
|
||||
try:
|
||||
devices = list_devices()
|
||||
list_err = None
|
||||
except Exception as exc: # noqa: BLE001
|
||||
devices = []
|
||||
list_err = str(exc)
|
||||
cable = devices[0] if devices else None
|
||||
if devices:
|
||||
auto = STATE.auto_status if _session_alive() else "케이블 감지됨 — 연결을 누르세요"
|
||||
elif list_err:
|
||||
auto = f"USB 오류: {list_err}"
|
||||
else:
|
||||
auto = (
|
||||
"JUC500 미감지 — python -m juc500_xfer doctor 로 진단하세요"
|
||||
if not STATE.connecting
|
||||
else STATE.auto_status
|
||||
)
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
@@ -417,13 +432,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"root": STATE.peer_name or "JUC500",
|
||||
"remote_start": "",
|
||||
"remote_receive": "",
|
||||
"auto_status": STATE.auto_status,
|
||||
"auto_status": auto if not _session_alive() else STATE.auto_status,
|
||||
"progress": STATE.last_progress,
|
||||
"device": {
|
||||
"serial": cable.serial if cable else None,
|
||||
"product": cable.product if cable else None,
|
||||
"count": len(devices),
|
||||
"vid": f"{cable.vid:04x}" if cable else None,
|
||||
"pid": f"{cable.pid:04x}" if cable else None,
|
||||
},
|
||||
"usb_error": list_err,
|
||||
"inputshare": {
|
||||
"supported": False,
|
||||
"enabled": False,
|
||||
@@ -434,6 +452,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
)
|
||||
return
|
||||
|
||||
if path == "/api/doctor":
|
||||
self._json(200, doctor().to_dict())
|
||||
return
|
||||
|
||||
if path == "/api/settings/ports":
|
||||
# Reuse settings modal: expose local_root / serial as "ports"-like rows
|
||||
self._json(
|
||||
|
||||
+294
-111
@@ -1,32 +1,26 @@
|
||||
"""Low-level USB access to the j5create JUC500 Smart Data Link cable.
|
||||
|
||||
The cable enumerates as USB VID:PID 0711:7500 with six interfaces:
|
||||
|
||||
0 CDC Communication (control notifications)
|
||||
1 CDC Data (bulk 0x02 / 0x83) – proprietary control path
|
||||
2 Mass Storage (WORMHOLE virtual CD, claimed by OS)
|
||||
3 HID mouse (KM switch, claimed by OS)
|
||||
4 HID keyboard (KM switch, claimed by OS)
|
||||
5 Vendor Specific (bulk 0x08/0x89 + 0x0A/0x8B) – data bridge
|
||||
|
||||
On Windows, WinUSB does not implement libusb_get_configuration /
|
||||
kernel-driver detach. Only Interface 5 (Vendor) is required for our
|
||||
protocol; CDC interfaces are optional.
|
||||
Device: VID 0x0711 / PID 0x7500 (product "Smart Data Link"), composite:
|
||||
IF5 Vendor bulk is required for our protocol on Windows (WinUSB).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
import usb.core
|
||||
import usb.util
|
||||
|
||||
VID = 0x0711
|
||||
PID = 0x7500
|
||||
# Some firmware / sibling OEM IDs under the same vendor
|
||||
ALT_PIDS = {0x7500, 0x5500, 0x7000, 0x0700}
|
||||
|
||||
IF_CDC_COMM = 0
|
||||
IF_CDC_DATA = 1
|
||||
@@ -37,66 +31,147 @@ EP_DATA_IN = 0x89
|
||||
EP_CTRL_OUT = 0x0A
|
||||
EP_CTRL_IN = 0x8B
|
||||
|
||||
EP_CDC_OUT = 0x02
|
||||
EP_CDC_IN = 0x83
|
||||
|
||||
MAX_PACKET = 512
|
||||
|
||||
_IS_WIN = sys.platform == "win32"
|
||||
|
||||
_PRODUCT_HINTS = ("smart data", "data link", "wormhole", "juc500", "oti")
|
||||
|
||||
def _libusb_backend():
|
||||
"""Locate libusb-1.0 on macOS / Windows / Linux."""
|
||||
import usb.backend.libusb1
|
||||
|
||||
candidates = []
|
||||
def _package_roots() -> list[Path]:
|
||||
here = Path(__file__).resolve().parent
|
||||
roots = [here.parent, Path.cwd()]
|
||||
if getattr(sys, "frozen", False):
|
||||
roots.insert(0, Path(sys.executable).resolve().parent)
|
||||
mei = getattr(sys, "_MEIPASS", None)
|
||||
if mei:
|
||||
candidates += [
|
||||
os.path.join(mei, "libusb-1.0.dll"),
|
||||
os.path.join(mei, "libusb-1.0.dylib"),
|
||||
os.path.join(mei, "libusb-1.0.so"),
|
||||
]
|
||||
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
candidates += [
|
||||
os.path.join(exe_dir, "libusb-1.0.dll"),
|
||||
os.path.join(exe_dir, "_internal", "libusb-1.0.dll"),
|
||||
]
|
||||
if sys.platform == "darwin":
|
||||
roots.insert(0, Path(mei))
|
||||
# walk up a few levels looking for vendor/
|
||||
for base in list(roots):
|
||||
p = base
|
||||
for _ in range(4):
|
||||
roots.append(p)
|
||||
if (p / "vendor").is_dir():
|
||||
break
|
||||
p = p.parent
|
||||
# unique preserve order
|
||||
seen: set[str] = set()
|
||||
out: list[Path] = []
|
||||
for r in roots:
|
||||
key = str(r)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def _libusb_candidates() -> list[str]:
|
||||
names = []
|
||||
if _IS_WIN:
|
||||
names = ["libusb-1.0.dll"]
|
||||
elif sys.platform == "darwin":
|
||||
names = ["libusb-1.0.dylib"]
|
||||
else:
|
||||
names = ["libusb-1.0.so", "libusb-1.0.so.0"]
|
||||
|
||||
candidates: list[str] = []
|
||||
if getattr(sys, "frozen", False):
|
||||
mei = getattr(sys, "_MEIPASS", None)
|
||||
exe_dir = Path(sys.executable).resolve().parent
|
||||
if mei:
|
||||
for n in names:
|
||||
candidates.append(str(Path(mei) / n))
|
||||
for n in names:
|
||||
candidates.append(str(exe_dir / n))
|
||||
candidates.append(str(exe_dir / "_internal" / n))
|
||||
|
||||
for root in _package_roots():
|
||||
for n in names:
|
||||
candidates.append(str(root / "vendor" / n))
|
||||
candidates.append(str(root / n))
|
||||
|
||||
if _IS_WIN:
|
||||
# venv Scripts / beside python
|
||||
py_dir = Path(sys.executable).resolve().parent
|
||||
for n in names:
|
||||
candidates.append(str(py_dir / n))
|
||||
candidates.append(str(py_dir / "DLLs" / n))
|
||||
pf = os.environ.get("PROGRAMFILES", r"C:\Program Files")
|
||||
candidates.append(os.path.join(pf, "libusb", "libusb-1.0.dll"))
|
||||
candidates.append("libusb-1.0.dll") # PATH search
|
||||
elif sys.platform == "darwin":
|
||||
candidates += [
|
||||
os.path.expanduser("~/homebrew/lib/libusb-1.0.dylib"),
|
||||
"/opt/homebrew/lib/libusb-1.0.dylib",
|
||||
"/usr/local/lib/libusb-1.0.dylib",
|
||||
]
|
||||
elif _IS_WIN:
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
root = os.path.dirname(here)
|
||||
candidates += [
|
||||
os.path.join(root, "vendor", "libusb-1.0.dll"),
|
||||
os.path.join(os.environ.get("PROGRAMFILES", ""), "libusb", "libusb-1.0.dll"),
|
||||
"libusb-1.0.dll",
|
||||
]
|
||||
else:
|
||||
candidates += ["libusb-1.0.so", "libusb-1.0.so.0"]
|
||||
|
||||
for path in candidates:
|
||||
if not path:
|
||||
continue
|
||||
# dedupe
|
||||
seen: set[str] = set()
|
||||
out: list[str] = []
|
||||
for c in candidates:
|
||||
if c and c not in seen:
|
||||
seen.add(c)
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
|
||||
_BACKEND = None
|
||||
_BACKEND_PATH: Optional[str] = None
|
||||
|
||||
|
||||
def _libusb_backend():
|
||||
"""Locate / cache libusb-1.0 backend."""
|
||||
global _BACKEND, _BACKEND_PATH
|
||||
if _BACKEND is not None:
|
||||
return _BACKEND
|
||||
|
||||
import usb.backend.libusb1
|
||||
|
||||
last_err: Optional[BaseException] = None
|
||||
for path in _libusb_candidates():
|
||||
absolute = os.path.sep in path or (len(path) > 1 and path[1] == ":")
|
||||
if absolute and not os.path.exists(path):
|
||||
continue
|
||||
be = usb.backend.libusb1.get_backend(find_library=lambda _n, p=path: p)
|
||||
if be is not None:
|
||||
return be
|
||||
try:
|
||||
be = usb.backend.libusb1.get_backend(find_library=lambda _n, p=path: p)
|
||||
if be is not None:
|
||||
# smoke: enumerate (may be empty)
|
||||
list(usb.core.find(find_all=True, backend=be))
|
||||
_BACKEND = be
|
||||
_BACKEND_PATH = path
|
||||
return be
|
||||
except Exception as exc: # noqa: BLE001
|
||||
last_err = exc
|
||||
continue
|
||||
|
||||
be = usb.backend.libusb1.get_backend()
|
||||
if be is None:
|
||||
raise RuntimeError(
|
||||
"libusb-1.0 를 찾을 수 없습니다. "
|
||||
"macOS: brew install libusb / Windows: vendor/libusb-1.0.dll 배치"
|
||||
)
|
||||
return be
|
||||
try:
|
||||
be = usb.backend.libusb1.get_backend()
|
||||
if be is not None:
|
||||
list(usb.core.find(find_all=True, backend=be))
|
||||
_BACKEND = be
|
||||
_BACKEND_PATH = "(system)"
|
||||
return be
|
||||
except Exception as exc: # noqa: BLE001
|
||||
last_err = exc
|
||||
|
||||
hint = (
|
||||
"Windows: 프로젝트의 vendor\\libusb-1.0.dll 을 "
|
||||
".venv\\Scripts\\ 또는 python.exe 옆로 복사하세요."
|
||||
if _IS_WIN
|
||||
else "macOS: brew install libusb"
|
||||
)
|
||||
detail = f" ({last_err})" if last_err else ""
|
||||
raise RuntimeError(f"libusb-1.0 를 찾을 수 없습니다{detail}. {hint}")
|
||||
|
||||
|
||||
def libusb_path() -> Optional[str]:
|
||||
try:
|
||||
_libusb_backend()
|
||||
except Exception:
|
||||
return None
|
||||
return _BACKEND_PATH
|
||||
|
||||
|
||||
def _usb_err_text(exc: BaseException) -> str:
|
||||
@@ -109,11 +184,7 @@ def _usb_err_text(exc: BaseException) -> str:
|
||||
|
||||
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
|
||||
)
|
||||
return "not supported" in text or "unimplemented" in text or "enosys" in text
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -123,36 +194,171 @@ class DeviceInfo:
|
||||
serial: str
|
||||
product: str
|
||||
manufacturer: str
|
||||
vid: int = VID
|
||||
pid: int = PID
|
||||
|
||||
|
||||
def _safe_str(dev: usb.core.Device, index: int, default: str = "") -> str:
|
||||
if not index:
|
||||
return default
|
||||
try:
|
||||
return (usb.util.get_string(dev, index) or default).strip()
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _looks_like_juc500(dev: usb.core.Device, product: str) -> bool:
|
||||
if dev.idVendor != VID:
|
||||
return False
|
||||
if int(dev.idProduct) in ALT_PIDS or int(dev.idProduct) == PID:
|
||||
return True
|
||||
p = (product or "").lower()
|
||||
return any(h in p for h in _PRODUCT_HINTS)
|
||||
|
||||
|
||||
def iter_raw_devices():
|
||||
backend = _libusb_backend()
|
||||
return usb.core.find(find_all=True, backend=backend)
|
||||
|
||||
|
||||
def find_juc500_devices() -> list[usb.core.Device]:
|
||||
found: list[usb.core.Device] = []
|
||||
for dev in iter_raw_devices():
|
||||
product = _safe_str(dev, getattr(dev, "iProduct", 0), "")
|
||||
if _looks_like_juc500(dev, product):
|
||||
found.append(dev)
|
||||
# Prefer exact 0711:7500 first
|
||||
found.sort(key=lambda d: (0 if (d.idVendor == VID and d.idProduct == PID) else 1))
|
||||
return found
|
||||
|
||||
|
||||
def list_devices() -> list[DeviceInfo]:
|
||||
backend = _libusb_backend()
|
||||
out: list[DeviceInfo] = []
|
||||
for dev in usb.core.find(find_all=True, idVendor=VID, idProduct=PID, backend=backend):
|
||||
try:
|
||||
serial = usb.util.get_string(dev, dev.iSerialNumber) or ""
|
||||
except Exception:
|
||||
serial = ""
|
||||
try:
|
||||
product = usb.util.get_string(dev, dev.iProduct) or "Smart Data Link"
|
||||
except Exception:
|
||||
product = "Smart Data Link"
|
||||
try:
|
||||
manufacturer = usb.util.get_string(dev, dev.iManufacturer) or ""
|
||||
except Exception:
|
||||
manufacturer = ""
|
||||
for dev in find_juc500_devices():
|
||||
out.append(
|
||||
DeviceInfo(
|
||||
bus=dev.bus,
|
||||
address=dev.address,
|
||||
serial=serial.strip(),
|
||||
product=product.strip(),
|
||||
manufacturer=manufacturer.strip(),
|
||||
bus=int(dev.bus),
|
||||
address=int(dev.address),
|
||||
serial=_safe_str(dev, getattr(dev, "iSerialNumber", 0)),
|
||||
product=_safe_str(dev, getattr(dev, "iProduct", 0), "Smart Data Link")
|
||||
or "Smart Data Link",
|
||||
manufacturer=_safe_str(dev, getattr(dev, "iManufacturer", 0)),
|
||||
vid=int(dev.idVendor),
|
||||
pid=int(dev.idProduct),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _windows_pnp_vid0711() -> list[dict[str, str]]:
|
||||
"""Fallback: ask Windows PnP if VID_0711 is present (even without WinUSB)."""
|
||||
if not _IS_WIN:
|
||||
return []
|
||||
ps = (
|
||||
"Get-PnpDevice -PresentOnly | "
|
||||
"Where-Object { $_.InstanceId -match 'VID_0711' } | "
|
||||
"Select-Object Status, Class, FriendlyName, InstanceId | "
|
||||
"ConvertTo-Json -Compress"
|
||||
)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", ps],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
raw = (proc.stdout or "").strip()
|
||||
if not raw:
|
||||
return []
|
||||
import json
|
||||
|
||||
data = json.loads(raw)
|
||||
if isinstance(data, dict):
|
||||
data = [data]
|
||||
return [
|
||||
{
|
||||
"status": str(d.get("Status") or ""),
|
||||
"class": str(d.get("Class") or ""),
|
||||
"name": str(d.get("FriendlyName") or ""),
|
||||
"id": str(d.get("InstanceId") or ""),
|
||||
}
|
||||
for d in data
|
||||
]
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return [{"error": str(exc)}]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DoctorReport:
|
||||
ok: bool
|
||||
libusb_path: Optional[str] = None
|
||||
libusb_error: Optional[str] = None
|
||||
usb_device_count: int = 0
|
||||
juc500: list[dict[str, Any]] = field(default_factory=list)
|
||||
sample_devices: list[str] = field(default_factory=list)
|
||||
pnp_vid0711: list[dict[str, str]] = field(default_factory=list)
|
||||
hints: list[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
def doctor() -> DoctorReport:
|
||||
report = DoctorReport(ok=False)
|
||||
try:
|
||||
be = _libusb_backend()
|
||||
report.libusb_path = _BACKEND_PATH
|
||||
all_devs = list(usb.core.find(find_all=True, backend=be))
|
||||
report.usb_device_count = len(all_devs)
|
||||
for d in all_devs[:25]:
|
||||
prod = _safe_str(d, getattr(d, "iProduct", 0), "")
|
||||
report.sample_devices.append(
|
||||
f"{d.idVendor:04x}:{d.idProduct:04x} bus={d.bus} addr={d.address} {prod!r}"
|
||||
)
|
||||
for info in list_devices():
|
||||
report.juc500.append(asdict(info))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
report.libusb_error = str(exc)
|
||||
report.hints.append(str(exc))
|
||||
|
||||
report.pnp_vid0711 = _windows_pnp_vid0711()
|
||||
|
||||
if report.juc500:
|
||||
report.ok = True
|
||||
report.hints.append("JUC500 장치가 libusb로 보입니다. 「연결」을 진행하세요.")
|
||||
else:
|
||||
if report.libusb_error:
|
||||
report.hints.append(
|
||||
"libusb DLL 문제일 수 있습니다. vendor\\libusb-1.0.dll 을 "
|
||||
".venv\\Scripts\\ 에 복사한 뒤 다시 실행하세요."
|
||||
)
|
||||
elif report.usb_device_count == 0:
|
||||
report.hints.append(
|
||||
"libusb가 USB 장치를 하나도 보지 못합니다. "
|
||||
"다른 USB 장치도 안 보이면 DLL/권한 문제입니다."
|
||||
)
|
||||
elif report.pnp_vid0711 and not any("error" in x for x in report.pnp_vid0711):
|
||||
report.hints.append(
|
||||
"Windows는 VID_0711 케이블을 인식하지만 libusb가 JUC500을 못 엽니다. "
|
||||
"Zadig로 Smart Data Link → Interface 5(MI_05)에 WinUSB를 설치하세요. "
|
||||
"공식 Wormhole 드라이버가 있으면 제거하세요."
|
||||
)
|
||||
else:
|
||||
report.hints.append(
|
||||
"이 PC에서 JUC500(0711:7500)이 보이지 않습니다. "
|
||||
"케이블을 이 PC USB 포트에 직접 연결했는지, "
|
||||
"반대쪽은 상대 PC에 꽂혀 있는지 확인하세요. "
|
||||
"(양쪽 끝이 각각 다른 PC에 연결되어야 합니다.)"
|
||||
)
|
||||
if _IS_WIN:
|
||||
report.hints.append(
|
||||
"장치 관리자에서 'Smart Data Link' / '알 수 없는 장치' / "
|
||||
"WORMHOLE CD 롬이 있는지 확인하세요."
|
||||
)
|
||||
return report
|
||||
|
||||
|
||||
class Juc500Link:
|
||||
"""Claim Vendor (+ optional CDC) interfaces and provide framed bulk I/O."""
|
||||
|
||||
@@ -164,28 +370,18 @@ class Juc500Link:
|
||||
self.open()
|
||||
|
||||
def open(self) -> None:
|
||||
backend = _libusb_backend()
|
||||
matches = list(
|
||||
usb.core.find(find_all=True, idVendor=VID, idProduct=PID, backend=backend)
|
||||
)
|
||||
matches = find_juc500_devices()
|
||||
if not matches:
|
||||
rep = doctor()
|
||||
hints = "\n".join(f"- {h}" for h in rep.hints)
|
||||
raise RuntimeError(
|
||||
"JUC500(0711:7500 Smart Data Link) 를 찾지 못했습니다. "
|
||||
"케이블이 연결되어 있는지 확인하세요."
|
||||
+ (
|
||||
" Windows: Zadig로 Interface 5에 WinUSB를 설치했는지 확인하세요."
|
||||
if _IS_WIN
|
||||
else ""
|
||||
)
|
||||
"JUC500(Smart Data Link / 0711:7500) 장치를 찾지 못했습니다.\n" + hints
|
||||
)
|
||||
|
||||
chosen = None
|
||||
for dev in matches:
|
||||
if self._serial:
|
||||
try:
|
||||
s = (usb.util.get_string(dev, dev.iSerialNumber) or "").strip()
|
||||
except Exception:
|
||||
s = ""
|
||||
s = _safe_str(dev, getattr(dev, "iSerialNumber", 0))
|
||||
if s != self._serial.strip():
|
||||
continue
|
||||
chosen = dev
|
||||
@@ -200,29 +396,20 @@ class Juc500Link:
|
||||
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
|
||||
ctx._active_cfg_index = 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -251,9 +438,9 @@ class Juc500Link:
|
||||
hint = ""
|
||||
if _IS_WIN:
|
||||
hint = (
|
||||
f"\nZadig에서 Smart Data Link → Interface {ifnum} 에 "
|
||||
"WinUSB 드라이버를 설치하세요. "
|
||||
"(데이터 전송은 Interface 5가 필수입니다.)"
|
||||
f"\nZadig에서 Smart Data Link → Interface {ifnum} (MI_0{ifnum}) 에 "
|
||||
"WinUSB 를 설치하세요. 데이터 전송은 Interface 5가 필수입니다.\n"
|
||||
"공식 j5create Wormhole 드라이버가 있으면 제거하고 다시 시도하세요."
|
||||
)
|
||||
raise RuntimeError(f"USB interface {ifnum} 점유 실패: {exc}{hint}") from exc
|
||||
return
|
||||
@@ -262,10 +449,7 @@ class Juc500Link:
|
||||
@property
|
||||
def serial(self) -> str:
|
||||
assert self._dev is not None
|
||||
try:
|
||||
return (usb.util.get_string(self._dev, self._dev.iSerialNumber) or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
return _safe_str(self._dev, getattr(self._dev, "iSerialNumber", 0))
|
||||
|
||||
def write_data(self, payload: bytes, timeout_ms: Optional[int] = None) -> int:
|
||||
return self._bulk_write(EP_DATA_OUT, payload, timeout_ms)
|
||||
@@ -314,7 +498,6 @@ class Juc500Link:
|
||||
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:
|
||||
|
||||
@@ -16,9 +16,18 @@ if errorlevel 1 (
|
||||
python -m pip install --find-links=vendor\wheels -e .
|
||||
)
|
||||
|
||||
REM libusb DLL 을 venv Scripts 로 복사 (장치 인식에 필수)
|
||||
if exist vendor\libusb-1.0.dll (
|
||||
copy /Y vendor\libusb-1.0.dll .venv\Scripts\libusb-1.0.dll >nul
|
||||
echo libusb-1.0.dll -^> .venv\Scripts\
|
||||
) else (
|
||||
echo [!] vendor\libusb-1.0.dll 없음 — USB 장치를 못 찾을 수 있습니다.
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Setup done. Next:
|
||||
echo .venv\Scripts\activate
|
||||
echo python -m juc500_xfer doctor
|
||||
echo python -m juc500_xfer info
|
||||
echo python -m juc500_xfer gui
|
||||
echo See docs\WINDOWS.md for Zadig / WinUSB steps.
|
||||
|
||||
Reference in New Issue
Block a user