juc500 정상동작

This commit is contained in:
Macbook
2026-07-21 21:04:25 +09:00
parent a423db0556
commit bb9874b931
2917 changed files with 708025 additions and 668 deletions
@@ -0,0 +1,78 @@
@echo off
REM Offline overlay update for closed-network Windows 11.
REM Run from the extracted update folder (or pass DEST as arg 1).
setlocal EnableExtensions
set "SRC=%~dp0"
if "%SRC:~-1%"=="\" set "SRC=%SRC:~0,-1%"
REM If this bat lives in scripts\, SRC is package root when called as scripts\apply...
if exist "%SRC%\juc500_xfer\usb_link.py" goto :have_src
if exist "%SRC%\..\juc500_xfer\usb_link.py" (
set "SRC=%SRC%\.."
goto :have_src
)
echo [!] juc500_xfer not found next to this script.
echo Extract the offline update zip first, then run this bat from that folder.
exit /b 1
:have_src
set "DEST=%~1"
if "%DEST%"=="" set "DEST=C:\dev\juc500"
echo === JUC500 offline update ===
echo SRC = %SRC%
echo DEST= %DEST%
if not exist "%DEST%" (
echo [!] DEST missing: %DEST%
echo Create it or pass path: apply_offline_update.bat D:\juc500
exit /b 1
)
echo.
echo Copying runtime sources...
robocopy "%SRC%\juc500_xfer" "%DEST%\juc500_xfer" /E /XD __pycache__ .git /NFL /NDL /NJH /NJS /nc /ns /np
if errorlevel 8 exit /b 1
if exist "%SRC%\scripts" (
robocopy "%SRC%\scripts" "%DEST%\scripts" /E /XD __pycache__ /NFL /NDL /NJH /NJS /nc /ns /np
)
for %%F in (start.bat start.sh start_win.sh start_both.sh juc500_app.py requirements.txt README.md) do (
if exist "%SRC%\%%F" copy /Y "%SRC%\%%F" "%DEST%\%%F" >nul
)
if exist "%SRC%\vendor\libusb-1.0.dll" (
if not exist "%DEST%\vendor" mkdir "%DEST%\vendor"
copy /Y "%SRC%\vendor\libusb-1.0.dll" "%DEST%\vendor\libusb-1.0.dll" >nul
if exist "%DEST%\.venv\Scripts" copy /Y "%SRC%\vendor\libusb-1.0.dll" "%DEST%\.venv\Scripts\libusb-1.0.dll" >nul
)
if exist "%SRC%\vendor\wheels" (
if not exist "%DEST%\vendor\wheels" mkdir "%DEST%\vendor\wheels"
robocopy "%SRC%\vendor\wheels" "%DEST%\vendor\wheels" *.whl /NFL /NDL /NJH /NJS /nc /ns /np
)
if exist "%SRC%\vendor\tools\zadig.exe" (
if not exist "%DEST%\vendor\tools" mkdir "%DEST%\vendor\tools"
copy /Y "%SRC%\vendor\tools\zadig.exe" "%DEST%\vendor\tools\zadig.exe" >nul
)
if exist "%SRC%\docs" (
if not exist "%DEST%\docs" mkdir "%DEST%\docs"
robocopy "%SRC%\docs" "%DEST%\docs" /E /NFL /NDL /NJH /NJS /nc /ns /np
)
echo.
echo Verifying build tag...
if exist "%DEST%\.venv\Scripts\python.exe" (
"%DEST%\.venv\Scripts\python.exe" -c "from juc500_xfer.usb_link import XFER_BUILD; print('XFER_BUILD=', XFER_BUILD)"
) else (
echo [.venv missing — run start.bat once to bootstrap from vendor]
)
echo.
echo Done. Close JUC500 GUI, then:
echo cd /d %DEST%
echo start.bat
echo GUI should show build tag smartkmlink (or newer).
echo SmartKMLink: Zadig MI_05 WinUSB — vendor\tools\zadig.exe
endlocal
exit /b 0
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# Windows 오프라인 초기 설정 — Git Bash / MSYS (cmd.exe 경로 변환 없이 직접 실행)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
WHEELS="$ROOT/vendor/wheels"
GETPIP="$ROOT/vendor/get-pip.py"
LIBUSB="$ROOT/vendor/libusb-1.0.dll"
EMBED_PY="$ROOT/vendor/python-windows/python.exe"
VENV_PY="$ROOT/.venv/Scripts/python.exe"
MAC_VENV="$ROOT/.venv/bin/python"
if [[ ! -f "$WHEELS/pyusb-1.2.1-py3-none-any.whl" || ! -f "$LIBUSB" ]]; then
echo "[!] vendor 번들이 불완전합니다. 압축 시 vendor/ 폴더를 반드시 포함하세요." >&2
echo " 필요: vendor/wheels, vendor/libusb-1.0.dll, vendor/get-pip.py" >&2
exit 1
fi
# Mac .venv 가 zip 에 섞이면 Windows 에서 깨짐
if [[ -f "$MAC_VENV" && ! -f "$VENV_PY" ]]; then
echo "[*] Mac .venv 제거 중…"
rm -rf "$ROOT/.venv"
fi
venv_ok() {
[[ -f "$VENV_PY" ]] || return 1
PYTHONPATH="$ROOT${PYTHONPATH:+:$PYTHONPATH}" "$VENV_PY" -c "import usb" 2>/dev/null || return 1
PYTHONPATH="$ROOT${PYTHONPATH:+:$PYTHONPATH}" "$VENV_PY" -c "
from juc500_xfer.usb_link import libusb_path
import sys
sys.exit(0 if libusb_path() else 1)
" 2>/dev/null
}
if venv_ok; then
exit 0
fi
echo "============================================"
echo " JUC500 Windows 오프라인 초기 설정"
echo "============================================"
pick_boot_py() {
if command -v py >/dev/null 2>&1; then
if py -3 -c "import sys" 2>/dev/null; then
echo "py -3"
return 0
fi
fi
if command -v python >/dev/null 2>&1; then
if python -c "import sys" 2>/dev/null; then
echo "python"
return 0
fi
fi
if [[ -f "$EMBED_PY" ]]; then
echo "$EMBED_PY"
echo "Using vendored Python: vendor/python-windows/python.exe" >&2
return 0
fi
return 1
}
BOOT_PY="$(pick_boot_py)" || {
echo "[!] Python 없음. vendor/python-windows 가 있는지 확인하세요." >&2
exit 1
}
echo "Bootstrap: $BOOT_PY"
run_boot() {
# shellcheck disable=SC2086
$BOOT_PY "$@"
}
if ! run_boot -c "import virtualenv" 2>/dev/null; then
echo "Installing virtualenv from vendor/wheels …"
if ! run_boot -m pip install --no-index --find-links="$WHEELS" virtualenv 2>/dev/null; then
if [[ -f "$GETPIP" ]]; then
run_boot "$GETPIP" --no-index --find-links="$WHEELS" pip setuptools wheel
run_boot -m pip install --no-index --find-links="$WHEELS" virtualenv
else
echo "[!] virtualenv 설치 실패" >&2
exit 1
fi
fi
fi
if [[ ! -f "$VENV_PY" ]]; then
echo "Creating .venv (offline)…"
run_boot -m virtualenv --no-periodic-update --no-setuptools --no-pip "$ROOT/.venv"
run_boot "$GETPIP" --no-index --find-links="$WHEELS" pip setuptools wheel packaging
elif ! "$VENV_PY" -m pip --version >/dev/null 2>&1; then
echo "[*] .venv 손상 — 재생성…"
rm -rf "$ROOT/.venv"
run_boot -m virtualenv --no-periodic-update --no-setuptools --no-pip "$ROOT/.venv"
run_boot "$GETPIP" --no-index --find-links="$WHEELS" pip setuptools wheel packaging
fi
echo "Installing dependencies from vendor/wheels (no PyPI)…"
"$VENV_PY" -m pip install --no-index --find-links="$WHEELS" -U pip setuptools wheel packaging
"$VENV_PY" -m pip install --no-index --find-links="$WHEELS" -r "$ROOT/requirements.txt"
"$VENV_PY" -m pip install --no-index --find-links="$WHEELS" --no-build-isolation -e "$ROOT"
cp -f "$LIBUSB" "$ROOT/.venv/Scripts/libusb-1.0.dll"
echo "libusb-1.0.dll → .venv/Scripts/"
if ! venv_ok; then
echo "[!] 초기 설정 후에도 .venv 검증 실패" >&2
exit 1
fi
echo "[*] Windows 초기 설정 완료"
exit 0
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# 개발 적용: sync_peer (동기화) + 수동 재시작 안내
# 1) 소스 수정
# 2) 이 스크립트 또는 ./scripts/sync_peer.sh
# 3) ./scripts/stop_tauri_watchers.sh (Tauri 케이블 감시 종료)
# 4) ./start.sh (이 PC)
# 5) 상대 PC: cd C:/dev/juc500 && ./start.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if [[ -x "$ROOT/scripts/stop_tauri_watchers.sh" ]]; then
"$ROOT/scripts/stop_tauri_watchers.sh" || true
fi
export JUC500_SYNC_RESTART=0
exec "$ROOT/scripts/sync_peer.sh" "$@"
@@ -0,0 +1,22 @@
@echo off
setlocal EnableExtensions
cd /d "%~dp0.."
set "ROOT=%CD%"
set "SRC=%ROOT%\vendor\libusb-1.0.dll"
if not exist "%SRC%" (
echo [!] Missing vendor\libusb-1.0.dll
echo Copy the full repo including vendor\ from the Mac, or run scripts\setup_windows.bat
exit /b 1
)
if not exist "%ROOT%\.venv\Scripts\python.exe" (
echo [!] .venv missing. Run: scripts\setup_windows.bat
exit /b 1
)
copy /Y "%SRC%" "%ROOT%\.venv\Scripts\libusb-1.0.dll" >nul
echo libusb-1.0.dll -^> .venv\Scripts\
"%ROOT%\.venv\Scripts\python.exe" -c "from juc500_xfer.usb_link import libusb_path; p=libusb_path(); print('libusb OK:', p or 'FAILED'); import sys; sys.exit(0 if p else 1)"
exit /b %ERRORLEVEL%
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# macOS 로그인 시 JUC500 Tauri 감시 앱 자동 실행 (케이블 연결 → GUI)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LABEL="com.juc500.desktop"
PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist"
LAUNCHER="$ROOT/scripts/launch_tauri.sh"
BIN="$ROOT/juc500-app/src-tauri/target/release/juc500-app"
if [[ -x "$BIN" ]]; then
RUN_CMD="$BIN"
elif [[ -x "$LAUNCHER" ]]; then
RUN_CMD="/bin/bash"
RUN_ARGS=("$LAUNCHER")
else
echo "Tauri 앱 없음. 먼저: cd juc500-app && npm install && npm run tauri build" >&2
exit 1
fi
mkdir -p "$HOME/Library/LaunchAgents"
chmod +x "$ROOT/scripts/launch_tauri.sh"
if [[ "$RUN_CMD" == "/bin/bash" ]]; then
cat >"$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>${LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>${LAUNCHER}</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>JUC500_ROOT</key><string>${ROOT}</string>
<key>JUC500_CABLE_AUTOLAUNCH</key><string>1</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>${ROOT}/.juc500/tauri-launch.log</string>
<key>StandardErrorPath</key><string>${ROOT}/.juc500/tauri-launch.log</string>
</dict>
</plist>
EOF
else
cat >"$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>${LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>${BIN}</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>JUC500_ROOT</key><string>${ROOT}</string>
<key>JUC500_CABLE_AUTOLAUNCH</key><string>1</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>${ROOT}/.juc500/tauri-launch.log</string>
<key>StandardErrorPath</key><string>${ROOT}/.juc500/tauri-launch.log</string>
</dict>
</plist>
EOF
fi
launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$PLIST"
launchctl enable "gui/$(id -u)/${LABEL}"
echo "설치 완료: ${PLIST}"
echo "로그인 시 트레이 감시 앱이 실행되고, 케이블 연결 시 GUI가 열립니다."
@@ -0,0 +1,25 @@
@echo off
REM Windows 로그인 시 JUC500 Tauri 감시 앱 자동 실행
setlocal EnableExtensions
cd /d "%~dp0.."
set "ROOT=%CD%"
set "BIN=%ROOT%\juc500-app\src-tauri\target\release\juc500-app.exe"
if not exist "%BIN%" (
echo [!] Build first: cd juc500-app ^&^& npm install ^&^& npm run tauri build
exit /b 1
)
set "STARTUP=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup"
set "LINK=%STARTUP%\JUC500.lnk"
set "LAUNCHER=%ROOT%\scripts\launch_tauri.bat"
powershell -NoProfile -Command ^
"$s=(New-Object -ComObject WScript.Shell).CreateShortcut('%LINK%');" ^
"$s.TargetPath='%LAUNCHER%';" ^
"$s.WorkingDirectory='%ROOT%';" ^
"$s.Description='JUC500 cable auto-launch';" ^
"$s.Save()"
echo Installed startup shortcut: %LINK%
echo Tray watcher runs at logon; GUI opens when cable is plugged.
@@ -0,0 +1,17 @@
@echo off
REM JUC500 Tauri shell — auto-launch on cable connect
cd /d "%~dp0.."
set "JUC500_ROOT=%CD%"
set "JUC500_USE_TAURI=1"
set "BIN=%CD%\juc500-app\src-tauri\target\release\juc500-app.exe"
if exist "%BIN%" goto run
set "BIN=%CD%\juc500-app\src-tauri\target\debug\juc500-app.exe"
if exist "%BIN%" goto run
echo [!] Tauri app not built. Run: cd juc500-app ^&^& npm install ^&^& npm run tauri build
exit /b 1
:run
start "" "%BIN%"
exit /b 0
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# JUC500 Tauri 셸 — USB 케이블 연결 시 Python 백엔드 + GUI 자동 기동
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
export JUC500_ROOT="$ROOT"
# 케이블 자동 실행은 install_autolaunch_* 설치 시 JUC500_CABLE_AUTOLAUNCH=1 로 켬
export JUC500_CABLE_AUTOLAUNCH="${JUC500_CABLE_AUTOLAUNCH:-0}"
export JUC500_USE_TAURI=1
APP_DIR="$ROOT/juc500-app"
if [[ ! -d "$APP_DIR/src-tauri" ]]; then
echo "juc500-app 없음. Tauri 프로젝트를 먼저 빌드하세요." >&2
exit 1
fi
# Release 바이너리 우선, 없으면 dev
BIN=""
for candidate in \
"$APP_DIR/src-tauri/target/release/juc500-app" \
"$APP_DIR/src-tauri/target/debug/juc500-app"; do
if [[ -x "$candidate" ]]; then
BIN="$candidate"
break
fi
done
if [[ -n "$BIN" ]]; then
exec "$BIN"
fi
if ! command -v npm >/dev/null 2>&1; then
echo "npm 없음 — Node.js 설치 후: cd juc500-app && npm install && npm run tauri build" >&2
exit 1
fi
cd "$APP_DIR"
if [[ ! -d node_modules ]]; then
npm install
fi
exec npm run tauri -- dev
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# Build offline Windows update package (USB stick / closed network).
# Includes runtime sources + vendor wheels/libusb/zadig + apply bat.
# Does NOT require internet on the Windows peer.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
STAMP="$(date +%Y%m%d-%H%M%S)"
BUILD="$("$ROOT/.venv/bin/python" -c "from juc500_xfer.usb_link import XFER_BUILD; print(XFER_BUILD)" 2>/dev/null || echo unknown)"
OUT_DIR="${1:-$ROOT/dist-offline/juc500-offline-update-${STAMP}}"
ZIP_OUT="${2:-$ROOT/dist-offline/juc500-offline-update-${STAMP}.zip}"
mkdir -p "$OUT_DIR" "$ROOT/dist-offline"
echo "=== Offline Windows update package ==="
echo "XFER_BUILD=$BUILD"
echo "OUT_DIR=$OUT_DIR"
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR/juc500_xfer/static" "$OUT_DIR/scripts" "$OUT_DIR/vendor/wheels" "$OUT_DIR/vendor/tools" "$OUT_DIR/docs"
# Runtime Python package (all modules — SmartKMLink lives in usb_link.py)
rsync -a --delete \
--exclude '__pycache__' --exclude '*.py[cod]' --exclude '.DS_Store' \
"$ROOT/juc500_xfer/" "$OUT_DIR/juc500_xfer/"
# Launchers / entry
for f in juc500_app.py start.bat start.sh start_win.sh start_both.sh requirements.txt README.md; do
[[ -f "$ROOT/$f" ]] && cp -f "$ROOT/$f" "$OUT_DIR/$f"
done
# Scripts (bootstrap + apply)
rsync -a --exclude '__pycache__' "$ROOT/scripts/" "$OUT_DIR/scripts/"
cp -f "$ROOT/scripts/apply_offline_update.bat" "$OUT_DIR/apply_offline_update.bat"
# Vendor offline essentials
cp -f "$ROOT/vendor/libusb-1.0.dll" "$OUT_DIR/vendor/libusb-1.0.dll"
cp -f "$ROOT/vendor/wheels/"*.whl "$OUT_DIR/vendor/wheels/" 2>/dev/null || true
[[ -f "$ROOT/vendor/wheels/README.md" ]] && cp -f "$ROOT/vendor/wheels/README.md" "$OUT_DIR/vendor/wheels/"
[[ -f "$ROOT/vendor/README.md" ]] && cp -f "$ROOT/vendor/README.md" "$OUT_DIR/vendor/"
[[ -f "$ROOT/vendor/get-pip.py" ]] && cp -f "$ROOT/vendor/get-pip.py" "$OUT_DIR/vendor/"
if [[ -f "$ROOT/vendor/tools/zadig.exe" ]]; then
cp -f "$ROOT/vendor/tools/zadig.exe" "$OUT_DIR/vendor/tools/zadig.exe"
fi
# Docs
for f in OFFLINE-WINDOWS.md WINDOWS.md WINDOWS-ACCESS-DENIED.md; do
[[ -f "$ROOT/docs/$f" ]] && cp -f "$ROOT/docs/$f" "$OUT_DIR/docs/$f"
done
cat >"$OUT_DIR/BUILD.txt" <<EOF
JUC500 offline update
XFER_BUILD=$BUILD
created=$STAMP
SmartKMLink VID:PID=removed — JUC500 only (0711:7500 IF5)
Windows closed-network apply:
1) Copy this folder (or unzip) to the peer PC via USB stick
2) Close JUC500 GUI on Windows
3) Double-click apply_offline_update.bat
(default DEST=C:\\dev\\juc500 — or: apply_offline_update.bat D:\\path\\juc500)
4) cd C:\\dev\\juc500 && start.bat
5) GUI version line must show: $BUILD
6) JUC500 IF5: vendor\\tools\\zadig.exe → 0711:7500 → MI_05 → WinUSB
Full fresh install (includes embeddable Python):
./scripts/make_windows_bundle.sh
EOF
cat >"$OUT_DIR/APPLY-WINDOWS.txt" <<'EOF'
폐쇄망 Windows 11 — 오프라인 업데이트
====================================
1. 이 폴더 전체를 USB로 Windows PC에 복사
2. JUC500 GUI 종료
3. apply_offline_update.bat 실행 (기본 대상 C:\dev\juc500)
4. C:\dev\juc500 에서 start.bat 실행
5. 상단 빌드 태그가 BUILD.txt 의 XFER_BUILD 와 같은지 확인
6. JUC500(0711:7500): vendor\tools\zadig.exe
→ Options → List All Devices → Smart Data Link → Interface 5 (MI_05) → WinUSB
인터넷·pip 설치 불필요. vendor\wheels · libusb · (선택) zadig 포함.
EOF
rm -f "$ZIP_OUT"
(
cd "$OUT_DIR"
zip -r -q "$ZIP_OUT" .
)
# Also drop a copy on Desktop for easy USB copy
DESK="$HOME/Desktop/juc500-offline-update-${STAMP}.zip"
cp -f "$ZIP_OUT" "$DESK"
bytes="$(wc -c <"$ZIP_OUT" | tr -d ' ')"
echo "완료: $ZIP_OUT ($bytes bytes)"
echo "Desktop: $DESK"
echo "폴더: $OUT_DIR"
echo "XFER_BUILD=$BUILD"
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Mac/Linux → Windows 배포용 zip 생성 (vendor 포함, .venv 제외)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
STAMP="$(date +%Y%m%d)"
OUT="${1:-$ROOT/../juc500-windows-${STAMP}.zip}"
if [[ ! -f "$ROOT/vendor/libusb-1.0.dll" ]]; then
echo "[!] vendor/libusb-1.0.dll 없음" >&2
exit 1
fi
if [[ ! -f "$ROOT/vendor/wheels/pyusb-1.2.1-py3-none-any.whl" ]]; then
echo "[!] vendor/wheels 없음" >&2
exit 1
fi
if [[ ! -f "$ROOT/vendor/tools/zadig.exe" ]]; then
echo "[!] vendor/tools/zadig.exe 없음 — WinUSB 오프라인 도구. ./scripts/make_offline_windows_update.sh 전에 준비하세요." >&2
# non-fatal: bundle still usable if peer already has Zadig
fi
echo "=== Windows 배포 zip 생성 ==="
echo "출력: $OUT"
# zip: 프로젝트 루트 내용을 아카이브 루트에 넣음 (juc500/ 접두사 없음)
rm -f "$OUT"
(
cd "$ROOT"
zip -r -q "$OUT" . \
-x ".venv/*" \
-x ".venv-build/*" \
-x ".venv-build-win/*" \
-x ".venv-offline-test/*" \
-x ".git/*" \
-x "*/__pycache__/*" \
-x "*.py[cod]" \
-x "*.egg-info/*" \
-x "dist/*" \
-x "dist-windows/*" \
-x "build/*" \
-x "juc500-app/node_modules/*" \
-x "juc500-app/src-tauri/target/*" \
-x ".DS_Store" \
-x "*.zip" \
-x "*.tar.gz" \
-x ".juc500/*" \
-x "hub.log"
)
bytes="$(wc -c <"$OUT" | tr -d ' ')"
echo "완료: $OUT ($bytes bytes)"
echo ""
echo "Windows에서:"
echo " 1) zip 압축 해제 (예: C:\\dev\\juc500)"
echo " 2) Git Bash: cd /c/dev/juc500 && ./start_win.sh"
echo " (첫 실행 시 vendor 로 .venv 자동 생성)"
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# juc500 — 이 Mac에서 JUC500 GUI 프로세스를 종료하고 백그라운드로 다시 실행
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
is_windows() {
case "$(uname -s 2>/dev/null || echo unknown)" in
MINGW*|MSYS*|CYGWIN*) return 0 ;;
*) return 1 ;;
esac
}
PY=""
if is_windows && [[ -f "$ROOT/.venv/Scripts/python.exe" ]]; then
PY="$ROOT/.venv/Scripts/python.exe"
elif [[ -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
exit 1
fi
LOG_DIR="$ROOT/.juc500"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/restart.log"
HOST_TAG="$(hostname -s 2>/dev/null || hostname)"
echo "[$HOST_TAG] JUC500 종료 중…"
kill_msg="$(
"$PY" -c "
from juc500_xfer.wormhole_kill import kill_other_juc500_processes
r = kill_other_juc500_processes(force=True)
print(r.get('message') or '')
" 2>/dev/null || true
)"
if [[ -n "${kill_msg}" ]]; then
echo "[$HOST_TAG] $kill_msg"
fi
sleep 0.6
echo "[$HOST_TAG] JUC500 시작 중… (로그: $LOG_FILE)"
if is_windows && [[ -f "$ROOT/start.sh" ]]; then
nohup bash "$ROOT/start.sh" >>"$LOG_FILE" 2>&1 &
elif is_windows; then
WIN_ROOT="$ROOT"
if command -v cygpath >/dev/null 2>&1; then
WIN_ROOT="$(cygpath -w "$ROOT")"
fi
if [[ -f "$ROOT/start.bat" ]]; then
nohup cmd.exe /c "ping 127.0.0.1 -n 3 >nul & cd /d \"$WIN_ROOT\" & call \"$WIN_ROOT\\start.bat\"" >>"$LOG_FILE" 2>&1 &
elif [[ -f "$ROOT/start_win.sh" ]]; then
nohup bash "$ROOT/start_win.sh" >>"$LOG_FILE" 2>&1 &
else
echo "[$HOST_TAG] start.sh / start.bat 없음" >&2
exit 1
fi
else
nohup "$ROOT/start.sh" >>"$LOG_FILE" 2>&1 &
fi
echo "[$HOST_TAG] PID $!"
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if ! command -v brew >/dev/null 2>&1; then
echo "Homebrew 가 필요합니다: https://brew.sh"
exit 1
fi
brew list libusb >/dev/null 2>&1 || brew install libusb
python3 -m venv .venv
# shellcheck disable=SC1091
source .venv/bin/activate
pip install -U pip
# vendor wheel 우선 (오프라인 가능)
pip install --no-index --find-links=vendor/wheels -r requirements.txt \
|| pip install --find-links=vendor/wheels -r requirements.txt
pip install --no-index --find-links=vendor/wheels --no-build-isolation -e . \
|| pip install --find-links=vendor/wheels -e .
echo
echo "설치 완료. 실행:"
echo " ./start.sh"
echo "예:"
echo " source $ROOT/.venv/bin/activate"
echo " juc500 info"
echo " juc500 gui"
echo " juc500 recv -y --dest ~/Downloads"
echo " juc500 send /path/to/file"
@@ -0,0 +1,108 @@
@echo off
setlocal EnableExtensions
cd /d "%~dp0.."
set "ROOT=%CD%"
set "WHEELS=%ROOT%\vendor\wheels"
set "GETPIP=%ROOT%\vendor\get-pip.py"
set "LIBUSB=%ROOT%\vendor\libusb-1.0.dll"
set "EMBED_PY=%ROOT%\vendor\python-windows\python.exe"
echo ============================================
echo JUC500 Windows offline setup
echo ============================================
if not exist "%WHEELS%\pyusb-1.2.1-py3-none-any.whl" (
echo [!] Missing vendor\wheels. Copy the full source tree including vendor\.
exit /b 1
)
if not exist "%LIBUSB%" (
echo [!] Missing vendor\libusb-1.0.dll
exit /b 1
)
REM Remove Mac-copied broken .venv
if exist "%ROOT%\.venv\bin\python" (
if not exist "%ROOT%\.venv\Scripts\python.exe" (
echo [!] Mac .venv detected - removing...
rmdir /s /q "%ROOT%\.venv"
)
)
REM Choose bootstrap Python: system first, else vendored embeddable
set "BOOT_PY="
where py >nul 2>&1 && set "BOOT_PY=py -3"
if not defined BOOT_PY (
where python >nul 2>&1 && set "BOOT_PY=python"
)
if not defined BOOT_PY (
if exist "%EMBED_PY%" (
set "BOOT_PY=%EMBED_PY%"
echo Using vendored Python: vendor\python-windows\python.exe
)
)
if not defined BOOT_PY (
echo [!] No Python found. vendor\python-windows is missing.
exit /b 1
)
echo Bootstrap: %BOOT_PY%
REM Ensure virtualenv module available (embeddable has no stdlib venv)
%BOOT_PY% -c "import virtualenv" 2>nul
if errorlevel 1 (
echo Installing virtualenv from vendor\wheels ...
%BOOT_PY% -m pip install --no-index --find-links="%WHEELS%" virtualenv
if errorlevel 1 (
if exist "%GETPIP%" (
%BOOT_PY% "%GETPIP%" --no-index --find-links="%WHEELS%" pip setuptools wheel
%BOOT_PY% -m pip install --no-index --find-links="%WHEELS%" virtualenv
)
)
)
if not exist "%ROOT%\.venv\Scripts\python.exe" (
echo Creating .venv (fully offline, no pip seed from internet)...
%BOOT_PY% -m virtualenv --no-periodic-update --no-setuptools --no-pip "%ROOT%\.venv"
if errorlevel 1 (
echo [!] virtualenv failed.
exit /b 1
)
"%ROOT%\.venv\Scripts\python.exe" "%GETPIP%" --no-index --find-links="%WHEELS%" pip setuptools wheel packaging
if errorlevel 1 (
echo [!] get-pip offline bootstrap failed.
exit /b 1
)
goto install_deps
)
"%ROOT%\.venv\Scripts\python.exe" -c "import usb" 2>nul
if errorlevel 1 (
echo [!] .venv incomplete — reinstalling dependencies...
goto install_deps
)
goto copy_libusb
:install_deps
echo Installing dependencies from vendor\wheels (no PyPI)...
"%ROOT%\.venv\Scripts\python.exe" -m pip install --no-index --find-links="%WHEELS%" -U pip setuptools wheel packaging
"%ROOT%\.venv\Scripts\python.exe" -m pip install --no-index --find-links="%WHEELS%" -r "%ROOT%\requirements.txt"
if errorlevel 1 (
echo [!] requirements install failed.
exit /b 1
)
"%ROOT%\.venv\Scripts\python.exe" -m pip install --no-index --find-links="%WHEELS%" --no-build-isolation -e "%ROOT%"
if errorlevel 1 (
echo [!] editable install failed.
exit /b 1
)
:copy_libusb
copy /Y "%LIBUSB%" "%ROOT%\.venv\Scripts\libusb-1.0.dll" >nul
echo libusb-1.0.dll -^> .venv\Scripts\
echo.
echo Setup done (offline). Run:
echo .\start.bat
echo or Git Bash: ./start_win.sh
echo.
endlocal
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Tauri 케이블 감시 앱 종료 (개발 중 ./start.sh 와 충돌 방지)
set -euo pipefail
stopped=0
for pattern in \
"juc500-app/src-tauri/target/release/juc500-app" \
"juc500-app/src-tauri/target/debug/juc500-app" \
"/Applications/JUC500.app/Contents/MacOS/juc500-app" \
"juc500-app.exe"; do
if pkill -f "$pattern" 2>/dev/null; then
echo "종료: $pattern"
stopped=1
fi
done
if [[ "$stopped" == "0" ]]; then
echo "실행 중인 Tauri juc500-app 없음"
fi
@@ -0,0 +1,233 @@
#!/usr/bin/env bash
# juc500 — 수정한 파일을 USB 연결된 상대편으로 전송 (동기화만, 재시작은 수동)
#
# 개발 절차:
# 1) 소스 수정
# 2) ./scripts/sync_peer.sh ← 이 스크립트 (상대로 파일 전송)
# 3) 이 PC에서 ./start.sh
# 4) 상대 PC (Void/Git Bash): cd C:/dev/juc500 && ./start.sh
#
# 상대 경로 (절대 경로, 슬래시 사용):
# Windows 기본: C:/dev/juc500
# Mac Studio: /Users/aidev/dev/juc500/juc500 (USE_MAC_PEER_PATHS=1)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
GUI_URL="${JUC500_GUI_URL:-http://127.0.0.1:8765}"
# Dev-only manual sync. Runtime does not auto-sync; set JUC500_AUTO_SYNC_PEER=1 to enable on connect.
# Windows peer at C:\dev\juc500 (Void에서 ./start.sh 실행 경로)
PEER_ROOT="${JUC500_PEER_ROOT:-C:/dev/juc500}"
PEER_ROOT_ALT="${JUC500_PEER_ROOT_ALT:-C:/dev/juc500/juc500}"
# Mac peer (browse 홈 기준 상대경로) — JUC500_PEER_ROOT 비우고 USE_MAC_PEER_PATHS=1
MAC_PEER_XFER="dev/juc500/juc500/juc500_xfer"
MAC_PEER_STATIC="dev/juc500/juc500/juc500_xfer/static"
MAC_PEER_SCRIPTS="dev/juc500/juc500/scripts"
MAC_PEER_ROOT_DIR="dev/juc500/juc500"
if [[ "${USE_MAC_PEER_PATHS:-0}" == "1" ]]; then
PEER_XFER="$MAC_PEER_XFER"
PEER_STATIC="$MAC_PEER_STATIC"
PEER_SCRIPTS="$MAC_PEER_SCRIPTS"
PEER_ROOT_DIR="$MAC_PEER_ROOT_DIR"
else
PEER_XFER="${PEER_ROOT}/juc500_xfer"
PEER_STATIC="${PEER_ROOT}/juc500_xfer/static"
PEER_SCRIPTS="${PEER_ROOT}/scripts"
PEER_ROOT_DIR="${PEER_ROOT}"
if [[ "$PEER_ROOT_ALT" != "$PEER_ROOT" ]]; then
PEER_XFER_ALT="${PEER_ROOT_ALT}/juc500_xfer"
PEER_STATIC_ALT="${PEER_ROOT_ALT}/juc500_xfer/static"
PEER_SCRIPTS_ALT="${PEER_ROOT_ALT}/scripts"
PEER_ROOT_DIR_ALT="${PEER_ROOT_ALT}"
fi
fi
PY=""
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
exit 1
fi
send_files() {
local dest="$1"
shift
"$PY" - "$dest" "$GUI_URL" "$@" <<'PY'
import json, sys, urllib.request
dest, gui_url = sys.argv[1], sys.argv[2]
paths = sys.argv[3:]
body = json.dumps({"paths": paths, "dest_dir": dest}).encode()
req = urllib.request.Request(
f"{gui_url}/api/local/send",
data=body,
method="POST",
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as r:
print(r.read().decode())
PY
}
if ! curl -sf -m3 "$GUI_URL/api/status" >/dev/null 2>&1; then
echo "GUI가 떠 있지 않습니다. 먼저 ./start.sh 로 실행하세요." >&2
exit 1
fi
CONNECTED=$("$PY" -c "
import json, urllib.request
with urllib.request.urlopen('$GUI_URL/api/status', timeout=3) as r:
print(json.load(r).get('connected'))
" 2>/dev/null || echo "False")
if [[ "$CONNECTED" != "True" ]]; then
echo "상대편에 연결되지 않았습니다. 양쪽 GUI에서 「연결」을 누르세요." >&2
exit 1
fi
if [[ $# -gt 0 ]]; then
for f in "$@"; do
if [[ ! -f "$f" ]]; then
echo "파일 없음: $f" >&2
exit 1
fi
done
send_files "$PEER_XFER" "$@"
else
echo "=== 기본 파일 전송 → ${PEER_XFER} ==="
XFER_FILES=(
"$ROOT/juc500_xfer/gui_server.py"
"$ROOT/juc500_xfer/usb_link.py"
"$ROOT/juc500_xfer/inputshare.py"
"$ROOT/juc500_xfer/protocol.py"
"$ROOT/juc500_xfer/peer_sync.py"
"$ROOT/juc500_xfer/clipboard_files.py"
"$ROOT/juc500_xfer/__init__.py"
)
STATIC_FILES=(
"$ROOT/juc500_xfer/static/app.js"
"$ROOT/juc500_xfer/static/index.html"
"$ROOT/juc500_xfer/static/styles.css"
)
send_files "$PEER_XFER" "${XFER_FILES[@]}"
send_files "$PEER_STATIC" "${STATIC_FILES[@]}"
if [[ -n "${PEER_XFER_ALT:-}" ]]; then
echo "=== legacy editable 경로 → ${PEER_XFER_ALT} ==="
send_files "$PEER_XFER_ALT" "${XFER_FILES[@]}"
send_files "$PEER_STATIC_ALT" "${STATIC_FILES[@]}"
fi
if [[ -f "$ROOT/scripts/peer_restart.sh" ]]; then
send_files "$PEER_SCRIPTS" "$ROOT/scripts/peer_restart.sh"
if [[ -n "${PEER_SCRIPTS_ALT:-}" ]]; then
send_files "$PEER_SCRIPTS_ALT" "$ROOT/scripts/peer_restart.sh"
fi
fi
if [[ -f "$ROOT/start.sh" ]]; then
send_files "$PEER_ROOT_DIR" "$ROOT/start.sh"
if [[ -n "${PEER_ROOT_DIR_ALT:-}" ]]; then
send_files "$PEER_ROOT_DIR_ALT" "$ROOT/start.sh"
fi
fi
if [[ -f "$ROOT/start.bat" ]]; then
send_files "$PEER_ROOT_DIR" "$ROOT/start.bat"
if [[ -n "${PEER_ROOT_DIR_ALT:-}" ]]; then
send_files "$PEER_ROOT_DIR_ALT" "$ROOT/start.bat"
fi
fi
if [[ -f "$ROOT/start_win.sh" ]]; then
send_files "$PEER_ROOT_DIR" "$ROOT/start_win.sh"
if [[ -n "${PEER_ROOT_DIR_ALT:-}" ]]; then
send_files "$PEER_ROOT_DIR_ALT" "$ROOT/start_win.sh"
fi
fi
if [[ -f "$ROOT/start_both.sh" ]]; then
send_files "$PEER_ROOT_DIR" "$ROOT/start_both.sh"
if [[ -n "${PEER_ROOT_DIR_ALT:-}" ]]; then
send_files "$PEER_ROOT_DIR_ALT" "$ROOT/start_both.sh"
fi
fi
if [[ -f "$ROOT/README.md" ]]; then
send_files "$PEER_ROOT_DIR" "$ROOT/README.md"
if [[ -n "${PEER_ROOT_DIR_ALT:-}" ]]; then
send_files "$PEER_ROOT_DIR_ALT" "$ROOT/README.md"
fi
fi
if [[ -f "$ROOT/vendor/libusb-1.0.dll" ]]; then
echo "=== libusb DLL → ${PEER_ROOT}/vendor ==="
send_files "${PEER_ROOT}/vendor" "$ROOT/vendor/libusb-1.0.dll"
if [[ -n "${PEER_ROOT_ALT:-}" && "$PEER_ROOT_ALT" != "$PEER_ROOT" ]]; then
send_files "${PEER_ROOT_ALT}/vendor" "$ROOT/vendor/libusb-1.0.dll"
fi
fi
for win_script in ensure_libusb_windows.bat setup_windows.bat bootstrap_windows.sh make_windows_bundle.sh make_offline_windows_update.sh apply_offline_update.bat; do
if [[ -f "$ROOT/scripts/$win_script" ]]; then
send_files "$PEER_SCRIPTS" "$ROOT/scripts/$win_script"
if [[ -n "${PEER_SCRIPTS_ALT:-}" ]]; then
send_files "$PEER_SCRIPTS_ALT" "$ROOT/scripts/$win_script"
fi
fi
done
if [[ -f "$ROOT/vendor/tools/zadig.exe" ]]; then
echo "=== zadig → ${PEER_ROOT}/vendor/tools ==="
send_files "${PEER_ROOT}/vendor/tools" "$ROOT/vendor/tools/zadig.exe"
fi
fi
LOCAL_INPUTSHARE=$("$PY" -c "from juc500_xfer.inputshare import INPUTSHARE_BUILD; print(INPUTSHARE_BUILD)" 2>/dev/null || echo "unknown")
LOCAL_XFER=$("$PY" -c "from juc500_xfer.usb_link import XFER_BUILD; print(XFER_BUILD)" 2>/dev/null || echo "unknown")
echo ""
echo "로컬 build — inputshare: $LOCAL_INPUTSHARE · xfer: $LOCAL_XFER"
echo "상대편 build 확인 중 (RPC ping, 재시작 전이면 구버전일 수 있음)…"
read -r PEER_INPUTSHARE PEER_XFER < <(
curl -sf -m10 "$GUI_URL/api/peer/ping" 2>/dev/null | "$PY" -c "
import json,sys
try:
d=json.load(sys.stdin)
print(d.get('inputshare_build') or 'unknown', d.get('xfer_build') or 'unknown')
except Exception:
print('check-failed check-failed')
" 2>/dev/null || echo "check-failed check-failed"
)
echo "상대편 build — inputshare: $PEER_INPUTSHARE · xfer: $PEER_XFER"
if [[ "$PEER_INPUTSHARE" == "$LOCAL_INPUTSHARE" && "$PEER_XFER" == "$LOCAL_XFER" ]]; then
echo "✓ 상대편 build 일치"
else
echo "⚠ 상대편 build 불일치 — 아래 3·4단계에서 양쪽 ./start.sh 로 새 코드 로드"
fi
print_dev_workflow() {
echo ""
echo "=== 개발 적용 절차 (남은 단계) ==="
echo "3) 이 PC: cd $ROOT && ./start.sh"
if [[ "${USE_MAC_PEER_PATHS:-0}" == "1" ]]; then
echo "4) 상대 Mac: cd /Users/aidev/dev/juc500/juc500 && ./start.sh"
else
echo "4) 상대 PC: cd ${PEER_ROOT} && ./start.sh (Void/Git Bash)"
echo " 시작 시 'JUC500 build: ...juc700-win9 | ...\\juc500_xfer\\usb_link.py' 확인"
if [[ -n "${PEER_XFER_ALT:-}" ]]; then
echo " ※ legacy editable 경로(${PEER_XFER_ALT})에도 동기화함"
fi
fi
echo ""
echo "build 확인: curl -s $GUI_URL/api/status | grep xfer_build"
echo " curl -s $GUI_URL/api/peer/ping"
}
# 개발 중에는 RPC 자동 재시작을 쓰지 않음 (루프·경로 오류 방지). 필요 시 JUC500_SYNC_RESTART=1
if [[ "${JUC500_SYNC_RESTART:-0}" == "1" ]]; then
echo ""
echo "=== RPC 자동 재시작 (JUC500_SYNC_RESTART=1) ==="
resp="$(curl -sf -m25 -X POST "$GUI_URL/api/restart/peer" -H 'Content-Type: application/json' -d '{}' || true)"
if [[ -n "$resp" ]]; then
echo "$resp"
else
echo "⚠ 상대 RPC 재시작 실패 — 4단계에서 수동 ./start.sh" >&2
fi
fi
print_dev_workflow
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# macOS 로그인 자동 실행(Tauri 케이블 감시) 제거
set -euo pipefail
LABEL="com.juc500.desktop"
PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist"
launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
if [[ -f "$PLIST" ]]; then
rm -f "$PLIST"
echo "제거: $PLIST"
else
echo "LaunchAgent 없음: $PLIST"
fi
"$(cd "$(dirname "$0")" && pwd)/stop_tauri_watchers.sh"
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""Verify JUC500 GUI connect + listdir via local API."""
from __future__ import annotations
import json
import sys
import time
import urllib.error
import urllib.request
GUI = "http://127.0.0.1:8765"
POLL = 1.0
CONNECT_DEADLINE = 180.0
def api(path: str, *, method: str = "GET", body: dict | None = None, timeout: float = 15.0):
data = None
headers = {}
if body is not None:
data = json.dumps(body).encode()
headers["Content-Type"] = "application/json"
req = urllib.request.Request(f"{GUI}{path}", data=data, method=method, headers=headers)
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode())
def status() -> dict:
return api("/api/status", timeout=8.0)
def wait_connected(deadline: float, *, label: str) -> dict:
end = time.monotonic() + deadline
last: dict = {}
started = time.monotonic()
last_retry = started
while time.monotonic() < end:
now = time.monotonic()
try:
last = status()
except Exception as exc:
print(f"[{label}] status error: {exc}", flush=True)
time.sleep(POLL)
continue
conn = bool(last.get("connected"))
model = last.get("cable_model")
connecting = bool(last.get("connecting"))
err = last.get("connect_error")
print(
f"[{label}] connected={conn} model={model} connecting={connecting} err={err!r}",
flush=True,
)
if conn:
return last
if err and not connecting:
retryable = any(
k in (err or "")
for k in (
"session stopped",
"연결 시간 초과",
"연결되지 않았습니다",
"Access denied",
"점유 실패",
)
)
if retryable and now - last_retry >= 5.0:
try:
api("/api/disconnect", method="POST", body={})
time.sleep(1.0)
api("/api/connect/auto", method="POST", body={})
print(f"[{label}] retry after: {err}", flush=True)
except Exception as exc:
print(f"[{label}] retry error: {exc}", flush=True)
last_retry = now
time.sleep(POLL)
continue
raise RuntimeError(f"{label} failed: {err}")
if now - last_retry >= 70.0 and (connecting or now - started >= 25.0):
try:
api("/api/disconnect", method="POST", body={})
time.sleep(1.0)
api("/api/connect/auto", method="POST", body={})
print(f"[{label}] retry disconnect+connect/auto", flush=True)
except Exception as exc:
print(f"[{label}] retry error: {exc}", flush=True)
last_retry = now
time.sleep(POLL)
raise TimeoutError(f"{label}: not connected within {deadline:.0f}s — last={last}")
def verify_listdir(label: str) -> int:
try:
data = api("/api/listdir?path=__roots__", timeout=30.0)
except urllib.error.HTTPError as exc:
body = exc.read().decode(errors="replace")
raise RuntimeError(f"{label} listdir HTTP {exc.code}: {body}") from exc
n = len(data.get("entries") or [])
print(f"[{label}] listdir roots={n}", flush=True)
if n <= 0:
raise RuntimeError(f"{label} listdir returned no entries")
return n
def assert_juc500(s: dict, label: str) -> None:
active = (s.get("cable_model") or "").upper()
dev = s.get("active_cable") or s.get("device") or {}
pid = (dev.get("pid") or "").lower()
if active != "JUC500":
raise RuntimeError(f"{label}: cable_model={active!r} expected JUC500")
if pid and pid != "7500":
raise RuntimeError(f"{label}: unexpected pid={pid} for JUC500")
print(
f"[{label}] OK model={active} pid={pid or '7500'} host={s.get('hostname')}",
flush=True,
)
def main() -> int:
print("JUC500 connect verification", flush=True)
try:
s0 = status()
except Exception as exc:
print(f"GUI not reachable at {GUI}: {exc}", file=sys.stderr)
return 1
devices = s0.get("devices") or []
print(f"Detected devices: {len(devices)}", flush=True)
if not devices:
print("WARN: no JUC500 detected — plug cable and retry", flush=True)
if not s0.get("connected"):
print("Waiting for autoconnect on JUC500…", flush=True)
s = wait_connected(CONNECT_DEADLINE, label="JUC500")
assert_juc500(s, "JUC500-connected")
verify_listdir("JUC500")
try:
ping = api("/api/peer/ping", timeout=15.0)
print(f"[JUC500] peer ping: {ping}", flush=True)
except Exception as exc:
print(f"[JUC500] peer ping skipped: {exc}", flush=True)
print("\n=== ALL CHECKS PASSED ===", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())