در این مینی پروژه قصد داریم LED قرار داده شده بر روی ماژول ESP32 DevKit V1 را به حالت چشمک زن ببریم و با استفاده از یک وباپلیکیشن، امکان فعال/غیرفعال کردن چراغ و تنظیم سرعت چشمک زدن LED را فراهم میکنیم.
سخت افزار مورد نیاز:
- در این مثال ما از برد ESP32 DevKit V1 استفاده کردیم اما شما میتونید از هر Board از خانواده ESP مثل NodeMCU ESP8266 و سایر برد ها استفاده کنید فقط کافی هست بخش هایی از کد رو منطبق بر اون بردتون پیاده کنید
- کابل USB برای اتصال بورد شما به کامپیوتر
زبان استفاده شده برای توسعه کد بر روی میکروکنترلر, Arduino است که در ادامه بررسی میکنیم.
کتابخانه های استفاده شده:
// libraries
#include "Arduino.h"
#include "WiFi.h"
#include "LittleFS.h"
#include "AsyncTCP.h"
#include "ESPAsyncWebServer.h"
#include "Arduino_JSON.h"
- Arduino.h : کتابخانه built-in که توابع داخلی آردیونو را برای ما فراخوانی میکند
- WiFi.h: ارتباط با وای فای داخلی برد (built-in)
- LittleFS.h : کار با بخشی از حافظه فلش داخل میکروکنترلر (built-in)
- AsyncTCP.h : کار با پروتکل WebSocket و پشتیبانی از اون (me-no-dev/AsyncTCP)
- ESPAsyncWebServer.h : ساخت وب سرور (me-no-dev/ESPAsyncWebServer)
- Arduino_JSON.h : تجزیه و ساخت جیسان (arduino-libraries/Arduino_JSON)
ماکرو ها و متغییر های سراسری:
#define LED_PIN 2
// global varibles
bool blinkStatus = false; // Indicates whether LED blinking is active.
unsigned long blinkDelay = 500; // Delay in milliseconds between LED state changes.
bool ledState = false; // Current state of the LED (HIGH or LOW).
unsigned long previousMillis = 0; // Timestamp of the last LED state change.
AsyncWebSocket ws("/ws"); // WebSocket route at `/ws`.
AsyncWebServer server(80); // Web server running on port 80.
در ادامه کد ما توابع مختلفی را نوشتیم که در جای مناسب فقط فراخوانی انجام شود
/**
* @brief Initialize WiFi in Access Point (AP) mode.
*
* ### Configuration
* | Parameter | Value |
* |------------|---------------|
* | SSID | ESP32_BLINK |
* | Password | 12345678 |
* | IP Address | 192.168.4.8 |
*/
void initWiFi()
{
WiFi.mode(WIFI_AP);
const char *ap_ssid = "ESP32_BLINK";
const char *ap_password = "12345678";
IPAddress apIP(192, 168, 4, 8);
IPAddress apGateway(192, 168, 4, 1);
IPAddress apSubnet(255, 255, 255, 0);
if (!WiFi.softAPConfig(apIP, apGateway, apSubnet))
{
Serial.println("softAPConfig failed!");
}
bool ok = WiFi.softAP(ap_ssid, ap_password);
if (ok)
{
Serial.println("Access Point created successfully!");
Serial.print("SSID: ");
Serial.println(ap_ssid);
Serial.print("Password: ");
Serial.println(ap_password);
Serial.print("Access Point IP address: ");
Serial.println(WiFi.softAPIP());
}
else
{
Serial.println("Failed to create Access Point!");
}
}
/**
* @brief Generates a JSON string containing the current blink status and delay.
*
* @example Example JSON output
* ```json
* {
* "status": 1,
* "delay": 500
* }
* ```
*
* @return String containing the JSON representation of blink status and delay.
*/
String statusData()
{
JSONVar readings;
readings["status"] = String(blinkStatus);
readings["delay"] = String(blinkDelay);
String jsonString = JSON.stringify(readings);
return jsonString;
}
/**
* @brief WebSocket event handler.
*
* @param server Pointer to the WebSocket server instance.
* @param client Pointer to the WebSocket client.
* @param type Type of WebSocket event (connect, disconnect, data, etc.).
* @param arg Additional event-specific data.
* @param data Data sent by the client (as an array of bytes).
* @param len Length of the data array.
*/
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len)
{
switch (type)
{
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n",
client->id(), client->remoteIP().toString().c_str());
client->text(statusData());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
/**
* @brief Handles messages received from the WebSocket client.
*
* @param arg Event-specific argument (castable to AwsFrameInfo).
* @param data Data sent by the client (as an array of bytes).
* @param len Length of the received data.
*/
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
{
AwsFrameInfo *info = (AwsFrameInfo *)arg; // Cast the generic argument to frame info.
// Ensure this is a complete text frame.
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)
{
data[len] = 0; // Null-terminate the received data.
String msg = (char *)data; // Convert byte array to String.
Serial.print("Received Data: ");
Serial.println(msg);
if (msg == "BLINK_ON")
{
blinkStatus = true;
}
else if (msg == "BLINK_OFF")
{
blinkStatus = false;
digitalWrite(LED_PIN, LOW);
}
else if (msg.startsWith("DELAY:"))
{
blinkDelay = msg.substring(6).toInt(); // Extract delay value from message.
}
}
}
و تابع بالا به WebSocket و سپس WebSocket را به وب سرور رجیستر میکنیم.
/**
* @brief Initializes the WebSocket and registers its event handler.
*/
void initWebSocket()
{
ws.onEvent(onEvent);
server.addHandler(&ws);
}
تابعی برای ارسال وضعیت فعلی LED به تمام کلاینتها تعریف میکنیم.
/**
* @brief Sends the current LED status and delay to all connected WebSocket clients.
*/
void notifyClients()
{
ws.textAll(statusData());
}
در نهایت کل منطق کد ما نوشته شده و سپس شروع میکنیم به فراخوانی توابع در جای مناسب که دو تابع اصلی آردیونو یعنی setup و loop بصورت زیر نوشته میشود:
/**
* @brief Main setup function.
* Initializes serial communication, WiFi, file system, and WebSocket server.
*/
void setup()
{
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
initWiFi();
initLittleFS();
initWebSocket();
if (LittleFS.exists("/index.html"))
{
Serial.println("index.html FOUND");
}
else
{
Serial.println("index.html NOT FOUND");
}
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
server.begin();
}
unsigned long lastPrint = 0;
/**
* @brief Main loop.
* Handles LED blinking, WebSocket cleanup, and client updates.
*/
void loop()
{
static unsigned long lastStatusSend = 0;
unsigned long currentMillis = millis();
// Close inactive WebSocket connections.
ws.cleanupClients();
// Handle LED blinking.
if (blinkStatus && currentMillis - previousMillis >= blinkDelay)
{
previousMillis = currentMillis;
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
// Periodically send status updates to all clients.
if (currentMillis - lastStatusSend >= 500)
{
notifyClients();
lastStatusSend = currentMillis;
}
}
تا این لحظه, میکروکنترلر ما اکسس پوینت را ایجاد میکند تا به شبکه آن وصل بشیم سپس در مسیر وب سوکت یعنی /ws به درخواست ها با پروتکل وب سوکت پاسخ میدهد و اطلاعات را بدرستی رد و بدل میکند و فایل های html را هم روی وب سرور خودش سرو میکنه, تنها کاری که باقی مانده، ساخت یک رابط گرافیکی ساده برای نمایش وضعیت و کنترل LED است که در ساده ترین حالت میتوان رابط کاربری با تکنولوژی های فرانت اند نوشت.
در بخش فرانت اند پروژه ما ماژولی برای کار با وب سرور و پروتکل وب سوکت نوشتیم و سپس در صفحات دیگر کافی هست تا از ان استفاده کرد, درواقع این ماژول هسته اصلی فرانت اند ما میشود که ما ان را با ساختار VueJS نوشتیم در اینجا بهدلیل حجم بالای کدهای فرانتاند، فقط ماژول اصلی را معرفی میکنیم و شما میتوانید با import بخش های لازم و طراحی ظاهر مدنظرتان از ان استفاده کنید.
// socket.js
import { ref } from 'vue'
const socket = ref(null)
const isConnected = ref(false)
const blinkStatus = ref(false)
const blinkDelay = ref(500)
const lastMessage = ref(null)
let firstConnectionDataReceived = false
function connect(url = `${location.origin.replace(/^http/, 'ws')}/ws`) {
if (socket.value) return
socket.value = new WebSocket(url)
socket.value.onopen = () => {
isConnected.value = true
firstConnectionDataReceived = false
}
socket.value.onclose = () => {
isConnected.value = false
}
socket.value.onerror = () => {
isConnected.value = false
}
socket.value.onmessage = (event) => {
lastMessage.value = event.data
try {
const data = JSON.parse(event.data)
if (data.status !== undefined)
blinkStatus.value = data.status === "1" || data.status === "true"
if (data.delay !== undefined) {
const delayVal = parseInt(data.delay)
blinkDelay.value = delayVal
if (!firstConnectionDataReceived) {
localStorage.setItem('rangeValue', delayVal)
firstConnectionDataReceived = true
}
}
} catch (e) {
console.error("Invalid JSON:", e)
}
}
}
function sendMessage(msg) {
if (socket.value && isConnected.value) socket.value.send(msg)
}
function turnOnBlinking() {
sendMessage("BLINK_ON")
}
function turnOffBlinking() {
sendMessage("BLINK_OFF")
}
function sendBlinkDelay(value) {
sendMessage(`DELAY:${value}`)
}
function useSocketState() {
return { socket, isConnected, blinkStatus, blinkDelay, lastMessage }
}
export default {
connect,
turnOnBlinking,
turnOffBlinking,
sendBlinkDelay,
useSocketState
}
متغییر های تعریف شده از نوع ref هستند که استاندارد ویو جی اس برای تعریف متغییر های واکنش گرا هست تا با هر تغییری در هر لحظه بر روی المان های استفاده شده اعمال شود.
تابع connect وظیفه ارتباط با وب سرور بصورت وب سوکت را دارد ادرس درخواست را بصورت ${location.origin.replace(/^http/, ‘ws’)}/ws تعریف کردیم تا بصورت داینامیک همان دامنه ای که در ان قرار دارد بگیرد و بوسیله Regex(عبارات با قاعده) پروتکل http با ws جایگزین میشود.
در ادامه تابع طوری نوشته شده که شی socket فقط یکبار ایجاد شود و در دفعه های بعدی از نو ساخته نشود.
هندلر هایی برای حالت های بازشدن کانکشن, قطع شدن و دریافت مسیج هم تعریف شده تا رفتار درستی رو از خود نشان دهد.
تابع sendMessage وظیفه ارسال پیامی که در پارامتر وارد شده را دارد و در تابع های turnOnBlinking و turnOffBlinking و sendBlinkDelay از همان تابع استفاده شده و دستورات مخصوص را که در کد سمت آردیونو نوشته شده سینک کنند.
در پست بعدی به نحوهی آپلود فایلهای فرانتاند (HTML، CSS و JS) در حافظهی داخلی ESP32 میپردازیم تا پروژه بهصورت کامل و مستقل اجرا شود.
منابع
lastminuteengineers. “How to Create an ESP32 Web Server with WebSockets in Arduino IDE.” Last Minute Engineers, 21 Oct. 2023, [https://lastminuteengineers.com/esp32-websocket-tutorial/]. [1]
