Skocz do zawartości

AJAX + (+ JSON) + ESP8266 = automatyczna aktualizacja odczytów czujników (BME280) bez konieczności odświeżania strony


rziomber

Pomocna odpowiedź

AJAX umożliwia przekazywanie danych pomiędzy klientem a serwerem WWW bez konieczności przeładowania strony. Dodając do tego timer w JavaScript możemy uzyskać świeże dane na stronie generowanej przez ESP8266. Na początek stwórzmy w PHP najprostszą stronę WWW prezentującą aktualną godzinę pobieraną z serwera (nie "JavaScriptovy" czas z przeglądarki - w końcu docelowo chcemy pobierać dane z czujników podłączonych do "serwera WWW" postawionego na ESP8266):

<?
if ($_REQUEST["time"]) {
  echo date("G:i:s");
  exit;
}
?>

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>AJAX test</title>
</head>
<body>
<span id="time"></span>

<script>
myTimer();
var myVar = setInterval(myTimer, 1000);

function myTimer() {
var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById("time").innerHTML =
      this.responseText;
      }
  };
  xhttp.open("GET", "test.php?time=1", true);
  xhttp.send();
}
</script>

</body>
</html>

1.thumb.png.b04797e73a40b960147625218239ea4d.png

Timer co sekundę przywołuje funkcję myTimer();

var myVar = setInterval(myTimer, 1000);

a ta pobiera zawartość podstrony test.php?time=1 i wstawia ją do elementu o nazwie "time" (document.getElementById("time").innerHTML =  this.responseText;).

Mamy tu tylko jeden przekazywany parametr. Co zrobić, by aktualizować kilka różnych wartości? Przywykłem do podstrony przekazującej parametry rozdzielone znakiem ; (odczyt1;odczyt2;odczyt3). Dzięki JavaScriptowej funkcji split możemy podzielić taki ciąg znaków na tablice, a potem przydzielić jej części do elementów SPAN o określonym ID.

<?
if ($_REQUEST["time"]) {
  echo date("d.m.y;G:i:s");
  exit;
}
?>

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>AJAX test</title>
</head>
<body>
Date: <span id="date"></span><br>
Time: <span id="time"></span>

<script>
myTimer();
var myVar = setInterval(myTimer, 1000);

function myTimer() {
var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
		var str = this.responseText; 
		var values = str.split(";");
		      document.getElementById("date").innerHTML =
      values[0];
	        document.getElementById("time").innerHTML =
      values[1];
      }
  };
  xhttp.open("GET", "test.php?time=1", true);
  xhttp.send();
}
</script>

</body>
</html>

  2.thumb.png.7da4d34f6162996fd0065bc7d61c8574.png

Działa! PHP po zainstalowania serwera np NGINX możemy uruchomić na Raspberry Pi. W połączeniu (komenda system()) z zewnętrznym skryptem Python lub programem w C korzystającym z WiringPi otrzymamy stronę z odczytami czujników hostowaną na Raspberry Pi!

Spróbujmy wreszcie zaprogramować mikrokontroler ESP8266. Podłączmy najpierw czujnik BME280.

5.thumb.png.9dd3dc538233812817b8b315c4fcd815.pngCRW_0069.thumb.jpg.cf159ba95724908e9468736f21204f7e.jpg

/*
I2C
D4 - SCL
D3 - SDA
*/

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

char* ssid = "Weather";
//const char *password = "";

ESP8266WebServer server(80);
Adafruit_BME280 bme;

void setup() {
Serial.begin(9600);
Wire.begin(D3, D4);
Wire.setClock(100000);
if (!bme.begin(0x76)) //changed from default I2C adress 0x77
{
Serial.println("Nie odnaleziono czujnika BMP085 / BMP180");
while (1) {
}
}
IPAddress apIP(192, 168, 1, 1);
WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
// WiFi.softAP(ssid, password);
WiFi.softAP(ssid);
server.on("/", handleRoot);
server.on("/sensors", handleSensors);
server.begin();
}

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

void handleRoot() {
String content = "<html> <head><title>Weather</title></head><body>";
content += "<DIV style=\"display:table; font-size: large;\"><DIV style=\"border-style: solid;\">BME280:<BR>Temperature: <span id=\"tempBME\"></span>C<br>Humidity: <span id=\"humBME\"></span>%<br>Pressure: <span id=\"presBME\"></span>Pa<br></DIV>";
content += "<script>myTimer();var myVar = setInterval(myTimer, 1000);function myTimer() {var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var str = this.responseText; var values = str.split(\";\"); document.getElementById(\"tempBME\").innerHTML = values[0]; document.getElementById(\"humBME\").innerHTML = values[1]; document.getElementById(\"presBME\").innerHTML = values[2];} }; xhttp.open(\"GET\", \"sensors\", true); xhttp.send();}</script>";
content += "</body></html>";
server.send(200, "text/html", content);
}

