RobotDYN - Arduino MEGA 2560 a ESP8266 - obousměrná komunikace - základ

13.02.2022 Arduino #arduino #esp #robodyn #server

RobotDYN poskytuje Arduino MEGA 2560 a ESP8266 na jedné desce. Obě platformy mohou mezi sebou komunikovat. Ideální řešení například pro IoT, kdy chceme ovládat spotřebiče prostřednictvím WiFi, popřípadě sledovat hodnoty senzorů apod. V tomto článku je ukázka využití asynchronní komunikace mezi webovým serverem a desku Arduino MEGA na platformě od RobotDYN.


Deska RobotDyn byla popsána v článku RobotDyn - Arduino Mega2560 s vestavěným WiFi a ESP8266, ve kterém je také návod na zprovoznění webového serveru, aby pracovat s příkazy AT. Dále je v článku uveden postup pro postupné nahrávání kódu do jednotlivých platforem desky.

Zde je uveden příklad, jak komunikovat s Mega prostřednictvím ESP, na kterém je webový server. Proč spojovat tyto dvě platformy a nepoužít pouze ESP8266? Pokud budeme potřebovat více analogových nebo digitálních vstupů, potom deska ESP nebude dostačující. Tím, že se použije platforma Mega, získáme velké množství vstupů.

Program pro ESP8266

Pro zprovoznění webového serveru s asynchronním voláním, je nutné nastavení přepínače DIP následujícím způsobem:

1 2 3 4 5 6 7 8
CH340 připojit k ESP8266 (nahrát program) OFF OFF OFF OFF ON ON ON NOUSE

Následně se nahraje programový kód do ESP8266.

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266mDNS.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>

const char* ssid = "MegaEsp";
const char* password = "12345678";

AsyncWebServer server(80);
MDNSResponder mdns;

int led_pin = 13;

const char* PARAM_INPUT_1 = "state";


// Variables will change:
int ledState = LOW;          // the current state of the output pin

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">
      <style>
        html {font-family: Arial; display: inline-block; text-align: center;}
        h2 {font-size: 18px;}
        p {font-size: 12px;}
        body {
                font-family: Arial,Helvetica,sans-serif;
                background: #181818;
                color: #EFEFEF;
                font-size: 16px
            }
            section.main {
                display: flex
            }

            section.main {
                flex-direction: column
            }
        #content {
                display: flex;
                flex-wrap: wrap;
                align-items: stretch
            }
            .input-group {
                display: flex;
                flex-wrap: nowrap;
                line-height: 22px;
                margin: 5px 0;
                width:100rem;
            }

            .input-group>label {
                display: inline-block;
                padding-right: 10px;
                text-align: left;
            }

            .input-group input,.input-group select {
                flex-grow: 1
            }
        .switch {
                display: block;
                position: absolute;
                font-size: 16px;
                height: 0;
                right:0;
            }

            .switch input {
                outline: 0;
                opacity: 0;
                width: 0;
                height: 0;
                position: absolute
            }

            .slider {
                width: 50px;
                height: 22px;
                cursor: pointer;
                background-color: grey;
                position: absolute;
                right: 10px;
            }

            .slider,.slider:before {
                display: inline-block;
                transition: .4s
            }

            .slider:before {
                position: relative;
                content: "";
                height: 16px;
                width: 16px;
                left: -14px;
                top: 3px;
                background-color: #fff
            }

            input:checked+.slider {
                background-color: #ff3034
            }

            input:checked+.slider:before {
                -webkit-transform: translateX(26px);
                transform: translateX(26px)
            }
      </style>
    </head>
    <body>
        <section class="main">
        <div id="content">
            <h2>ESP Web Server</h2>

            %BUTTONPLACEHOLDER%

        </div>
        </section>
    <script>function toggleCheckbox(element) {
      var xhr = new XMLHttpRequest();
      if(element.checked){ xhr.open("GET", "/update?state=1", true); }
      else { xhr.open("GET", "/update?state=0", true); }
      xhr.send();
    }

    setInterval(function ( ) {
      var xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          var inputChecked;
          var outputStateM;
          if( this.responseText == 1){ 
            inputChecked = true;
            outputStateM = "On";
          }
          else { 
            inputChecked = false;
            outputStateM = "Off";
          }
          document.getElementById("output").checked = inputChecked;
          document.getElementById("outputState").innerHTML = outputStateM;
        }
      };
      xhttp.open("GET", "/state", true);
      xhttp.send();
    }, 1000 ) ;
    </script>
    </body>
    </html>
)rawliteral";

