Skip to content

Arduino UNO R4 WiFi 热点直连控制开发项目指南

1. 项目简介与硬件需求

项目简介

本项目演示如何利用 Arduino UNO R4 WiFi 开发板作为无线接入点(AP 模式),建立独立的局域网。手机或电脑 APP 无需连接外部路由器,即可直接连接该热点,并通过 HTTP 请求实现对板载 LED 等外设的远程控制。该方案适用于无网络环境下的设备调试、便携式控制及快速原型验证。

硬件需求

  • 主控板:Arduino UNO R4 WiFi(基于 Renesas RA4M1 微控制器与 ESP32-S3 协处理器)。
  • 控制终端:智能手机、平板或电脑(用于连接热点并发送控制指令)。
  • 外设(可选):板载 LED 即可满足演示需求,也可外接继电器或电机进行扩展。
  • 数据线:USB-C 数据线(用于供电与程序烧录)。

2. 电路连接说明

本示例主要依赖 UNO R4 WiFi 的板载资源,无需复杂的外部接线:

  • 板载 LED:UNO R4 WiFi 的板载 LED 默认连接在数字引脚 D13
  • 供电方式:通过 USB-C 接口连接电脑或 5V 电源适配器即可。
  • 外部负载(扩展):若需控制外部设备,可将负载正极接 D13(或其他自定义引脚),负极接 GND。注意 UNO R4 WiFi 的 GPIO 最大输出电流有限,驱动大功率设备需加装驱动模块。

3. Arduino 代码示例

以下代码配置 UNO R4 WiFi 为 AP 模式,并启动一个简易 HTTP 服务器。当终端访问特定 URL 时,即可控制板载 LED。

wifi_ap_web.cpp

cpp
/*
*   WIFI WEB as Server
*/
//#include <WiFiS3.h>
#include "wifi_ap_web.h"

// WiFi AP 配置
const char* ssid = "R4WIFI";
const char* password = "12345678";

// 创建WiFi服务器(端口80)
WiFiServer server(80);
static WiFiClient client;

// LED 状态(由主程序管理)
extern bool ledState;

// WiFi 状态管理
#define WIFI_STATUS_INIT            0
#define WIFI_STATUS_INIT2CONNECTED  1
#define WIFI_STATUS_CONNECTED       3
#define WIFI_STATUS_DISCONNECTED    4
static int eCurrentWifiStatus = WIFI_STATUS_INIT;

// 前向声明
static void sendCorsResponse(WiFiClient& client, bool state);
static void sendJsonResponse(WiFiClient& client, bool state);
static void sendHtmlPage(WiFiClient& client, bool state);
static void sendNotFound(WiFiClient& client);
static void wifiPrintIpaddress();

// ============ WiFi 初始化 ============
void wifiweb_init() {
    Serial.println("正在配置AP模式...");
    WiFi.beginAP(ssid, password);
    Serial.println("等待AP启动");
    // 状态机会处理连接状态
}

// ============ 打印 IP 地址 ============
static void wifiPrintIpaddress() {
    Serial.println("\n✅ AP模式已启动");
    Serial.print("IP地址: ");
    Serial.println(WiFi.localIP());
    Serial.print("SSID: ");
    Serial.println(ssid);
    Serial.print("密码: ");
    Serial.println(password);
    server.begin();
    Serial.println("✅ Web服务器已启动");
}

// ============ WiFi 状态机 ============
#if 0
static int getWifiStatus() {
    // 简化版:只要 AP 启动就认为连接正常
    static bool apStarted = false;
    
    if (!apStarted) {
        // 检查 AP 是否已经启动
        IPAddress ip = WiFi.localIP();
        if (ip != INADDR_NONE && ip != IPAddress(0, 0, 0, 0)) {
            apStarted = true;
            wifiPrintIpaddress();
            eCurrentWifiStatus = WIFI_STATUS_CONNECTED;
        }
        return eCurrentWifiStatus;
    }
    
    // 如果 AP 已启动,直接返回 CONNECTED
    eCurrentWifiStatus = WIFI_STATUS_CONNECTED;
    return eCurrentWifiStatus;
}
#else

