Compare commits
48 Commits
844d3c89ae
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c4b9d03e14 | |||
| afbf4cad61 | |||
| e8218a2ab5 | |||
| d6bc57dbab | |||
| 464160c1f7 | |||
| efca4a6b7a | |||
| 000301355f | |||
| a85732aa80 | |||
| 306243eb6a | |||
| 161e0e654c | |||
| 5347a728da | |||
| 9eddc702b6 | |||
| 2d2bd69780 | |||
| 48f64a6858 | |||
|
|
7e1f0bef36 | ||
|
|
d91a309419 | ||
|
|
6da91c7d26 | ||
|
|
f52ad363a2 | ||
|
|
796c8d3431 | ||
|
|
06036c6c73 | ||
|
|
eeb984ebfe | ||
|
|
80e1c97000 | ||
|
|
3dba9fde32 | ||
|
|
8cebf062a2 | ||
|
|
c9597cf1a0 | ||
|
|
cdf2a5f5ac | ||
|
|
000dc4a46c | ||
|
|
be49f32b50 | ||
|
|
6c1c0cf955 | ||
|
|
d89c2340da | ||
|
|
7e44744c31 | ||
|
|
684e9728dd | ||
|
|
f61a0a4305 | ||
|
|
4d13fdec8c | ||
|
|
811c1be3b9 | ||
|
|
c3dd87f635 | ||
|
|
18a949464e | ||
|
|
f363836712 | ||
|
|
e1e3fa95cd | ||
|
|
43117a6a04 | ||
|
|
45e1370ca5 | ||
|
|
7761b438d3 | ||
|
|
8990d48d51 | ||
|
|
9e67969fd1 | ||
|
|
6a97f68255 | ||
|
|
f1c24f75b5 | ||
|
|
f71d448047 | ||
|
|
e4e99c9aed |
181
install.sh
181
install.sh
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# clawd 一键安装脚本
|
||||
# 用法:curl -fsSL https://raw.githubusercontent.com/stswangzhiping/clawd/main/install.sh | bash
|
||||
# 需要 root 权限,需要已安装 Node.js >= 18
|
||||
# clawd installer
|
||||
# Run: curl -fsSL https://git.cutos.ai/claw-daemon/clawd/raw/branch/main/install.sh | sudo bash
|
||||
# Requires root and Node.js >= 18
|
||||
|
||||
set -e
|
||||
|
||||
@@ -11,26 +11,26 @@ info() { echo -e "${GREEN}[clawd]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[clawd]${NC} $*"; }
|
||||
error() { echo -e "${RED}[clawd]${NC} $*"; exit 1; }
|
||||
|
||||
# ── 检查 root ────────────────────────────────────────────────────────────────
|
||||
# Check root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
error "请以 root 身份运行(sudo bash install.sh)"
|
||||
error "Please run as root: sudo bash install.sh"
|
||||
fi
|
||||
|
||||
# ── 检查 Node.js ─────────────────────────────────────────────────────────────
|
||||
# Check Node.js
|
||||
if ! command -v node &>/dev/null; then
|
||||
error "未找到 Node.js,请先安装 Node.js >= 18"
|
||||
error "Node.js not found. Please install Node.js >= 18"
|
||||
fi
|
||||
|
||||
NODE_VER=$(node -e "process.stdout.write(process.versions.node)")
|
||||
MAJOR=$(echo "$NODE_VER" | cut -d. -f1)
|
||||
if [ "$MAJOR" -lt 18 ]; then
|
||||
error "Node.js 版本过低(当前 $NODE_VER),需要 >= 18"
|
||||
error "Node.js version $NODE_VER is too old. Requires >= 18"
|
||||
fi
|
||||
info "Node.js $NODE_VER ✓"
|
||||
info "Node.js $NODE_VER OK"
|
||||
|
||||
# ── 检查/安装 dnsmasq(WiFi 配网需要)──────────────────────────────────────
|
||||
# Install dnsmasq (required for WiFi captive portal)
|
||||
if ! command -v dnsmasq &>/dev/null; then
|
||||
info "安装 dnsmasq(WiFi 配网所需)..."
|
||||
info "Installing dnsmasq for WiFi captive portal..."
|
||||
if command -v apt-get &>/dev/null; then
|
||||
apt-get install -y -qq dnsmasq >/dev/null 2>&1
|
||||
elif command -v yum &>/dev/null; then
|
||||
@@ -38,25 +38,25 @@ if ! command -v dnsmasq &>/dev/null; then
|
||||
elif command -v apk &>/dev/null; then
|
||||
apk add --quiet dnsmasq >/dev/null 2>&1
|
||||
else
|
||||
warn "无法自动安装 dnsmasq,WiFi 配网功能可能不可用"
|
||||
warn "Cannot install dnsmasq. WiFi captive portal may not work."
|
||||
fi
|
||||
# 禁止 dnsmasq 系统服务自启(clawd 自己管理)
|
||||
# Disable system dnsmasq; clawd manages it directly
|
||||
systemctl disable dnsmasq 2>/dev/null || true
|
||||
systemctl stop dnsmasq 2>/dev/null || true
|
||||
fi
|
||||
if command -v dnsmasq &>/dev/null; then
|
||||
info "dnsmasq ✓"
|
||||
info "dnsmasq OK"
|
||||
fi
|
||||
|
||||
# ── 启用 NetworkManager(WiFi 配网需要)──────────────────────────────────────
|
||||
# Configure NetworkManager for WiFi
|
||||
if command -v nmcli &>/dev/null; then
|
||||
if ! systemctl is-active --quiet NetworkManager 2>/dev/null; then
|
||||
info "启用 NetworkManager..."
|
||||
info "Starting NetworkManager..."
|
||||
systemctl enable --now NetworkManager 2>/dev/null || true
|
||||
fi
|
||||
info "NetworkManager ✓"
|
||||
info "NetworkManager OK"
|
||||
|
||||
# 预写 DNS 劫持配置(运行时 /etc 可能为只读)
|
||||
# Write captive-portal DNS config
|
||||
NM_DNSMASQ_DIR="/etc/NetworkManager/dnsmasq-shared.d"
|
||||
mkdir -p "$NM_DNSMASQ_DIR"
|
||||
cat > "$NM_DNSMASQ_DIR/clawd-captive.conf" << 'DNSCONF'
|
||||
@@ -64,20 +64,20 @@ if command -v nmcli &>/dev/null; then
|
||||
# All DNS queries resolve to gateway to trigger captive portal
|
||||
address=/#/10.42.0.1
|
||||
DNSCONF
|
||||
info "DNS 劫持配置已写入 $NM_DNSMASQ_DIR ✓"
|
||||
info "DNS captive config written to $NM_DNSMASQ_DIR"
|
||||
fi
|
||||
|
||||
# ── WiFi rfkill 解锁(部分设备默认禁用 WiFi)────────────────────────────────
|
||||
# Unblock WiFi via rfkill
|
||||
for rf in /sys/class/rfkill/rfkill*; do
|
||||
if [ -f "$rf/type" ] && [ "$(cat "$rf/type")" = "wlan" ]; then
|
||||
if [ "$(cat "$rf/soft")" = "1" ]; then
|
||||
info "解锁 WiFi ($(basename "$rf"))..."
|
||||
info "Unblocking WiFi ($(basename "$rf"))..."
|
||||
echo 0 > "$rf/soft"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# 持久化:独立脚本 + systemd 服务,确保开机自动解锁 WiFi
|
||||
# Install rfkill unblock script + systemd unit for persistence
|
||||
RFKILL_SCRIPT="/usr/local/bin/clawd-unblock-wifi.sh"
|
||||
cat > "$RFKILL_SCRIPT" << 'SCRIPT'
|
||||
#!/bin/sh
|
||||
@@ -108,51 +108,74 @@ WantedBy=multi-user.target
|
||||
UNIT
|
||||
systemctl daemon-reload
|
||||
systemctl enable clawd-rfkill
|
||||
info "WiFi rfkill 解锁服务已创建 ✓"
|
||||
info "WiFi rfkill service installed"
|
||||
|
||||
# ── 安装 ttyd(Web 终端)────────────────────────────────────────────────────
|
||||
info "安装 ttyd..."
|
||||
# Install ttyd (Web terminal)
|
||||
info "Installing ttyd..."
|
||||
if apt-get install -y ttyd >/dev/null 2>&1; then
|
||||
info "ttyd 已安装 ✓"
|
||||
info "ttyd installed OK"
|
||||
else
|
||||
warn "ttyd 安装失败,Web 终端功能将不可用"
|
||||
warn "ttyd install failed. Web terminal will not be available."
|
||||
fi
|
||||
|
||||
# ── 安装 clawd ───────────────────────────────────────────────────────────────
|
||||
# Clone / update clawd
|
||||
INSTALL_DIR="/opt/clawd"
|
||||
CONFIG_DIR="/etc/clawd"
|
||||
ENV_FILE="$CONFIG_DIR/env"
|
||||
info "安装到 $INSTALL_DIR ..."
|
||||
info "Setting up $INSTALL_DIR ..."
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# 下载源码(若目录已有 package.json,视为离线/已解压部署,跳过 git/tarball;避免设备无法访问 github.com)
|
||||
if [ -f "package.json" ]; then
|
||||
info "检测到已有源码,跳过 git/tarball 下载"
|
||||
elif command -v git &>/dev/null; then
|
||||
if [ -d ".git" ]; then
|
||||
git pull --quiet
|
||||
else
|
||||
git clone --depth=1 https://github.com/stswangzhiping/clawd.git .
|
||||
# Use git if available, fall back to tarball
|
||||
CUTOS_REPO="https://git.cutos.ai/claw-daemon/clawd.git"
|
||||
if command -v git &>/dev/null && [ -d ".git" ]; then
|
||||
CURRENT_REMOTE=$(git remote get-url origin 2>/dev/null || echo "")
|
||||
if echo "$CURRENT_REMOTE" | grep -q "github.com"; then
|
||||
info "Migrating git remote to git.cutos.ai ..."
|
||||
git remote set-url origin "$CUTOS_REPO"
|
||||
fi
|
||||
info "Pulling latest code..."
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
git clean -fd
|
||||
elif [ -f "package.json" ]; then
|
||||
info "Files already present, skipping git clone"
|
||||
elif command -v git &>/dev/null; then
|
||||
git clone --depth=1 "$CUTOS_REPO" .
|
||||
else
|
||||
TARBALL_URL="https://github.com/stswangzhiping/clawd/archive/refs/heads/main.tar.gz"
|
||||
TARBALL_URL="https://git.cutos.ai/claw-daemon/clawd/archive/main.tar.gz"
|
||||
curl -fsSL "$TARBALL_URL" | tar -xz --strip-components=1
|
||||
fi
|
||||
|
||||
# 安装依赖
|
||||
info "安装 npm 依赖..."
|
||||
# Install npm dependencies
|
||||
info "Running npm install..."
|
||||
npm install --omit=dev --silent
|
||||
|
||||
# 创建可执行链接
|
||||
# Create symlink
|
||||
ln -sf "$INSTALL_DIR/bin/clawd.js" /usr/local/bin/clawd
|
||||
chmod +x "$INSTALL_DIR/bin/clawd.js"
|
||||
|
||||
info "clawd 已安装到 /usr/local/bin/clawd ✓"
|
||||
info "clawd symlinked to /usr/local/bin/clawd"
|
||||
|
||||
# Install RK3588S LVGL demo
|
||||
DEVICE_MODEL="$(tr -d '\0' </proc/device-tree/model 2>/dev/null || true)"
|
||||
if echo "$DEVICE_MODEL" | grep -qi 'RK3588S'; then
|
||||
DEMO_SRC="$INSTALL_DIR/lib/resource/3588s/demo"
|
||||
DEMO_DST="/usr/bin/demo"
|
||||
if [ -f "$DEMO_SRC" ]; then
|
||||
info "RK3588S detected, installing LVGL demo to $DEMO_DST"
|
||||
if [ -f "$DEMO_DST" ] && [ ! -f "${DEMO_DST}.clawd-bak" ]; then
|
||||
cp "$DEMO_DST" "${DEMO_DST}.clawd-bak"
|
||||
info "Backup created: ${DEMO_DST}.clawd-bak"
|
||||
fi
|
||||
install -m 0755 "$DEMO_SRC" "$DEMO_DST"
|
||||
else
|
||||
warn "RK3588S demo binary not found: $DEMO_SRC"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 创建配置目录 + 环境变量文件 ──────────────────────────────────────────────
|
||||
# Write default config files
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
if [ ! -f "$CONFIG_DIR/config.json" ]; then
|
||||
@@ -164,120 +187,116 @@ if [ ! -f "$CONFIG_DIR/config.json" ]; then
|
||||
"heartbeat_interval": 30
|
||||
}
|
||||
EOF
|
||||
info "配置文件已创建:$CONFIG_DIR/config.json ✓"
|
||||
info "Default config written to $CONFIG_DIR/config.json"
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
cat > "$ENV_FILE" <<EOF
|
||||
# clawd 环境变量(systemd EnvironmentFile)
|
||||
# 日志级别: debug / info / warn / error
|
||||
# clawd environment (loaded by systemd EnvironmentFile)
|
||||
# Log level: debug / info / warn / error
|
||||
CLAWD_LOG_LEVEL=info
|
||||
# 是否写日志文件(0=仅 journald)
|
||||
# Log to file (0 = journald only)
|
||||
CLAWD_LOG_FILE=1
|
||||
# 自定义服务器地址(留空则读 config.json)
|
||||
# Override server URL (default from config.json)
|
||||
# CLAWD_SERVER=wss://claw.cutos.ai/ws
|
||||
# BtMonitor(bluetoothctl)默认在程序内关闭,无需在此写 CLAWD_DISABLE_BT。
|
||||
# 若产品需要蓝牙指示灯,取消下一行注释:
|
||||
# Enable Bluetooth monitor (bluetoothctl); disabled by default
|
||||
# CLAWD_ENABLE_BT=1
|
||||
# OpenVFD sysfs 根路径(默认 /sys/class/leds/openvfd)
|
||||
# OpenVFD sysfs path (default: /sys/class/leds/openvfd)
|
||||
# CLAWD_OPENVFD_PATH=/sys/class/leds/openvfd
|
||||
# 数码管 vfdservice 管道(默认 /tmp/openvfd_service)
|
||||
# vfdservice pipe path (default: /tmp/openvfd_service)
|
||||
# CLAWD_VFD_PIPE=/tmp/openvfd_service
|
||||
# 多网口/特殊板型可固定 LAN 灯监控的以太网口(默认由 clawd 自动锁定首次 carrier 口)
|
||||
# Wired LAN interface for carrier detection
|
||||
# CLAWD_ETH_IFACE=end0
|
||||
EOF
|
||||
info "环境变量文件已创建:$ENV_FILE ✓"
|
||||
info "Default env file written to $ENV_FILE"
|
||||
fi
|
||||
|
||||
# ── 创建日志目录 ─────────────────────────────────────────────────────────────
|
||||
# Create log directory
|
||||
mkdir -p "$CONFIG_DIR/logs"
|
||||
info "日志目录:$CONFIG_DIR/logs ✓"
|
||||
info "Log directory: $CONFIG_DIR/logs"
|
||||
|
||||
# ── 创建 systemd service ────────────────────────────────────────────────────
|
||||
# Write systemd service file
|
||||
NODE_BIN=$(command -v node)
|
||||
SERVICE_FILE="/etc/systemd/system/clawd.service"
|
||||
|
||||
cat > "$SERVICE_FILE" <<EOF
|
||||
[Unit]
|
||||
Description=Claw Box Daemon
|
||||
Documentation=https://github.com/stswangzhiping/clawd
|
||||
Documentation=https://git.cutos.ai/claw-daemon/clawd
|
||||
After=NetworkManager.service
|
||||
Wants=NetworkManager.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# systemd-notify 由子进程执行,默认 NotifyAccess=main 会拒收;需 all 才能喂 WatchdogSec
|
||||
# NotifyAccess=all required for systemd-notify with WatchdogSec
|
||||
NotifyAccess=all
|
||||
EnvironmentFile=$ENV_FILE
|
||||
ExecStart=$NODE_BIN $INSTALL_DIR/bin/clawd.js
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
|
||||
# 重启策略
|
||||
# Restart policy
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
# 旧版 systemd 不认 StartLimitIntervalSec,用 StartLimitInterval=(秒)
|
||||
StartLimitInterval=300
|
||||
StartLimitBurst=10
|
||||
|
||||
# 优雅停止(10s 内 SIGTERM,超时 SIGKILL)
|
||||
# Allow 10s for graceful shutdown before SIGKILL
|
||||
TimeoutStopSec=10
|
||||
KillMode=mixed
|
||||
KillSignal=SIGTERM
|
||||
|
||||
# 资源限制(防止失控)
|
||||
# Resource limits
|
||||
MemoryMax=256M
|
||||
CPUQuota=50%
|
||||
TasksMax=64
|
||||
|
||||
# 安全加固(ttyd 子进程需要 setuid sudo,不能用 NoNewPrivileges/strict)
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=$CONFIG_DIR /tmp
|
||||
# Sandbox disabled: clawd needs to write system/config files on some devices
|
||||
|
||||
# 日志
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=clawd
|
||||
|
||||
# systemd Watchdog(60s 无响应视为挂死)
|
||||
# systemd Watchdog: restart if no heartbeat within 60s
|
||||
WatchdogSec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
info "systemd 服务文件已创建 ✓"
|
||||
info "systemd service file written"
|
||||
|
||||
# ── journald 日志限制(可选) ────────────────────────────────────────────────
|
||||
# Configure journald retention
|
||||
JOURNAL_CONF="/etc/systemd/journald.conf.d/clawd.conf"
|
||||
if [ ! -f "$JOURNAL_CONF" ]; then
|
||||
mkdir -p /etc/systemd/journald.conf.d
|
||||
cat > "$JOURNAL_CONF" <<EOF
|
||||
# clawd journald 限制
|
||||
# clawd journald limits
|
||||
[Journal]
|
||||
SystemMaxUse=100M
|
||||
MaxFileSec=7day
|
||||
EOF
|
||||
systemctl restart systemd-journald 2>/dev/null || true
|
||||
info "journald 日志限制已配置 ✓"
|
||||
info "journald config written"
|
||||
fi
|
||||
|
||||
# ── 启用并启动 ──────────────────────────────────────────────────────────────
|
||||
# Enable and start clawd
|
||||
systemctl daemon-reload
|
||||
systemctl enable clawd
|
||||
systemctl restart clawd
|
||||
|
||||
sleep 2
|
||||
if systemctl is-active --quiet clawd; then
|
||||
info "clawd 服务运行中 ✓"
|
||||
info "clawd is running"
|
||||
echo ""
|
||||
echo " 查看日志: journalctl -u clawd -f"
|
||||
echo " 查看状态: systemctl status clawd"
|
||||
echo " 停止服务: systemctl stop clawd"
|
||||
echo " 配置文件: $CONFIG_DIR/config.json"
|
||||
echo " 环境变量: $ENV_FILE"
|
||||
echo " 文件日志: $CONFIG_DIR/logs/clawd.log"
|
||||
echo " Logs: journalctl -u clawd -f"
|
||||
echo " Status: systemctl status clawd"
|
||||
echo " Stop: systemctl stop clawd"
|
||||
echo " Config: $CONFIG_DIR/config.json"
|
||||
echo " Env: $ENV_FILE"
|
||||
echo " Log dir: $CONFIG_DIR/logs/clawd.log"
|
||||
echo ""
|
||||
else
|
||||
warn "服务启动失败,请检查日志:"
|
||||
warn "clawd failed to start. Check logs:"
|
||||
echo " journalctl -u clawd -n 50 --no-pager"
|
||||
fi
|
||||
|
||||
454
lib/channel/weixin.js
Normal file
454
lib/channel/weixin.js
Normal file
@@ -0,0 +1,454 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* channel.weixin — WeChat login via ilinkai.weixin.qq.com API.
|
||||
*
|
||||
* Ported from the reference weixin-login.js script.
|
||||
* Requires Node.js >= 18 (global fetch).
|
||||
*
|
||||
* method: login
|
||||
* params : { callId, timeout, emit }
|
||||
* returns: { abort }
|
||||
*
|
||||
* emit(payload) sends a sys-call reply upstream:
|
||||
* { action:'event', event:'qrcode', data:{ url, expire:30, index } }
|
||||
* { action:'progress', event:'scanned', data:{ status:'waiting_confirm' } }
|
||||
* { action:'finish', event:'success', data:{ accountId } }
|
||||
* { action:'finish', event:'failed', code, message }
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const log = require('../logger');
|
||||
|
||||
// ── Constants (from reference script) ────────────────────────────────────────
|
||||
|
||||
const FIXED_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
||||
const DEFAULT_BOT_TYPE = '3';
|
||||
const QR_LONG_POLL_TIMEOUT_MS = 35_000;
|
||||
const ACTIVE_LOGIN_TTL_MS = 5 * 60_000;
|
||||
const MAX_QR_REFRESH_COUNT = 3;
|
||||
const CHANNEL_VERSION = '2.4.3';
|
||||
const ILINK_APP_ID = 'bot';
|
||||
const ILINK_APP_CLIENT_VERSION = String(_buildClientVersion(CHANNEL_VERSION));
|
||||
|
||||
function _buildClientVersion(version) {
|
||||
const parts = String(version).split('.').map(p => parseInt(p, 10));
|
||||
const major = parts[0] || 0;
|
||||
const minor = parts[1] || 0;
|
||||
const patch = parts[2] || 0;
|
||||
return ((major & 0xff) << 16) | ((minor & 0xff) << 8) | (patch & 0xff);
|
||||
}
|
||||
|
||||
// ── State-dir helpers (mirrors reference script) ─────────────────────────────
|
||||
|
||||
function _resolveStateDir() {
|
||||
return path.join('/home/sts', '.openclaw');
|
||||
}
|
||||
|
||||
function _ensureStsOwnership(filePath) {
|
||||
try {
|
||||
execFileSync('chown', ['sts:sts', filePath], { stdio: 'ignore' });
|
||||
} catch (err) {
|
||||
log.warn('weixin', `chown sts:sts failed for ${filePath}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function _resolveWeixinStateDir() { return path.join(_resolveStateDir(), 'openclaw-weixin'); }
|
||||
function _resolveAccountIndexPath(){ return path.join(_resolveWeixinStateDir(), 'accounts.json'); }
|
||||
function _resolveAccountsDir() { return path.join(_resolveWeixinStateDir(), 'accounts'); }
|
||||
function _resolveAccountPath(id) { return path.join(_resolveAccountsDir(), `${id}.json`); }
|
||||
|
||||
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
||||
const INVALID_CHARS = /[^a-z0-9_-]+/g;
|
||||
|
||||
function _normalizeAccountId(value) {
|
||||
const trimmed = String(value || '').trim();
|
||||
if (!trimmed) return 'default';
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (VALID_ID_RE.test(trimmed)) return lower;
|
||||
return lower.replace(INVALID_CHARS, '-').replace(/^-+/, '').replace(/-+$/, '').slice(0, 64) || 'default';
|
||||
}
|
||||
|
||||
function _listIndexedAccountIds() {
|
||||
try {
|
||||
const p = _resolveAccountIndexPath();
|
||||
if (!fs.existsSync(p)) return [];
|
||||
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
return Array.isArray(parsed) ? parsed.filter(id => typeof id === 'string' && id.trim()) : [];
|
||||
} catch (_) { return []; }
|
||||
}
|
||||
|
||||
function _registerAccountId(accountId) {
|
||||
fs.mkdirSync(_resolveWeixinStateDir(), { recursive: true });
|
||||
const existing = _listIndexedAccountIds();
|
||||
if (existing.includes(accountId)) return;
|
||||
const indexPath = _resolveAccountIndexPath();
|
||||
fs.writeFileSync(indexPath, JSON.stringify([...existing, accountId], null, 2), 'utf8');
|
||||
_ensureStsOwnership(indexPath);
|
||||
}
|
||||
|
||||
function _loadAccount(accountId) {
|
||||
try {
|
||||
const p = _resolveAccountPath(accountId);
|
||||
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
} catch (_) {}
|
||||
// legacy token fallback
|
||||
try {
|
||||
const legacyPath = path.join(_resolveStateDir(), 'credentials', 'openclaw-weixin', 'credentials.json');
|
||||
if (fs.existsSync(legacyPath)) {
|
||||
const parsed = JSON.parse(fs.readFileSync(legacyPath, 'utf8'));
|
||||
if (typeof parsed.token === 'string') return { token: parsed.token };
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _saveAccount(accountId, update) {
|
||||
fs.mkdirSync(_resolveAccountsDir(), { recursive: true });
|
||||
const existing = _loadAccount(accountId) || {};
|
||||
const token = (update.token || '').trim() || existing.token;
|
||||
const baseUrl = (update.baseUrl || '').trim() || existing.baseUrl;
|
||||
const userId = (update.userId || '').trim() || (existing.userId || '').trim() || undefined;
|
||||
const data = {
|
||||
...(token ? { token, savedAt: new Date().toISOString() } : {}),
|
||||
...(baseUrl ? { baseUrl } : {}),
|
||||
...(userId ? { userId } : {}),
|
||||
};
|
||||
const filePath = _resolveAccountPath(accountId);
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||
_ensureStsOwnership(filePath);
|
||||
try { fs.chmodSync(filePath, 0o600); } catch (_) {}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function _clearStaleAccountsForUserId(currentAccountId, userId) {
|
||||
if (!userId) return;
|
||||
for (const id of _listIndexedAccountIds()) {
|
||||
if (id === currentAccountId) continue;
|
||||
const data = _loadAccount(id);
|
||||
if (data && (data.userId || '').trim() === userId) {
|
||||
log.info('weixin', `removing stale account with same userId: ${id}`);
|
||||
try { fs.unlinkSync(_resolveAccountPath(id)); } catch (_) {}
|
||||
const existing = _listIndexedAccountIds();
|
||||
const indexPath = _resolveAccountIndexPath();
|
||||
fs.writeFileSync(indexPath, JSON.stringify(existing.filter(x => x !== id), null, 2), 'utf8');
|
||||
_ensureStsOwnership(indexPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _getLocalBotTokenList() {
|
||||
const tokens = [];
|
||||
const ids = _listIndexedAccountIds();
|
||||
for (let i = ids.length - 1; i >= 0 && tokens.length < 10; i--) {
|
||||
const token = (_loadAccount(ids[i]) || {}).token;
|
||||
if (token && token.trim()) tokens.push(token.trim());
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// ── HTTP helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function _commonHeaders() {
|
||||
return {
|
||||
'iLink-App-Id': ILINK_APP_ID,
|
||||
'iLink-App-ClientVersion': ILINK_APP_CLIENT_VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
function _randomWechatUin() {
|
||||
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
||||
return Buffer.from(String(uint32), 'utf8').toString('base64');
|
||||
}
|
||||
|
||||
function _postHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'AuthorizationType': 'ilink_bot_token',
|
||||
'X-WECHAT-UIN': _randomWechatUin(),
|
||||
..._commonHeaders(),
|
||||
};
|
||||
}
|
||||
|
||||
async function _postJson(baseUrl, endpoint, body, timeoutMs) {
|
||||
const url = `${baseUrl.replace(/\/$/, '')}/${endpoint}`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs || 15_000);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: _postHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
||||
return JSON.parse(text);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function _getJson(baseUrl, endpoint, timeoutMs) {
|
||||
const url = `${baseUrl.replace(/\/$/, '')}/${endpoint}`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs || QR_LONG_POLL_TIMEOUT_MS + 5_000);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: _commonHeaders(),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
||||
return JSON.parse(text);
|
||||
} catch (err) {
|
||||
if (err && err.name === 'AbortError') return { status: 'wait' };
|
||||
throw err;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// ── WeChat API calls ──────────────────────────────────────────────────────────
|
||||
|
||||
async function _fetchQRCode(botType) {
|
||||
const localTokenList = _getLocalBotTokenList();
|
||||
const data = await _postJson(
|
||||
FIXED_BASE_URL,
|
||||
`ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
|
||||
{ local_token_list: localTokenList },
|
||||
15_000
|
||||
);
|
||||
if (data.ret !== undefined && data.ret !== 0) {
|
||||
throw new Error(`get_bot_qrcode ret=${data.ret} errmsg=${data.errmsg || ''}`);
|
||||
}
|
||||
if (!data.qrcode || !data.qrcode_img_content) {
|
||||
throw new Error(`get_bot_qrcode response missing qrcode/qrcode_img_content`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function _pollQRStatus(apiBaseUrl, qrcode) {
|
||||
try {
|
||||
const endpoint = `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
|
||||
const data = await _getJson(apiBaseUrl, endpoint, QR_LONG_POLL_TIMEOUT_MS);
|
||||
if (data.ret !== undefined && data.ret !== 0) {
|
||||
throw new Error(`get_qrcode_status ret=${data.ret} errmsg=${data.errmsg || ''}`);
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (err && err.name === 'AbortError') return { status: 'wait' };
|
||||
log.warn('weixin', `pollQRStatus error (will retry): ${err.message}`);
|
||||
return { status: 'wait' };
|
||||
}
|
||||
}
|
||||
|
||||
function _bumpOpenClawConfigTimestamp() {
|
||||
const stateDir = _resolveStateDir();
|
||||
const candidates = [
|
||||
process.env.OPENCLAW_CONFIG || '',
|
||||
path.join(stateDir, 'openclaw.json'),
|
||||
].filter(Boolean);
|
||||
const configPath = candidates.find(p => { try { return fs.existsSync(p); } catch (_) { return false; } });
|
||||
if (!configPath) return;
|
||||
try {
|
||||
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
if (!cfg.channels) cfg.channels = {};
|
||||
if (!cfg.channels['openclaw-weixin']) cfg.channels['openclaw-weixin'] = {};
|
||||
cfg.channels['openclaw-weixin'].channelConfigUpdatedAt = new Date().toISOString();
|
||||
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf8');
|
||||
log.info('weixin', `bumped openclaw.json timestamp: ${configPath}`);
|
||||
} catch (err) {
|
||||
log.warn('weixin', `bump timestamp failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── login() ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Start WeChat QR login.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.callId - sys-call id (for logging)
|
||||
* @param {number} opts.timeout - overall timeout in seconds (default 180)
|
||||
* @param {string} opts.botType - ilink bot type (default '3')
|
||||
* @param {function} opts.emit - emit(payload) send event upstream
|
||||
* @returns {{ abort: function, onReply: function }} task handle
|
||||
*/
|
||||
function login({ callId, timeout = 180, botType = DEFAULT_BOT_TYPE, emit }) {
|
||||
let aborted = false;
|
||||
let finished = false;
|
||||
|
||||
function abort() {
|
||||
aborted = true;
|
||||
log.info('weixin', `callId=${callId} aborted`);
|
||||
}
|
||||
|
||||
const timeoutMs = Math.max(timeout * 1000, 30_000);
|
||||
|
||||
// Run async without blocking caller
|
||||
_runLogin({ callId, timeoutMs, botType, emit, isAborted: () => aborted })
|
||||
.then(() => { finished = true; })
|
||||
.catch((err) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
log.error('weixin', `callId=${callId} login error: ${err.message}`);
|
||||
emit({ action: 'finish', event: 'failed', code: 500, message: err.message });
|
||||
});
|
||||
|
||||
return { abort };
|
||||
}
|
||||
|
||||
async function _runLogin({ callId, timeoutMs, botType, emit, isAborted }) {
|
||||
let qrRefreshCount = 1;
|
||||
let scannedEmitted = false;
|
||||
let activeLogin = null;
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
async function startOrRefreshQr() {
|
||||
const qrData = await _fetchQRCode(botType);
|
||||
activeLogin = {
|
||||
qrcode: qrData.qrcode,
|
||||
qrcodeUrl: qrData.qrcode_img_content,
|
||||
startedAt: Date.now(),
|
||||
apiBaseUrl: FIXED_BASE_URL,
|
||||
};
|
||||
emit({
|
||||
action: 'event',
|
||||
event: 'qrcode',
|
||||
// url = qrcode_img_content: the WeChat URL encoded inside the QR image (use this for QR rendering)
|
||||
// code = qrcode: the polling ticket used by _pollQRStatus (NOT for QR rendering)
|
||||
data: { url: activeLogin.qrcodeUrl, code: activeLogin.qrcode, expire: 30, index: qrRefreshCount },
|
||||
});
|
||||
log.info('weixin', `callId=${callId} qrcode emitted index=${qrRefreshCount}`);
|
||||
}
|
||||
|
||||
await startOrRefreshQr();
|
||||
|
||||
while (!isAborted() && Date.now() < deadline) {
|
||||
// Refresh if local TTL exceeded
|
||||
if (Date.now() - activeLogin.startedAt >= ACTIVE_LOGIN_TTL_MS) {
|
||||
log.info('weixin', `callId=${callId} QR TTL expired, refreshing`);
|
||||
qrRefreshCount++;
|
||||
if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
|
||||
emit({ action: 'finish', event: 'failed', code: 1003, message: 'QR expired too many times' });
|
||||
return;
|
||||
}
|
||||
await startOrRefreshQr();
|
||||
scannedEmitted = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const status = await _pollQRStatus(activeLogin.apiBaseUrl, activeLogin.qrcode);
|
||||
|
||||
if (isAborted()) return;
|
||||
|
||||
switch (status.status) {
|
||||
case 'wait':
|
||||
break;
|
||||
|
||||
case 'scaned':
|
||||
case 'scaned_but_redirect': {
|
||||
if (status.redirect_host) {
|
||||
activeLogin.apiBaseUrl = `https://${status.redirect_host}`;
|
||||
}
|
||||
if (!scannedEmitted) {
|
||||
scannedEmitted = true;
|
||||
emit({ action: 'progress', event: 'scanned', data: { status: 'waiting_confirm' } });
|
||||
log.info('weixin', `callId=${callId} scanned`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'need_verifycode': {
|
||||
// Verify code entry not supported; treat as failure
|
||||
log.warn('weixin', `callId=${callId} need_verifycode not supported`);
|
||||
emit({ action: 'finish', event: 'failed', code: 1005, message: 'verify code required (not supported)' });
|
||||
return;
|
||||
}
|
||||
|
||||
case 'expired': {
|
||||
log.info('weixin', `callId=${callId} QR expired, refreshing`);
|
||||
qrRefreshCount++;
|
||||
if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
|
||||
emit({ action: 'finish', event: 'failed', code: 1001, message: 'qrcode expired' });
|
||||
return;
|
||||
}
|
||||
await startOrRefreshQr();
|
||||
scannedEmitted = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'verify_code_blocked': {
|
||||
log.warn('weixin', `callId=${callId} verify_code_blocked`);
|
||||
qrRefreshCount++;
|
||||
if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
|
||||
emit({ action: 'finish', event: 'failed', code: 1002, message: 'verify_code blocked' });
|
||||
return;
|
||||
}
|
||||
await startOrRefreshQr();
|
||||
scannedEmitted = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'binded_redirect': {
|
||||
// WeChat server considers this OpenClaw already bound.
|
||||
// Check if local token exists — if yes, treat as success; if not, local state was
|
||||
// cleared (e.g. logout) but remote binding wasn't revoked → state inconsistency.
|
||||
const indexedIds = _listIndexedAccountIds();
|
||||
const hasLocalToken = indexedIds.some(id => {
|
||||
const acct = _loadAccount(id);
|
||||
return acct && acct.token;
|
||||
});
|
||||
if (hasLocalToken) {
|
||||
log.info('weixin', `callId=${callId} already connected, local token intact`);
|
||||
emit({ action: 'finish', event: 'success', data: { accountId: indexedIds[0] } });
|
||||
} else {
|
||||
log.warn('weixin', `callId=${callId} binded_redirect but local token missing — state inconsistency`);
|
||||
emit({
|
||||
action: 'finish', event: 'failed', code: 1010,
|
||||
message: '此微信账号已绑定过本设备,但本地登录记录已丢失。请换用另一个微信账号扫码重新登录。',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 'confirmed': {
|
||||
if (!status.ilink_bot_id) throw new Error('confirmed: missing ilink_bot_id');
|
||||
if (!status.bot_token) throw new Error('confirmed: missing bot_token');
|
||||
const accountId = _normalizeAccountId(status.ilink_bot_id);
|
||||
const filePath = _saveAccount(accountId, {
|
||||
token: status.bot_token,
|
||||
baseUrl: status.baseurl,
|
||||
userId: status.ilink_user_id,
|
||||
});
|
||||
_registerAccountId(accountId);
|
||||
if (status.ilink_user_id) _clearStaleAccountsForUserId(accountId, status.ilink_user_id);
|
||||
_bumpOpenClawConfigTimestamp();
|
||||
log.info('weixin', `callId=${callId} login success accountId=${accountId} file=${filePath}`);
|
||||
emit({ action: 'finish', event: 'success', data: { accountId } });
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
log.warn('weixin', `callId=${callId} unknown status: ${JSON.stringify(status)}`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Brief pause between polls to avoid tight looping on 'wait'
|
||||
if (status.status === 'wait') {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAborted()) {
|
||||
emit({ action: 'finish', event: 'failed', code: 1004, message: 'login timeout' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { login };
|
||||
1568
lib/client.js
1568
lib/client.js
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const log = require('./logger');
|
||||
const { isRK3566, readDeviceModel } = require('./led/detect');
|
||||
const { isRK3566, isRK3588, readDeviceModel } = require('./led/detect');
|
||||
|
||||
function loadImpl() {
|
||||
const forced = String(process.env.CLAWD_LED_IMPL || '').trim().toLowerCase();
|
||||
@@ -10,6 +10,8 @@ function loadImpl() {
|
||||
let name;
|
||||
if (forced) {
|
||||
name = forced;
|
||||
} else if (isRK3588()) {
|
||||
name = 'rk3588-lvgl';
|
||||
} else if (isRK3566()) {
|
||||
name = 'rk3566';
|
||||
} else {
|
||||
@@ -21,6 +23,10 @@ function loadImpl() {
|
||||
log.info('led', `LED/VFD backend → rk3566-openvfd (${model || 'unknown model'})`);
|
||||
return require('./led/rk3566-openvfd');
|
||||
}
|
||||
if (name === 'rk3588-lvgl' || name === '3588' || name === 'rk3588') {
|
||||
log.info('led', `LED/VFD backend → rk3588-lvgl (${model || 'unknown model'})`);
|
||||
return require('./led/rk3588-lvgl');
|
||||
}
|
||||
if (name === 'noop' || name === 'none' || name === 'off') {
|
||||
log.info('led', `LED/VFD backend → noop (${model || 'unknown model'})`);
|
||||
return require('./led/noop');
|
||||
|
||||
@@ -16,7 +16,12 @@ function isRK3566() {
|
||||
return /RK3566/i.test(readDeviceModel());
|
||||
}
|
||||
|
||||
function isRK3588() {
|
||||
return /RK3588/i.test(readDeviceModel());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readDeviceModel,
|
||||
isRK3566,
|
||||
isRK3588,
|
||||
};
|
||||
|
||||
81
lib/led/rk3588-lvgl.js
Normal file
81
lib/led/rk3588-lvgl.js
Normal file
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const log = require('../logger');
|
||||
|
||||
const LVGL_CMD_FIFO = process.env.CLAWD_LVGL_CMD_FIFO || '/tmp/lvgl_cmd';
|
||||
|
||||
function writeLvglCommand(command) {
|
||||
try {
|
||||
const fd = fs.openSync(LVGL_CMD_FIFO, fs.constants.O_WRONLY | fs.constants.O_NONBLOCK);
|
||||
fs.writeSync(fd, `${command}\n`);
|
||||
fs.closeSync(fd);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log.warn('display', `lvgl cmd failed (${command}): ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class BasicLed {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this._current = null;
|
||||
}
|
||||
on() { this._current = 'on'; log.debug('led', `[rk3588-lvgl] ${this.name} on`); }
|
||||
off() { this._current = 'off'; log.debug('led', `[rk3588-lvgl] ${this.name} off`); }
|
||||
blink() { this._current = 'blink'; log.debug('led', `[rk3588-lvgl] ${this.name} blink`); }
|
||||
destroy() { this._current = 'off'; log.debug('led', `[rk3588-lvgl] ${this.name} destroy`); }
|
||||
}
|
||||
|
||||
class StatusLed {
|
||||
setSetup() { log.debug('led', '[rk3588-lvgl] status setup'); }
|
||||
setApps() { log.debug('led', '[rk3588-lvgl] status apps'); }
|
||||
off() { log.debug('led', '[rk3588-lvgl] status off'); }
|
||||
}
|
||||
|
||||
class Display {
|
||||
showAP() {
|
||||
if (writeLvglCommand('show_ap')) {
|
||||
log.info('display', '显示屏 → AP(闪烁)');
|
||||
}
|
||||
}
|
||||
|
||||
showConn() {
|
||||
if (writeLvglCommand('show_conn')) {
|
||||
log.info('display', '显示屏 → Conn(闪烁)');
|
||||
}
|
||||
}
|
||||
|
||||
showErr0() {
|
||||
if (writeLvglCommand('show_err0')) {
|
||||
log.info('display', '显示屏 → Err0');
|
||||
}
|
||||
}
|
||||
|
||||
showTime() {
|
||||
if (writeLvglCommand('show_time')) {
|
||||
log.info('display', '显示屏 → 时间');
|
||||
}
|
||||
}
|
||||
|
||||
showPin(pin) {
|
||||
const s = String(pin || '').padStart(4, '0').slice(-4);
|
||||
if (writeLvglCommand(`show_pin:${s}`)) {
|
||||
log.info('display', `显示屏 → PIN: ${s}(闪烁)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LanLed {
|
||||
start() { log.debug('led', '[rk3588-lvgl] LAN start ignored'); }
|
||||
stop() { log.debug('led', '[rk3588-lvgl] LAN stop ignored'); }
|
||||
}
|
||||
|
||||
const led = new BasicLed('wifi');
|
||||
led.bt = new BasicLed('bt');
|
||||
led.status = new StatusLed();
|
||||
led.display = new Display();
|
||||
led.lan = new LanLed();
|
||||
|
||||
module.exports = led;
|
||||
298
lib/network.js
298
lib/network.js
@@ -10,6 +10,7 @@ const AP_IP = '10.42.0.1';
|
||||
const AP_PASSWORD = '12345678';
|
||||
const AP_IFACE = process.env.CLAWD_WIFI_IFACE || '';
|
||||
const CON_NAME = 'clawd-hotspot';
|
||||
const AP_RETRY_TOKEN_FILE = '/run/clawd-ap-retry.token';
|
||||
|
||||
/** 产品 RJ45 在 sysfs 中的默认名;等价于检测 `cat /sys/class/net/end0/carrier` */
|
||||
const DEFAULT_ETH_IFACE = 'end0';
|
||||
@@ -92,28 +93,15 @@ function hasLanCableCarrier() {
|
||||
return hasWiredCarrier();
|
||||
}
|
||||
|
||||
function _tryPingInternet() {
|
||||
function _tryPingDefaultInternet() {
|
||||
try {
|
||||
run('ping -c 1 -W 3 8.8.8.8');
|
||||
return true;
|
||||
} catch (_) {}
|
||||
|
||||
// 开热点时默认路由可能走 wlan,无 -I 的 ping 会误判;指定有线口再试
|
||||
const wired = getWiredIfaceWithCarrier();
|
||||
if (wired) {
|
||||
try {
|
||||
run(`ping -c 1 -W 3 -I ${wired} 8.8.8.8`);
|
||||
return true;
|
||||
} catch (_) {}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅经有线口 ping 公网(不依赖默认路由)。
|
||||
* AP 开启时 hasInternet() 易误判;维持 WS / 网络监视时用此兜底。
|
||||
*/
|
||||
function hasWiredInternetProbe() {
|
||||
function _tryPingWiredInternet() {
|
||||
const wired = getWiredIfaceWithCarrier();
|
||||
if (!wired) return false;
|
||||
try {
|
||||
@@ -124,18 +112,31 @@ function hasWiredInternetProbe() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否有互联网连接(nmcli 连通性 + ping 兜底)
|
||||
* 仅经有线口 ping 公网(不依赖默认路由)。
|
||||
*/
|
||||
function hasWiredInternetProbe() {
|
||||
return _tryPingWiredInternet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否有真实互联网连接。
|
||||
* 注意:NetworkManager 的 limited/local 可能只是 AP 本地网络或 captive 状态,不能当公网可用。
|
||||
*/
|
||||
function hasInternet() {
|
||||
const wifiSta = isWifiStaConnected();
|
||||
const wired = getWiredIfaceWithCarrier();
|
||||
|
||||
// 物理层快检:无 WiFi STA 且无任何有线 carrier → 立即 false(nmcli 有缓存,不可信)
|
||||
if (!isWifiStaConnected() && !hasWiredCarrier()) return false;
|
||||
if (!wifiSta && !wired) return false;
|
||||
|
||||
try {
|
||||
const out = run('nmcli networking connectivity check').trim();
|
||||
if (out === 'full' || out === 'limited') return true;
|
||||
if (out === 'full') return true;
|
||||
} catch (_) {}
|
||||
|
||||
return _tryPingInternet();
|
||||
if (wifiSta) return _tryPingDefaultInternet();
|
||||
if (wired) return _tryPingWiredInternet();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,6 +266,7 @@ function nmcliAsync(args, timeoutMs = 60000) {
|
||||
* @returns {Promise<{ success: boolean, error?: string }>}
|
||||
*/
|
||||
async function connectWifi(ssid, password) {
|
||||
cancelHotspotRadioRetry(`准备连接 WiFi: ${ssid}`);
|
||||
const iface = getWifiIface();
|
||||
log.info('network', `尝试连接 WiFi: ${ssid}(ifname=${iface})`);
|
||||
try {
|
||||
@@ -272,14 +274,36 @@ async function connectWifi(ssid, password) {
|
||||
await nmcliAsync(['connection', 'delete', ssid], 15000);
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
await nmcliAsync(['device', 'set', iface, 'managed', 'yes'], 8000);
|
||||
} catch (_) {}
|
||||
await _resetWifiRadioForSTA(iface);
|
||||
|
||||
const args = ['device', 'wifi', 'connect', ssid];
|
||||
if (password) args.push('password', password);
|
||||
args.push('ifname', iface);
|
||||
await nmcliAsync(args, 120000);
|
||||
if (password) {
|
||||
// 显式创建 STA profile,并固定为 WPA2-PSK only。
|
||||
// RK3588/Broadcom DHD 对 NetworkManager 默认生成的 SAE/FT/WPA-PSK-SHA256 混合参数不稳定,
|
||||
// 可能表现为一直 associating -> disconnected,最后误报“需要密钥”。
|
||||
await nmcliAsync([
|
||||
'connection', 'add',
|
||||
'type', 'wifi',
|
||||
'ifname', iface,
|
||||
'con-name', ssid,
|
||||
'ssid', ssid,
|
||||
], 15000);
|
||||
|
||||
await nmcliAsync([
|
||||
'connection', 'modify', ssid,
|
||||
// 连接成功前先禁止自动连接,避免失败恢复 AP 时 NM 又自动抢占 wlan0。
|
||||
'connection.autoconnect', 'no',
|
||||
'802-11-wireless-security.key-mgmt', 'wpa-psk',
|
||||
'802-11-wireless-security.proto', 'rsn',
|
||||
'802-11-wireless-security.pairwise', 'ccmp',
|
||||
'802-11-wireless-security.group', 'ccmp',
|
||||
'802-11-wireless-security.pmf', 'disable',
|
||||
'802-11-wireless-security.psk', password,
|
||||
], 15000);
|
||||
|
||||
await nmcliAsync(['connection', 'up', 'id', ssid, 'ifname', iface], 120000);
|
||||
} else {
|
||||
await nmcliAsync(['device', 'wifi', 'connect', ssid, 'ifname', iface], 120000);
|
||||
}
|
||||
await _ensureActiveWifiAutoconnect();
|
||||
|
||||
const deadline = Date.now() + CONNECT_WIFI_STA_WAIT_MS;
|
||||
@@ -299,11 +323,201 @@ async function connectWifi(ssid, password) {
|
||||
}
|
||||
return { success: false, error: '超时:网卡未进入已连接状态' };
|
||||
} catch (e) {
|
||||
try { await nmcliAsync(['connection', 'modify', ssid, 'connection.autoconnect', 'no'], 8000); } catch (_) {}
|
||||
log.error('network', `WiFi 连接失败: ${e.message}`);
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
function _newHotspotRetryToken() {
|
||||
const token = `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
||||
try {
|
||||
fs.writeFileSync(AP_RETRY_TOKEN_FILE, token, { mode: 0o600 });
|
||||
} catch (e) {
|
||||
log.warn('network', `写入 AP retry token 失败: ${e.message}`);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function cancelHotspotRadioRetry(reason = 'cancel') {
|
||||
try {
|
||||
fs.unlinkSync(AP_RETRY_TOKEN_FILE);
|
||||
log.info('network', `已取消后台 AP retry: ${reason}`);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function _resetWifiRadioForSTA(iface, reason = '准备连接 STA 前重置 WiFi radio') {
|
||||
log.warn('network', `${reason}: ${iface}`);
|
||||
|
||||
try { await nmcliAsync(['connection', 'down', CON_NAME], 8000); } catch (_) {}
|
||||
try { await nmcliAsync(['connection', 'delete', CON_NAME], 8000); } catch (_) {}
|
||||
try { await nmcliAsync(['device', 'disconnect', iface], 8000); } catch (_) {}
|
||||
|
||||
try {
|
||||
await nmcliAsync(['radio', 'wifi', 'off'], 10000);
|
||||
} catch (e) {
|
||||
log.warn('network', `关闭 WiFi radio 失败: ${e.message}`);
|
||||
}
|
||||
|
||||
await _delay(2500);
|
||||
|
||||
try {
|
||||
await nmcliAsync(['radio', 'wifi', 'on'], 10000);
|
||||
} catch (e) {
|
||||
log.warn('network', `开启 WiFi radio 失败: ${e.message}`);
|
||||
}
|
||||
|
||||
await _delay(5000);
|
||||
try { await nmcliAsync(['device', 'set', iface, 'managed', 'yes'], 8000); } catch (_) {}
|
||||
try { await nmcliAsync(['device', 'wifi', 'rescan', 'ifname', iface], 15000); } catch (_) {}
|
||||
await _delay(1500);
|
||||
}
|
||||
|
||||
function _resetWifiRadioForAP(iface, reason = '准备 AP 前重置 WiFi radio') {
|
||||
log.warn('network', `${reason}: ${iface}`);
|
||||
|
||||
try { nmcliSync(['connection', 'down', CON_NAME], 8000); } catch (_) {}
|
||||
try { nmcliSync(['connection', 'delete', CON_NAME], 8000); } catch (_) {}
|
||||
try { nmcliSync(['device', 'disconnect', iface], 8000); } catch (_) {}
|
||||
|
||||
try {
|
||||
nmcliSync(['radio', 'wifi', 'off'], 10000);
|
||||
} catch (e) {
|
||||
log.warn('network', `关闭 WiFi radio 失败: ${e.message}`);
|
||||
}
|
||||
|
||||
sleep(2500);
|
||||
|
||||
try {
|
||||
nmcliSync(['radio', 'wifi', 'on'], 10000);
|
||||
} catch (e) {
|
||||
log.warn('network', `开启 WiFi radio 失败: ${e.message}`);
|
||||
}
|
||||
|
||||
sleep(5000);
|
||||
try { nmcliSync(['device', 'set', iface, 'managed', 'yes'], 8000); } catch (_) {}
|
||||
}
|
||||
|
||||
function _spawnHotspotRadioRetry(ssid, iface) {
|
||||
const token = _newHotspotRetryToken();
|
||||
const script = `
|
||||
set -u
|
||||
log() { logger -t clawd-ap-retry "$*"; }
|
||||
check_token() {
|
||||
if [ ! -f "$TOKEN_FILE" ] || [ "$(cat "$TOKEN_FILE" 2>/dev/null || true)" != "$TOKEN" ]; then
|
||||
log "AP retry canceled"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
log "AP retry started: ssid=$SSID iface=$IFACE"
|
||||
check_token
|
||||
nmcli connection down "$CON_NAME" >/dev/null 2>&1 || true
|
||||
nmcli connection delete "$CON_NAME" >/dev/null 2>&1 || true
|
||||
nmcli device disconnect "$IFACE" >/dev/null 2>&1 || true
|
||||
check_token
|
||||
nmcli radio wifi off >/dev/null 2>&1 || true
|
||||
sleep 2.5
|
||||
# If canceled while radio is off, always turn it back on before exiting.
|
||||
nmcli radio wifi on >/dev/null 2>&1 || true
|
||||
sleep 5
|
||||
check_token
|
||||
nmcli device set "$IFACE" managed yes >/dev/null 2>&1 || true
|
||||
check_token
|
||||
if ! nmcli connection add type wifi ifname "$IFACE" con-name "$CON_NAME" ssid "$SSID" >/dev/null 2>&1; then
|
||||
log "AP retry failed: connection add failed"
|
||||
exit 1
|
||||
fi
|
||||
args=(
|
||||
connection modify "$CON_NAME"
|
||||
connection.autoconnect no
|
||||
802-11-wireless.mode ap
|
||||
802-11-wireless.band bg
|
||||
802-11-wireless.channel 1
|
||||
802-11-wireless-security.key-mgmt wpa-psk
|
||||
802-11-wireless-security.proto rsn
|
||||
802-11-wireless-security.pairwise ccmp
|
||||
802-11-wireless-security.group ccmp
|
||||
802-11-wireless-security.pmf disable
|
||||
ipv4.method shared
|
||||
ipv4.addresses "$AP_IP/24"
|
||||
ipv6.method ignore
|
||||
)
|
||||
if [ -n "\${AP_PASSWORD:-}" ]; then
|
||||
args+=(802-11-wireless-security.psk "$AP_PASSWORD")
|
||||
fi
|
||||
check_token
|
||||
if ! nmcli "\${args[@]}" >/dev/null 2>&1; then
|
||||
log "AP retry failed: connection modify failed"
|
||||
exit 1
|
||||
fi
|
||||
check_token
|
||||
if nmcli connection up "$CON_NAME" >/dev/null 2>&1; then
|
||||
if [ ! -f "$TOKEN_FILE" ] || [ "$(cat "$TOKEN_FILE" 2>/dev/null || true)" != "$TOKEN" ]; then
|
||||
log "AP retry canceled after connection up; tearing hotspot down"
|
||||
nmcli connection down "$CON_NAME" >/dev/null 2>&1 || true
|
||||
nmcli connection delete "$CON_NAME" >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
fi
|
||||
log "AP retry success: $SSID"
|
||||
rm -f "$TOKEN_FILE"
|
||||
else
|
||||
log "AP retry failed: connection up failed"
|
||||
exit 1
|
||||
fi
|
||||
`;
|
||||
|
||||
const child = spawn('/bin/bash', ['-lc', script], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: {
|
||||
...process.env,
|
||||
SSID: ssid,
|
||||
IFACE: iface,
|
||||
CON_NAME,
|
||||
AP_IP,
|
||||
AP_PASSWORD: AP_PASSWORD || '',
|
||||
TOKEN_FILE: AP_RETRY_TOKEN_FILE,
|
||||
TOKEN: token,
|
||||
},
|
||||
});
|
||||
child.unref();
|
||||
}
|
||||
|
||||
function _createHotspotProfile(ssid, iface) {
|
||||
nmcliSync([
|
||||
'connection', 'add',
|
||||
'type', 'wifi',
|
||||
'ifname', iface,
|
||||
'con-name', CON_NAME,
|
||||
'ssid', ssid,
|
||||
], 15000);
|
||||
|
||||
const modifyArgs = [
|
||||
'connection', 'modify', CON_NAME,
|
||||
'connection.autoconnect', 'no',
|
||||
'802-11-wireless.mode', 'ap',
|
||||
'802-11-wireless.band', 'bg',
|
||||
'802-11-wireless.channel', '1',
|
||||
'802-11-wireless-security.key-mgmt', 'wpa-psk',
|
||||
'802-11-wireless-security.proto', 'rsn',
|
||||
'802-11-wireless-security.pairwise', 'ccmp',
|
||||
'802-11-wireless-security.group', 'ccmp',
|
||||
'802-11-wireless-security.pmf', 'disable',
|
||||
'ipv4.method', 'shared',
|
||||
'ipv4.addresses', `${AP_IP}/24`,
|
||||
'ipv6.method', 'ignore',
|
||||
];
|
||||
if (AP_PASSWORD) {
|
||||
modifyArgs.push('802-11-wireless-security.psk', AP_PASSWORD);
|
||||
}
|
||||
nmcliSync(modifyArgs, 15000);
|
||||
}
|
||||
|
||||
function _activateHotspot(ssid, iface, timeoutMs = 8000) {
|
||||
_createHotspotProfile(ssid, iface);
|
||||
nmcliSync(['connection', 'up', CON_NAME], timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 WiFi AP 热点
|
||||
*/
|
||||
@@ -313,29 +527,24 @@ function startAP(clawId) {
|
||||
|
||||
log.info('network', `启动 AP 热点: ${ssid} (${iface})`);
|
||||
|
||||
// 关闭已有热点
|
||||
// 关闭已有热点,并在重新拉起 AP 前真正 power-cycle WiFi 芯片。
|
||||
// RK3588/Broadcom DHD 在 LAN 断开后切 AP 时,单纯 ip link down/up 不一定清掉固件残留状态。
|
||||
stopAP();
|
||||
_resetWifiRadioForAP(iface, '准备 AP 前重置 WiFi radio');
|
||||
|
||||
try {
|
||||
// nmcli 创建热点(开放网络)
|
||||
const cmd = [
|
||||
'nmcli device wifi hotspot',
|
||||
`ifname ${iface}`,
|
||||
`con-name ${CON_NAME}`,
|
||||
`ssid "${ssid}"`,
|
||||
'band bg',
|
||||
];
|
||||
// 如果需要密码
|
||||
if (AP_PASSWORD) {
|
||||
cmd.push(`password "${AP_PASSWORD}"`);
|
||||
}
|
||||
run(cmd.join(' '));
|
||||
// 显式创建并激活热点,固定为 WPA2-PSK only。
|
||||
// 避免 NetworkManager 自动生成 WPA-PSK-SHA256/SAE 混合配置,部分 3588 Broadcom DHD 驱动重启 AP 时会拒绝该 RSN 参数。
|
||||
try {
|
||||
nmcliSync(['connection', 'modify', CON_NAME, 'connection.autoconnect', 'no'], 8000);
|
||||
} catch (_) {}
|
||||
_activateHotspot(ssid, iface, 8000);
|
||||
} catch (firstError) {
|
||||
log.warn('network', `AP 启动未在短超时内完成,后台再次重置 WiFi radio 后重试;避免阻塞 watchdog: ${firstError.message}`);
|
||||
_spawnHotspotRadioRetry(ssid, iface);
|
||||
return { ssid, ip: AP_IP, iface, pending: true };
|
||||
}
|
||||
|
||||
// 等待 AP 启动
|
||||
sleep(2000);
|
||||
sleep(1000);
|
||||
log.info('network', `AP 已启动: ${ssid}, 网关 ${AP_IP}`);
|
||||
return { ssid, ip: AP_IP, iface };
|
||||
} catch (e) {
|
||||
@@ -348,6 +557,7 @@ function startAP(clawId) {
|
||||
* 关闭热点,恢复普通 WiFi 模式
|
||||
*/
|
||||
function stopAP() {
|
||||
cancelHotspotRadioRetry('停止 AP');
|
||||
try {
|
||||
run(`nmcli connection down ${CON_NAME}`);
|
||||
} catch (_) {}
|
||||
@@ -454,6 +664,7 @@ async function _ensureActiveWifiAutoconnect() {
|
||||
* clawd 只做调度;真正的认证、DHCP、重连细节仍交给 NM。
|
||||
*/
|
||||
async function connectSavedWifiConnections() {
|
||||
cancelHotspotRadioRetry('准备连接已保存 WiFi');
|
||||
const iface = getWifiIface();
|
||||
const profiles = listSavedWifiConnections();
|
||||
if (profiles.length === 0) {
|
||||
@@ -574,6 +785,7 @@ module.exports = {
|
||||
connectWifi,
|
||||
startAP,
|
||||
stopAP,
|
||||
cancelHotspotRadioRetry,
|
||||
AP_IP,
|
||||
getLocalIps,
|
||||
getLocalNetworks,
|
||||
|
||||
@@ -5,6 +5,7 @@ const path = require('path');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { exec } = require('child_process');
|
||||
const log = require('./logger');
|
||||
const { resolveOpenclawConfigFile } = require('./frpc');
|
||||
|
||||
@@ -83,6 +84,23 @@ function writeJsonFile(filePath, obj) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 终止 openclaw-gateway 进程,由 systemd --user 自动重新拉起以读取新配置。
|
||||
* 每次写盘 openclaw.json 成功后应调用一次。
|
||||
* 使用异步 exec,不阻塞 Node.js 事件循环,避免干扰 LED / VFD 等后续操作。
|
||||
*/
|
||||
function restartGateway() {
|
||||
exec('pkill -9 -x openclaw-gateway', (err) => {
|
||||
if (err && err.code !== 1) {
|
||||
log.warn('openclaw-provider', `restartGateway: ${err.message}`);
|
||||
} else if (!err) {
|
||||
log.info('openclaw-provider', 'openclaw-gateway 已终止,等待自动重启');
|
||||
} else {
|
||||
log.info('openclaw-provider', 'openclaw-gateway 进程不存在,无需终止');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步:从 openclaw.json 删除指定 provider(解绑)。
|
||||
* 若 primary 指向该 provider,先置为空串。
|
||||
@@ -126,6 +144,7 @@ function removeProviderByName(providerId) {
|
||||
}
|
||||
|
||||
writeJsonFile(configFile, config);
|
||||
restartGateway();
|
||||
log.info('openclaw-provider', `provider 已移除: ${providerId}`);
|
||||
}
|
||||
|
||||
@@ -144,6 +163,68 @@ function removeProviderFromConfig(config, providerId) {
|
||||
}
|
||||
}
|
||||
|
||||
const WEB_SEARCH_BASE_URL = 'https://web-search.cutos.ai/';
|
||||
|
||||
/**
|
||||
* 写入 .env 中的 KEY=value,已存在相同行则跳过,存在不同值则替换,不存在则追加。
|
||||
*/
|
||||
function ensureEnvVar(envFile, key, value) {
|
||||
const line = `${key}="${value}"`;
|
||||
let content = '';
|
||||
try { content = fs.readFileSync(envFile, 'utf8'); } catch (_) {}
|
||||
|
||||
const re = new RegExp(`^${key}=.*`, 'm');
|
||||
if (re.test(content)) {
|
||||
const updated = content.replace(re, line);
|
||||
if (updated !== content) {
|
||||
fs.writeFileSync(envFile, updated, 'utf8');
|
||||
}
|
||||
} else {
|
||||
const sep = content.length && !content.endsWith('\n') ? '\n' : '';
|
||||
fs.writeFileSync(envFile, content + sep + line + '\n', 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并补全 searxng web search 配置(openclaw.json),返回 true 表示有修改。
|
||||
*/
|
||||
function ensureWebSearchConfig(configFile, config) {
|
||||
let dirty = false;
|
||||
|
||||
// .env: SEARXNG_BASE_URL
|
||||
try {
|
||||
const envFile = path.join(path.dirname(configFile), '.env');
|
||||
ensureEnvVar(envFile, 'SEARXNG_BASE_URL', WEB_SEARCH_BASE_URL);
|
||||
} catch (e) {
|
||||
log.warn('openclaw-provider', `ensureEnvVar failed: ${e.message}`);
|
||||
}
|
||||
|
||||
// tools.web.search / tools.web.fetch
|
||||
const curSearch = config.tools?.web?.search;
|
||||
const curFetch = config.tools?.web?.fetch;
|
||||
if (!curSearch?.openaiCodex || curFetch?.enabled !== true) {
|
||||
if (!config.tools) config.tools = {};
|
||||
if (!config.tools.web) config.tools.web = {};
|
||||
config.tools.web.search = { openaiCodex: {} };
|
||||
config.tools.web.fetch = { enabled: true };
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// plugins.entries.searxng
|
||||
const cur = config.plugins?.entries?.searxng;
|
||||
if (!cur || cur.enabled !== true || cur.config?.webSearch?.baseUrl !== WEB_SEARCH_BASE_URL) {
|
||||
if (!config.plugins) config.plugins = {};
|
||||
if (!config.plugins.entries) config.plugins.entries = {};
|
||||
config.plugins.entries.searxng = {
|
||||
config: { webSearch: { baseUrl: WEB_SEARCH_BASE_URL } },
|
||||
enabled: true,
|
||||
};
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
function addProviderSync(configFile, providerId, baseUrl, apiKey, models, defaultModelRaw) {
|
||||
const config = readJsonFile(configFile);
|
||||
|
||||
@@ -187,7 +268,10 @@ function addProviderSync(configFile, providerId, baseUrl, apiKey, models, defaul
|
||||
mode: 'api_key',
|
||||
};
|
||||
|
||||
ensureWebSearchConfig(configFile, config);
|
||||
|
||||
writeJsonFile(configFile, config);
|
||||
restartGateway();
|
||||
log.info('openclaw-provider', `provider 已写入: ${providerId}(${models.length} 个模型)`);
|
||||
}
|
||||
|
||||
@@ -233,7 +317,14 @@ function applyFullProviderFromVps(provider, onDone) {
|
||||
const curMd5 = computeModelsMd5(cur.models || []);
|
||||
const newMd5 = computeModelsMd5(list);
|
||||
if (curApiKey === apiKey && curMd5 === newMd5) {
|
||||
log.info('openclaw-provider', `provider 无变化(apiKey + 模型列表相同),跳过写盘`);
|
||||
// provider 无变化,但仍检查 web search 配置
|
||||
if (ensureWebSearchConfig(configFile, existing)) {
|
||||
writeJsonFile(configFile, existing);
|
||||
restartGateway();
|
||||
log.info('openclaw-provider', 'web search config applied (provider unchanged)');
|
||||
} else {
|
||||
log.info('openclaw-provider', `provider 无变化(apiKey + 模型列表相同),跳过写盘`);
|
||||
}
|
||||
if (typeof onDone === 'function') { try { onDone(); } catch (e) { log.warn('openclaw-provider', `onDone: ${e.message}`); } }
|
||||
return;
|
||||
}
|
||||
@@ -298,10 +389,18 @@ function refreshModelsIfChanged(onDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// provider 不存在时也要确保 web search 配置
|
||||
const providers = config.models?.providers || {};
|
||||
const providerId = Object.keys(providers)[0];
|
||||
if (!providerId) {
|
||||
log.info('openclaw-provider', 'refreshModels: 未找到已配置的 provider,跳过');
|
||||
try {
|
||||
if (ensureWebSearchConfig(configFile, config)) {
|
||||
writeJsonFile(configFile, config);
|
||||
restartGateway();
|
||||
log.info('openclaw-provider', 'web search config applied (no provider)');
|
||||
}
|
||||
} catch (_) {}
|
||||
log.info('openclaw-provider', 'refreshModels: 未找到已配置的 provider,跳过模型刷新');
|
||||
if (typeof onDone === 'function') onDone();
|
||||
return;
|
||||
}
|
||||
@@ -323,7 +422,17 @@ function refreshModelsIfChanged(onDone) {
|
||||
const newMd5 = computeModelsMd5(newModels);
|
||||
|
||||
if (currentMd5 === newMd5) {
|
||||
log.info('openclaw-provider', `模型列表未变化(${newModels.length} 个),跳过更新`);
|
||||
// 模型未变,但仍检查 web search 配置是否缺失或过期
|
||||
try {
|
||||
const cfg = readJsonFile(configFile);
|
||||
if (ensureWebSearchConfig(configFile, cfg)) {
|
||||
writeJsonFile(configFile, cfg);
|
||||
restartGateway();
|
||||
log.info('openclaw-provider', 'web search config repaired');
|
||||
} else {
|
||||
log.info('openclaw-provider', `模型列表未变化(${newModels.length} 个),跳过更新`);
|
||||
}
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,360 +1,362 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const log = require('./logger');
|
||||
const { hasInternet, hasSavedWifiConnection, connectSavedWifiConnections, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, getWifiIface, AP_IP } = require('./network');
|
||||
const { DnsHijack } = require('./dns-hijack');
|
||||
const { CaptiveServer } = require('./captive-server');
|
||||
const led = require('./led');
|
||||
|
||||
const MONITOR_INTERVAL_MS = 15_000;
|
||||
const WIFI_RECONNECT_MAX_ROUNDS = 3;
|
||||
const WIFI_RECONNECT_ROUND_DELAY_MS = 5_000;
|
||||
const AP_SAVED_WIFI_RETRY_INTERVAL_MS = 180_000;
|
||||
const AP_MIN_UP_BEFORE_RETRY_MS = 60_000;
|
||||
|
||||
/**
|
||||
* AP 常驻配网管理器。
|
||||
*
|
||||
* 规则:
|
||||
* - 启动时:WiFi STA 优先;有已保存 WiFi 时主动让 NM 重连,最多 3 轮
|
||||
* - 有线网络可用时:通知网络就绪,但不自动开启 AP
|
||||
* - 自动开 AP 的唯一兜底:无有线/无 WiFi,且无 saved WiFi 或 saved WiFi 3 轮失败
|
||||
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则按网络状态决定是否重新开 AP
|
||||
* - AP 状态下:若仍无有线网络,低频释放 wlan0 并尝试 saved WiFi
|
||||
*/
|
||||
class ProvisionManager extends EventEmitter {
|
||||
constructor(clawId) {
|
||||
super();
|
||||
this._clawId = clawId || 'Setup';
|
||||
this._state = 'idle'; // 'idle' | 'ap' | 'connecting' | 'sta' | 'wired'
|
||||
this._dns = null;
|
||||
this._server = null;
|
||||
this._monitorTimer = null;
|
||||
this._monitorBusy = false;
|
||||
this._apStartedAt = 0;
|
||||
this._lastApSavedWifiRetryAt = 0;
|
||||
}
|
||||
|
||||
/** 是否正处于 AP 模式(WiFi 热点广播中) */
|
||||
isApMode() { return this._state === 'ap'; }
|
||||
|
||||
async start() {
|
||||
led.off(); // WiFi 灯初始状态:熄灭
|
||||
|
||||
// WiFi STA 已连接 → 直接进入 STA 模式
|
||||
if (isWifiStaConnected()) {
|
||||
this._state = 'sta';
|
||||
log.info('provision', 'WiFi STA 已连接,AP 不启动');
|
||||
this._emitNetworkReady();
|
||||
this._startMonitor();
|
||||
return;
|
||||
}
|
||||
|
||||
// 网络已就绪时先启动 WS;hasInternet() 可能来自 WiFi,也可能来自有线,不能直接当作 wired。
|
||||
if (hasInternet()) {
|
||||
if (isWifiStaConnected()) {
|
||||
this._state = 'sta';
|
||||
log.info('provision', 'WiFi STA 已连接,AP 不启动');
|
||||
this._emitNetworkReady();
|
||||
} else {
|
||||
this._state = 'wired';
|
||||
log.info('provision', '有线网络就绪,启动 WS;不自动开启 AP');
|
||||
led.off();
|
||||
this._emitNetworkReady();
|
||||
}
|
||||
this._startMonitor();
|
||||
return;
|
||||
}
|
||||
|
||||
// 无有线可用时,有 saved WiFi 才主动让 NetworkManager 重连;不要只被动等待 NM autoconnect。
|
||||
if (hasSavedWifiConnection()) {
|
||||
log.info('provision', `发现已保存的 WiFi 配置,主动重连(最多 ${WIFI_RECONNECT_MAX_ROUNDS} 轮)...`);
|
||||
this._state = 'connecting';
|
||||
led.blink();
|
||||
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
|
||||
if (connected) {
|
||||
this._state = 'sta';
|
||||
log.info('provision', '已保存 WiFi 重连成功,AP 不启动');
|
||||
this._emitNetworkReady();
|
||||
this._startMonitor();
|
||||
return;
|
||||
}
|
||||
log.warn('provision', '已保存 WiFi 重连失败');
|
||||
}
|
||||
|
||||
// 无有线、无 WiFi;且无 saved WiFi 或 saved WiFi 3 轮失败 → 开 AP 兜底配网。
|
||||
this._enterAP();
|
||||
this._startMonitor();
|
||||
}
|
||||
|
||||
_emitNetworkReady() {
|
||||
if (hasInternet()) {
|
||||
// WiFi 灯只在 STA 模式下亮(有线有网而 WiFi 在 AP 模式时不亮)
|
||||
if (this._state === 'sta') led.on();
|
||||
this.emit('network-ready');
|
||||
} else {
|
||||
log.warn('provision', 'hasInternet() 返回 false,LED 保持熄灭');
|
||||
}
|
||||
}
|
||||
|
||||
async _trySavedWifiReconnectRounds(rounds = WIFI_RECONNECT_MAX_ROUNDS) {
|
||||
for (let i = 1; i <= rounds; i++) {
|
||||
if (isWifiStaConnected()) return true;
|
||||
log.info('provision', `尝试已保存 WiFi 重连:第 ${i}/${rounds} 轮`);
|
||||
const result = await connectSavedWifiConnections();
|
||||
if (result.success || isWifiStaConnected()) return true;
|
||||
if (i < rounds) {
|
||||
await new Promise((r) => setTimeout(r, WIFI_RECONNECT_ROUND_DELAY_MS));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._stopMonitor();
|
||||
this._stopAll();
|
||||
this._state = 'idle';
|
||||
led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器
|
||||
}
|
||||
|
||||
// ── 进入 AP 模式 ─────────────────────────────────────────────────────────
|
||||
|
||||
_enterAP() {
|
||||
if (this._state === 'ap') return;
|
||||
|
||||
led.off(); // AP 模式:WiFi 未连接,WiFi 灯熄灭
|
||||
if (!hasInternet()) led.display.showAP(); // 无网时立即显示 AP,有线时等 WS 连接后再定
|
||||
|
||||
try {
|
||||
// 若上次进程退出前留下 clawd-hotspot,必须先释放 wlan0;否则会在 AP 模式下扫描,列表可能只剩 2.4G/自身热点。
|
||||
stopAP();
|
||||
|
||||
// AP 模式下无法扫描 WiFi,必须在开 AP 之前扫描并缓存
|
||||
log.info('provision', '扫描周边 WiFi...');
|
||||
this._cachedWifiList = scanWifi();
|
||||
log.info('provision', `扫描到 ${this._cachedWifiList.length} 个网络: ${this._cachedWifiList.map(w => `${w.ssid}${w.band ? `(${w.band})` : ''}`).join(', ')}`);
|
||||
|
||||
// 写 DNS 劫持配置(NM 启动热点时加载);接口名与热点一致,勿写死 wlan0
|
||||
this._dns = new DnsHijack();
|
||||
this._dns.start(getWifiIface(), AP_IP);
|
||||
|
||||
const ap = startAP(this._clawId);
|
||||
|
||||
this._server = new CaptiveServer({
|
||||
clawId: this._clawId,
|
||||
cachedWifiList: this._cachedWifiList,
|
||||
onConnect: (ssid, password) => this._handleWifiConnect(ssid, password),
|
||||
});
|
||||
this._server.startListening();
|
||||
|
||||
this._state = 'ap';
|
||||
this._apStartedAt = Date.now();
|
||||
this._lastApSavedWifiRetryAt = 0;
|
||||
log.info('provision', `AP 常驻模式已启动: ${ap.ssid}, 密码 12345678`);
|
||||
log.info('provision', `配网地址: http://10.42.0.1`);
|
||||
} catch (e) {
|
||||
log.error('provision', `AP 启动失败: ${e.message}`);
|
||||
if (this._state !== 'sta') this._state = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
// ── 用户提交 WiFi 凭证 ───────────────────────────────────────────────────
|
||||
|
||||
async _handleWifiConnect(ssid, password) {
|
||||
if (this._state === 'connecting') return { success: false, error: '正在连接中,请稍候' };
|
||||
|
||||
this._state = 'connecting';
|
||||
log.info('provision', `用户请求连接 WiFi: ${ssid}`);
|
||||
led.blink(); // 正在连接 → 闪烁
|
||||
|
||||
try {
|
||||
this._stopAPServices();
|
||||
|
||||
// 关热点后射频/模式切换需要时间,立刻 connect 在部分板子上会失败
|
||||
await new Promise((r) => setTimeout(r, 3500));
|
||||
|
||||
const result = await connectWifi(ssid, password);
|
||||
|
||||
if (result.success) {
|
||||
this._state = 'sta';
|
||||
log.info('provision', `WiFi 已连接: ${ssid}`);
|
||||
led.on(); // WiFi 灯:连接成功 → 常亮
|
||||
this.emit('network-ready');
|
||||
return result;
|
||||
}
|
||||
|
||||
log.warn('provision', `WiFi 连接失败: ${result.error},按当前网络状态恢复`);
|
||||
this._recoverAfterWifiFailure();
|
||||
return result;
|
||||
} catch (e) {
|
||||
log.error('provision', `配网过程异常: ${e.message}`);
|
||||
this._recoverAfterWifiFailure();
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/** WiFi 连接失败后:有线可用则保持 wired;否则开 AP 兜底。 */
|
||||
_recoverAfterWifiFailure() {
|
||||
if (hasInternet()) {
|
||||
this._state = 'wired';
|
||||
led.off();
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
this._safeReenterAP();
|
||||
}
|
||||
|
||||
/** 重新开 AP;失败时勿把 _state 永久卡在 connecting */
|
||||
_safeReenterAP() {
|
||||
try {
|
||||
this._enterAP();
|
||||
} catch (e) {
|
||||
log.error('provision', `重新启动 AP 失败: ${e.message}`);
|
||||
this._state = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
// ── WiFi 状态监控 ─────────────────────────────────────────────────────────
|
||||
|
||||
_startMonitor() {
|
||||
this._monitorTimer = setInterval(() => {
|
||||
if (this._monitorBusy) return;
|
||||
this._monitorBusy = true;
|
||||
this._monitorTick()
|
||||
.catch((e) => log.error('provision', `WiFi 状态监控异常: ${e.message}`))
|
||||
.finally(() => { this._monitorBusy = false; });
|
||||
}, MONITOR_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async _monitorTick() {
|
||||
if (this._state === 'connecting') return;
|
||||
|
||||
const wifiUp = isWifiStaConnected();
|
||||
|
||||
if (wifiUp && this._state !== 'sta') {
|
||||
if (this._state === 'ap') {
|
||||
log.info('provision', 'WiFi 已外部连接,关闭 AP');
|
||||
this._stopAPServices();
|
||||
}
|
||||
this._state = 'sta';
|
||||
this._emitNetworkReady();
|
||||
}
|
||||
|
||||
if (this._state === 'sta' && !wifiUp) {
|
||||
log.warn('provision', 'WiFi 连接已断开,尝试恢复网络');
|
||||
await this._recoverNetworkWithoutWifi();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state === 'wired') {
|
||||
if (!hasInternet()) {
|
||||
log.warn('provision', '有线网络不可用,尝试恢复 WiFi');
|
||||
await this._recoverNetworkWithoutWifi();
|
||||
return;
|
||||
}
|
||||
led.off();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state === 'ap') {
|
||||
if (hasInternet()) {
|
||||
log.info('provision', '检测到有线网络可用,关闭 AP');
|
||||
this._stopAPServices();
|
||||
this._state = 'wired';
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasSavedWifiConnection() && this._shouldRetrySavedWifiFromAP()) {
|
||||
await this._retrySavedWifiFromAP();
|
||||
return;
|
||||
}
|
||||
led.off();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async _recoverNetworkWithoutWifi() {
|
||||
this._state = 'connecting';
|
||||
led.blink();
|
||||
|
||||
if (hasSavedWifiConnection()) {
|
||||
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
|
||||
if (connected) {
|
||||
this._state = 'sta';
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasInternet()) {
|
||||
this._state = 'wired';
|
||||
led.off();
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
|
||||
this._safeReenterAP();
|
||||
}
|
||||
|
||||
_shouldRetrySavedWifiFromAP() {
|
||||
const now = Date.now();
|
||||
if (this._apStartedAt && now - this._apStartedAt < AP_MIN_UP_BEFORE_RETRY_MS) return false;
|
||||
if (this._lastApSavedWifiRetryAt && now - this._lastApSavedWifiRetryAt < AP_SAVED_WIFI_RETRY_INTERVAL_MS) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async _retrySavedWifiFromAP() {
|
||||
this._lastApSavedWifiRetryAt = Date.now();
|
||||
log.info('provision', 'AP 模式下定期尝试已保存 WiFi');
|
||||
this._state = 'connecting';
|
||||
led.blink();
|
||||
this._stopAPServices();
|
||||
await new Promise((r) => setTimeout(r, 3500));
|
||||
|
||||
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
|
||||
if (connected) {
|
||||
this._state = 'sta';
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasInternet()) {
|
||||
this._state = 'wired';
|
||||
led.off();
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn('provision', 'AP 模式下重试已保存 WiFi 失败,恢复 AP');
|
||||
this._safeReenterAP();
|
||||
}
|
||||
|
||||
_stopMonitor() {
|
||||
if (this._monitorTimer) {
|
||||
clearInterval(this._monitorTimer);
|
||||
this._monitorTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 清理 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
_stopAPServices() {
|
||||
if (this._server) {
|
||||
this._server.stop();
|
||||
this._server = null;
|
||||
}
|
||||
if (this._dns) {
|
||||
this._dns.stop();
|
||||
this._dns = null;
|
||||
}
|
||||
stopAP();
|
||||
}
|
||||
|
||||
_stopAll() {
|
||||
this._stopAPServices();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ProvisionManager };
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const log = require('./logger');
|
||||
const { hasInternet, hasWiredInternetProbe, hasSavedWifiConnection, connectSavedWifiConnections, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, getWifiIface, AP_IP } = require('./network');
|
||||
const { DnsHijack } = require('./dns-hijack');
|
||||
const { CaptiveServer } = require('./captive-server');
|
||||
const led = require('./led');
|
||||
|
||||
const MONITOR_INTERVAL_MS = 15_000;
|
||||
const WIFI_RECONNECT_MAX_ROUNDS = 3;
|
||||
const WIFI_RECONNECT_ROUND_DELAY_MS = 5_000;
|
||||
const AP_SAVED_WIFI_RETRY_INTERVAL_MS = 180_000;
|
||||
const AP_MIN_UP_BEFORE_RETRY_MS = 60_000;
|
||||
|
||||
/**
|
||||
* AP 常驻配网管理器。
|
||||
*
|
||||
* 规则:
|
||||
* - 启动时:WiFi STA 优先;有已保存 WiFi 时主动让 NM 重连,最多 3 轮
|
||||
* - 有线网络可用时:通知网络就绪,但不自动开启 AP
|
||||
* - 自动开 AP 的唯一兜底:无有线/无 WiFi,且无 saved WiFi 或 saved WiFi 3 轮失败
|
||||
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则按网络状态决定是否重新开 AP
|
||||
* - AP 状态下:若仍无有线网络,低频释放 wlan0 并尝试 saved WiFi
|
||||
*/
|
||||
class ProvisionManager extends EventEmitter {
|
||||
constructor(clawId) {
|
||||
super();
|
||||
this._clawId = clawId || 'Setup';
|
||||
this._state = 'idle'; // 'idle' | 'ap' | 'connecting' | 'sta' | 'wired'
|
||||
this._dns = null;
|
||||
this._server = null;
|
||||
this._monitorTimer = null;
|
||||
this._monitorBusy = false;
|
||||
this._apStartedAt = 0;
|
||||
this._lastApSavedWifiRetryAt = 0;
|
||||
}
|
||||
|
||||
/** 是否正处于 AP 模式(WiFi 热点广播中) */
|
||||
isApMode() { return this._state === 'ap'; }
|
||||
|
||||
async start() {
|
||||
led.off(); // WiFi 灯初始状态:熄灭
|
||||
|
||||
// WiFi STA 已连接 → 直接进入 STA 模式
|
||||
if (isWifiStaConnected()) {
|
||||
this._state = 'sta';
|
||||
log.info('provision', 'WiFi STA 已连接,AP 不启动');
|
||||
this._emitNetworkReady();
|
||||
this._startMonitor();
|
||||
return;
|
||||
}
|
||||
|
||||
// 网络已就绪时先启动 WS;hasInternet() 可能来自 WiFi,也可能来自有线,不能直接当作 wired。
|
||||
if (hasInternet()) {
|
||||
if (isWifiStaConnected()) {
|
||||
this._state = 'sta';
|
||||
log.info('provision', 'WiFi STA 已连接,AP 不启动');
|
||||
this._emitNetworkReady();
|
||||
} else {
|
||||
this._state = 'wired';
|
||||
log.info('provision', '有线网络就绪,启动 WS;不自动开启 AP');
|
||||
led.off();
|
||||
this._emitNetworkReady();
|
||||
}
|
||||
this._startMonitor();
|
||||
return;
|
||||
}
|
||||
|
||||
// 无有线可用时,有 saved WiFi 才主动让 NetworkManager 重连;不要只被动等待 NM autoconnect。
|
||||
if (hasSavedWifiConnection()) {
|
||||
log.info('provision', `发现已保存的 WiFi 配置,主动重连(最多 ${WIFI_RECONNECT_MAX_ROUNDS} 轮)...`);
|
||||
this._state = 'connecting';
|
||||
led.blink();
|
||||
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
|
||||
if (connected) {
|
||||
this._state = 'sta';
|
||||
log.info('provision', '已保存 WiFi 重连成功,AP 不启动');
|
||||
this._emitNetworkReady();
|
||||
this._startMonitor();
|
||||
return;
|
||||
}
|
||||
log.warn('provision', '已保存 WiFi 重连失败');
|
||||
}
|
||||
|
||||
// 无有线、无 WiFi;且无 saved WiFi 或 saved WiFi 3 轮失败 → 开 AP 兜底配网。
|
||||
this._enterAP();
|
||||
this._startMonitor();
|
||||
}
|
||||
|
||||
_emitNetworkReady() {
|
||||
if (hasInternet()) {
|
||||
// WiFi 灯只在 STA 模式下亮(有线有网而 WiFi 在 AP 模式时不亮)
|
||||
if (this._state === 'sta') led.on();
|
||||
this.emit('network-ready');
|
||||
} else {
|
||||
log.warn('provision', 'hasInternet() 返回 false,LED 保持熄灭');
|
||||
}
|
||||
}
|
||||
|
||||
async _trySavedWifiReconnectRounds(rounds = WIFI_RECONNECT_MAX_ROUNDS) {
|
||||
for (let i = 1; i <= rounds; i++) {
|
||||
if (isWifiStaConnected()) return true;
|
||||
log.info('provision', `尝试已保存 WiFi 重连:第 ${i}/${rounds} 轮`);
|
||||
const result = await connectSavedWifiConnections();
|
||||
if (result.success || isWifiStaConnected()) return true;
|
||||
if (i < rounds) {
|
||||
await new Promise((r) => setTimeout(r, WIFI_RECONNECT_ROUND_DELAY_MS));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._stopMonitor();
|
||||
this._stopAll();
|
||||
this._state = 'idle';
|
||||
led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器
|
||||
}
|
||||
|
||||
// ── 进入 AP 模式 ─────────────────────────────────────────────────────────
|
||||
|
||||
_enterAP() {
|
||||
if (this._state === 'ap') return;
|
||||
|
||||
led.off(); // AP 模式:WiFi 未连接,WiFi 灯熄灭
|
||||
if (!hasInternet()) led.display.showAP(); // 无网时立即显示 AP,有线时等 WS 连接后再定
|
||||
|
||||
try {
|
||||
// 若上次进程退出前留下 clawd-hotspot,必须先释放 wlan0;否则会在 AP 模式下扫描,列表可能只剩 2.4G/自身热点。
|
||||
stopAP();
|
||||
|
||||
// AP 模式下无法扫描 WiFi,必须在开 AP 之前扫描并缓存
|
||||
log.info('provision', '扫描周边 WiFi...');
|
||||
this._cachedWifiList = scanWifi();
|
||||
log.info('provision', `扫描到 ${this._cachedWifiList.length} 个网络: ${this._cachedWifiList.map(w => `${w.ssid}${w.band ? `(${w.band})` : ''}`).join(', ')}`);
|
||||
|
||||
// 写 DNS 劫持配置(NM 启动热点时加载);接口名与热点一致,勿写死 wlan0
|
||||
this._dns = new DnsHijack();
|
||||
this._dns.start(getWifiIface(), AP_IP);
|
||||
|
||||
const ap = startAP(this._clawId);
|
||||
|
||||
this._server = new CaptiveServer({
|
||||
clawId: this._clawId,
|
||||
cachedWifiList: this._cachedWifiList,
|
||||
onConnect: (ssid, password) => this._handleWifiConnect(ssid, password),
|
||||
});
|
||||
this._server.startListening();
|
||||
|
||||
this._state = 'ap';
|
||||
this._apStartedAt = Date.now();
|
||||
this._lastApSavedWifiRetryAt = 0;
|
||||
log.info('provision', `AP 常驻模式已启动: ${ap.ssid}, 密码 12345678`);
|
||||
log.info('provision', `配网地址: http://10.42.0.1`);
|
||||
} catch (e) {
|
||||
log.error('provision', `AP 启动失败: ${e.message}`);
|
||||
if (this._state !== 'sta') this._state = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
// ── 用户提交 WiFi 凭证 ───────────────────────────────────────────────────
|
||||
|
||||
async _handleWifiConnect(ssid, password) {
|
||||
if (this._state === 'connecting') return { success: false, error: '正在连接中,请稍候' };
|
||||
|
||||
this._state = 'connecting';
|
||||
log.info('provision', `用户请求连接 WiFi: ${ssid}`);
|
||||
led.blink(); // 正在连接 → 闪烁
|
||||
|
||||
try {
|
||||
this._stopAPServices();
|
||||
|
||||
// 关热点后射频/模式切换需要时间,立刻 connect 在部分板子上会失败
|
||||
await new Promise((r) => setTimeout(r, 3500));
|
||||
|
||||
const result = await connectWifi(ssid, password);
|
||||
|
||||
if (result.success) {
|
||||
this._state = 'sta';
|
||||
log.info('provision', `WiFi 已连接: ${ssid}`);
|
||||
led.on(); // WiFi 灯:连接成功 → 常亮
|
||||
this.emit('network-ready');
|
||||
return result;
|
||||
}
|
||||
|
||||
log.warn('provision', `WiFi 连接失败: ${result.error},按当前网络状态恢复`);
|
||||
this._recoverAfterWifiFailure();
|
||||
return result;
|
||||
} catch (e) {
|
||||
log.error('provision', `配网过程异常: ${e.message}`);
|
||||
this._recoverAfterWifiFailure();
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/** WiFi 连接失败后:有线可用则保持 wired;否则开 AP 兜底。 */
|
||||
_recoverAfterWifiFailure() {
|
||||
if (hasInternet()) {
|
||||
this._state = 'wired';
|
||||
led.off();
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
this._safeReenterAP();
|
||||
}
|
||||
|
||||
/** 重新开 AP;失败时勿把 _state 永久卡在 connecting */
|
||||
_safeReenterAP() {
|
||||
try {
|
||||
this._enterAP();
|
||||
} catch (e) {
|
||||
log.error('provision', `重新启动 AP 失败: ${e.message}`);
|
||||
this._state = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
// ── WiFi 状态监控 ─────────────────────────────────────────────────────────
|
||||
|
||||
_startMonitor() {
|
||||
this._monitorTimer = setInterval(() => {
|
||||
if (this._monitorBusy) return;
|
||||
this._monitorBusy = true;
|
||||
this._monitorTick()
|
||||
.catch((e) => log.error('provision', `WiFi 状态监控异常: ${e.message}`))
|
||||
.finally(() => { this._monitorBusy = false; });
|
||||
}, MONITOR_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async _monitorTick() {
|
||||
if (this._state === 'connecting') return;
|
||||
|
||||
const wifiUp = isWifiStaConnected();
|
||||
|
||||
if (wifiUp && this._state !== 'sta') {
|
||||
if (this._state === 'ap') {
|
||||
log.info('provision', 'WiFi 已外部连接,关闭 AP');
|
||||
this._stopAPServices();
|
||||
}
|
||||
this._state = 'sta';
|
||||
this._emitNetworkReady();
|
||||
}
|
||||
|
||||
if (this._state === 'sta' && !wifiUp) {
|
||||
log.warn('provision', 'WiFi 连接已断开,尝试恢复网络');
|
||||
await this._recoverNetworkWithoutWifi();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state === 'wired') {
|
||||
if (!hasInternet()) {
|
||||
log.warn('provision', '有线网络不可用,尝试恢复 WiFi');
|
||||
await this._recoverNetworkWithoutWifi();
|
||||
return;
|
||||
}
|
||||
led.off();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state === 'ap') {
|
||||
// AP 模式下 hasInternet() 可能被热点本地网络 / NetworkManager limited 状态误判。
|
||||
// 只有明确探测到有线口可访问公网时,才关闭配网 AP。
|
||||
if (hasWiredInternetProbe()) {
|
||||
log.info('provision', '检测到有线网络可用,关闭 AP');
|
||||
this._stopAPServices();
|
||||
this._state = 'wired';
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasSavedWifiConnection() && this._shouldRetrySavedWifiFromAP()) {
|
||||
await this._retrySavedWifiFromAP();
|
||||
return;
|
||||
}
|
||||
led.off();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async _recoverNetworkWithoutWifi() {
|
||||
this._state = 'connecting';
|
||||
led.blink();
|
||||
|
||||
if (hasSavedWifiConnection()) {
|
||||
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
|
||||
if (connected) {
|
||||
this._state = 'sta';
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasInternet()) {
|
||||
this._state = 'wired';
|
||||
led.off();
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
|
||||
this._safeReenterAP();
|
||||
}
|
||||
|
||||
_shouldRetrySavedWifiFromAP() {
|
||||
const now = Date.now();
|
||||
if (this._apStartedAt && now - this._apStartedAt < AP_MIN_UP_BEFORE_RETRY_MS) return false;
|
||||
if (this._lastApSavedWifiRetryAt && now - this._lastApSavedWifiRetryAt < AP_SAVED_WIFI_RETRY_INTERVAL_MS) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async _retrySavedWifiFromAP() {
|
||||
this._lastApSavedWifiRetryAt = Date.now();
|
||||
log.info('provision', 'AP 模式下定期尝试已保存 WiFi');
|
||||
this._state = 'connecting';
|
||||
led.blink();
|
||||
this._stopAPServices();
|
||||
await new Promise((r) => setTimeout(r, 3500));
|
||||
|
||||
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
|
||||
if (connected) {
|
||||
this._state = 'sta';
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasInternet()) {
|
||||
this._state = 'wired';
|
||||
led.off();
|
||||
this._emitNetworkReady();
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn('provision', 'AP 模式下重试已保存 WiFi 失败,恢复 AP');
|
||||
this._safeReenterAP();
|
||||
}
|
||||
|
||||
_stopMonitor() {
|
||||
if (this._monitorTimer) {
|
||||
clearInterval(this._monitorTimer);
|
||||
this._monitorTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 清理 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
_stopAPServices() {
|
||||
if (this._server) {
|
||||
this._server.stop();
|
||||
this._server = null;
|
||||
}
|
||||
if (this._dns) {
|
||||
this._dns.stop();
|
||||
this._dns = null;
|
||||
}
|
||||
stopAP();
|
||||
}
|
||||
|
||||
_stopAll() {
|
||||
this._stopAPServices();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ProvisionManager };
|
||||
|
||||
39
lib/resource/3588s/README.md
Normal file
39
lib/resource/3588s/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# RK3588S demo resource
|
||||
|
||||
This directory contains the LVGL demo binary deployed on RK3588S devices by `install.sh`.
|
||||
|
||||
## Files
|
||||
|
||||
- `demo` — prebuilt LVGL UI binary installed to `/usr/bin/demo` on RK3588S boards
|
||||
|
||||
## Source
|
||||
|
||||
Current binary source on the build machine:
|
||||
|
||||
- `/home/sts/share/小屏demo开发指南/lvgl源码/lv_port_linux_v2/lv_port_linux/build/bin/demo`
|
||||
|
||||
## Purpose
|
||||
|
||||
This demo provides the small-screen UI used on RK3588S devices, including FIFO-based control through:
|
||||
|
||||
- `/tmp/lvgl_cmd`
|
||||
|
||||
Supported commands expected by the current clawd RK3588 LVGL backend include:
|
||||
|
||||
- `show_text:AP`
|
||||
- `show_text:Conn`
|
||||
- `show_text:Err0`
|
||||
- `show_text:<PIN>`
|
||||
- `show_time`
|
||||
|
||||
## Install behavior
|
||||
|
||||
During `install.sh`, if `/proc/device-tree/model` matches `RK3588S`, clawd will:
|
||||
|
||||
1. Back up existing `/usr/bin/demo` to `/usr/bin/demo.clawd-bak` if not already backed up
|
||||
2. Install this `demo` binary to `/usr/bin/demo`
|
||||
|
||||
## Notes
|
||||
|
||||
- The binary is hardware-specific and intended for RK3588S boards.
|
||||
- Replacing the binary should be done together with verification of `/tmp/lvgl_cmd` behavior and screen rendering.
|
||||
BIN
lib/resource/3588s/demo
Executable file
BIN
lib/resource/3588s/demo
Executable file
Binary file not shown.
21
lib/resource/3588s/src/LICENSE
Executable file
21
lib/resource/3588s/src/LICENSE
Executable file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Littlev Graphics Library
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
105
lib/resource/3588s/src/Makefile
Executable file
105
lib/resource/3588s/src/Makefile
Executable file
@@ -0,0 +1,105 @@
|
||||
ROOT_DIR := $(shell pwd)
|
||||
|
||||
LVGL_DIR_NAME ?= lvgl
|
||||
|
||||
CC ?= gcc
|
||||
|
||||
TARGET_NAME := demo
|
||||
|
||||
BUILD_DIR := build
|
||||
OBJ_DIR := $(BUILD_DIR)/obj
|
||||
BIN_DIR := $(BUILD_DIR)/bin
|
||||
TARGET := $(BIN_DIR)/$(TARGET_NAME)
|
||||
|
||||
IMG_SRC_DIR := user/images
|
||||
|
||||
# 图片文件名固定为 logo,后缀可以是 png / jpg / jpeg / bmp
|
||||
LOGO_NAME := logo
|
||||
LOGO_IMG_EXTS := png jpg jpeg bmp
|
||||
LOGO_IMG_FILES := $(foreach ext,$(LOGO_IMG_EXTS),$(IMG_SRC_DIR)/$(LOGO_NAME).$(ext))
|
||||
LOGO_IMG := $(firstword $(wildcard $(LOGO_IMG_FILES)))
|
||||
|
||||
LOGO_C := $(LOGO_NAME).c
|
||||
|
||||
LVGL_IMG_CONV := lv_img_conv
|
||||
LVGL_IMG_CF := CF_TRUE_COLOR_ALPHA
|
||||
|
||||
APP_CSRCS := $(filter-out logo.c,$(wildcard *.c))
|
||||
APP_CSRCS += $(shell if [ -d src ]; then find src -type f -name "*.c"; fi)
|
||||
|
||||
LVGL_CSRCS := $(shell if [ -d $(LVGL_DIR_NAME)/src ]; then find $(LVGL_DIR_NAME)/src -type f -name "*.c"; fi)
|
||||
|
||||
LV_DRIVERS_CSRCS := $(shell if [ -d lv_drivers ]; then find lv_drivers -type f -name "*.c"; fi)
|
||||
|
||||
CSRCS := $(APP_CSRCS)
|
||||
CSRCS += $(LVGL_CSRCS)
|
||||
CSRCS += $(LV_DRIVERS_CSRCS)
|
||||
CSRCS += $(LOGO_C)
|
||||
|
||||
OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(CSRCS))
|
||||
|
||||
CFLAGS ?= -O3 -g0
|
||||
CFLAGS += -Wall
|
||||
CFLAGS += -I.
|
||||
CFLAGS += -I$(LVGL_DIR_NAME)
|
||||
CFLAGS += -I$(LVGL_DIR_NAME)/src
|
||||
CFLAGS += -Ilv_drivers
|
||||
CFLAGS += -Iinclude
|
||||
|
||||
LDFLAGS += -lm
|
||||
|
||||
.PHONY: all
|
||||
all: $(TARGET)
|
||||
|
||||
$(BIN_DIR):
|
||||
mkdir -p $(BIN_DIR)
|
||||
|
||||
$(OBJ_DIR):
|
||||
mkdir -p $(OBJ_DIR)
|
||||
|
||||
$(LOGO_C): $(LOGO_IMG)
|
||||
@if [ -z "$(LOGO_IMG)" ]; then \
|
||||
echo "Error: 未找到图片文件"; \
|
||||
echo "请将图片命名为以下任意一种格式,并放入 $(IMG_SRC_DIR) 目录:"; \
|
||||
echo " logo.png"; \
|
||||
echo " logo.jpg"; \
|
||||
echo " logo.jpeg"; \
|
||||
echo " logo.bmp"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Use image: $(LOGO_IMG)"
|
||||
rm -f $(LOGO_C)
|
||||
cp $(LOGO_IMG) ./$(notdir $(LOGO_IMG))
|
||||
$(LVGL_IMG_CONV) $(notdir $(LOGO_IMG)) -f -c $(LVGL_IMG_CF)
|
||||
rm -f ./$(notdir $(LOGO_IMG))
|
||||
|
||||
$(OBJ_DIR)/%.o: %.c
|
||||
mkdir -p $(dir $@)
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
$(TARGET): $(LOGO_C) $(OBJS) | $(BIN_DIR)
|
||||
$(CC) $(OBJS) -o $(TARGET) $(LDFLAGS)
|
||||
@echo "Build success: $(TARGET)"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(OBJ_DIR)
|
||||
rm -f $(TARGET)
|
||||
|
||||
.PHONY: imgclean
|
||||
imgclean:
|
||||
rm -f $(LOGO_C)
|
||||
|
||||
.PHONY: distclean
|
||||
distclean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
rm -f $(LOGO_C)
|
||||
|
||||
.PHONY: info
|
||||
info:
|
||||
@echo "TARGET = $(TARGET)"
|
||||
@echo "IMG_SRC_DIR = $(IMG_SRC_DIR)"
|
||||
@echo "LOGO_IMG = $(LOGO_IMG)"
|
||||
@echo "LOGO_C = $(LOGO_C)"
|
||||
@echo "Support image formats: $(LOGO_IMG_EXTS)"
|
||||
@echo "CSRCS = $(CSRCS)"
|
||||
8
lib/resource/3588s/src/README.md
Executable file
8
lib/resource/3588s/src/README.md
Executable file
@@ -0,0 +1,8 @@
|
||||
# LVGL for frame buffer device
|
||||
|
||||
LVGL configured to work with /dev/fb0 on Linux.
|
||||
|
||||
When cloning this repository, also make sure to download submodules (`git submodule update --init --recursive`) otherwise you will be missing key components.
|
||||
|
||||
Check out this blog post for a step by step tutorial:
|
||||
https://blog.lvgl.io/2018-01-03/linux_fb
|
||||
16
lib/resource/3588s/src/S50-lv_demo
Executable file
16
lib/resource/3588s/src/S50-lv_demo
Executable file
@@ -0,0 +1,16 @@
|
||||
#! /bin/sh
|
||||
|
||||
start() {
|
||||
demo &
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start}"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit $?
|
||||
BIN
lib/resource/3588s/src/build/bin/demo
Executable file
BIN
lib/resource/3588s/src/build/bin/demo
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/logo.o
Executable file
BIN
lib/resource/3588s/src/build/obj/logo.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/GC9A01.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/GC9A01.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/ILI9341.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/ILI9341.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/R61581.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/R61581.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/SHARP_MIP.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/SHARP_MIP.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/SSD1963.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/SSD1963.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/ST7565.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/ST7565.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/UC1610.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/UC1610.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/drm.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/drm.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/fbdev.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/display/fbdev.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/gtkdrv/gtkdrv.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/gtkdrv/gtkdrv.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/AD_touch.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/AD_touch.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/FT5406EE8.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/FT5406EE8.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/XPT2046.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/XPT2046.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/evdev.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/evdev.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/libinput.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/libinput.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/xkb.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/indev/xkb.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/sdl/sdl.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/sdl/sdl.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/sdl/sdl_gpu.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/sdl/sdl_gpu.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/wayland/wayland.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/wayland/wayland.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/win32drv/win32drv.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/win32drv/win32drv.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lv_drivers/win_drv.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lv_drivers/win_drv.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_disp.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_disp.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_event.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_event.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_group.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_group.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_indev.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_indev.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_indev_scroll.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_indev_scroll.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_class.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_class.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_draw.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_draw.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_pos.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_pos.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_scroll.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_scroll.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_style.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_style.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_style_gen.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_style_gen.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_tree.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_obj_tree.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_refr.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_refr.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_theme.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/core/lv_theme.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_arc.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_arc.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_img.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_img.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_label.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_label.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_line.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_line.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_mask.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_mask.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_rect.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_rect.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_triangle.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_draw_triangle.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_img_buf.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_img_buf.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_img_cache.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_img_cache.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_img_decoder.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/lv_img_decoder.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/nxp_pxp/lv_gpu_nxp_pxp.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/nxp_pxp/lv_gpu_nxp_pxp.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/nxp_pxp/lv_gpu_nxp_pxp_osa.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/nxp_pxp/lv_gpu_nxp_pxp_osa.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/nxp_vglite/lv_gpu_nxp_vglite.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/nxp_vglite/lv_gpu_nxp_vglite.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_arc.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_arc.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_bg.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_bg.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_composite.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_composite.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_img.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_img.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_label.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_label.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_line.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_line.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_mask.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_mask.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_polygon.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_polygon.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_rect.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_rect.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_stack_blur.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_stack_blur.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_texture_cache.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_texture_cache.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_utils.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sdl/lv_draw_sdl_utils.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/stm32_dma2d/lv_gpu_stm32_dma2d.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/stm32_dma2d/lv_gpu_stm32_dma2d.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_arc.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_arc.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_blend.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_blend.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_dither.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_dither.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_gradient.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_gradient.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_img.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_img.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_letter.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_letter.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_line.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_line.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_polygon.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_polygon.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_rect.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/draw/sw/lv_draw_sw_rect.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/layouts/flex/lv_flex.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/layouts/flex/lv_flex.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/layouts/grid/lv_grid.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/layouts/grid/lv_grid.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/bmp/lv_bmp.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/bmp/lv_bmp.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/ffmpeg/lv_ffmpeg.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/ffmpeg/lv_ffmpeg.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/freetype/lv_freetype.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/freetype/lv_freetype.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/fsdrv/lv_fs_fatfs.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/fsdrv/lv_fs_fatfs.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/fsdrv/lv_fs_posix.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/fsdrv/lv_fs_posix.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/fsdrv/lv_fs_stdio.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/fsdrv/lv_fs_stdio.o
Executable file
Binary file not shown.
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/fsdrv/lv_fs_win32.o
Executable file
BIN
lib/resource/3588s/src/build/obj/lvgl/src/extra/libs/fsdrv/lv_fs_win32.o
Executable file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user