String outputState(){
  int st=Serial1.read();
  if(st==1){
    return "checked";
  }
  else {
    return "";
  }
  return "";
}

String processor(const String& var){
  //Serial.println(var);
  if(var == "BUTTONPLACEHOLDER"){
    String buttons ="";
    String outputStateValue = outputState();
    buttons+= "<div class="input-group" id="output-group"><label for="output">LED <span id="outputState"></span></label><div class="switch"><input id="output" type="checkbox" class="default-action" onchange="toggleCheckbox(this)" id="output" " + outputStateValue + "><label class="slider" for="output"></label></div></div>";
    return buttons;
  }
  return String();
}

void setup() {
  Serial1.begin(115200);
  Serial.begin(115200);
  WiFi.softAP(ssid, password);
  IPAddress IP = WiFi.softAPIP();
  Serial.print("AP IP address: ");
  Serial.println(IP);

  pinMode(led_pin, OUTPUT);
  digitalWrite(led_pin, LOW);

  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }

  // Information about kontrolet
  Serial.println("");
  Serial.println("ESP8266 board info:");
  Serial.print("tChip ID: ");
  Serial.println(ESP.getFlashChipId());
  Serial.print("tCore Version: ");
  Serial.println(ESP.getCoreVersion());
  Serial.print("tChip Real Size: ");
  Serial.println(ESP.getFlashChipRealSize());
  Serial.print("tChip Flash Size: ");
  Serial.println(ESP.getFlashChipSize());
  Serial.print("tChip Flash Speed: ");
  Serial.println(ESP.getFlashChipSpeed());
  Serial.print("tChip Speed: ");
  Serial.println(ESP.getCpuFreqMHz());
  Serial.print("tChip Mode: ");
  Serial.println(ESP.getFlashChipMode());
  Serial.print("tSketch Size: ");
  Serial.println(ESP.getSketchSize());
  Serial.print("tSketch Free Space: ");
  Serial.println(ESP.getFreeSketchSpace());

  /* Settings for public WiFi 
  WiFi.begin(ssid, password);
  Serial.println("");
     
  // connect to local wifi NO to AP wifi

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }

  Serial.println("");
  Serial.print("Connected to "); 
  Serial.println(ssid);
  Serial.print("IP address: "); 
  Serial.println(WiFi.localIP());
  */

  Serial.print("AP IP address: ");
  Serial.println(IP);

  if (mdns.begin("esp8266", WiFi.localIP())) {
    Serial.println("MDNS responder started");
  }

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

  server.on("/state", HTTP_GET, [] (AsyncWebServerRequest *request) {
    request->send(200, "text/plain", String(Serial.read()).c_str());
  });

  server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage;
    String inputParam;
    // GET input1 value on <ESP_IP>/update?state=<inputMessage>
    if (request->hasParam(PARAM_INPUT_1)) {
      inputMessage = "["+request->getParam(PARAM_INPUT_1)->value()+"]";
      inputParam = PARAM_INPUT_1;
      //digitalWrite(output, inputMessage.toInt());
      Serial.println(inputMessage);
      ledState = !ledState;
    }
    else {
      inputMessage = "No message sent";
      inputParam = "none";
    }
    Serial.println(inputMessage);
    request->send(200, "text/plain", "OK");
  });
  
  server.begin();
  Serial.println("HTTP server started");

}

void loop() {
  //server.handleClient();

}

 

Jak kód funguje

V následujících odstavcích je vysvětleno, jak kód funguje.

Import knihoven

Nejdříve se naimportují požadované knihovny. The WiFiClient, ESPAsyncWebServer a ESAsyncTCP jsou potřebné k sestavení webového serveru.

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266mDNS.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>

 

Nastavení přihlašovacích údajů k síti

V tomto příkladu je modul ESP8266 reprezentován AccessPoint. Lze se, ale připojit k již existující síti. Pro oba případy je nutné se vložit síťové přihlašovací údaje do následujících proměnných.

const char* ssid = "MegaEsp";
const char* password = "12345678";

 

Definice proměnných

Pro tento ukázkový příklad, kdy se ovládá LED dioda, je definován vstup a název parametru pro asynchronní odesílání zpráv.

int led_pin = 13;
const char* PARAM_INPUT_1 = "state";

// Variables will change:
int ledState = LOW;          // the current state of the output pin

  

Vytvoření objektu AsyncWebServer na portu 80.

AsyncWebServer server(80);

 

Nastavení ESP8266 jako přístupového bodu (AP)

V sekci setup() se pro nastavení ESP8266 jako přístupového bodu využívá metoda softAP():

 WiFi.softAP(ssid, password);