void handleSensors() {
String content = String(bme.readTemperature()) + ";" + String(bme.readHumidity()) + ";" + String((int)bme.readPressure()) + ";";
server.send(200, "text/html", content);
}

3.thumb.png.7b8478be227341c49602e14b6875486f.png

Rozdzielanie danych przecinkiem czy średnikiem nie jest sposobem "zbyt profesjonalnym". Przy dużej ilości zmiennych łatwo też o pomyłkę. Dlatego lepiej wtedy stosować bardziej odpowiednie formaty danych: JSON czy XML. Ze względu na "bliskość" JSON z JavaScript skupię się tylko na nim. Gotowa biblioteka ArduinoJson wygeneruje dane za nas. Więcej informacji znajdziemy w rozdziale Serialize with ArduinoJson dokumentacji technicznej.

/*
  I2C
  D4 - SCL
  D3 - SDA
*/

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <ArduinoJson.h> //https://github.com/bblanchon/ArduinoJson

char* ssid = "Weather";
//const char *password = "";

ESP8266WebServer server(80);
Adafruit_BME280 bme;

void setup() {
  Serial.begin(9600);
  Wire.begin(D3, D4);
  Wire.setClock(100000);
  if (!bme.begin(0x76)) //changed from default I2C adress 0x77
  {
    Serial.println("Nie odnaleziono czujnika BMP085 / BMP180");
    while (1) {
    }
  }
  IPAddress apIP(192, 168, 1, 1);
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
  // WiFi.softAP(ssid, password);
  WiFi.softAP(ssid);
  server.on("/", handleRoot);
  server.on("/sensors", handleSensors);
  server.begin();
}

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

void handleRoot() {
  String content = "<html> <head><title>Weather</title></head><body>";
  content += "<DIV style=\"display:table; font-size: large;\"><DIV style=\"border-style: solid;\">BME280:<BR>Temperature: <span id=\"tempBME\"></span>C<br>Humidity: <span id=\"humBME\"></span>%<br>Pressure: <span id=\"presBME\"></span>Pa<br></DIV>";
  content += "<script>myTimer();var myVar = setInterval(myTimer, 1000);function myTimer() {var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var str = this.responseText; var values = JSON.parse(str); document.getElementById(\"tempBME\").innerHTML = values.temp; document.getElementById(\"humBME\").innerHTML = values.hum; document.getElementById(\"presBME\").innerHTML = values.press;} }; xhttp.open(\"GET\", \"sensors\", true); xhttp.send();}</script>";
  content += "</body></html>";
  server.send(200, "text/html", content);
}

void handleSensors() {
  String content;
  StaticJsonBuffer<400> jsonBuffer;
  JsonObject& root = jsonBuffer.createObject();
  root["temp"] = bme.readTemperature();
  root["hum"] = bme.readHumidity();
  root["press"] = (int)bme.readPressure();

  root.printTo(content);
  server.send(200, "text/html", content);
}

Strona prezentować się będzie tak samo jak poprzednio. Zyskamy za to wygodny i czytelny sposób dopisywania nowych danych

root["temp"] = bme.readTemperature();

oraz ich odczytywania w kodzie źródłowym strony:

