• Система автоматизации с открытым исходным кодом на базе esp8266/esp32 микроконтроллеров и приложения IoT Manager. Наша группа в Telegram

ESP8266 и несколько клиентов

Melandr

Member
Спасибо за пример и подсказки. А в коде у Вас есть комментарий "В корень SPIFFS надо загрузить веб приложение" - что подразумевается html с js ?
 

Melandr

Member
Добрый вечер!
Нашел пример использования websockets в асинхронном вебсервере
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp8266-nodemcu-websocket-server-arduino/
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/

// Import required libraries
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>

// Replace with your network credentials
const char* ssid = "DD-WRT";
const char* password = "gCU8YNZs";

bool ledState = 0;
const int ledPin = 2;

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style>
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
h2{
font-size: 1.5rem;
font-weight: bold;
color: #143642;
}
.topnav {
overflow: hidden;
background-color: #143642;
}
body {
margin: 0;
}
.content {
padding: 30px;
max-width: 600px;
margin: 0 auto;
}
.card {
background-color: #F8F7F9;;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
padding-top:10px;
padding-bottom:20px;
}
.button {
padding: 15px 50px;
font-size: 24px;
text-align: center;
outline: none;
color: #fff;
background-color: #0f8b8d;
border: none;
border-radius: 5px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
/*.button:hover {background-color: #0f8b8d}*/
.button:active {
background-color: #0f8b8d;
box-shadow: 2 2px #CDCDCD;
transform: translateY(2px);
}
.state {
font-size: 1.5rem;
color:#8c8c8c;
font-weight: bold;
}
</style>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
<div class="topnav">
<h1>ESP WebSocket Server</h1>
</div>
<div class="content">
<div class="card">
<h2>Output - GPIO 2</h2>
<p class="state">state: <span id="state">%STATE%</span></p>
<p><button id="button" class="button">Toggle</button></p>
</div>
</div>
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onLoad);
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage; // <-- add this line
}
function onOpen(event) {
console.log('Connection opened');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
var state;
if (event.data == "0"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
function onLoad(event) {
initWebSocket();
initButton();
}
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
function toggle(){
websocket.send('toggle');
}
</script>
</body>
</html>
)rawliteral";

void notifyClients() {
ws.textAll(String(ledState));
}

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
if (strcmp((char*)data, "toggle") == 0) {
ledState = !ledState;
notifyClients();
}
}
}

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());
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;
}
}

void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}

String processor(const String& var){
Serial.println(var);
if(var == "STATE"){
if (ledState){
return "ON";
}
else{
return "OFF";
}
}
}

void setup(){
// Serial port for debugging purposes
Serial.begin(115200);

pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, HIGH);

// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}

// Print ESP Local IP Address
Serial.println(WiFi.localIP());

initWebSocket();

// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});

// Start server
server.begin();
}

void loop() {
ws.cleanupClients();
digitalWrite(ledPin, ledState);
}
Сравниваю с Вашим примером. Я так понимаю, что используя json можно передавать строку и обновлять показания на клиентах. Мне непонятно, в данном примере используется щелчок по кнопке в веб-интерфейсе, далее передается "toggle" на ESP. В ESP сравниваются полученные данные со строкой и выполняется инвертирование выхода светодиода. Но есть пару вопросов.
1. Если я буду передавать несколько значений, обработчик на ESP будет один, с разными условиями проверки полученных данных, точнее сначала распарсивания, а потом проверки?
2. Допустим у меня с ESP передается какое-то быстро меняющееся значение, меня интересует период обновления 500 мс. Значит для этой переменной мне необходим будет другой обработчик в коде ESP? Как задается период обновления данных?

Извините за глупые вопросы, но голова кругом идет от javascript, html и от других вебтехнологий.
ЗЫ: Также вопрос по JS - Есть функции инициализации кнопки и отработки клика по кнопке.
function initButton() {

document.getElementById('button').addEventListener('click', toggle);

}

function toggle(){

websocket.send('toggle');

}
Необходимо для каждой кнопки делать инициализацию и обработку клика или можно передавать в функцию элемент (кнопку), по которой щелкнули и взывать общий обработчик передавая ему id нажатой кнопки и в нем уже по id делать switch?
 

Melandr

Member
И еще вопрос. Для синхронного сервера есть библиотека WebSocketsServer.h - она аналогична плагину websockets для асинхронного вебсервера? Так как документации практически нет, либо смотреть по исходникам библиотеки, с минимумом комментариев на английском, либо разбираться по примерам реализации чужого кода. А примеров работы с вебсокетами для асинхронного вебсервера не особо много.
Правда непонятно в чем принципиальные преимущества асинхронного сервера над синхронным?
 