static int getWifiStatus() {
    switch(eCurrentWifiStatus) {
        case WIFI_STATUS_INIT:
            // AP模式:检查IP是否分配了
            if (WiFi.localIP() != INADDR_NONE) {
                eCurrentWifiStatus = WIFI_STATUS_INIT2CONNECTED;
                wifiPrintIpaddress();
            }
            break;
        case WIFI_STATUS_INIT2CONNECTED:
            // 如果有客户端连接,或者AP已就绪
            if (WiFi.localIP() != INADDR_NONE) {
                eCurrentWifiStatus = WIFI_STATUS_CONNECTED;
            }
            break;
        case WIFI_STATUS_CONNECTED:
            // 检查AP是否仍然活跃
            if (WiFi.localIP() == INADDR_NONE) {
                eCurrentWifiStatus = WIFI_STATUS_DISCONNECTED;
            }
            break;
        case WIFI_STATUS_DISCONNECTED:
        default:
            eCurrentWifiStatus = WIFI_STATUS_INIT;
            break;
    }
    return eCurrentWifiStatus;
}
#endif
// ============ 主处理函数 ============
int wifiweb_getCommand() {
    int cmd = 0;

    int wifiStatus = getWifiStatus();
    if (wifiStatus != WIFI_STATUS_CONNECTED) {
        // WiFi 未就绪,返回错误
        return (wifiStatus == WIFI_STATUS_INIT2CONNECTED) ? WIFI_CMD_WIFI_CONNECTED : WIFI_CMD_WIFI_ERROR;
    }

    // 监听客户端连接
    client = server.available();

    if (client) {
        Serial.println("📡 新客户端连接");

        // 读取HTTP请求
        String request = "";
        while (client.connected()) {
            if (client.available()) {
                char c = client.read();
                request += c;
                if (c == '\n' && request.endsWith("\r\n\r\n")) {
                    break;
                }
            }
        }

        // 解析请求路径
        String path = "";
        if (request.length() > 0) {
            int firstSpace = request.indexOf(' ');
            int secondSpace = request.indexOf(' ', firstSpace + 1);
            if (firstSpace > 0 && secondSpace > 0) {
                path = request.substring(firstSpace + 1, secondSpace);
            }
        }

        Serial.print("请求路径: ");
        Serial.println(path);

        // 处理不同路径
        if (path == "/H" || path == "/led/on") {
            Serial.println("💡 LED 开启");
            cmd = WIFI_CMD_ON;
            sendCorsResponse(client, true);
        }
        else if (path == "/L" || path == "/led/off") {
            Serial.println("💡 LED 关闭");
            cmd = WIFI_CMD_OFF;
            sendCorsResponse(client, false);
        }
        else if (path == "/T" || path == "/led/toggle") {
            Serial.println("🔄 LED 翻转");
            cmd = WIFI_CMD_TOGGLE;
            sendCorsResponse(client, ledState);
        }
        else if (path == "/status" || path == "/status/") {
            Serial.println("Get status");
            sendJsonResponse(client, ledState);
        }
        else if (path == "/" || path == "/index.html") {
            sendHtmlPage(client, ledState);
        }
        else {
            sendNotFound(client);
        }

        delay(10);
        client.stop();
        Serial.println("🔌 连接已关闭");
    }
    return cmd;
}

// ============ 响应函数 ============

static void sendCorsResponse(WiFiClient& client, bool state) {
    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: text/plain");
    client.println("Access-Control-Allow-Origin: *");
    client.println("Access-Control-Allow-Methods: GET, OPTIONS");
    client.println("Access-Control-Allow-Headers: *");
    client.println("Connection: close");
    client.println();
    client.print("LED状态: ");
    client.println(state ? "ON" : "OFF");
}

static void sendJsonResponse(WiFiClient& client, bool state) {
    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: application/json");
    client.println("Access-Control-Allow-Origin: *");
    client.println("Access-Control-Allow-Methods: GET, OPTIONS");
    client.println("Access-Control-Allow-Headers: *");
    client.println("Connection: close");
    client.println();

    client.print("{\"status\":\"ok\",\"led\":");
    client.print(state ? "true" : "false");
    client.print(",\"message\":\"");
    client.print(state ? "LED已开启" : "LED已关闭");
    client.println("\"}");
}