Existují také další volitelné parametry, které lze metodě softAP() předat:

.softAP(const char* ssid, const char* password, int channel, int ssid_hidden, int max_connection)
  • ssid (definováno dříve): maximálně 31 znaků
  • password (definováno dříve): minimálně 8 znaků. Pokud není zadáno, přístupový bod bude otevřený (maximálně 63 znaků)
  • channel: Číslo kanálu Wi-Fi (1-13). Výchozí hodnota je 1
  • ssid_hidden: pokud je nastaveno na true, skryje SSID
  • max_connection: maximální počet současně připojených stanic, od 0 do 8

Dále se získá IP adresa přístupového bodu pomocí softAPIP() a vytiskne je v Serial Monitor.

IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);

Webová stránka - přepínač

Webová stránka obsahuje definici stylů a skriptů pro asynchronní volání událostí. Veškerý HTML kód je uložen v proměnné index_html. V HTML není zahrnuto tlačítko. To proto, aby se dali měnit jeho vlastnosti v závislosti na aktuálním stavu LED. JE tedy vytvořen pro tlačítko zástupný symbol %BUTTONPLACEHOLDER%, který bude nahrazen textem HTML pro vytvoření tlačítka později v kódu (to se provádí ve funkci procesor()).

<div id="content">
            <h2>ESP Web Server</h2>

            %BUTTONPLACEHOLDER%

</div>

 

procesor()

Funkce procesor() nahrazuje všechny zástupné symboly v HTML textu skutečnými hodnotami. Nejprve zkontroluje, zda texty HTML obsahují nějaké zástupné symboly %BUTTONPLACEHOLDER%.

if(var == "BUTTONPLACEHOLDER"){

Poté se zavolá funkce outputState(), která vrací aktuální stav výstupu. Uloží se do proměnné outputStateValue.

String outputStateValue = outputState();

V této funkci je čtení ze sériového monitoru Serial1.read(). Tím se získává aktuální stav z platformy Mega 2560.

String outputState(){
  int st=Serial1.read();
  if(st==1){
    return "checked";
  } else {
    return "";
  }
  return "";
}

Poté se použije tato hodnota k vytvoření textu HTML pro zobrazení tlačítka se správným stavem:

buttons+= "<div class="input-group" id="output-group"><label for="output">LED <span id="outputState"></span></label><div class="switch"><input id="output" type="checkbox" class="default-action" onchange="toggleCheckbox(this)" id="output" " + outputStateValue + "><label class="slider" for="output"></label></div></div>";

Požadavek HTTP GET na změnu stavu výstupu (JavaScript)

Když se stiskne tlačítko, je zavolána funkce toggleCheckbox(). Tato funkce vytvoří požadavek na různé adresy URL pro zapnutí nebo vypnutí LED.

function toggleCheckbox(element) {
      var xhr = new XMLHttpRequest();
      if(element.checked){ xhr.open("GET", "/update?state=1", true); }
      else { xhr.open("GET", "/update?state=0", true); }
      xhr.send();
    }

Chceme-li rozsvítit LED, odešle se požadavek na adresu /update?state=1:

if(element.checked){ xhr.open("GET", "/update?state=1", true); }

V opačném případě se vytvoří požadavek na adresu /update?state=0.

HTTP GET požadavek na aktualizaci stavu (JavaScript)

Aby byl stav výstupu na webovém serveru aktualizovaný, volá se následující funkce, která každou sekundu zadá nový požadavek na adresu /state.

setInterval(function ( ) {
      var xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          var inputChecked;
          var outputStateM;
          if( this.responseText == 1){ 
            inputChecked = true;
            outputStateM = "On";
          }
          else { 
            inputChecked = false;
            outputStateM = "Off";
          }
          document.getElementById("output").checked = inputChecked;
          document.getElementById("outputState").innerHTML = outputStateM;
        }
      };
      xhttp.open("GET", "/state", true);
      xhttp.send();
    }, 1000 ) ;

Zpracování požadavků

Potom se musí zajistit, co se stane, když ESP8266 obdrží požadavky na tyto URL adresy.

Když je požadavek přijat na root / URL, odešle se HTML stránka a také procesor.

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

Následující řádky zkontrolují, zda jsme obdrželi požadavek na URL /update?state=1 nebo /update?state=0. Na sériový monitor se zašle hodnota parametru v podobě inputMessage a dále se změní ledState. Proměnná ledState, zde plní pouze informační funkci, protože klíčové je odeslání hodnoty na sériový monitor. Je také důležitá skladba resp. obsah proměnné inputMessage. Hodnota parametru je obalena hranatými závorkami, protože tuto zprávu bude číst ze sériového vstupu program v Mega 2560. Pokud by se nepoužívala druhá platforma, tak by bylo možné již rovnou poslat hodnotu na digitální vstup. Ale to by bylo možné pouze v případě, že LED je připojena k ESP8266.

