F FisherHub Docs

04. 网页控制与进阶

内嵌网页架构

ESP32-S3 的 flash 空间足够将整个前端网页作为字节数组嵌入固件。用户访问 http://192.168.4.1/ 时,服务器直接返回这个内嵌页面,不依赖外部 CDN 或文件系统。

完整 HTML 页面

以下是一个完整的单页面应用,包含滑块控制、快捷按钮和角度实时反馈。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESPClaw 控制面板</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    font-family: -apple-system, sans-serif;
    background: #0f172a;
    color: #e2e8f0;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
  }
  .card {
    background: #1e293b;
    border-radius: 20px;
    padding: 32px;
    width: 360px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.5);
  }
  h1 { font-size: 20px; margin-bottom: 24px; text-align: center; }
  .angle-display {
    font-size: 48px;
    font-weight: 700;
    text-align: center;
    color: #38bdf8;
    margin-bottom: 8px;
  }
  .angle-label { text-align: center; color: #94a3b8; margin-bottom: 24px; }

  /* 自定义滑块 */
  input[type="range"] {
    -webkit-appearance: none;
    width: 100%;
    height: 6px;
    background: #334155;
    border-radius: 3px;
    outline: none;
    margin-bottom: 16px;
  }
  input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 28px;
    height: 28px;
    background: #38bdf8;
    border-radius: 50%;
    cursor: pointer;
    box-shadow: 0 0 20px rgba(56,189,248,0.4);
  }

  .button-row {
    display: flex;
    gap: 12px;
    margin-top: 20px;
  }
  .btn {
    flex: 1;
    padding: 14px;
    border: none;
    border-radius: 12px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: transform 0.1s;
  }
  .btn:active { transform: scale(0.96); }
  .btn-open { background: #22c55e; color: #052e16; }
  .btn-close { background: #ef4444; color: #450a0a; }
  .btn-mid { background: #eab308; color: #422006; }

  .status {
    text-align: center;
    margin-top: 16px;
    font-size: 13px;
    color: #64748b;
  }
  .connected { color: #22c55e; }
  .disconnected { color: #ef4444; }
</style>
</head>
<body>
<div class="card">
  <h1>ESPClaw 机械爪</h1>
  <div class="angle-display" id="angleDisplay">90</div>
  <div class="angle-label">当前角度</div>

  <input type="range" id="angleSlider" min="0" max="180" value="90">

  <div class="button-row">
    <button class="btn btn-open" id="btnOpen">张开</button>
    <button class="btn btn-mid" id="btnMid">中间</button>
    <button class="btn btn-close" id="btnClose">闭合</button>
  </div>

  <div class="status" id="status">正在连接...</div>
</div>

<script>
const slider = document.getElementById('angleSlider');
const display = document.getElementById('angleDisplay');
const statusEl = document.getElementById('status');

let ws = null;
let lastSent = -1;

function connect() {
  ws = new WebSocket('ws://192.168.4.1/ws');

  ws.onopen = () => {
    statusEl.textContent = '已连接';
    statusEl.className = 'status connected';
  };

  ws.onclose = () => {
    statusEl.textContent = '连接断开,3 秒后重连...';
    statusEl.className = 'status disconnected';
    setTimeout(connect, 3000);
  };

  ws.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data);
      if (data.angle !== undefined) {
        display.textContent = data.angle;
        if (data.angle !== parseInt(slider.value)) {
          slider.value = data.angle;
        }
      }
    } catch (e) { /* ignore */ }
  };
}

// 滑块控制:松开鼠标时才发送
let sendTimer = null;
slider.addEventListener('input', () => {
  display.textContent = slider.value;
  clearTimeout(sendTimer);
  sendTimer = setTimeout(() => sendAngle(slider.value), 50);
});

function sendAngle(angle) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ angle: parseInt(angle) }));
  }
}

function sendCommand(cmd) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ cmd: cmd }));
  }
}

// 快捷按钮
document.getElementById('btnOpen').onclick = () => sendCommand('open');
document.getElementById('btnMid').onclick = () => sendCommand('mid');
document.getElementById('btnClose').onclick = () => sendCommand('close');

connect();
</script>
</body>
</html>

页面功能说明

  • 滑块:拖动控制机械爪角度,松开后发送 WebSocket 指令
  • 角度显示:实时显示当前角度,支持来自 ESP32 的回显更新
  • 快捷按钮:一键开到最大(180°)、中间(90°)、完全闭合(0°)
  • 连接状态:显示 WebSocket 连接状态,断开自动重连

C 端适配

如果你在固件中内嵌该页面,需要将 HTML 转换为 C 字节数组。使用 ESP-IDF 自带的工具:

python $IDF_PATH/components/spiffs/flash_tools/make_flash_page.py \
  -i index.html -o web_page.h

或者手动用 Python 转换:

# html_to_header.py
with open("index.html", "r") as f:
    html = f.read()

with open("web_page.h", "w") as h:
    h.write("#pragma once\n\n")
    h.write(f"const size_t web_page_html_len = {len(html)};\n\n")
    h.write("const char web_page_html[] = {\n")
    for i in range(0, len(html), 16):
        chunk = html[i:i+16]
        line = ", ".join(f"0x{ord(c):02x}" for c in chunk)
        h.write(f"  {line},\n")
    h.write("};\n")