static void sendHtmlPage(WiFiClient& client, bool state) {
    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: text/html");
    client.println("Access-Control-Allow-Origin: *");
    client.println("Connection: close");
    client.println();

    client.println("<!DOCTYPE html>");
    client.println("<html>");
    client.println("<head><meta charset='UTF-8'><title>LED Control</title></head>");
    client.println("<body style='text-align:center;font-family:Arial;margin-top:50px;'>");
    client.println("<h1>💡 Arduino UNO R4 WiFi</h1>");
    client.println("<p>LED状态: <strong>" + String(state ? "ON" : "OFF") + "</strong></p>");
    client.println("<button onclick=\"fetch('/H')\" style='padding:15px 30px;background:#4CAF50;color:white;border:none;border-radius:5px;margin:5px;'>开灯</button>");
    client.println("<button onclick=\"fetch('/L')\" style='padding:15px 30px;background:#f44336;color:white;border:none;border-radius:5px;margin:5px;'>关灯</button>");
    client.println("<button onclick=\"fetch('/T')\" style='padding:15px 30px;background:#2196F3;color:white;border:none;border-radius:5px;margin:5px;'>翻转</button>");
    client.println("<br><br><button onclick=\"location.reload()\" style='padding:10px 20px;background:#607D8B;color:white;border:none;border-radius:5px;'>刷新状态</button>");
    client.println("</body></html>");
}

static void sendNotFound(WiFiClient& client) {
    client.println("HTTP/1.1 404 Not Found");
    client.println("Content-Type: text/plain");
    client.println("Access-Control-Allow-Origin: *");
    client.println("Connection: close");
    client.println();
    client.println("404 - 路径未找到");
}

wifi_ap_web.h

cpp
#ifndef WIFIWEB_H
#define WIFIWEB_H

#include <WiFiS3.h>

#ifdef __cplusplus
extern "C" {
#endif

#define WIFI_CMD_ON     1
#define WIFI_CMD_OFF    2
#define WIFI_CMD_TOGGLE 3
#define WIFI_CMD_WIFI_CONNECTED 10
#define WIFI_CMD_WIFI_ERROR     11

// 函数声明
void wifiweb_init();
int wifiweb_getCommand();

#ifdef __cplusplus
}
#endif

#endif

Main.cpp

cpp

#include <U8g2lib.h>
#include <Wire.h>
#include "wifi_ap_web.h"

/*
*   display module
*/
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ SCL, /* data=*/ SDA, /* reset=*/ U8X8_PIN_NONE);

void display_init()
{
  u8g2.begin();
  u8g2.enableUTF8Print();
}

void display_task()
{
  static int preMs = 0;
  int curMs = millis();
  if(curMs - preMs < 1000)
  {
    return;
  }
  preMs = curMs;  // 修正:应该是 preMs = curMs
  u8g2.clearBuffer();

  u8g2.setFont(u8g2_font_ncenB08_tr);
  u8g2.drawStr(0, 10, "Hello, U8g2!");

  u8g2.setFont(u8g2_font_wqy12_t_gb2312a);
  u8g2.drawUTF8(0, 25, "这是温度照度示例");

  u8g2.drawLine(0, 30, 127, 30);
  u8g2.drawBox(10, 40, 20, 10);
  u8g2.drawCircle(50, 50, 12, U8G2_DRAW_ALL);

  u8g2.sendBuffer(); 
}

/*
*  LED module
*/
const int ledPin = LED_BUILTIN;
bool ledState = false;  // 这个变量在 wifi_ap_web.cpp 中被 extern 引用

/*
*   main loop
*/
void setup() {
  Serial.begin(9600);

  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);
  ledState = false;

  display_init();
  
  Serial.println("wifi web init ...");
  wifiweb_init();
}