document.getElementById(\"tempBME\").innerHTML = values.temp;

Nie przejmujemy się już numeracją elementów długiej tablicy.

Sytuację możemy też odwrócić tworząc stronę WWW, której formularz sterować będzie pracą naszego fizycznego urządzenia. Oczywiście nadal niekonieczne będzie przeładowanie strony, by wysłać dane.

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>

char* ssid = "Weather";
//const char *password = "";
int ledValue = 0;

ESP8266WebServer server(80);

void setup() {
  Serial.begin(9600);
  IPAddress apIP(192, 168, 1, 1);
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
  // WiFi.softAP(ssid, password);
  WiFi.softAP(ssid);
  server.on("/", handleRoot);
  server.begin();
  pinMode(D1, OUTPUT);
  analogWriteRange(100); //http://esp8266.github.io/Arduino/versions/2.0.0/doc/reference.html
  analogWriteFreq(500);
}

void loop() {
  server.handleClient();
  analogWrite(D1, ledValue);
}

void handleRoot() {
  if (server.hasArg("ledVal") && server.arg("ledVal").toInt() >= 0 && server.arg("ledVal").toInt() <= 100) {
    ledValue = server.arg("ledVal").toInt();
    Serial.print("ledVal ");
    Serial.println(ledValue);
    server.send(200, "text/html", "");
    return;
  }

  //https://www.w3schools.com/howto/howto_js_rangeslider.asp
  //https://stackoverflow.com/questions/9713058/send-post-data-using-xmlhttprequest

  String content = "<!DOCTYPE html>"
                   "<html>"
                   "<head>"
                   "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
                   "<title>Luminosity setter</title>"
                   "<style>"
                   ".slidecontainer {"
                   "width: 1024px;"
                   "margin: 0 auto;"
                   "text-align: center;"
                   "}"
                   ".slider {"
                   "    -webkit-appearance: none;"
                   "    width: 100%;"
                   "    height: 25px;"
                   "    background: #d3d3d3;"
                   "    outline: none;"
                   "    opacity: 0.7;"
                   "    -webkit-transition: .2s;"
                   "    transition: opacity .2s;"
                   //                 "    margin-left: 20px;"
                   //                  "    margin-right: 20px;"
                   "}"
                   ".slider:hover {"
                   "    opacity: 1;"
                   "}"
                   ".slider::-webkit-slider-thumb {"
                   "    -webkit-appearance: none;"
                   "    appearance: none;"
                   "    width: 25px;"
                   "    height: 25px;"
                   "    background: #4CAF50;"
                   "    cursor: pointer;"
                   "}"
                   ".slider::-moz-range-thumb {"
                   "    width: 25px;"
                   "    height: 25px;"
                   "    background: #4CAF50;"
                   "    cursor: pointer;"
                   "}"
                   "</style>"
                   "</head>"
                   "<body>"
                   "<div class=\"slidecontainer\">"
                   "  <input type=\"range\" min=\"0\" max=\"100\" value=\"" + String(ledValue) + "\" class=\"slider\" id=\"myRange\">"
                   "  <p>Value: <span id=\"demo\"></span>%</p>"
                   "</div>"
                   "<script>"
                   "document.getElementById(\"demo\").innerHTML = document.getElementById(\"myRange\").value;"
                   "document.getElementById(\"myRange\").oninput = function() {"
                   "  document.getElementById(\"demo\").innerHTML = this.value;"
                   "var data = new FormData();"
                   "data.append('ledVal', this.value);"
                   "var xhr = new XMLHttpRequest();"
                   "xhr.open('POST', '/', true);"
                   "xhr.onload = function () {"
                   "    console.log(this.responseText);"
                   "};"
                   "xhr.send(data);"
                   "}"
                   "</script>"
                   "</body>"
                   "</html>";
  server.send(200, "text/html", content);
}

4.thumb.png.0e2abb464bd0bffe008ddb73529d75a7.png

W tym przypadku przesunięcie suwaka na stronie sterującej wywoła natychmiastową zmianę jasności diody LED podłączonej odpowiednim rezystorem do pinu D1.

  • Pomogłeś! 1
Link do komentarza
Share on other sites

Podoba Ci się ten projekt? Zostaw pozytywny komentarz i daj znać autorowi, że zbudował coś fajnego!

Masz uwagi? Napisz kulturalnie co warto zmienić. Doceń pracę autora nad konstrukcją oraz opisem.

@rziomber, właśnie zaakceptowałem Twój opis, możesz go teraz zgłosić do akcji rabatowej umieszczając link w temacie zbiorczym. Dziękuję za przedstawienie ciekawego projektu, zachęcam do prezentowania kolejnych DIY oraz aktywności na naszym forum 🙂 

Przy okazji proszę inne osoby planujące opis projektów tego typu o wcześniejsze zgłaszanie się w celu wstępnej weryfikacji projektu (procedura jest opisana w ogłoszeniu).

Link do komentarza
Share on other sites

Cześć, po dodaniu w tagu <HEAD> tego fragmentu: <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

Pojawiają się błędy w JS. Nie jestem w stanie stwierdzić co i jak, bo jestem poniżej amatora w JS, HTML itp. Więc może ktoś bardziej oblatany w temacie będzie w stanie znaleźć rozwiązanie?

Pozdrawiam.

Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

(edytowany)

Nieco bardziej zautomatyzowany sposób prezentacji odczytu z czujników, który możemy wykorzystać w przykładzie z JSON.

<!DOCTYPE html>
<html>
<body>

<div id="sensor-values">
Temperatura: <span id="temperature"></span><br>
Wilgotność: <span id="humidity"></span><br>
Ciśnienie: <span id="pressure"></span><br>
</div>

<script>
var values = new Array();
values['temperature'] = 23.4;
values['humidity'] = 50.4;
values['pressure'] = 1000;

var childSpans = document.getElementById('sensor-values').getElementsByTagName('span');

for(i=0; i < childSpans.length; i++)
{
  if (typeof values[childSpans[i].id] !== 'undefined' && values[childSpans[i].id] !== null) {
  childSpans[i].textContent = values[childSpans[i].id];
  }
}
</script>

</body>
</html>

Zamiast ręcznie tworzyć tablicę values z wartościami jak w tym przykładzie, oczywiście sensowniej przeparsować JSON odczytywany cyklicznie dzięki AJAX z ESP8266. Zobacz var values = JSON.parse(str); z kodu w pierwszym poście.

Dzięki temu zaoszczędzimy sobie konieczności ręcznego przypisywania każdego odczytu.

document.getElementById(\"tempBME\").innerHTML = values.temp;
document.getElementById(\"humBME\").innerHTML = values.hum;
document.getElementById(\"presBME\").innerHTML = values.press;

Gotowy kod dla ESP8266 przy okazji z dołączonym Captive Portal może wyglądać tak:

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <ArduinoJson.h> //https://github.com/bblanchon/ArduinoJson
#include <DNSServer.h>

char* ssid = "Weather";
//const char *password = "";

ESP8266WebServer server(80);
DNSServer dnsServer;
Adafruit_BME280 bme;

void setup() {
  Serial.begin(9600);
  Wire.begin(D3, D4);
  Wire.setClock(100000);
  if (!bme.begin(0x76)) //changed from default I2C adress 0x77
  {
    Serial.println("Nie odnaleziono czujnika BMP085 / BMP180");
    while (1) {
    }
  }
  IPAddress apIP(192, 168, 1, 1);
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
  // WiFi.softAP(ssid, password);
  WiFi.softAP(ssid);
  dnsServer.start(53/*DNS_PORT*/, "*", apIP);
  server.on("/", handleRoot);
  server.on("/sensors", handleSensors);
  server.begin();
}

void loop() {
  server.handleClient();
  dnsServer.processNextRequest();
}

void handleRoot() {
  String content = R"rawliteral(<!DOCTYPE html><html><head>
<meta charset="UTF-8">
<title>Weather</title></head><body>
<DIV style="display:table; font-size: large;" id="sensor-values">
<DIV style="border-style: solid;">BME280:<BR>
Temperature: <span id="tempBME"></spanC<br>
Humidity: <span id="humBME"></span>%<br>
Pressure: <span id="pressBME"></span>Pa<br></DIV></DIV>
<script>myTimer();
var myVar = setInterval(myTimer, 1000);
function myTimer() {
var xhttp = new XMLHttpRequest(); 
xhttp.onreadystatechange = function() { 
  if (this.readyState == 4 && this.status == 200) { 
    var str = this.responseText; 
    var values = JSON.parse(str); 

    var childSpans = document.getElementById("sensor-values").getElementsByTagName("span");
    for(i=0; i < childSpans.length; i++){
      if (typeof values[childSpans[i].id] !== 'undefined' && values[childSpans[i].id] !== null) {
        childSpans[i].textContent = values[childSpans[i].id];
        }
    }
  }
}; 
    xhttp.open("GET", "/sensors", true); 
    xhttp.send();}</script>
</body></html>)rawliteral";
  server.send(200, "text/html", content);
}

void handleSensors() {
  String content;
  DynamicJsonDocument root(400);
  root["tempBME"] = String(bme.readTemperature(), 1);
  root["humBME"] = String(bme.readHumidity(), 1);
  root["pressBME"] = (int)bme.readPressure();
  serializeJson(root, content);
  server.send(200, "text/html", content);
}

 

Edytowano przez rziomber
Link do komentarza
Share on other sites

Witam

@rziomber...Wgrałem ten ostatni kod, ale nie działa.. Uzupełniłem brakujące dane typu nazwa sieci, niestety nie działa. Dobrze by było umieścić działający kod...Zwłaszcza że to ciekawe rozwiązanie...

  • Lubię! 1
Link do komentarza
Share on other sites

(edytowany)

Przed chwilą wgrałem kod zamieszczony w poście z 8 listopada. Działa bez problemu.

Możesz mieć BME280 pod innym adresem I2C. Zwróć uwagę na linijkę

if (!bme.begin(0x76)) //changed from default I2C adress 0x77

W którym momencie pojawia się kłopot?

weather.png

40 minut temu, kisoft napisał:

Uzupełniłem brakujące dane typu nazwa sieci,

W moim przykładzie sam ESP tworzy sieć WiFi, nie łączy się z już istniejącą. Oczywiście moża to zmienić podmieniając linijkę kodu.

 

Edytowano przez rziomber
Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.