server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage;
    String inputParam;
    // GET input1 value on <ESP_IP>/update?state=<inputMessage>
    if (request->hasParam(PARAM_INPUT_1)) {
      inputMessage = "["+request->getParam(PARAM_INPUT_1)->value()+"]";
      inputParam = PARAM_INPUT_1;
      //digitalWrite(output, inputMessage.toInt());
      Serial.println(inputMessage);
      ledState = !ledState;
    }
    else {
      inputMessage = "No message sent";
      inputParam = "none";
    }
    Serial.println(inputMessage);
    request->send(200, "text/plain", "OK");
  });

Když je přijat požadavek na URL /state, odešle se aktuální stav výstupu:

server.on("/state", HTTP_GET, [] (AsyncWebServerRequest *request) {
    request->send(200, "text/plain", String(Serial.read()).c_str());
  });

Smyčka loop() je v tomto případě prázdná, protože ovládání rozsvícení a zhasínání LED zajišťuje část Mega 2560. Do ESP8266 by ve smyčce byl program jen tehdy, pokud by LED byla připojena na pin k ESP8266.

Program pro Mega2560

Aby bylo možné využívat více senzorů, využije se platforma Mega2560, která je součástí desky RobotDYN. Pro naprogramování Mega2560 je nutné přepnout přepínače DIP následujícím způsobem:

1 2 3 4 5 6 7 8
CH340 připojit k ATmega2560 (nahrát program) OFF OFF ON ON OFF OFF OFF NOUSE

Po nastavení přepínačů a připojení desky k napájení, nahraje se následující programový kód:

#include <Arduino.h>

#define PIN_LED 8
String inString;
int actState=0;

void setup() {
  Serial.begin(115200);
  Serial3.begin(115200);
}

void loop() {
  Serial.println(actState);
  Serial3.write(actState);
  //delay(1000);
}

void serialEvent3() {
  while (Serial3.available()) {
    char inChar = Serial3.read();
    //Serial.write(inChar);
    inString += inChar;
    if (inChar == ']') {
      if (inString.indexOf("[1]")>0) {
        digitalWrite(PIN_LED, HIGH);
        actState=1;
      }
      else if (inString.indexOf("[0]")>0) {
        digitalWrite(PIN_LED, LOW);
        actState=0;
      }
      else
      {
        Serial.println("Wrong command");
      }
      inString = "";
    }
  }
}

Jakmile je kód úspěšně nahrán do Mega2560, nastaví se DIP přepínače do následujících poloh.

1 2 3 4 5 6 7 8
CH340 připojit k Mega2560 COM3 připojit k ESP8266 ON ON ON ON OFF OFF OFF NOUSE

Jak kód funguje

V následujících odstavcích je vysvětleno, jak funguje kód pro Mega2560.

Definice proměnných

Proměnní PIN_LED obsahuje pin, na kterém je připojena LED.

#define PIN_LED 8
String inString;
int actState=0;

Získávání dat z ESP8266

Z platformy ESP8266 jsou data získávána prostřednictví Serial3.Read().

void serialEvent3() {
  while (Serial3.available()) {
    char inChar = Serial3.read();
    //Serial.write(inChar);
    inString += inChar;
    if (inChar == ']') {
      if (inString.indexOf("[1]")>0) {
        digitalWrite(PIN_LED, HIGH);
        actState=1;
      }
      else if (inString.indexOf("[0]")>0) {
        digitalWrite(PIN_LED, LOW);
        actState=0;
      }
      else
      {
        Serial.println("Wrong command");
      }
      inString = "";
    }
  }
}

Funkce serialEvent3() odchytává data zaslaná sériovým monitorem. V cyklu while se neustále čte vstup a ukládá se do proměnné inChar. Následně se testuje, kdy zaslaný řetězec obsahuje znak pravé složené závorky - ]. Tento znak ukazuje, že se jedná o konec hodnoty zaslané jako parameter z ESP8266. V podmínce if se pak testuje, zda hodnota parametru nabývá [1] pro zapnutí LED nebo [0] pro vypnutí LED. Aktualizuje se proměnná actState, která jek ve smyčce loop() odesílána zpět do ESP8266.

void loop() {
  Serial.println(actState);
  Serial3.write(actState);
  //delay(1000);
}