void wifiweb_task()
{
  int cmd = wifiweb_getCommand();
  switch(cmd)
  {
      case WIFI_CMD_ON: // led on
          digitalWrite(ledPin, HIGH);
          ledState = true;
          break;
      case WIFI_CMD_OFF: // led off
          digitalWrite(ledPin, LOW);
          ledState = false;
          break;
      case WIFI_CMD_TOGGLE: // led toggle
          ledState = !ledState;
          digitalWrite(ledPin, ledState ? HIGH : LOW);
          break;
      case WIFI_CMD_WIFI_CONNECTED: // wifi connected
          Serial.println("Wifi Connected !");
          break;       
      default:
          break;
  }
}

void loop() {
  display_task();
 
  wifiweb_task();

  delay(10);
}

control.html

html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>LED控制(带状态读取)</title>
    <style>
        body { font-family: Arial; max-width: 500px; margin: 50px auto; text-align: center; }
        .led { display: inline-block; width: 30px; height: 30px; border-radius: 50%; 
               background: #ccc; margin: 20px; transition: 0.3s; }
        .led.on { background: #ffeb3b; box-shadow: 0 0 30px #ffeb3b; }
        button { padding: 12px 30px; margin: 5px; border: none; border-radius: 5px; 
                 cursor: pointer; font-size: 16px; }
        .btn-on { background: #4CAF50; color: white; }
        .btn-off { background: #f44336; color: white; }
        .btn-toggle { background: #2196F3; color: white; }
        .btn-status { background: #607D8B; color: white; }
        #log { margin-top: 20px; padding: 10px; background: #f0f0f0; border-radius: 5px; font-size: 14px; }
    </style>
</head>
<body>
    <h1>💡 LED控制</h1>
    <p>Arduino IP: <input type="text" id="ipInput" value="192.168.4.1" style="width:120px;text-align:center;"></p>
    
    <div class="led" id="ledIndicator"></div>
    <p>LED状态: <strong id="ledStatus">未知</strong></p>
    
    <div>
        <button class="btn-on" onclick="sendCmd('H')">🔆 开灯</button>
        <button class="btn-off" onclick="sendCmd('L')">🔅 关灯</button>
        <button class="btn-toggle" onclick="sendCmd('T')">🔄 翻转</button>
    </div>
    <br>
    <button class="btn-status" onclick="getStatus()">📡 获取真实状态</button>
    
    <div id="log">就绪</div>

    <script>
        const ipInput = document.getElementById('ipInput');
        const ledIndicator = document.getElementById('ledIndicator');
        const ledStatus = document.getElementById('ledStatus');
        const logDiv = document.getElementById('log');

        function getIP() { return ipInput.value.trim() || '192.168.4.1'; }

        function log(msg) { 
            logDiv.textContent = msg; 
            console.log(msg); 
        }

        async function sendCmd(cmd) {
            const ip = getIP();
            const url = `http://${ip}/${cmd}`;
            log(`发送: ${cmd}`);
            try {
                const response = await fetch(url);
                if (response.ok) {
                    // 命令发送成功后,延迟一下再获取状态
                    setTimeout(getStatus, 200);
                } else {
                    log(`❌ 错误: ${response.status}`);
                }
            } catch(err) {
                log(`❌ 连接失败: ${err.message}`);
            }
        }

        async function getStatus() {
            const ip = getIP();
            const url = `http://${ip}/status`;
            log('📡 获取状态...');
            try {
                const response = await fetch(url);
                if (!response.ok) {
                    log(`❌ HTTP ${response.status}`);
                    return;
                }
                const data = await response.json();
                log(`✅ 状态: LED = ${data.led ? '开启' : '关闭'}`);
                updateUI(data.led);
            } catch(err) {
                log(`❌ 获取状态失败: ${err.message}`);
            }
        }

        function updateUI(state) {
            if (state) {
                ledIndicator.className = 'led on';
                ledStatus.textContent = '开启';
                ledStatus.style.color = '#4CAF50';
            } else {
                ledIndicator.className = 'led';
                ledStatus.textContent = '关闭';
                ledStatus.style.color = '#f44336';
            }
        }

        // 页面加载时自动获取状态
        window.onload = function() {
            setTimeout(getStatus, 500);
        };
    </script>
</body>
</html>