EvgeniyS

Member
И еще вопрос. Для синхронного сервера есть библиотека WebSocketsServer.h - она аналогична плагину websockets для асинхронного вебсервера? Так как документации практически нет, либо смотреть по исходникам библиотеки, с минимумом комментариев на английском, либо разбираться по примерам реализации чужого кода. А примеров работы с вебсокетами для асинхронного вебсервера не особо много.
Правда непонятно в чем принципиальные преимущества асинхронного сервера над синхронным?
Основное различие в том, что синхронный сервер работает в цикле loop и нужно следить чтобы HandleServer не блокировал работу Wifi, т.е. если у вас имеется внутри долгий по времени (более 40-50мс) обработчик, то надо будет прерывать его, вставляя в код yield() или delay(0) для нормальной работы wifi сети. Асинхронный сервер работает в своем отдельном цикле и самостоятельно прерывается для нормальной работы wifi сети. Ну и как по мне, ESPAsyncWebServer более богат функционалом и удобен в использовании. В любом случае, обе библиотеки - рабочий инструмент, так что выбор за вами.
 

EvgeniyS

Member
Набросал для вас небольшой сервер на вебсокетах для примера ссылка
Исходный код сервера:
C++:
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#define NUM_PTR_FUN 3

const uint8_t interval_1 = 50; //ms.
const uint16_t interval_2 = 500; //ms.
uint32_t timer_1 = 0;
uint32_t timer_2 = 0;
uint32_t freeHeap = 0;
bool sendFreeHeap = false;
bool needToUpdateStates = false;
IPAddress ipAP{192, 168, 4, 1};
AsyncWebServer server(80);
AsyncWebSocket ws1("/ws");
// Отправка состояний кнопок (GPIO)
void sendStates(AsyncWebSocketClient * client = NULL){
    StaticJsonBuffer<200> jsonBuffer; // создаем буфер
    JsonArray& root = jsonBuffer.createArray(); //создаем ссылку на массив
    root.add(0); // добавляем элемент в корневой массив
    JsonArray& states = root.createNestedArray(); // создаем ссылку на вложенный массив
    states.add(digitalRead(0)); // и добавляем туда данные
    states.add(digitalRead(2));
    states.add(digitalRead(4));
    states.add(digitalRead(5));
    size_t len = root.measureLength(); // записываем длину массива
    AsyncWebSocketMessageBuffer * buffer = ws1.makeBuffer(len); // создаем буфер для отправки данных
    if (buffer) {
        root.printTo((char *)buffer->get(), len + 1); // если буфер успешно создан то записывем туда данные
        if (client) {
            client->text(buffer); // если клиент указан то отправляем содержимое буфера клиенту
        } else {
            ws1.textAll(buffer); // если клиент не указан то отправляем содержимое буфера всем подключеным клиентам
        }
    }
}
// Обработчик управления GPIO с веб интерфейса
void H_pinChange(JsonVariant payload, AsyncWebSocketClient * client){
  digitalWrite(payload,!digitalRead(payload));
  needToUpdateStates = true;
}
// Обработчик управления free heap с веб интерфейса
void H_freeHeap(JsonVariant payload, AsyncWebSocketClient * client){
  sendFreeHeap = payload;
}
// Обработчик управления получения ID с веб интерфейса
void H_getChipId(JsonVariant payload, AsyncWebSocketClient * client){
    StaticJsonBuffer<50> jsonBuffer;
    JsonArray& root = jsonBuffer.createArray();
    root.add(payload);
    root.add(ESP.getChipId());
    size_t len = root.measureLength();
    AsyncWebSocketMessageBuffer * buffer = ws1.makeBuffer(len);
    if (buffer) {
        root.printTo((char *)buffer->get(), len + 1);
        client->text(buffer);
    }
}

// массив указателей на функции обработчиков
void (*arrFnPtr[NUM_PTR_FUN])(JsonVariant payload, AsyncWebSocketClient * client) = {
      H_pinChange,   // [0] - управление GPIO
      H_freeHeap,    // [1] - отправлять free heap (да\нет)
      H_getChipId    // [2] - получить ID
      // дальше можно дописывать для добавления новых обработчиков, при этом необходимо увеличить NUM_PTR_FUN до нужного размера
};

// Парсер входящих ws сообщений
// Число в первой ячейке массива(root[0]), является индексом указателя на функцию
// таким образом парсер определяет какую функцию-обработчик использовать
void readJsonData(const char *data, AsyncWebSocketClient * client){
  Serial.printf("from ws data: %s client: %u\n", data, client->id());
  DynamicJsonBuffer jsonBuffer;
  JsonArray& root = jsonBuffer.parseArray(data);
  if(root.success()){
    arrFnPtr[root[0].as<uint8_t>()](root[1].as<JsonVariant>(),client);
  }
}

