fix: resolve merge conflict in zeroclaw_hw_firmware_loop.sh

This commit is contained in:
L'électron rare
2026-03-26 09:59:49 +01:00
parent 23459e9e37
commit 1def59efbc
3 changed files with 296 additions and 122 deletions
+96 -13
View File
@@ -27,6 +27,7 @@ Usage:
$(basename "$0") <rtc|zacus|path> --hardware
$(basename "$0") <rtc|zacus|path> --provider-check
$(basename "$0") <rtc|zacus|path> --cheap -m "message"
$(basename "$0") <rtc|zacus|path> --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"
+196 -109
View File
@@ -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 <<USAGE
@@ -106,60 +107,102 @@ env_exists() {
rg -q "^\[env:${env_name}\]" "$FW_DIR/platformio.ini"
}
serial_port_signature() {
local port="$1"
cd "$FW_DIR"
python3 - "$port" <<'PY'
pick_port_for_target() {
local pick_target="$1"
local pick_hint="$2"
local devices_json
devices_json="$(cd "$FW_DIR" && pio device list --json-output 2>/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
+4
View File
@@ -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 <<USAGE
@@ -193,6 +195,8 @@ fi
PAYLOAD="$(python3 -c 'import json,sys;print(json.dumps({"message":sys.argv[1]}))' "$MESSAGE")"
TMP_BODY="$(mktemp)"
HTTP_STATUS="$(curl -sS -o "$TMP_BODY" -w "%{http_code}" -X POST "http://$HOST:$PORT/webhook" \
--connect-timeout "$CONNECT_TIMEOUT_SECS" \
--max-time "$TIMEOUT_SECS" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" || true)"