OTA 远程升级

ESP32-S3 支持 OTA(Over-the-Air)固件升级。ESPClaw 使用两个 OTA 分区,切换启动。

分区表

创建 partitions.csv

# Name,   Type, SubType, Offset,  Size
nvs,      data, nvs,     0x9000,  0x5000
otadata,  data, ota,     0xe000,  0x2000
ota_0,    app,  ota_0,   0x10000, 2M
ota_1,    app,  ota_1,   0x210000,2M

OTA 服务器实现

在 HTTP 服务器上添加一个 OTA 上传端点:

#include "esp_ota_ops.h"
#include "esp_http_server.h"

static esp_err_t ota_post_handler(httpd_req_t *req)
{
    char ota_buf[1024];
    esp_ota_handle_t ota_handle;
    const esp_partition_t *ota_partition =
        esp_ota_get_next_update_partition(NULL);

    esp_ota_begin(ota_partition, OTA_SIZE_UNKNOWN, &ota_handle);

    int remaining = req->content_len;
    while (remaining > 0) {
        int recv = httpd_req_recv(req, ota_buf,
                    MIN(remaining, sizeof(ota_buf)));
        if (recv <= 0) return ESP_FAIL;

        esp_ota_write(ota_handle, ota_buf, recv);
        remaining -= recv;
    }

    esp_ota_end(ota_handle);
    esp_ota_set_boot_partition(ota_partition);
    esp_restart();
    return ESP_OK;
}

配合网页端的升级入口,在控制页面中加入固件上传按钮。这样以后更新固件不需要插 USB 线,通过网络就能完成。

手势控制扩展思路

ESP32-S3 带有丰富的 GPIO 和外设接口,可以扩展多种控制方式。

方案一:PAJ7620 手势传感器

PAJ7620 是一款 I2C 接口的手势识别芯片,能识别上、下、左、右、前、后等手势。

// 伪代码示例
void gesture_task(void *arg)
{
    paj7620_init();

    while (1) {
        GestureType gesture = paj7620_read_gesture();
        switch (gesture) {
            case GESTURE_UP:
                smooth_move_to(180);   // 上挥手 → 张开
                break;
            case GESTURE_DOWN:
                smooth_move_to(0);     // 下挥手 → 闭合
                break;
            case GESTURE_LEFT:
                smooth_move_to(90);    // 左挥手 → 中间
                break;
            default:
                break;
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

接线:PAJ7620 的 SDA 接 GPIO 8,SCL 接 GPIO 9,VCC 接 3.3V,GND 共地。

方案二:手机加速度传感器

手机上 Web 页面可以通过 DeviceMotionEvent API 读取加速度数据,用倾斜角度控制机械爪:

window.addEventListener('devicemotion', (event) => {
  const z = event.accelerationIncludingGravity.z;
  // z 范围 -9.8 ~ 9.8,映射到 0 ~ 180 度
  const angle = Math.round((z + 9.8) / 19.6 * 180);
  sendAngle(Math.max(0, Math.min(180, angle)));
});

ROS2 集成方向

ESPClaw 虽然是一个简单的单舵机项目,但它可以作为 ROS2 生态中的一个传感器/执行器节点。思路如下:

架构

ROS2 主控(PC 或 Jetson Nano)

        │  Micro-ROS (UART / WiFi / USB)

   ESP32-S3 (ESPClaw)

   SG90 舵机

Micro-ROS 集成

Micro-ROS 将 ROS2 的客户端库移植到微控制器上。ESP32-S3 可以通过 WiFi 用 UDP 或 TCP 与 ROS2 主控通信。

// 伪代码:Micro-ROS 订阅角度话题
#include <rcl/rcl.h>
#include <std_msgs/msg/int32.h>

void micro_ros_task(void *arg)
{
    rcl_node_t node = rcl_get_zero_initialized_node();
    rcl_subscription_t sub = rcl_get_zero_initialized_subscription();

    // ... 初始化 Micro-ROS、创建节点、创建订阅者 ...

    while (1) {
        std_msgs__msg__Int32 msg;
        rcl_ret_t ret = rcl_take(sub, &msg, NULL, NULL);

        if (ret == RCL_RET_OK) {
            smooth_move_to(msg.data);  // 收到角度指令
        }

        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

这样,ESPClaw 就成为 ROS2 系统中的一个关节执行器,可以用 ROS2 的 joint_state_publisher 或 MoveIt 来规划机械爪动作。

完整功能清单

至此,ESPClaw 项目涵盖的功能:

  • SG90 舵机驱动(LEDC PWM)
  • WiFi AP 模式(无路由器直连)
  • HTTP + WebSocket 控制服务器
  • 内嵌网页控制界面(滑块 + 按钮)
  • 平滑角度过渡算法
  • OTA 远程固件升级
  • 手势传感器控制(扩展)
  • Micro-ROS 集成(扩展)
  • 多舵机联动(扩展)

[x] 的是你已经实现的功能,带 [ ] 的是进阶方向。根据自己的兴趣和时间,可以选择合适的扩展方向继续探索。