// Приемник ws сообщений (упрощенный на 1 фрейм)
void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) {
  if(type == WS_EVT_CONNECT){
      Serial.printf("client id: %u connected\n", client->id());
      sendStates(client);
    } else if(type == WS_EVT_DATA){
    AwsFrameInfo * info = (AwsFrameInfo*)arg;
    data[info->len] = '\0';      
    readJsonData((char*)data,client);
  }
}

// Обработчик несуществующей страницы
void notFound(AsyncWebServerRequest *request) {
  request->send(404, "text/plain", "Not found");
  }
// Инициализация сервера
void serverInit(){
      ws1.onEvent(onWsEvent);
      server.addHandler(&ws1);
      server.serveStatic("/", SPIFFS, "/").setCacheControl("max-age=31536000");
      server.onNotFound(notFound);
      server.begin();
  }

// Инициализация wifi сети
void wifiInit(){
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(ipAP, ipAP, IPAddress(255, 255, 255, 0));
    WiFi.softAP("espAP", "");
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  pinMode(0,OUTPUT);
  pinMode(2,OUTPUT);
  pinMode(4,OUTPUT);
  pinMode(5,OUTPUT);
  SPIFFS.begin();
  wifiInit();
  serverInit();
}

void loop() {
  if(millis()>timer_1){
    timer_1 = millis()+interval_1;
    if(needToUpdateStates){
      sendStates();
      needToUpdateStates = false;
    }
  }
  yield();
  if(millis()>timer_2){
    timer_2 = millis()+interval_2;
    ws1.printfAll("[3,%lu]",millis());
    if(sendFreeHeap){
      freeHeap = ESP.getFreeHeap();
      ws1.printfAll("[1,%u]",freeHeap);
    }
  }
  yield();
}
Веб морду написал на фреймворке (Svelte)

HTML:
<script>
import { onMount } from 'svelte';

const buttons = [0,2,4,5]
let states = [255,255,255,255]
let uptime = undefined
let timeIs = "unknown"
let showFreeHeap = false
let freeHeap = "unknown"
let online = false
let ws = ""
let chipInfo = undefined

onMount(() => { // WS-клиент + обработчик сообщений
                let startWs = () => {
            ws = new WebSocket('ws://' + document.location.host + '/ws', ['arduino']);
            //ws = new WebSocket('ws://192.168.4.1/ws');
            ws.onopen = e => online = true
            ws.onclose = e => online = false
            ws.onerror = e => online = false
            ws.onmessage = e => {
              console.log("from esp: ", e.data)
              let jsonStr = IsValidJSONString(e.data)
              if(jsonStr){
                switch (jsonStr[0]){
                  case 0:
                  states = jsonStr[1]
                  break
                  case 1:
                  freeHeap = jsonStr[1]
                  break
                  case 2:
                  chipInfo = jsonStr[1]
                  break
                  case 3:
                  uptime = jsonStr[1]
                  break
                }
              }
            }
          }
          let check = () => {
            if (!ws || ws.readyState != 1) {
                ws.close()
                online = false
                startWs()
            }
          }
          let IsValidJSONString =(str)=> {
            let jsonS
            try {
              jsonS = JSON.parse(str);
            } catch (e) {
              return false;
            }
            return jsonS;
          }
         
          startWs();
          setInterval(check, 5000);
        });

let bClick = (i)=>{
  let str = "[0,"+i+"]"
  ws.send(str);
}

let getChipId = ()=>{
  ws.send("[2,2]")
}

let toggleFreeHeap = ()=>{
  let str = "[1,["+showFreeHeap+"]]"
  ws.send(str)
}

$: {if(uptime != undefined){
    let d, h, m, s;
    s = Math.floor(uptime / 1000);
    m = Math.floor(s / 60);
    s = s % 60;
    h = Math.floor(m / 60);
    m = m % 60;
    d = Math.floor(h / 24);
    h = h % 24;
    h += d * 24;
    timeIs =  h + ':' + m + ':' + s;
  }else{
    timeIs = "unknown"
  }
}


