Compare commits
50 Commits
4be305d0e2
...
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 | ||
|
|
844d3c89ae | ||
|
|
f8789876f5 |
11
bin/clawd.js
11
bin/clawd.js
@@ -7,6 +7,7 @@ require('../lib/systemd-env');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
const { ClawClient } = require('../lib/client');
|
const { ClawClient } = require('../lib/client');
|
||||||
|
const config = require('../lib/config');
|
||||||
const log = require('../lib/logger');
|
const log = require('../lib/logger');
|
||||||
const { pollSms } = require('../drivers/sim/sms-reader');
|
const { pollSms } = require('../drivers/sim/sms-reader');
|
||||||
|
|
||||||
@@ -17,6 +18,16 @@ exec(`bash "${bindScript}"`, (err, stdout, stderr) => {
|
|||||||
else log.info('clawd', `bind-quectel-serial: ok`);
|
else log.info('clawd', `bind-quectel-serial: ok`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 同步 Samba 共享密码(idempotent,失败不影响主流程)
|
||||||
|
const cfg = config.load();
|
||||||
|
if (cfg.share_key) {
|
||||||
|
const shareKey = cfg.share_key.replace(/'/g, "'\\''");
|
||||||
|
exec(`printf '%s\\n%s\\n' '${shareKey}' '${shareKey}' | smbpasswd -a sts -s 2>/dev/null`, (err) => {
|
||||||
|
if (err) log.warn('clawd', `smbpasswd sync failed (samba not installed?): ${err.message}`);
|
||||||
|
else log.info('clawd', 'smbpasswd synced for user sts');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let smsTimer = null;
|
let smsTimer = null;
|
||||||
let smsPolling = false;
|
let smsPolling = false;
|
||||||
|
|
||||||
|
|||||||
181
install.sh
181
install.sh
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# clawd 一键安装脚本
|
# clawd installer
|
||||||
# 用法:curl -fsSL https://raw.githubusercontent.com/stswangzhiping/clawd/main/install.sh | bash
|
# Run: curl -fsSL https://git.cutos.ai/claw-daemon/clawd/raw/branch/main/install.sh | sudo bash
|
||||||
# 需要 root 权限,需要已安装 Node.js >= 18
|
# Requires root and Node.js >= 18
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -11,26 +11,26 @@ info() { echo -e "${GREEN}[clawd]${NC} $*"; }
|
|||||||
warn() { echo -e "${YELLOW}[clawd]${NC} $*"; }
|
warn() { echo -e "${YELLOW}[clawd]${NC} $*"; }
|
||||||
error() { echo -e "${RED}[clawd]${NC} $*"; exit 1; }
|
error() { echo -e "${RED}[clawd]${NC} $*"; exit 1; }
|
||||||
|
|
||||||
# ── 检查 root ────────────────────────────────────────────────────────────────
|
# Check root
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
error "请以 root 身份运行(sudo bash install.sh)"
|
error "Please run as root: sudo bash install.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 检查 Node.js ─────────────────────────────────────────────────────────────
|
# Check Node.js
|
||||||
if ! command -v node &>/dev/null; then
|
if ! command -v node &>/dev/null; then
|
||||||
error "未找到 Node.js,请先安装 Node.js >= 18"
|
error "Node.js not found. Please install Node.js >= 18"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
NODE_VER=$(node -e "process.stdout.write(process.versions.node)")
|
NODE_VER=$(node -e "process.stdout.write(process.versions.node)")
|
||||||
MAJOR=$(echo "$NODE_VER" | cut -d. -f1)
|
MAJOR=$(echo "$NODE_VER" | cut -d. -f1)
|
||||||
if [ "$MAJOR" -lt 18 ]; then
|
if [ "$MAJOR" -lt 18 ]; then
|
||||||
error "Node.js 版本过低(当前 $NODE_VER),需要 >= 18"
|
error "Node.js version $NODE_VER is too old. Requires >= 18"
|
||||||
fi
|
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
|
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
|
if command -v apt-get &>/dev/null; then
|
||||||
apt-get install -y -qq dnsmasq >/dev/null 2>&1
|
apt-get install -y -qq dnsmasq >/dev/null 2>&1
|
||||||
elif command -v yum &>/dev/null; then
|
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
|
elif command -v apk &>/dev/null; then
|
||||||
apk add --quiet dnsmasq >/dev/null 2>&1
|
apk add --quiet dnsmasq >/dev/null 2>&1
|
||||||
else
|
else
|
||||||
warn "无法自动安装 dnsmasq,WiFi 配网功能可能不可用"
|
warn "Cannot install dnsmasq. WiFi captive portal may not work."
|
||||||
fi
|
fi
|
||||||
# 禁止 dnsmasq 系统服务自启(clawd 自己管理)
|
# Disable system dnsmasq; clawd manages it directly
|
||||||
systemctl disable dnsmasq 2>/dev/null || true
|
systemctl disable dnsmasq 2>/dev/null || true
|
||||||
systemctl stop dnsmasq 2>/dev/null || true
|
systemctl stop dnsmasq 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
if command -v dnsmasq &>/dev/null; then
|
if command -v dnsmasq &>/dev/null; then
|
||||||
info "dnsmasq ✓"
|
info "dnsmasq OK"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 启用 NetworkManager(WiFi 配网需要)──────────────────────────────────────
|
# Configure NetworkManager for WiFi
|
||||||
if command -v nmcli &>/dev/null; then
|
if command -v nmcli &>/dev/null; then
|
||||||
if ! systemctl is-active --quiet NetworkManager 2>/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
|
systemctl enable --now NetworkManager 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
info "NetworkManager ✓"
|
info "NetworkManager OK"
|
||||||
|
|
||||||
# 预写 DNS 劫持配置(运行时 /etc 可能为只读)
|
# Write captive-portal DNS config
|
||||||
NM_DNSMASQ_DIR="/etc/NetworkManager/dnsmasq-shared.d"
|
NM_DNSMASQ_DIR="/etc/NetworkManager/dnsmasq-shared.d"
|
||||||
mkdir -p "$NM_DNSMASQ_DIR"
|
mkdir -p "$NM_DNSMASQ_DIR"
|
||||||
cat > "$NM_DNSMASQ_DIR/clawd-captive.conf" << 'DNSCONF'
|
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
|
# All DNS queries resolve to gateway to trigger captive portal
|
||||||
address=/#/10.42.0.1
|
address=/#/10.42.0.1
|
||||||
DNSCONF
|
DNSCONF
|
||||||
info "DNS 劫持配置已写入 $NM_DNSMASQ_DIR ✓"
|
info "DNS captive config written to $NM_DNSMASQ_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── WiFi rfkill 解锁(部分设备默认禁用 WiFi)────────────────────────────────
|
# Unblock WiFi via rfkill
|
||||||
for rf in /sys/class/rfkill/rfkill*; do
|
for rf in /sys/class/rfkill/rfkill*; do
|
||||||
if [ -f "$rf/type" ] && [ "$(cat "$rf/type")" = "wlan" ]; then
|
if [ -f "$rf/type" ] && [ "$(cat "$rf/type")" = "wlan" ]; then
|
||||||
if [ "$(cat "$rf/soft")" = "1" ]; then
|
if [ "$(cat "$rf/soft")" = "1" ]; then
|
||||||
info "解锁 WiFi ($(basename "$rf"))..."
|
info "Unblocking WiFi ($(basename "$rf"))..."
|
||||||
echo 0 > "$rf/soft"
|
echo 0 > "$rf/soft"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# 持久化:独立脚本 + systemd 服务,确保开机自动解锁 WiFi
|
# Install rfkill unblock script + systemd unit for persistence
|
||||||
RFKILL_SCRIPT="/usr/local/bin/clawd-unblock-wifi.sh"
|
RFKILL_SCRIPT="/usr/local/bin/clawd-unblock-wifi.sh"
|
||||||
cat > "$RFKILL_SCRIPT" << 'SCRIPT'
|
cat > "$RFKILL_SCRIPT" << 'SCRIPT'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
@@ -108,51 +108,74 @@ WantedBy=multi-user.target
|
|||||||
UNIT
|
UNIT
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable clawd-rfkill
|
systemctl enable clawd-rfkill
|
||||||
info "WiFi rfkill 解锁服务已创建 ✓"
|
info "WiFi rfkill service installed"
|
||||||
|
|
||||||
# ── 安装 ttyd(Web 终端)────────────────────────────────────────────────────
|
# Install ttyd (Web terminal)
|
||||||
info "安装 ttyd..."
|
info "Installing ttyd..."
|
||||||
if apt-get install -y ttyd >/dev/null 2>&1; then
|
if apt-get install -y ttyd >/dev/null 2>&1; then
|
||||||
info "ttyd 已安装 ✓"
|
info "ttyd installed OK"
|
||||||
else
|
else
|
||||||
warn "ttyd 安装失败,Web 终端功能将不可用"
|
warn "ttyd install failed. Web terminal will not be available."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 安装 clawd ───────────────────────────────────────────────────────────────
|
# Clone / update clawd
|
||||||
INSTALL_DIR="/opt/clawd"
|
INSTALL_DIR="/opt/clawd"
|
||||||
CONFIG_DIR="/etc/clawd"
|
CONFIG_DIR="/etc/clawd"
|
||||||
ENV_FILE="$CONFIG_DIR/env"
|
ENV_FILE="$CONFIG_DIR/env"
|
||||||
info "安装到 $INSTALL_DIR ..."
|
info "Setting up $INSTALL_DIR ..."
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
cd "$INSTALL_DIR"
|
cd "$INSTALL_DIR"
|
||||||
|
|
||||||
# 下载源码(若目录已有 package.json,视为离线/已解压部署,跳过 git/tarball;避免设备无法访问 github.com)
|
# Use git if available, fall back to tarball
|
||||||
if [ -f "package.json" ]; then
|
CUTOS_REPO="https://git.cutos.ai/claw-daemon/clawd.git"
|
||||||
info "检测到已有源码,跳过 git/tarball 下载"
|
if command -v git &>/dev/null && [ -d ".git" ]; then
|
||||||
elif command -v git &>/dev/null; then
|
CURRENT_REMOTE=$(git remote get-url origin 2>/dev/null || echo "")
|
||||||
if [ -d ".git" ]; then
|
if echo "$CURRENT_REMOTE" | grep -q "github.com"; then
|
||||||
git pull --quiet
|
info "Migrating git remote to git.cutos.ai ..."
|
||||||
else
|
git remote set-url origin "$CUTOS_REPO"
|
||||||
git clone --depth=1 https://github.com/stswangzhiping/clawd.git .
|
|
||||||
fi
|
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
|
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
|
curl -fsSL "$TARBALL_URL" | tar -xz --strip-components=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 安装依赖
|
# Install npm dependencies
|
||||||
info "安装 npm 依赖..."
|
info "Running npm install..."
|
||||||
npm install --omit=dev --silent
|
npm install --omit=dev --silent
|
||||||
|
|
||||||
# 创建可执行链接
|
# Create symlink
|
||||||
ln -sf "$INSTALL_DIR/bin/clawd.js" /usr/local/bin/clawd
|
ln -sf "$INSTALL_DIR/bin/clawd.js" /usr/local/bin/clawd
|
||||||
chmod +x "$INSTALL_DIR/bin/clawd.js"
|
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"
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
|
||||||
if [ ! -f "$CONFIG_DIR/config.json" ]; then
|
if [ ! -f "$CONFIG_DIR/config.json" ]; then
|
||||||
@@ -164,120 +187,116 @@ if [ ! -f "$CONFIG_DIR/config.json" ]; then
|
|||||||
"heartbeat_interval": 30
|
"heartbeat_interval": 30
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
info "配置文件已创建:$CONFIG_DIR/config.json ✓"
|
info "Default config written to $CONFIG_DIR/config.json"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
cat > "$ENV_FILE" <<EOF
|
cat > "$ENV_FILE" <<EOF
|
||||||
# clawd 环境变量(systemd EnvironmentFile)
|
# clawd environment (loaded by systemd EnvironmentFile)
|
||||||
# 日志级别: debug / info / warn / error
|
# Log level: debug / info / warn / error
|
||||||
CLAWD_LOG_LEVEL=info
|
CLAWD_LOG_LEVEL=info
|
||||||
# 是否写日志文件(0=仅 journald)
|
# Log to file (0 = journald only)
|
||||||
CLAWD_LOG_FILE=1
|
CLAWD_LOG_FILE=1
|
||||||
# 自定义服务器地址(留空则读 config.json)
|
# Override server URL (default from config.json)
|
||||||
# CLAWD_SERVER=wss://claw.cutos.ai/ws
|
# CLAWD_SERVER=wss://claw.cutos.ai/ws
|
||||||
# BtMonitor(bluetoothctl)默认在程序内关闭,无需在此写 CLAWD_DISABLE_BT。
|
# Enable Bluetooth monitor (bluetoothctl); disabled by default
|
||||||
# 若产品需要蓝牙指示灯,取消下一行注释:
|
|
||||||
# CLAWD_ENABLE_BT=1
|
# 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
|
# CLAWD_OPENVFD_PATH=/sys/class/leds/openvfd
|
||||||
# 数码管 vfdservice 管道(默认 /tmp/openvfd_service)
|
# vfdservice pipe path (default: /tmp/openvfd_service)
|
||||||
# CLAWD_VFD_PIPE=/tmp/openvfd_service
|
# CLAWD_VFD_PIPE=/tmp/openvfd_service
|
||||||
# 多网口/特殊板型可固定 LAN 灯监控的以太网口(默认由 clawd 自动锁定首次 carrier 口)
|
# Wired LAN interface for carrier detection
|
||||||
# CLAWD_ETH_IFACE=end0
|
# CLAWD_ETH_IFACE=end0
|
||||||
EOF
|
EOF
|
||||||
info "环境变量文件已创建:$ENV_FILE ✓"
|
info "Default env file written to $ENV_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 创建日志目录 ─────────────────────────────────────────────────────────────
|
# Create log directory
|
||||||
mkdir -p "$CONFIG_DIR/logs"
|
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)
|
NODE_BIN=$(command -v node)
|
||||||
SERVICE_FILE="/etc/systemd/system/clawd.service"
|
SERVICE_FILE="/etc/systemd/system/clawd.service"
|
||||||
|
|
||||||
cat > "$SERVICE_FILE" <<EOF
|
cat > "$SERVICE_FILE" <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Claw Box Daemon
|
Description=Claw Box Daemon
|
||||||
Documentation=https://github.com/stswangzhiping/clawd
|
Documentation=https://git.cutos.ai/claw-daemon/clawd
|
||||||
After=NetworkManager.service
|
After=NetworkManager.service
|
||||||
Wants=NetworkManager.service
|
Wants=NetworkManager.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
# systemd-notify 由子进程执行,默认 NotifyAccess=main 会拒收;需 all 才能喂 WatchdogSec
|
# NotifyAccess=all required for systemd-notify with WatchdogSec
|
||||||
NotifyAccess=all
|
NotifyAccess=all
|
||||||
EnvironmentFile=$ENV_FILE
|
EnvironmentFile=$ENV_FILE
|
||||||
ExecStart=$NODE_BIN $INSTALL_DIR/bin/clawd.js
|
ExecStart=$NODE_BIN $INSTALL_DIR/bin/clawd.js
|
||||||
WorkingDirectory=$INSTALL_DIR
|
WorkingDirectory=$INSTALL_DIR
|
||||||
|
|
||||||
# 重启策略
|
# Restart policy
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
# 旧版 systemd 不认 StartLimitIntervalSec,用 StartLimitInterval=(秒)
|
|
||||||
StartLimitInterval=300
|
StartLimitInterval=300
|
||||||
StartLimitBurst=10
|
StartLimitBurst=10
|
||||||
|
|
||||||
# 优雅停止(10s 内 SIGTERM,超时 SIGKILL)
|
# Allow 10s for graceful shutdown before SIGKILL
|
||||||
TimeoutStopSec=10
|
TimeoutStopSec=10
|
||||||
KillMode=mixed
|
KillMode=mixed
|
||||||
KillSignal=SIGTERM
|
KillSignal=SIGTERM
|
||||||
|
|
||||||
# 资源限制(防止失控)
|
# Resource limits
|
||||||
MemoryMax=256M
|
MemoryMax=256M
|
||||||
CPUQuota=50%
|
CPUQuota=50%
|
||||||
TasksMax=64
|
TasksMax=64
|
||||||
|
|
||||||
# 安全加固(ttyd 子进程需要 setuid sudo,不能用 NoNewPrivileges/strict)
|
# Sandbox disabled: clawd needs to write system/config files on some devices
|
||||||
ProtectSystem=full
|
|
||||||
ReadWritePaths=$CONFIG_DIR /tmp
|
|
||||||
|
|
||||||
# 日志
|
# Logging
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
SyslogIdentifier=clawd
|
SyslogIdentifier=clawd
|
||||||
|
|
||||||
# systemd Watchdog(60s 无响应视为挂死)
|
# systemd Watchdog: restart if no heartbeat within 60s
|
||||||
WatchdogSec=60
|
WatchdogSec=60
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
info "systemd 服务文件已创建 ✓"
|
info "systemd service file written"
|
||||||
|
|
||||||
# ── journald 日志限制(可选) ────────────────────────────────────────────────
|
# Configure journald retention
|
||||||
JOURNAL_CONF="/etc/systemd/journald.conf.d/clawd.conf"
|
JOURNAL_CONF="/etc/systemd/journald.conf.d/clawd.conf"
|
||||||
if [ ! -f "$JOURNAL_CONF" ]; then
|
if [ ! -f "$JOURNAL_CONF" ]; then
|
||||||
mkdir -p /etc/systemd/journald.conf.d
|
mkdir -p /etc/systemd/journald.conf.d
|
||||||
cat > "$JOURNAL_CONF" <<EOF
|
cat > "$JOURNAL_CONF" <<EOF
|
||||||
# clawd journald 限制
|
# clawd journald limits
|
||||||
[Journal]
|
[Journal]
|
||||||
SystemMaxUse=100M
|
SystemMaxUse=100M
|
||||||
MaxFileSec=7day
|
MaxFileSec=7day
|
||||||
EOF
|
EOF
|
||||||
systemctl restart systemd-journald 2>/dev/null || true
|
systemctl restart systemd-journald 2>/dev/null || true
|
||||||
info "journald 日志限制已配置 ✓"
|
info "journald config written"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 启用并启动 ──────────────────────────────────────────────────────────────
|
# Enable and start clawd
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable clawd
|
systemctl enable clawd
|
||||||
systemctl restart clawd
|
systemctl restart clawd
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
if systemctl is-active --quiet clawd; then
|
if systemctl is-active --quiet clawd; then
|
||||||
info "clawd 服务运行中 ✓"
|
info "clawd is running"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 查看日志: journalctl -u clawd -f"
|
echo " Logs: journalctl -u clawd -f"
|
||||||
echo " 查看状态: systemctl status clawd"
|
echo " Status: systemctl status clawd"
|
||||||
echo " 停止服务: systemctl stop clawd"
|
echo " Stop: systemctl stop clawd"
|
||||||
echo " 配置文件: $CONFIG_DIR/config.json"
|
echo " Config: $CONFIG_DIR/config.json"
|
||||||
echo " 环境变量: $ENV_FILE"
|
echo " Env: $ENV_FILE"
|
||||||
echo " 文件日志: $CONFIG_DIR/logs/clawd.log"
|
echo " Log dir: $CONFIG_DIR/logs/clawd.log"
|
||||||
echo ""
|
echo ""
|
||||||
else
|
else
|
||||||
warn "服务启动失败,请检查日志:"
|
warn "clawd failed to start. Check logs:"
|
||||||
echo " journalctl -u clawd -n 50 --no-pager"
|
echo " journalctl -u clawd -n 50 --no-pager"
|
||||||
fi
|
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 };
|
||||||
115
lib/client.js
115
lib/client.js
@@ -17,6 +17,7 @@ const { ProvisionManager } = require('./provisioning');
|
|||||||
const { BtMonitor } = require('./bt-monitor');
|
const { BtMonitor } = require('./bt-monitor');
|
||||||
const { hasInternet, hasWiredInternetProbe, getLocalIps, getLocalNetworks } = require('./network');
|
const { hasInternet, hasWiredInternetProbe, getLocalIps, getLocalNetworks } = require('./network');
|
||||||
const { applyFullProviderFromVps, removeProviderByName, refreshModelsIfChanged, isFullProvider } = require('./openclaw-provider');
|
const { applyFullProviderFromVps, removeProviderByName, refreshModelsIfChanged, isFullProvider } = require('./openclaw-provider');
|
||||||
|
const sysCall = require('./sys-call');
|
||||||
const led = require('./led');
|
const led = require('./led');
|
||||||
|
|
||||||
const MAX_BACKOFF_MS = 60_000;
|
const MAX_BACKOFF_MS = 60_000;
|
||||||
@@ -115,6 +116,9 @@ class ClawClient {
|
|||||||
led.status.setApps();
|
led.status.setApps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动即点亮 VFD,不等 WS / 联网流程
|
||||||
|
led.display.showTime();
|
||||||
|
|
||||||
this._startSdNotify();
|
this._startSdNotify();
|
||||||
|
|
||||||
// RJ45 链路轮询(OpenVFD play),与 WS 无关,进程起来即开始
|
// RJ45 链路轮询(OpenVFD play),与 WS 无关,进程起来即开始
|
||||||
@@ -262,11 +266,12 @@ class ClawClient {
|
|||||||
_connect() {
|
_connect() {
|
||||||
if (this._stopped) return;
|
if (this._stopped) return;
|
||||||
|
|
||||||
// AP 模式 + 无网:不建立 WS,5s 后重新检查(有线经 -I ping 仍通则建立,避免热点误挡 WS)
|
// AP 模式下 hasInternet() 可能被热点本地网络 / NetworkManager limited 状态误判。
|
||||||
if (this._provisionMgr && this._provisionMgr.isApMode() && !hasInternet() && !hasWiredInternetProbe()) {
|
// 只有明确探测到有线口可访问公网时,才允许进入 WS 连接流程并显示 Conn。
|
||||||
|
if (this._provisionMgr && this._provisionMgr.isApMode() && !hasWiredInternetProbe()) {
|
||||||
led.display.showAP();
|
led.display.showAP();
|
||||||
log.info('clawd', 'AP 模式无网络,5s 后重新检查...');
|
log.info('clawd', 'AP 模式无有线网络,5s 后重新检查...');
|
||||||
this._backoff = 1_000; // 有网时立即快速重连
|
this._backoff = 1_000; // 有线恢复时立即快速重连
|
||||||
this._wsFailCount = 0; // 不计入失败
|
this._wsFailCount = 0; // 不计入失败
|
||||||
setTimeout(() => this._connect(), 5_000);
|
setTimeout(() => this._connect(), 5_000);
|
||||||
return;
|
return;
|
||||||
@@ -377,6 +382,7 @@ class ClawClient {
|
|||||||
token: this._cfg.token ?? null,
|
token: this._cfg.token ?? null,
|
||||||
version: CLAWD_VERSION,
|
version: CLAWD_VERSION,
|
||||||
ssh_secret_key: this._cfg.ssh_secret_key ?? null,
|
ssh_secret_key: this._cfg.ssh_secret_key ?? null,
|
||||||
|
share_key: this._cfg.share_key ?? null,
|
||||||
headscale_joined: headscale.isInstalled() && headscale.isJoined(this._cfg.headscale_server || 'https://hs.claw.cutos.ai'),
|
headscale_joined: headscale.isInstalled() && headscale.isJoined(this._cfg.headscale_server || 'https://hs.claw.cutos.ai'),
|
||||||
local_ip: getLocalIps(),
|
local_ip: getLocalIps(),
|
||||||
local_networks: getLocalNetworks(),
|
local_networks: getLocalNetworks(),
|
||||||
@@ -402,6 +408,9 @@ class ClawClient {
|
|||||||
case 'upgrade':
|
case 'upgrade':
|
||||||
this._handleUpgrade(msg);
|
this._handleUpgrade(msg);
|
||||||
break;
|
break;
|
||||||
|
case 'sys-call':
|
||||||
|
sysCall.handle(msg, (reply) => this._send({ type: 'sys-call', ...reply }));
|
||||||
|
break;
|
||||||
case 'headscale_logout':
|
case 'headscale_logout':
|
||||||
headscale.logout().catch(e => log.error('headscale', 'logout 失败:', e.message));
|
headscale.logout().catch(e => log.error('headscale', 'logout 失败:', e.message));
|
||||||
break;
|
break;
|
||||||
@@ -507,13 +516,26 @@ class ClawClient {
|
|||||||
|
|
||||||
_setHostname(clawId) {
|
_setHostname(clawId) {
|
||||||
const hostname = `claw-${clawId}`;
|
const hostname = `claw-${clawId}`;
|
||||||
exec(`sudo hostname ${hostname}`, (err) => {
|
|
||||||
if (err) {
|
// 运行时 hostname(无需文件权限)
|
||||||
log.warn('clawd', `设置 hostname 失败: ${err.message}`);
|
exec(`hostname ${hostname}`, { shell: true });
|
||||||
} else {
|
|
||||||
log.info('clawd', `hostname → ${hostname}`);
|
// 写 /etc/hostname(fs 直接写文件,不需要目录可写)
|
||||||
}
|
try {
|
||||||
});
|
fs.writeFileSync('/etc/hostname', hostname + '\n', 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('clawd', `write /etc/hostname failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 /etc/hosts(fs 读写,绕过 sed -i 需要目录可写的限制)
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync('/etc/hosts', 'utf8');
|
||||||
|
const updated = content.replace(/^127\.0\.1\.1.*/m, `127.0.1.1 ${hostname}`);
|
||||||
|
fs.writeFileSync('/etc/hosts', updated, 'utf8');
|
||||||
|
log.info('clawd', `hostname -> ${hostname}`);
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('clawd', `write /etc/hosts failed: ${e.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── OpenClaw 配置 ────────────────────────────────────────────────────────────
|
// ── OpenClaw 配置 ────────────────────────────────────────────────────────────
|
||||||
@@ -668,48 +690,49 @@ class ClawClient {
|
|||||||
log.info('upgrade', `收到升级命令: ${CLAWD_VERSION} → ${targetVersion}`);
|
log.info('upgrade', `收到升级命令: ${CLAWD_VERSION} → ${targetVersion}`);
|
||||||
this._sendUpgradeProgress(5, 'starting');
|
this._sendUpgradeProgress(5, 'starting');
|
||||||
|
|
||||||
// 检查脚本是否存在
|
|
||||||
if (!fs.existsSync(scriptPath)) {
|
|
||||||
const err = `升级脚本不存在: ${scriptPath}`;
|
|
||||||
log.error('upgrade', err);
|
|
||||||
this._sendUpgradeProgress(0, 'failed', true, err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Step 1: Node.js 负责 git pull,拿到最新代码(含最新 update-clawd.sh)
|
||||||
|
this._sendUpgradeProgress(20, '拉取更新中');
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const child = exec(`bash "${scriptPath}" --no-restart`, { timeout: 300_000 });
|
const gitCmd = [
|
||||||
|
`git config --global --add safe.directory "${installDir}" 2>/dev/null || true`,
|
||||||
child.stdout.on('data', (data) => {
|
`cd "${installDir}"`,
|
||||||
const line = data.toString().trim();
|
`git remote get-url origin 2>/dev/null | grep -q github.com && git remote set-url origin https://git.cutos.ai/claw-daemon/clawd.git || true`,
|
||||||
log.info('upgrade', line);
|
`git fetch origin`,
|
||||||
|
`git reset --hard origin/main`,
|
||||||
// 根据脚本输出关键字上报进度
|
`git clean -fd`,
|
||||||
if (line.includes('Fetching latest')) this._sendUpgradeProgress(20, '拉取更新中');
|
].join(' && ');
|
||||||
else if (line.includes('Already up to date')) this._sendUpgradeProgress(100, 'already_up_to_date');
|
const child = exec(gitCmd, { timeout: 120_000 });
|
||||||
else if (line.includes('Updating working tree')) this._sendUpgradeProgress(50, '更新文件中');
|
child.stdout.on('data', d => log.info('upgrade', d.toString().trim()));
|
||||||
else if (line.includes('npm install')) this._sendUpgradeProgress(70, '安装依赖中');
|
child.stderr.on('data', d => log.warn('upgrade', d.toString().trim()));
|
||||||
else if (line.includes('No dependency')) this._sendUpgradeProgress(80, '无需安装依赖');
|
child.on('close', code => code === 0 ? resolve() : reject(new Error(`git pull 失败,退出码: ${code}`)));
|
||||||
else if (line.includes('Current commit')) this._sendUpgradeProgress(90, '即将重启');
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr.on('data', (data) => {
|
|
||||||
log.warn('upgrade', data.toString().trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('close', (code) => {
|
|
||||||
if (code === 0) resolve();
|
|
||||||
else reject(new Error(`脚本退出码: ${code}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', reject);
|
child.on('error', reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 脚本执行成功,通知服务端完成,然后退出让 systemd 重启
|
this._sendUpgradeProgress(50, '更新文件中');
|
||||||
|
|
||||||
|
// Step 2: 调用磁盘上已更新的 update-clawd.sh --no-pull(跳过 git,直接写 service 文件等)
|
||||||
|
if (!fs.existsSync(scriptPath)) {
|
||||||
|
throw new Error(`升级脚本不存在: ${scriptPath}`);
|
||||||
|
}
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const child = exec(`bash "${scriptPath}" --no-pull --no-restart`, { timeout: 180_000 });
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
const line = data.toString().trim();
|
||||||
|
log.info('upgrade', line);
|
||||||
|
if (line.includes('npm install')) this._sendUpgradeProgress(70, '安装依赖中');
|
||||||
|
else if (line.includes('No depend')) this._sendUpgradeProgress(80, '无需安装依赖');
|
||||||
|
else if (line.includes('Writing serv')) this._sendUpgradeProgress(85, '更新服务配置');
|
||||||
|
else if (line.includes('Current comm')) this._sendUpgradeProgress(90, '即将重启');
|
||||||
|
});
|
||||||
|
child.stderr.on('data', d => log.warn('upgrade', d.toString().trim()));
|
||||||
|
child.on('close', code => code === 0 ? resolve() : reject(new Error(`脚本退出码: ${code}`)));
|
||||||
|
child.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通知服务端完成,延迟 1.5 秒确保消息送达,再退出让 systemd 重启
|
||||||
this._sendUpgradeProgress(100, 'done');
|
this._sendUpgradeProgress(100, 'done');
|
||||||
log.info('upgrade', `升级至 v${targetVersion} 完成,即将重启...`);
|
log.info('upgrade', `升级至 v${targetVersion} 完成,即将重启...`);
|
||||||
|
|
||||||
// 延迟 1.5 秒确保进度消息送达,再退出
|
|
||||||
setTimeout(() => process.exit(0), 1500);
|
setTimeout(() => process.exit(0), 1500);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const DEFAULTS = {
|
|||||||
/** 云端已激活:用于启动/重连时立即点亮 alarm(pwr),不等首包 connected */
|
/** 云端已激活:用于启动/重连时立即点亮 alarm(pwr),不等首包 connected */
|
||||||
activated: false,
|
activated: false,
|
||||||
ssh_secret_key: null,
|
ssh_secret_key: null,
|
||||||
|
share_key: null,
|
||||||
headscale_server: 'https://hs.claw.cutos.ai',
|
headscale_server: 'https://hs.claw.cutos.ai',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,6 +29,33 @@ function _generateSshSecretKey() {
|
|||||||
return 'sk-' + bytes.toString('hex');
|
return 'sk-' + bytes.toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成可读性好的 Samba 共享密码,格式:xxxxxx-xxxxxx-xxxxXX(三段各6字符,连字符分隔)。
|
||||||
|
* 最后一段包含至少1个大写字母和1个数字,其余为小写字母。
|
||||||
|
* 示例:juqhuf-hykgyh-mykGi3
|
||||||
|
*/
|
||||||
|
function _generateShareKey() {
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const lower = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
const digits = '0123456789';
|
||||||
|
|
||||||
|
function rndChars(chars, n) {
|
||||||
|
const bytes = crypto.randomBytes(n);
|
||||||
|
return Array.from(bytes).map(b => chars[b % chars.length]).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const g1 = rndChars(lower, 6);
|
||||||
|
const g2 = rndChars(lower, 6);
|
||||||
|
// 第三段:4个小写 + 1个大写 + 1个数字,随机打乱顺序
|
||||||
|
const g3raw = (rndChars(lower, 4) + rndChars(upper, 1) + rndChars(digits, 1)).split('');
|
||||||
|
for (let i = g3raw.length - 1; i > 0; i--) {
|
||||||
|
const j = crypto.randomBytes(1)[0] % (i + 1);
|
||||||
|
[g3raw[i], g3raw[j]] = [g3raw[j], g3raw[i]];
|
||||||
|
}
|
||||||
|
return `${g1}-${g2}-${g3raw.join('')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
let cfg;
|
let cfg;
|
||||||
try {
|
try {
|
||||||
@@ -41,10 +69,16 @@ function load() {
|
|||||||
}
|
}
|
||||||
if (!cfg) cfg = Object.assign({}, DEFAULTS);
|
if (!cfg) cfg = Object.assign({}, DEFAULTS);
|
||||||
|
|
||||||
|
let dirty = false;
|
||||||
if (!cfg.ssh_secret_key) {
|
if (!cfg.ssh_secret_key) {
|
||||||
cfg.ssh_secret_key = _generateSshSecretKey();
|
cfg.ssh_secret_key = _generateSshSecretKey();
|
||||||
save(cfg);
|
dirty = true;
|
||||||
}
|
}
|
||||||
|
if (!cfg.share_key) {
|
||||||
|
cfg.share_key = _generateShareKey();
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
if (dirty) save(cfg);
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
const { isRK3566, readDeviceModel } = require('./led/detect');
|
const { isRK3566, isRK3588, readDeviceModel } = require('./led/detect');
|
||||||
|
|
||||||
function loadImpl() {
|
function loadImpl() {
|
||||||
const forced = String(process.env.CLAWD_LED_IMPL || '').trim().toLowerCase();
|
const forced = String(process.env.CLAWD_LED_IMPL || '').trim().toLowerCase();
|
||||||
@@ -10,6 +10,8 @@ function loadImpl() {
|
|||||||
let name;
|
let name;
|
||||||
if (forced) {
|
if (forced) {
|
||||||
name = forced;
|
name = forced;
|
||||||
|
} else if (isRK3588()) {
|
||||||
|
name = 'rk3588-lvgl';
|
||||||
} else if (isRK3566()) {
|
} else if (isRK3566()) {
|
||||||
name = 'rk3566';
|
name = 'rk3566';
|
||||||
} else {
|
} else {
|
||||||
@@ -21,6 +23,10 @@ function loadImpl() {
|
|||||||
log.info('led', `LED/VFD backend → rk3566-openvfd (${model || 'unknown model'})`);
|
log.info('led', `LED/VFD backend → rk3566-openvfd (${model || 'unknown model'})`);
|
||||||
return require('./led/rk3566-openvfd');
|
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') {
|
if (name === 'noop' || name === 'none' || name === 'off') {
|
||||||
log.info('led', `LED/VFD backend → noop (${model || 'unknown model'})`);
|
log.info('led', `LED/VFD backend → noop (${model || 'unknown model'})`);
|
||||||
return require('./led/noop');
|
return require('./led/noop');
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ function isRK3566() {
|
|||||||
return /RK3566/i.test(readDeviceModel());
|
return /RK3566/i.test(readDeviceModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRK3588() {
|
||||||
|
return /RK3588/i.test(readDeviceModel());
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
readDeviceModel,
|
readDeviceModel,
|
||||||
isRK3566,
|
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_PASSWORD = '12345678';
|
||||||
const AP_IFACE = process.env.CLAWD_WIFI_IFACE || '';
|
const AP_IFACE = process.env.CLAWD_WIFI_IFACE || '';
|
||||||
const CON_NAME = 'clawd-hotspot';
|
const CON_NAME = 'clawd-hotspot';
|
||||||
|
const AP_RETRY_TOKEN_FILE = '/run/clawd-ap-retry.token';
|
||||||
|
|
||||||
/** 产品 RJ45 在 sysfs 中的默认名;等价于检测 `cat /sys/class/net/end0/carrier` */
|
/** 产品 RJ45 在 sysfs 中的默认名;等价于检测 `cat /sys/class/net/end0/carrier` */
|
||||||
const DEFAULT_ETH_IFACE = 'end0';
|
const DEFAULT_ETH_IFACE = 'end0';
|
||||||
@@ -92,28 +93,15 @@ function hasLanCableCarrier() {
|
|||||||
return hasWiredCarrier();
|
return hasWiredCarrier();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _tryPingInternet() {
|
function _tryPingDefaultInternet() {
|
||||||
try {
|
try {
|
||||||
run('ping -c 1 -W 3 8.8.8.8');
|
run('ping -c 1 -W 3 8.8.8.8');
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {}
|
} 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function _tryPingWiredInternet() {
|
||||||
* 仅经有线口 ping 公网(不依赖默认路由)。
|
|
||||||
* AP 开启时 hasInternet() 易误判;维持 WS / 网络监视时用此兜底。
|
|
||||||
*/
|
|
||||||
function hasWiredInternetProbe() {
|
|
||||||
const wired = getWiredIfaceWithCarrier();
|
const wired = getWiredIfaceWithCarrier();
|
||||||
if (!wired) return false;
|
if (!wired) return false;
|
||||||
try {
|
try {
|
||||||
@@ -124,18 +112,31 @@ function hasWiredInternetProbe() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测是否有互联网连接(nmcli 连通性 + ping 兜底)
|
* 仅经有线口 ping 公网(不依赖默认路由)。
|
||||||
|
*/
|
||||||
|
function hasWiredInternetProbe() {
|
||||||
|
return _tryPingWiredInternet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否有真实互联网连接。
|
||||||
|
* 注意:NetworkManager 的 limited/local 可能只是 AP 本地网络或 captive 状态,不能当公网可用。
|
||||||
*/
|
*/
|
||||||
function hasInternet() {
|
function hasInternet() {
|
||||||
|
const wifiSta = isWifiStaConnected();
|
||||||
|
const wired = getWiredIfaceWithCarrier();
|
||||||
|
|
||||||
// 物理层快检:无 WiFi STA 且无任何有线 carrier → 立即 false(nmcli 有缓存,不可信)
|
// 物理层快检:无 WiFi STA 且无任何有线 carrier → 立即 false(nmcli 有缓存,不可信)
|
||||||
if (!isWifiStaConnected() && !hasWiredCarrier()) return false;
|
if (!wifiSta && !wired) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const out = run('nmcli networking connectivity check').trim();
|
const out = run('nmcli networking connectivity check').trim();
|
||||||
if (out === 'full' || out === 'limited') return true;
|
if (out === 'full') return true;
|
||||||
} catch (_) {}
|
} 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 }>}
|
* @returns {Promise<{ success: boolean, error?: string }>}
|
||||||
*/
|
*/
|
||||||
async function connectWifi(ssid, password) {
|
async function connectWifi(ssid, password) {
|
||||||
|
cancelHotspotRadioRetry(`准备连接 WiFi: ${ssid}`);
|
||||||
const iface = getWifiIface();
|
const iface = getWifiIface();
|
||||||
log.info('network', `尝试连接 WiFi: ${ssid}(ifname=${iface})`);
|
log.info('network', `尝试连接 WiFi: ${ssid}(ifname=${iface})`);
|
||||||
try {
|
try {
|
||||||
@@ -272,14 +274,36 @@ async function connectWifi(ssid, password) {
|
|||||||
await nmcliAsync(['connection', 'delete', ssid], 15000);
|
await nmcliAsync(['connection', 'delete', ssid], 15000);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
try {
|
await _resetWifiRadioForSTA(iface);
|
||||||
await nmcliAsync(['device', 'set', iface, 'managed', 'yes'], 8000);
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
const args = ['device', 'wifi', 'connect', ssid];
|
if (password) {
|
||||||
if (password) args.push('password', password);
|
// 显式创建 STA profile,并固定为 WPA2-PSK only。
|
||||||
args.push('ifname', iface);
|
// RK3588/Broadcom DHD 对 NetworkManager 默认生成的 SAE/FT/WPA-PSK-SHA256 混合参数不稳定,
|
||||||
await nmcliAsync(args, 120000);
|
// 可能表现为一直 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();
|
await _ensureActiveWifiAutoconnect();
|
||||||
|
|
||||||
const deadline = Date.now() + CONNECT_WIFI_STA_WAIT_MS;
|
const deadline = Date.now() + CONNECT_WIFI_STA_WAIT_MS;
|
||||||
@@ -299,11 +323,201 @@ async function connectWifi(ssid, password) {
|
|||||||
}
|
}
|
||||||
return { success: false, error: '超时:网卡未进入已连接状态' };
|
return { success: false, error: '超时:网卡未进入已连接状态' };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
try { await nmcliAsync(['connection', 'modify', ssid, 'connection.autoconnect', 'no'], 8000); } catch (_) {}
|
||||||
log.error('network', `WiFi 连接失败: ${e.message}`);
|
log.error('network', `WiFi 连接失败: ${e.message}`);
|
||||||
return { success: false, error: 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 热点
|
* 启动 WiFi AP 热点
|
||||||
*/
|
*/
|
||||||
@@ -313,29 +527,24 @@ function startAP(clawId) {
|
|||||||
|
|
||||||
log.info('network', `启动 AP 热点: ${ssid} (${iface})`);
|
log.info('network', `启动 AP 热点: ${ssid} (${iface})`);
|
||||||
|
|
||||||
// 关闭已有热点
|
// 关闭已有热点,并在重新拉起 AP 前真正 power-cycle WiFi 芯片。
|
||||||
|
// RK3588/Broadcom DHD 在 LAN 断开后切 AP 时,单纯 ip link down/up 不一定清掉固件残留状态。
|
||||||
stopAP();
|
stopAP();
|
||||||
|
_resetWifiRadioForAP(iface, '准备 AP 前重置 WiFi radio');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// nmcli 创建热点(开放网络)
|
// 显式创建并激活热点,固定为 WPA2-PSK only。
|
||||||
const cmd = [
|
// 避免 NetworkManager 自动生成 WPA-PSK-SHA256/SAE 混合配置,部分 3588 Broadcom DHD 驱动重启 AP 时会拒绝该 RSN 参数。
|
||||||
'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(' '));
|
|
||||||
try {
|
try {
|
||||||
nmcliSync(['connection', 'modify', CON_NAME, 'connection.autoconnect', 'no'], 8000);
|
_activateHotspot(ssid, iface, 8000);
|
||||||
} catch (_) {}
|
} catch (firstError) {
|
||||||
|
log.warn('network', `AP 启动未在短超时内完成,后台再次重置 WiFi radio 后重试;避免阻塞 watchdog: ${firstError.message}`);
|
||||||
|
_spawnHotspotRadioRetry(ssid, iface);
|
||||||
|
return { ssid, ip: AP_IP, iface, pending: true };
|
||||||
|
}
|
||||||
|
|
||||||
// 等待 AP 启动
|
// 等待 AP 启动
|
||||||
sleep(2000);
|
sleep(1000);
|
||||||
log.info('network', `AP 已启动: ${ssid}, 网关 ${AP_IP}`);
|
log.info('network', `AP 已启动: ${ssid}, 网关 ${AP_IP}`);
|
||||||
return { ssid, ip: AP_IP, iface };
|
return { ssid, ip: AP_IP, iface };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -348,6 +557,7 @@ function startAP(clawId) {
|
|||||||
* 关闭热点,恢复普通 WiFi 模式
|
* 关闭热点,恢复普通 WiFi 模式
|
||||||
*/
|
*/
|
||||||
function stopAP() {
|
function stopAP() {
|
||||||
|
cancelHotspotRadioRetry('停止 AP');
|
||||||
try {
|
try {
|
||||||
run(`nmcli connection down ${CON_NAME}`);
|
run(`nmcli connection down ${CON_NAME}`);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -454,6 +664,7 @@ async function _ensureActiveWifiAutoconnect() {
|
|||||||
* clawd 只做调度;真正的认证、DHCP、重连细节仍交给 NM。
|
* clawd 只做调度;真正的认证、DHCP、重连细节仍交给 NM。
|
||||||
*/
|
*/
|
||||||
async function connectSavedWifiConnections() {
|
async function connectSavedWifiConnections() {
|
||||||
|
cancelHotspotRadioRetry('准备连接已保存 WiFi');
|
||||||
const iface = getWifiIface();
|
const iface = getWifiIface();
|
||||||
const profiles = listSavedWifiConnections();
|
const profiles = listSavedWifiConnections();
|
||||||
if (profiles.length === 0) {
|
if (profiles.length === 0) {
|
||||||
@@ -574,6 +785,7 @@ module.exports = {
|
|||||||
connectWifi,
|
connectWifi,
|
||||||
startAP,
|
startAP,
|
||||||
stopAP,
|
stopAP,
|
||||||
|
cancelHotspotRadioRetry,
|
||||||
AP_IP,
|
AP_IP,
|
||||||
getLocalIps,
|
getLocalIps,
|
||||||
getLocalNetworks,
|
getLocalNetworks,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const path = require('path');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const { exec } = require('child_process');
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
const { resolveOpenclawConfigFile } = require('./frpc');
|
const { resolveOpenclawConfigFile } = require('./frpc');
|
||||||
|
|
||||||
@@ -83,6 +84,23 @@ function writeJsonFile(filePath, obj) {
|
|||||||
fs.writeFileSync(filePath, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
|
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(解绑)。
|
* 同步:从 openclaw.json 删除指定 provider(解绑)。
|
||||||
* 若 primary 指向该 provider,先置为空串。
|
* 若 primary 指向该 provider,先置为空串。
|
||||||
@@ -126,6 +144,7 @@ function removeProviderByName(providerId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeJsonFile(configFile, config);
|
writeJsonFile(configFile, config);
|
||||||
|
restartGateway();
|
||||||
log.info('openclaw-provider', `provider 已移除: ${providerId}`);
|
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) {
|
function addProviderSync(configFile, providerId, baseUrl, apiKey, models, defaultModelRaw) {
|
||||||
const config = readJsonFile(configFile);
|
const config = readJsonFile(configFile);
|
||||||
|
|
||||||
@@ -187,7 +268,10 @@ function addProviderSync(configFile, providerId, baseUrl, apiKey, models, defaul
|
|||||||
mode: 'api_key',
|
mode: 'api_key',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ensureWebSearchConfig(configFile, config);
|
||||||
|
|
||||||
writeJsonFile(configFile, config);
|
writeJsonFile(configFile, config);
|
||||||
|
restartGateway();
|
||||||
log.info('openclaw-provider', `provider 已写入: ${providerId}(${models.length} 个模型)`);
|
log.info('openclaw-provider', `provider 已写入: ${providerId}(${models.length} 个模型)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +317,14 @@ function applyFullProviderFromVps(provider, onDone) {
|
|||||||
const curMd5 = computeModelsMd5(cur.models || []);
|
const curMd5 = computeModelsMd5(cur.models || []);
|
||||||
const newMd5 = computeModelsMd5(list);
|
const newMd5 = computeModelsMd5(list);
|
||||||
if (curApiKey === apiKey && curMd5 === newMd5) {
|
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}`); } }
|
if (typeof onDone === 'function') { try { onDone(); } catch (e) { log.warn('openclaw-provider', `onDone: ${e.message}`); } }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -298,10 +389,18 @@ function refreshModelsIfChanged(onDone) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// provider 不存在时也要确保 web search 配置
|
||||||
const providers = config.models?.providers || {};
|
const providers = config.models?.providers || {};
|
||||||
const providerId = Object.keys(providers)[0];
|
const providerId = Object.keys(providers)[0];
|
||||||
if (!providerId) {
|
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();
|
if (typeof onDone === 'function') onDone();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -323,7 +422,17 @@ function refreshModelsIfChanged(onDone) {
|
|||||||
const newMd5 = computeModelsMd5(newModels);
|
const newMd5 = computeModelsMd5(newModels);
|
||||||
|
|
||||||
if (currentMd5 === newMd5) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
const { hasInternet, hasSavedWifiConnection, connectSavedWifiConnections, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, getWifiIface, AP_IP } = require('./network');
|
const { hasInternet, hasWiredInternetProbe, hasSavedWifiConnection, connectSavedWifiConnections, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, getWifiIface, AP_IP } = require('./network');
|
||||||
const { DnsHijack } = require('./dns-hijack');
|
const { DnsHijack } = require('./dns-hijack');
|
||||||
const { CaptiveServer } = require('./captive-server');
|
const { CaptiveServer } = require('./captive-server');
|
||||||
const led = require('./led');
|
const led = require('./led');
|
||||||
@@ -258,7 +258,9 @@ class ProvisionManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this._state === 'ap') {
|
if (this._state === 'ap') {
|
||||||
if (hasInternet()) {
|
// AP 模式下 hasInternet() 可能被热点本地网络 / NetworkManager limited 状态误判。
|
||||||
|
// 只有明确探测到有线口可访问公网时,才关闭配网 AP。
|
||||||
|
if (hasWiredInternetProbe()) {
|
||||||
log.info('provision', '检测到有线网络可用,关闭 AP');
|
log.info('provision', '检测到有线网络可用,关闭 AP');
|
||||||
this._stopAPServices();
|
this._stopAPServices();
|
||||||
this._state = 'wired';
|
this._state = 'wired';
|
||||||
|
|||||||
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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user