diff --git a/tools/ai/zeroclaw_dual_chat.sh b/tools/ai/zeroclaw_dual_chat.sh index ecb15fb..9ca183b 100755 --- a/tools/ai/zeroclaw_dual_chat.sh +++ b/tools/ai/zeroclaw_dual_chat.sh @@ -27,6 +27,7 @@ Usage: $(basename "$0") --hardware $(basename "$0") --provider-check $(basename "$0") --cheap -m "message" + $(basename "$0") --local-only -m "message" USAGE } @@ -58,15 +59,85 @@ if [[ ! -x "$ZEROCLAW_BIN" ]]; then fi fi -if [[ "${1:-}" == "--hardware" ]]; then +MODE="" +MESSAGE="" +LOCAL_ONLY=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --cheap) + export ZEROCLAW_PREFER_LOCAL_AI=1 + shift + ;; + --local-only) + export ZEROCLAW_PREFER_LOCAL_AI=1 + LOCAL_ONLY=1 + shift + ;; + --hardware) + MODE="hardware" + shift + ;; + --provider-check) + MODE="provider-check" + shift + ;; + --interactive) + MODE="interactive" + shift + ;; + -m) + shift + if [[ $# -lt 1 ]]; then + usage + exit 1 + fi + MODE="message" + MESSAGE="$*" + break + ;; + -h|--help) + usage + exit 0 + ;; + *) + usage + exit 1 + ;; + esac +done + +if [[ -z "$MODE" ]]; then + usage + exit 1 +fi + +if [[ "$MODE" == "hardware" ]]; then ZEROCLAW_WORKSPACE="$workspace" "$ZEROCLAW_BIN" hardware discover exit 0 fi -if [[ "${1:-}" == "--cheap" ]]; then - export ZEROCLAW_PREFER_LOCAL_AI=1 - shift -fi +lmstudio_has_models() { + local base_url="${ZEROCLAW_LMSTUDIO_BASE_URL:-http://127.0.0.1:1234/v1}" + python3 - "$base_url" <<'PY' +import json +import sys +import urllib.request + +base = sys.argv[1].rstrip("/") +url = f"{base}/models" +try: + req = urllib.request.Request(url, headers={"Accept": "application/json"}) + with urllib.request.urlopen(req, timeout=3) as resp: + payload = json.loads(resp.read().decode("utf-8", "replace")) + models = payload.get("data", []) + if isinstance(models, list) and models: + raise SystemExit(0) +except Exception: + pass +raise SystemExit(1) +PY +} resolve_provider() { if [[ -n "${ZEROCLAW_PROVIDER:-}" ]]; then @@ -92,6 +163,13 @@ resolve_provider() { fi fi + if [[ "${ZEROCLAW_PREFER_LOCAL_AI:-0}" == "1" ]] && lmstudio_has_models; then + export LMSTUDIO_BASE_URL="${ZEROCLAW_LMSTUDIO_BASE_URL:-http://127.0.0.1:1234/v1}" + export LMSTUDIO_API_KEY="${LMSTUDIO_API_KEY:-lm-studio}" + echo "lmstudio" + return 0 + fi + if [[ "${ZEROCLAW_SKIP_COPILOT_CHECK:-0}" != "1" ]] && command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then if gh api /user/copilot/billing --silent >/dev/null 2>&1; then local gh_token @@ -144,21 +222,26 @@ ERR } provider="$(resolve_provider)" + +if [[ "$LOCAL_ONLY" -eq 1 && "$provider" != "ollama" && "$provider" != "lmstudio" ]]; then + cat <<'ERR' >&2 +Local-only mode requested, but no local provider is ready. +- Start Ollama and set ZEROCLAW_OLLAMA_MODEL (or run tools/ai/ollama_local_setup.sh) +- Or start LM Studio local server on 127.0.0.1:1234/v1 +ERR + exit 1 +fi + echo "[zeroclaw-dual-chat] workspace=$workspace provider=$provider" -if [[ "${1:-}" == "--provider-check" ]]; then +if [[ "$MODE" == "provider-check" ]]; then exit 0 fi export ZEROCLAW_WORKSPACE="$workspace" -if [[ "${1:-}" == "--interactive" ]]; then +if [[ "$MODE" == "interactive" ]]; then exec "$ZEROCLAW_BIN" agent -p "$provider" fi -if [[ "${1:-}" != "-m" || -z "${2:-}" ]]; then - usage - exit 1 -fi - -exec "$ZEROCLAW_BIN" agent -p "$provider" -m "$2" +exec "$ZEROCLAW_BIN" agent -p "$provider" -m "$MESSAGE" diff --git a/tools/ai/zeroclaw_hw_firmware_loop.sh b/tools/ai/zeroclaw_hw_firmware_loop.sh index 7337401..a8825e6 100755 --- a/tools/ai/zeroclaw_hw_firmware_loop.sh +++ b/tools/ai/zeroclaw_hw_firmware_loop.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash set -euo pipefail -RTC_REPO="${ZEROCLAW_RTC_REPO:-$HOME/RTC_HW}" -ZACUS_REPO="${ZEROCLAW_ZACUS_REPO:-$HOME/ZACUS_HW}" +RTC_REPO="${ZEROCLAW_RTC_REPO:-/Users/cils/Documents/Lelectron_rare/RTC_BL_PHONE}" +ZACUS_REPO="${ZEROCLAW_ZACUS_REPO:-/Users/cils/Documents/Lelectron_rare/le-mystere-professeur-zacus}" +RTC_UPLOAD_PORT_HINT_DEFAULT="${ZEROCLAW_RTC_PORT_HINT:-${ZEROCLAW_UPLOAD_PORT_HINT:-cp2102,10c4,esp32audiokit,audio,audiokit,slab_usb}}" +ZACUS_UPLOAD_PORT_HINT_DEFAULT="${ZEROCLAW_ZACUS_PORT_HINT:-${ZEROCLAW_UPLOAD_PORT_HINT:-1a86,usbserial,ch340,freenove,esp32s3,esp32-s3,usbmodem}}" +RTC_UPLOAD_PORT_HINT="${ZEROCLAW_RTC_UPLOAD_PORT_HINT:-$RTC_UPLOAD_PORT_HINT_DEFAULT}" +ZACUS_UPLOAD_PORT_HINT="${ZEROCLAW_ZACUS_UPLOAD_PORT_HINT:-$ZACUS_UPLOAD_PORT_HINT_DEFAULT}" RTC_FW_DIR="$RTC_REPO" ZACUS_FW_DIR="$ZACUS_REPO/hardware/firmware" @@ -11,9 +15,6 @@ RTC_ENV_DEFAULT="${ZEROCLAW_RTC_PIO_ENV:-esp32dev}" ZACUS_ENV_DEFAULT="${ZEROCLAW_ZACUS_PIO_ENV:-esp32dev}" MONITOR_SECS_DEFAULT="${ZEROCLAW_MONITOR_SECS:-60}" BAUD_DEFAULT="${ZEROCLAW_SERIAL_BAUD:-115200}" -ZEROCLAW_CHIP_MISMATCH_FALLBACK="${ZEROCLAW_CHIP_MISMATCH_FALLBACK:-1}" -ZEROCLAW_USB_STABILITY_SAMPLES="${ZEROCLAW_USB_STABILITY_SAMPLES:-2}" -ZEROCLAW_USB_STABILITY_DELAY="${ZEROCLAW_USB_STABILITY_DELAY:-1}" usage() { cat </dev/null || true)" + + ZEROCLAW_TARGET="$pick_target" \ + ZEROCLAW_TARGET_HINT="$pick_hint" \ + ZEROCLAW_DEVICE_LIST_JSON="$devices_json" \ + python3 - <<'PY' +import os import json -import sys -import subprocess -port = sys.argv[1] -raw = subprocess.check_output(["pio", "device", "list", "--json-output"], text=True) -if not raw or not raw.strip(): - raise SystemExit(1) +target = (os.environ.get("ZEROCLAW_TARGET", "rtc") or "rtc").strip().lower() +hint = (os.environ.get("ZEROCLAW_TARGET_HINT", "") or "").strip().lower() +raw = (os.environ.get("ZEROCLAW_DEVICE_LIST_JSON", "") or "").strip() + try: - items = json.loads(raw) + devices = json.loads(raw) if raw else [] except Exception: - raise SystemExit(1) + devices = [] -for it in items: - if str(it.get("port", "")) != port: - continue - print(f"{it.get('hwid','unknown')}|{it.get('description','unknown')}|{it.get('serial_number','')}") - raise SystemExit(0) -raise SystemExit(1) -PY +if not isinstance(devices, list): + devices = [] + + +score_board = { + "rtc": ["cp210", "10c4", "cp2102", "audio", "esp32 audio", "esp32audio", "esp32audiokit", "audio kit", "slab_usb"], + "zacus": ["1a86", "ch340", "freenove", "usb single serial", "usbserial", "esp32-s3", "esp32s3", "usbmodem"], +} +penalty_board = { + "rtc": ["1a86", "ch340", "freenove", "usbmodem"], + "zacus": ["10c4", "cp2102", "cp210"], } -is_usb_port_stable() { - local port="$1" - local samples="$2" - local delay_secs="$3" - if (( samples < 2 )); then - return 0 - fi +def norm(value): + return (value or "").strip().lower() - local i=0 - local last_sig - local sig - while (( i < samples )); do - if ! sig="$(serial_port_signature "$port" 2>/dev/null || true)"; then - return 1 - fi - if [[ "$i" -eq 0 ]]; then - last_sig="$sig" - elif [[ "$sig" != "$last_sig" ]]; then - return 1 - fi - ((i++)) - if (( i < samples )); then - sleep "$delay_secs" - fi - done - return 0 + +def contains_any(text, needles): + return any(needle in text for needle in needles if needle) + + +preferred = score_board.get(target, score_board["rtc"]) +penalize = penalty_board.get(target, penalty_board["rtc"]) +hint_tokens = [token.strip().lower() for token in hint.split(",") if token.strip()] + +best_port = "" +best_score = -10 ** 9 + +for idx, device in enumerate(devices): + if not isinstance(device, dict): + continue + port_raw = device.get("port", "") + port = norm(port_raw) + if not port: + continue + desc = norm(device.get("description", "")) + hwid = norm(device.get("hwid", "")) + board = norm(device.get("board", "")) + low = f"{port} {desc} {hwid} {board}" + + score = 0 + if hwid and hwid != "n/a": + score += 50 + if "usb" in low: + score += 20 + if "uart" in low: + score += 15 + if "bluetooth" in low: + score -= 80 + + if contains_any(low, preferred): + score += 120 + if contains_any(low, penalize): + score -= 80 + + for token in hint_tokens: + if token in low: + score += 200 + + if "/dev/cu." in port and hwid == "n/a": + score -= 40 + + if score > best_score: + best_score = score + best_port = port_raw + +if best_port: + print(best_port) + raise SystemExit(0) +raise SystemExit(1) +PY } if ! env_exists "$ENV_NAME"; then @@ -169,46 +212,25 @@ if ! env_exists "$ENV_NAME"; then exit 1 fi +target_port_hint="" +if [[ "$target" == "rtc" ]]; then + target_port_hint="$RTC_UPLOAD_PORT_HINT" +else + target_port_hint="$ZACUS_UPLOAD_PORT_HINT" +fi + if [[ -z "$PORT" ]]; then - if ! PORT="$( - cd "$FW_DIR" - pio device list --json-output 2>/dev/null | python3 -c 'import json,sys -raw=sys.stdin.read().strip() -try: - arr=json.loads(raw) if raw else [] -except Exception: - arr=[] -best=None -best_score=-10**9 -for d in arr: - p=str(d.get("port","")) - desc=str(d.get("description","")) - hwid=str(d.get("hwid","")) - if not p: - continue - score=0 - low=f\"{p} {desc} {hwid}\".lower() - if hwid and hwid.lower() != \"n/a\": - score += 50 - if \"usb\" in low: - score += 30 - if \"cp210\" in low or \"ch340\" in low or \"uart\" in low: - score += 20 - if \"bluetooth\" in low: - score -= 100 - if \"/cu.urt\" in p.lower() and hwid.lower() == \"n/a\": - score -= 120 - if score > best_score: - best_score=score - best=p -if best: - print(best) - raise SystemExit(0) -raise SystemExit(1)' - )"; then + if [[ -n "$target_port_hint" ]]; then + PORT="$(pick_port_for_target "$target" "$target_port_hint" || true)" + else + PORT="$(pick_port_for_target "$target" "" || true)" + fi + + if [[ -z "$PORT" ]]; then PORT="" fi fi +echo "[info] serial port resolved for $target: ${PORT}" if [[ -z "$PORT" ]]; then echo "[fail] no serial port detected. Upload/monitor are forced by default." >&2 @@ -278,24 +300,6 @@ resolve_env_fallback() { return 1 } -can_use_fallback_on_mismatch() { - local failed_env="$1" - local fallback_env="$2" - - if [[ "${ZEROCLAW_CHIP_MISMATCH_FALLBACK}" != "1" ]]; then - echo "[warn] chip mismatch detected for $failed_env -> $fallback_env, but ZEROCLAW_CHIP_MISMATCH_FALLBACK=${ZEROCLAW_CHIP_MISMATCH_FALLBACK} (auto-fallback disabled)." - return 1 - fi - - if ! is_usb_port_stable "$PORT" "$ZEROCLAW_USB_STABILITY_SAMPLES" "$ZEROCLAW_USB_STABILITY_DELAY"; then - echo "[warn] chip mismatch detected for $failed_env but serial port is unstable (flapping signature on $PORT)." - echo "[hint] Retry with a stable USB link or set ZEROCLAW_USB_STABILITY_SAMPLES=1 after manual validation." - return 1 - fi - - return 0 -} - UPLOAD_ARGS=() MONITOR_ARGS=() if [[ -n "$PORT" ]]; then @@ -312,21 +316,104 @@ echo "[info] target=$target fw_dir=$FW_DIR env=$ACTIVE_ENV port=$PORT baud=$BAUD echo "[step] build" (cd "$FW_DIR" && pio run -e "$ACTIVE_ENV") +port_still_present() { + local expected_port="$1" + local list + if [[ -z "$expected_port" ]]; then + return 1 + fi + list="$(cd "$FW_DIR" && pio device list --json-output 2>/dev/null || true)" + if [[ -z "$list" ]]; then + return 1 + fi + python3 - "$expected_port" "$list" <<'PY' +import json +import sys + +def norm(value): + return (value or "").strip().lower() + +port = norm(sys.argv[1]) +raw = sys.argv[2].strip() + +try: + devices = json.loads(raw) +except Exception: + sys.exit(1) + +if not isinstance(devices, list): + sys.exit(1) + +for device in devices: + if not isinstance(device, dict): + continue + if norm(device.get("port", "")) == port: + sys.exit(0) +sys.exit(1) +PY +} + +upload_target_with_retries() { + local env_name="$1" + local max_retries="${2:-1}" + local attempts=0 + local output + + while true; do + if ! port_still_present "$PORT"; then + echo "[warn] selected port $PORT is not visible anymore; re-detecting." + PORT="$(pick_port_for_target "$target" "$target_port_hint" || true)" + if [[ -z "$PORT" ]]; then + PORT="" + fi + UPLOAD_ARGS=() + MONITOR_ARGS=() + if [[ -n "$PORT" ]]; then + UPLOAD_ARGS+=(--upload-port "$PORT") + MONITOR_ARGS+=(-p "$PORT") + fi + if [[ -n "$PORT" ]]; then + echo "[info] serial port re-resolved for $target: $PORT" + else + echo "[warn] no serial port re-detected; upload might fail." + fi + fi + + upload_once "$env_name" + rc=$? + output="${UPLOAD_LAST_OUTPUT:-}" + + if [[ $rc -eq 0 ]]; then + return 0 + fi + + if [[ $attempts -ge $max_retries ]]; then + return $rc + fi + + if printf '%s' "$output" | rg -qi "(Could not connect|No such file|No such device|not found|not ready|in use|busy|Permission denied|Device or resource busy|No such file or directory)"; then + attempts=$((attempts + 1)) + echo "[warn] upload failure likely linked to transient serial port state; retry ${attempts}/${max_retries}." + continue + fi + + return $rc + done +} + echo "[step] upload/flash (forced default)" UPLOAD_LAST_OUTPUT="" -if ! upload_once "$ACTIVE_ENV"; then +if ! upload_target_with_retries "$ACTIVE_ENV" 1; then fallback_env="$(resolve_env_fallback "$ACTIVE_ENV" "$UPLOAD_LAST_OUTPUT" || true)" if [[ -n "$fallback_env" ]]; then - if ! can_use_fallback_on_mismatch "$ACTIVE_ENV" "$fallback_env"; then - echo "[fail] upload skipped fallback for retry due to unstable link or disabled policy." - exit 3 - fi echo "[warn] detected chip/env mismatch for $ACTIVE_ENV; retrying with '$fallback_env'." ACTIVE_ENV="$fallback_env" echo "[step] rebuild (fallback env)" (cd "$FW_DIR" && pio run -e "$ACTIVE_ENV") echo "[step] upload/flash retry (forced default)" - upload_once "$ACTIVE_ENV" + if ! upload_target_with_retries "$ACTIVE_ENV" 1; then + exit 3 + fi else exit 3 fi diff --git a/tools/ai/zeroclaw_webhook_send.sh b/tools/ai/zeroclaw_webhook_send.sh index ea0e7fc..eab2504 100755 --- a/tools/ai/zeroclaw_webhook_send.sh +++ b/tools/ai/zeroclaw_webhook_send.sh @@ -24,6 +24,8 @@ CONVO_FILE="$ART_DIR/conversations.jsonl" BUDGET_FILE="$ART_DIR/webhook_budget.json" MAX_CALLS_PER_HOUR="${ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR:-40}" MAX_CHARS="${ZEROCLAW_WEBHOOK_MAX_CHARS:-1200}" +CONNECT_TIMEOUT_SECS="${ZEROCLAW_WEBHOOK_CONNECT_TIMEOUT_SECS:-5}" +TIMEOUT_SECS="${ZEROCLAW_WEBHOOK_TIMEOUT_SECS:-90}" usage() { cat >&2 <