</script>
<!--HTML----------------------------------------------------- -->
<header>
  <h1 class={online ? "c-green":"c-red"}>{online ? "online":"offline"}</h1>
  {#if online}
  <h1>Uptime:  {timeIs}</h1>
  {/if}
  {#if showFreeHeap}
    <h1>Free heap: {freeHeap} bytes</h1>
  {/if}
  <input type="checkbox" bind:checked={showFreeHeap} on:change={toggleFreeHeap} disabled={online ? false:true}>
  show free heap
</header><hr>
<main>
{#each buttons as b, i}
  <button class={(!online || states[i] == 255) ? "c-gray":(states[i] == 0) ? "c-red":"c-green"} on:click={()=>bClick(b)}>Gpio {b}</button><br>
{/each}
<hr>
{#if chipInfo == undefined}
  <button class="c-gray" on:click={getChipId}>Get chip ID</button>
{:else}
  <h1>ChipId: {chipInfo}</h1>
{/if}
</main>
<!--STYLE---------------------------------------------------- -->
<style>
:global(body){
    font-size: 26px;
    font-weight: bolder;
  }
button{
   margin: 10px;
   font-size: 32px;
   border-radius: 8px;
   font-weight: bolder;
   border: none;
}
input{

        width: 40px;
        height: 30px;
        font-size:26px;
        font-weight: bolder;
        white-space: pre;
}
.c-red{
   background-color: red;
}
.c-green{
   background-color: green;
}
.c-gray{
   background-color: silver;
}
</style>
JavaScript:
import App from './App.svelte';

const app = new App({
    target: document.body
});

export default app;
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>
    <title>example</title>
    <link rel='icon' type='image/png' href='/favicon.png'>
    <script defer src='/bundle.js'></script>
</head>

<body>
</body>
</html>
Проверил - работает.
П.С. Я не программист по профессии, у меня это на уровне хобби, поэтому написал как смог, надеюсь вам это поможет.
 

Melandr

Member
Доброй ночи! Спасибо за код. Правда с наскоку запустить не получилось, не создается файл bundle.js. Походу нужно установить svelte. Пока поставил node.js
 

Melandr

Member
Спасибо, запустил. получилось. А такой вопрос, попробовал скетч переделать под подключение к роутеру, но походу при компиляции в bundle.js попадает IP-адрес программной точки доступа 192.168.4.1 и соединение websocket не поднимается. Я попробовал установить svelte, но на втором этапе
npx degit sveltejs/template my-svelte-project
сыпет ошибки. Еще такой вопрос, почему то перестает обновляться информация на веб-странице, при этом управление выводом со страницы работает
 

vavanvanvanovich

New member
Добрый день, я новичок, и не могу понять как организовать подключение к esp8266 нескольких телефонов, 1 подключил через сокет, пробую подключиться с другого устройства так всё зависает
 

Melandr

Member
Добрый день, я новичок, и не могу понять как организовать подключение к esp8266 нескольких телефонов, 1 подключил через сокет, пробую подключиться с другого устройства так всё зависает
Делал примеры с асинхронным веб-сервером, пробовал подключаться со стационарного компьютера и телефона одновременно, все работает. По мануалам при реализации softAP на ESP возможно подключение 5 клиентов.
 

EvgeniyS

Member
Спасибо, запустил. получилось. А такой вопрос, попробовал скетч переделать под подключение к роутеру, но походу при компиляции в bundle.js попадает IP-адрес программной точки доступа 192.168.4.1 и соединение websocket не поднимается. Я попробовал установить svelte, но на втором этапе
npx degit sveltejs/template my-svelte-project
сыпет ошибки. Еще такой вопрос, почему то перестает обновляться информация на веб-странице, при этом управление выводом со страницы работает
Да, походу я накосячил и забыл раскоментить 1 строку и закометить другую.
ws = new WebSocket('ws://' + document.location.host + '/ws', ['arduino']);
//ws = new WebSocket('ws://192.168.4.1/ws');
 

Melandr

Member
EvgeniyS, а такой вопрос. Если я в скетче Ардуино изменю работу с json с версии 5 на версию 6, не нужно перекомпилировать файлы js?
PS: почему-то передача данных с ESP на страницу подвисает.
 

EvgeniyS

Member
EvgeniyS, а такой вопрос. Если я в скетче Ардуино изменю работу с json с версии 5 на версию 6, не нужно перекомпилировать файлы js?
Я не пробовал, но json 5 подключается в библиотеку сервера, поэтому скорее всего не получится собрать проект. На стороне клиента (js) проблем не будет json строка она и Африке json:)
 

Melandr

Member
соединение ws активно, но при нажатии кнопки не приходит ответ, пока страницу не обновишь
2021-03-18_141712.jpg
 

EvgeniyS

Member
Еще такой вопрос, почему то перестает обновляться информация на веб-странице, при этом управление выводом со страницы работает
Тоже заметил, пока не знаю почему, писал все это на "скорую руку". Первое что приходит на ум: заменить в loop yield() на delay(20), например
 
Сверху Снизу