Motivation: Angeregt durch Themen zu einer Modellautorennbahn habe ich mich damit beschäftigt, Daten mittels ESP-NOW an andere ESP32 zu übertragen (one to many) und gleichzeitig eine HTML-Seite via Webserver für einen Browser zur Verfügung zu stellen. Mir geht es um die grundsätzliche Machbarkeit, nicht um ein fertiges und geprüftes Projekt.
Quellen: An erster Stelle sind da die Tabs von Fips zu nennen, siehe auch Anleitung: Einführung zu fipsok.de.
ESP-NOW with ESP32: Send Data to Multiple Boards (one-to-many)
Vorgeschichte:
Serielle Datenstring vom Computer im Adruino Mega einlesen und aufteilen
Mit einem Nrf24 unterschiedliche Struct-Daten an mehrere Empfänger versenden
Datenflußbeispiel:
BRZ:15,015;BRF:Paul Mustermann;BRS:4;AR1:0;RZ1:0,000;F1:Friedhelm Busch;AR2:0;RZ2:0,000;F2:Max Jägermeister;AR3:0;RZ3:0,000;F3:Paul Mustermann;AR4:0;RZ4:0,000;F4:Thorsten Hesse;
Kennung | Daten | Mögliche Werte |
BRZ: | 6,777; | 0,000 bis 999,999 |
BRF: | Vorname Nachname; | Bis zu 25 Buchstaben |
BRS: | 4; | 0 bis 8 |
AR1: | 0; | 1-9999 |
RZ1: | 0,000; | 0,000 bis 999,999 |
F1: | Vorname Nachname; | Bis zu 25 Buchstaben |
AR2: | 0; | 1-9999 |
RZ2: | 0,000; | 0,000 bis 999,999 |
F2: | Vorname Nachname; | Bis zu 25 Buchstaben |
AR3: | 0; | 1-9999 |
RZ3: | 0,000 Mögliche Werte: | 0,000 bis 999,999 |
F3: | Vorname Nachname; | Bis zu 25 Buchstaben |
AR4: | 0; | 1-9999 |
RZ4: | 0,000; | 0,000 bis 999,999 |
F4: | Vorname Nachname; | Bis zu 25 Buchstaben |
Der Renn-PC wird von einem UNO oder dergleichen simuliert und gibt die Datenzeichenkette über die serielle Schnittstelle aus:
const char * msg[] = {"BRZ:15,015;BRF:Paul Mustermann;BRS:3;AR1:0;RZ1:5," , ";F1:Friedhelm Busch;AR2:0;RZ2:17," , ";F2:Max Jägermeister;AR3:0;RZ3:16," , ";F3:Paul Mustermann;AR4:0;RZ4:4," , ";F4:Thorsten Hesse;"};
const byte anzahl = sizeof(msg) / sizeof(msg[0]);
void setup()
{
Serial.begin(9600);
}
void loop()
{
for (byte j = 0; j < anzahl - 1; j++)
{
Serial.print(msg[j]);
Serial.print(random(100, 999));
}
Serial.print(msg[anzahl - 1]);
delay(1000);
}
Der ESP32-Sender nimmt im Tab Lesen die Daten an UART2 RX GPIO16 entgegen und wertet sie aus. Dabei wird die Kennung in eine Zahl gewandelt. Nach jedem Semikolon werden die Daten per ESP-NOW an die Empfänger verschickt und in einem Ergebnisfeld für die Webanzeige gespeichert.
/* verwendet wird Hardware Serial2
ESP32 UART2 RX = GPIO16
ESP32 UART2 TX = GPIO17
*/
// Variablen für die zu versendenen Daten
const byte MAXZEICHEN = 26;
const byte SPUREN = 4;
struct renn_struct {
uint8_t bed; // Bedeutung des nachfolgenden Textes
char txt[MAXZEICHEN]; // Text
} myData;
struct Ergebnis_struct {
char z[7]; // Rundenzeit
char f[25]; // Fahrer
} ergebnisse[SPUREN + 1];
void readSerial() {
char readChar; // Einzelnes Zeichen
static byte pos = 0; // Position im Array
static char buf[MAXZEICHEN] = {0}; // Zwischenspeicher
while (Serial2.available() > 0) {
readChar = Serial2.read(); // Einlesen
//DEBUG_P(readChar);
if (readChar == ';') { // Feldende
//DEBUG_L();
teileBuf(buf); // Übergeben zum teilen
pos = 0; // Position im Array rücksetzen
} else {
if (!isControl(readChar)) { // Zeichen ist kein Steuierzeichen
buf[pos] = readChar; // Dann aufnehmen
if (pos < MAXZEICHEN - 1) pos++; // neue Position setzen
buf[pos] = '\0'; // CharArray abschliessen
}
}
}
}
void teileBuf(char *buf) { // Teilt den Puffer
// DEBUG_P("buf: "); DEBUG_L(buf);
char *c; // Zeiger innerhalb des Puffers
c = strtok(buf, ":"); // Übernehme bis Trennzeichen 1
myData.bed = 0;
if (!strncmp(c, "BRZ", 3)) { // Feldname
myData.bed = 0x10; // BRZ
strncpy(myData.txt, strtok(NULL, ":"), MAXZEICHEN);
}
else if (!strncmp(c, "BRF", 3)) { // Feldname
myData.bed = 0x20; // BRF
strncpy(myData.txt, strtok(NULL, ":"), MAXZEICHEN);
}
else if (!strncmp(c, "BRS", 3)) { // Feldname
myData.bed = 0x30; // BRS
strncpy(myData.txt, strtok(NULL, ":"), MAXZEICHEN);
}
else if (!strncmp(c, "AR", 2)) { // Feldname
myData.bed = 0x10 + (c[2] - '0'); // ARn
strncpy(myData.txt, strtok(NULL, ":"), MAXZEICHEN);
}
else if (!strncmp(c, "RZ", 2)) {
myData.bed = 0x20 + (c[2] - '0'); // RZn
strncpy(myData.txt, strtok(NULL, ":"), MAXZEICHEN);
strncpy(ergebnisse[c[2] - '0'].z, myData.txt, MAXZEICHEN);
DEBUG_P(c[2] - '0'); DEBUG_P("\tergebnisse.z: "); DEBUG_L(ergebnisse[c[2] - '0'].z);
}
else if (!strncmp(c, "F", 1)) {
myData.bed = 0x30 + (c[1] - '0'); // Fn
strncpy(myData.txt, strtok(NULL, ":"), MAXZEICHEN);
strncpy(ergebnisse[c[1] - '0'].f, myData.txt, MAXZEICHEN);
DEBUG_P(c[1] - '0'); DEBUG_P("\tergebnisse.f: "); DEBUG_L(ergebnisse[c[1] - '0'].f);
}
serMon(c);
senden();
memset(buf, 0, MAXZEICHEN);
}
void serMon(char * c) {
DEBUG_P(" c: "); DEBUG_P(c);
DEBUG_P(" \tmyData.bed: 0x"); DEBUG_P(myData.bed, HEX);
DEBUG_P(" \tmyData.txt: "); DEBUG_L(myData.txt);
}
void senden() {
if (SlaveCnt > 0) { // check if slave channel is defined
esp_err_t result = esp_now_send(0, (uint8_t *) &myData, sizeof(renn_struct));
// DEBUG_L(result == ESP_OK ? "Sent with success" : "Error sending the data");
}
}
// callback when data is sent from Master to Slave
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
/*
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
DEBUG_P("Last Packet Sent to: "); DEBUG_L(macStr);
DEBUG_P("Last Packet Send Status: "); DEBUG_L(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
*/
}
void InitLesen() {
Serial2.begin(9600); // zum Empfang der Daten; UNO anstelle Renn-PC
}
Im Tab ESP_NOW ist die Suche nach zu verbindenden Empfängern vom IDE-Beispiel ESP32/ESPNow/Mult-Slave übernommen. In den ersten fünf Sekunden nach Reset oder bei Tastendruck sucht der Sender nach paarungswilligen Empfängern und merkt sich diese. Wer mit gewissen Begrifflichkeiten unglücklich ist, möge diese bitte ersetzen.
#define CHANNEL 1
#define PRINTSCANRESULTS 0
#define NUMSLAVES 20
esp_now_peer_info_t slaves[NUMSLAVES] = {}; // Global copy of slaves
int SlaveCnt = 0;
const byte paarPin = 33; // Eingang zum Paaren (pairing) mit Empfangs-ESPs
// Init ESP Now with fallback
void InitESPNow() {
pinMode(paarPin, INPUT_PULLUP);
DEBUG_L("ESP-NOW");
// This is the mac address of the Master in Station Mode
DEBUG_P("STA MAC: "); DEBUG_L(WiFi.macAddress());
// Init ESPNow with a fallback logic
WiFi.disconnect();
if (esp_now_init() == ESP_OK) {
DEBUG_L("ESPNow Init Success");
}
else {
DEBUG_L("ESPNow Init Failed");
ESP.restart();
}
// Once ESPNow is successfully Init, we will register for Send CB to
// get the status of Trasnmitted packet
esp_now_register_send_cb(OnDataSent);
}
// Scan for slaves in AP mode
void ScanForSlave() {
int8_t scanResults = WiFi.scanNetworks();
//reset slaves
memset(slaves, 0, sizeof(slaves));
SlaveCnt = 0;
DEBUG_L("");
if (scanResults == 0) {
DEBUG_L("No WiFi devices in AP Mode found");
} else {
DEBUG_P("Found "); DEBUG_P(scanResults); DEBUG_L(" devices ");
for (int i = 0; i < scanResults; ++i) {
// Print SSID and RSSI for each device found
String SSID = WiFi.SSID(i);
int32_t RSSI = WiFi.RSSI(i);
String BSSIDstr = WiFi.BSSIDstr(i);
if (PRINTSCANRESULTS) {
DEBUG_P(i + 1); DEBUG_P(": "); DEBUG_P(SSID); DEBUG_P(" ["); DEBUG_P(BSSIDstr); DEBUG_P("]"); DEBUG_P(" ("); DEBUG_P(RSSI); DEBUG_P(")"); DEBUG_L("");
}
delay(10);
// Check if the current device starts with `Slave`
if (SSID.indexOf("Slave") == 0) {
// SSID of interest
DEBUG_P(i + 1); DEBUG_P(": "); DEBUG_P(SSID); DEBUG_P(" ["); DEBUG_P(BSSIDstr); DEBUG_P("]"); DEBUG_P(" ("); DEBUG_P(RSSI); DEBUG_P(")"); DEBUG_L("");
// Get BSSID => Mac Address of the Slave
int mac[6];
if ( 6 == sscanf(BSSIDstr.c_str(), "%x:%x:%x:%x:%x:%x", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5] ) ) {
for (int ii = 0; ii < 6; ++ii ) {
slaves[SlaveCnt].peer_addr[ii] = (uint8_t) mac[ii];
}
}
slaves[SlaveCnt].channel = CHANNEL; // pick a channel
slaves[SlaveCnt].encrypt = 0; // no encryption
SlaveCnt++;
}
}
}
if (SlaveCnt > 0) {
DEBUG_P(SlaveCnt); DEBUG_L(" Slave(s) found, processing..");
} else {
DEBUG_L("No Slave Found, trying again.");
}
// clean up ram
WiFi.scanDelete();
}
// Check if the slave is already paired with the master.
// If not, pair the slave with master
void manageSlave() {
if (SlaveCnt > 0) {
for (int i = 0; i < SlaveCnt; i++) {
DEBUG_P("Processing: ");
for (int ii = 0; ii < 6; ++ii ) {
DEBUG_P((uint8_t) slaves[i].peer_addr[ii], HEX);
if (ii != 5) DEBUG_P(":");
}
DEBUG_P(" Status: ");
// check if the peer exists
bool exists = esp_now_is_peer_exist(slaves[i].peer_addr);
if (exists) {
// Slave already paired.
DEBUG_L("Already Paired");
} else {
// Slave not paired, attempt pair
esp_err_t addStatus = esp_now_add_peer(&slaves[i]);
if (addStatus == ESP_OK) {
// Pair success
DEBUG_L("Pair success");
} else if (addStatus == ESP_ERR_ESPNOW_NOT_INIT) {
// How did we get so far!!
DEBUG_L("ESPNOW Not Init");
} else if (addStatus == ESP_ERR_ESPNOW_ARG) {
DEBUG_L("Add Peer - Invalid Argument");
} else if (addStatus == ESP_ERR_ESPNOW_FULL) {
DEBUG_L("Peer list full");
} else if (addStatus == ESP_ERR_ESPNOW_NO_MEM) {
DEBUG_L("Out of memory");
} else if (addStatus == ESP_ERR_ESPNOW_EXIST) {
DEBUG_L("Peer Exists");
} else {
DEBUG_L("Not sure what happened");
}
delay(100);
}
}
} else {
// No slave found to process
DEBUG_L("No Slave found to process");
}
}
void slave() {
uint32_t jetzt = millis();
static uint32_t spaeter = millis();
if (jetzt >= spaeter) {
spaeter = jetzt + 1000;
if (!digitalRead(paarPin) || millis() < 5000) {
// In the loop we scan for slave
ScanForSlave();
// If Slave is found, it would be populate in `slave` variable
}
// We will check if `slave` is defined and then we proceed further
if (SlaveCnt > 0) { // check if slave channel is defined
// `slave` is defined
// Add slave as peer if it has not been added already
manageSlave();
// pair success or already paired
// Send data to device
//sendData();
} else {
DEBUG_L("Wait for pairing!");
}
}
}
Im Tab Rennen werden die JSON-Daten für die Aktualisierung der HTML-Datei zur Verfügung gestellt.
void rennen() { // Funktionsaufruf "rennen();" muss im Setup eingebunden werden
server.on("/rennen", []() {
server.send(200, "application/json", handleRennen());
#ifdef DEBUGGING
Serial.println(handleRennen());
#endif
});
}
String handleRennen() {
return (String)"{\"rennen\":[" +
"{\"F\":\"" + ergebnisse[1].f + "\",\"Z\":\"" + ergebnisse[1].z + "\" }," +
"{\"F\":\"" + ergebnisse[2].f + "\",\"Z\":\"" + ergebnisse[2].z + "\" }," +
"{\"F\":\"" + ergebnisse[3].f + "\",\"Z\":\"" + ergebnisse[3].z + "\" }," +
"{\"F\":\"" + ergebnisse[4].f + "\",\"Z\":\"" + ergebnisse[4].z + "\" }"
"]}";
}
Im Tab Connect wähle ich den WiFi-Modus WIFI_AP_STA, damit der ESP32 als Access Point sein eigenes, von einem Router unabhängiges WLAN einrichtet. Bitte SSID und Passwort an die eigenen Wünsche anpassen!
void Connect() { // Funktionsaufruf "Connect();" muss im Setup nach "spiffs();" eingebunden werden
const char *ssid = "Esp32AP"; // << kann bis zu 32 Zeichen haben
const char *password = "12345678"; // << mindestens 8 Zeichen jedoch nicht länger als 64 Zeichen
WiFi.mode(WIFI_AP_STA);
if (WiFi.softAP(ssid, password)) {
#ifdef DEBUGGING
Serial.printf("Verbinde dich mit dem Netzwerk \"%s\"\nGib die IP %s im Browser ein\n\n", ssid, WiFi.softAPIP().toString().c_str());
Serial.println("Mac Adresse des AP = " + WiFi.softAPmacAddress());
Serial.println("Broadcast IP des AP = " + WiFi.softAPBroadcastIP().toString());
#endif
} else {
#ifdef DEBUGGING
Serial.println("Fehler beim erstellen.");
#endif
}
}
Im Haupt-Tab wird OTA (Programm Over The Air verschicken) eingerichtet, allerdings können gerade am Anfang ein paar Informationen im seriellen Monitor hilfreich sein.
#include <WebServer.h>
#include <ArduinoOTA.h>
#include <SPIFFS.h>
#include <esp_now.h>
//#define DEBUGGING // Einkommentieren für die Serielle Ausgabe
#ifdef DEBUGGING
#define DEBUG_B(...) Serial.begin(__VA_ARGS__)
#define DEBUG_P(...) Serial.print(__VA_ARGS__)
#define DEBUG_L(...) Serial.println(__VA_ARGS__)
#define DEBUG_F(...) Serial.printf(__VA_ARGS__)
#else
#define DEBUG_B(...)
#define DEBUG_P(...)
#define DEBUG_L(...)
#define DEBUG_F(...)
#endif
WebServer server(80);
void setup() {
delay(500);
#ifdef DEBUGGING
Serial.begin(115200);
Serial.printf("\nSketchname: %s\nBuild: %s\t\tIDE: %d.%d.%d\n\n", __FILE__, __TIMESTAMP__, ARDUINO / 10000, ARDUINO % 10000 / 100, ARDUINO % 100 / 10 ? ARDUINO % 100 : ARDUINO % 10);
#endif
spiffs();
admin();
Connect();
InitESPNow();
InitLesen();
rennen();
ArduinoOTA.begin();
server.begin();
#ifdef DEBUGGING
Serial.println("HTTP Server gestartet\n\n");
#endif
}
void loop() {
ArduinoOTA.handle();
server.handleClient();
slave();
readSerial();
}
Alle anderen Tabs sind bei Fips beschrieben.
Admin.ino:
#include <rom/rtc.h>
const char* const PROGMEM flashChipMode[] = {"QIO", "QOUT", "DIO", "DOUT", "Unbekannt"};
const char* const PROGMEM resetReason[] = {"ERR", "Power on", "Unknown", "Software", "Watch dog", "Deep Sleep", "SLC module", "Timer Group 0", "Timer Group 1",
"RTC Watch dog", "Instrusion", "Time Group CPU", "Software CPU", "RTC Watch dog CPU", "Extern CPU",
"Voltage not stable", "RTC Watch dog RTC"
};
void admin() { // Funktionsaufruf "admin();" muss im Setup eingebunden werden
server.on("/admin/renew", handlerenew);
server.on("/admin/once", handleonce);
server.on("/reconnect", []() {
server.send(204, "");
WiFi.reconnect();
});
server.on("/restart", []() {
server.send(204, "");
//save(); //Einkommentieren wenn Werte vor dem Neustart gesichert werden sollen
ESP.restart();
});
}
void handlerenew() {
server.send(200, "application/json", R"([")" + runtime() + R"(",")" + temperatureRead() + R"(",")" + WiFi.RSSI() + R"("])"); // Json als Array
}
void handleonce() {
if (server.arg(0) != "") {
WiFi.setHostname(server.arg(0).c_str());
File file = SPIFFS.open("/config.json", FILE_WRITE);
file.printf("[\"%s\"]", WiFi.softAPgetHostname());
file.close();
}
String fname = String(__FILE__).substring( 3, String(__FILE__).lastIndexOf ('\\'));
String temp = R"({"File":")" + fname.substring(fname.lastIndexOf ('\\') + 1, fname.length()) + R"(", "Build":")" + (String)__DATE__ + " " + (String)__TIME__ +
R"(", "LocalIP":")" + WiFi.softAPIP().toString() + R"(", "Hostname":")" + WiFi.softAPgetHostname() + R"(", "SSID":"_ )" + WiFi.SSID() +
R"(", "GatewayIP":")" + WiFi.gatewayIP().toString() + R"(", "Channel":")" + WiFi.channel() + R"(", "MacAddress":")" + WiFi.softAPmacAddress() +
R"(", "SubnetMask":")" + WiFi.subnetMask().toString() + R"(", "BSSID":"_ )" + WiFi.BSSIDstr() + R"(", "ClientIP":")" + server.client().remoteIP().toString() +
R"(", "DnsIP":")" + WiFi.dnsIP().toString() + R"(", "Reset1":")" + resetReason[rtc_get_reset_reason(0)] +
R"(", "Reset2":")" + resetReason[rtc_get_reset_reason(1)] + R"(", "CpuFreqMHz":")" + ESP.getCpuFreqMHz() +
R"(", "FreeHeap":")" + formatBytes(ESP.getFreeHeap()) + R"(", "ChipSize":")" + formatBytes(ESP.getFlashChipSize()) +
R"(", "ChipSpeed":")" + ESP.getFlashChipSpeed() / 1000000 + R"(", "ChipMode":")" + flashChipMode[ESP.getFlashChipMode()] +
R"(", "IdeVersion":")" + ARDUINO + R"(", "SdkVersion":")" + ESP.getSdkVersion() + R"("})";
server.send(200, "application/json", temp); // Json als Objekt
}
String runtime() {
static uint8_t rolloverCounter = 0;
static auto letzteMillis = 0;
auto aktuelleMillis = millis();
if (aktuelleMillis < letzteMillis) { // prüft Millis Überlauf
rolloverCounter++;
#ifdef DEBUGGING
Serial.println("Millis Überlauf");
#endif
}
letzteMillis = aktuelleMillis;
auto sek = (0xFFFFFFFF / 1000 ) * rolloverCounter + (aktuelleMillis / 1000);
char buf[20];
snprintf(buf, sizeof(buf), sek < 86400 || sek > 172800 ? "%ld Tage %02ld:%02ld:%02ld" : "%ld Tag %02ld:%02ld:%02ld", sek / 86400, sek / 3600 % 24, sek / 60 % 60, sek % 60);
return buf;
}
Spiffs.ino:
#include <detail/RequestHandlersImpl.h>
#include <list>
const char HELPER[] PROGMEM = R"(<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="[]" multiple><button>Upload</button></form>Lade die spiffs.html hoch.)";
void spiffs() { // Funktionsaufruf "spiffs();" muss im Setup eingebunden werden
SPIFFS.begin(true);
server.on("/list", handleList);
server.on("/format", formatSpiffs);
server.on("/upload", HTTP_POST, sendResponce, handleFileUpload);
server.onNotFound([]() {
if (!handleFile(server.urlDecode(server.uri())))
server.send(404, "text/plain", "FileNotFound");
});
}
void handleList() { // Senden aller Daten an den Client
File root = SPIFFS.open("/");
typedef std::pair<String, int> prop;
std::list<prop> dirList; // Liste anlegen
while (File f = root.openNextFile()) dirList.emplace_back(f.name(), f.size()); // Liste füllen
dirList.sort([](const prop & f, const prop & l) { // Liste sortieren
if (server.arg(0) == "1") {
return f.second > l.second;
} else {
for (uint8_t i = 0; i < 31; i++) {
if (tolower(f.first[i]) < tolower(l.first[i])) return true;
else if (tolower(f.first[i]) > tolower(l.first[i])) return false;
}
return false;
}
});
String temp = "[";
for (auto& p : dirList) {
if (temp != "[") temp += ',';
temp += "{\"name\":\"" + p.first.substring(1) + "\",\"size\":\"" + formatBytes(p.second) + "\"}";
}
temp += R"(,{"usedBytes":")" + formatBytes(SPIFFS.usedBytes() * 1.05) + R"(",)" + // Berechnet den verwendeten Speicherplatz + 5% Sicherheitsaufschlag
R"("totalBytes":")" + formatBytes(SPIFFS.totalBytes()) + R"(","freeBytes":")" + // Zeigt die Größe des Speichers
(SPIFFS.totalBytes() - (SPIFFS.usedBytes() * 1.05)) + R"("}])"; // Berechnet den freien Speicherplatz + 5% Sicherheitsaufschlag
server.send(200, "application/json", temp);
}
bool handleFile(String&& path) {
if (server.hasArg("delete")) {
SPIFFS.remove(server.arg("delete")); // Datei löschen
sendResponce();
return true;
}
if (!SPIFFS.exists("/spiffs.html"))server.send(200, "text/html", HELPER); // ermöglicht das hochladen der spiffs.html
if (path.endsWith("/")) path += "index.html";
return SPIFFS.exists(path) ? ({File f = SPIFFS.open(path, "r"); server.streamFile(f, StaticRequestHandler::getContentType(path)); f.close(); true;}) : false;
}
void handleFileUpload() { // Dateien vom Rechnenknecht oder Klingelkasten ins SPIFFS schreiben
static File fsUploadFile;
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
if (upload.filename.length() > 30) {
upload.filename = upload.filename.substring(upload.filename.length() - 30, upload.filename.length()); // Dateinamen auf 30 Zeichen kürzen
}
#ifdef DEBUGGING
Serial.printf("handleFileUpload Name: /%s\n", upload.filename.c_str());
#endif
fsUploadFile = SPIFFS.open("/" + server.urlDecode(upload.filename), "w");
} else if (upload.status == UPLOAD_FILE_WRITE) {
#ifdef DEBUGGING
Serial.printf("handleFileUpload Data: %u\n", upload.currentSize);
#endif
if (fsUploadFile)
fsUploadFile.write(upload.buf, upload.currentSize);
} else if (upload.status == UPLOAD_FILE_END) {
if (fsUploadFile)
fsUploadFile.close();
#ifdef DEBUGGING
Serial.printf("handleFileUpload Size: %u\n", upload.totalSize);
#endif
}
}
void formatSpiffs() { //Formatiert den Speicher
SPIFFS.format();
sendResponce();
}
void sendResponce() {
server.sendHeader("Location", "spiffs.html");
server.send(303, "message/http");
}
const String formatBytes(size_t const& bytes) { // lesbare Anzeige der Speichergrößen
return bytes < 1024 ? static_cast<String>(bytes) + " Byte" : bytes < 1048576 ? static_cast<String>(bytes / 1024.0) + " KB" : static_cast<String>(bytes / 1048576.0) + " MB";
}
bool freeSpace(uint16_t const& printsize) { // Funktion zum genügend freier Platzbeim speichern in Logdateien zu prüfen ob noch verfügbar ist.
//Serial.printf("Funktion: %s meldet in Zeile: %d FreeSpace: %s\n", __PRETTY_FUNCTION__, __LINE__, formatBytes(SPIFFS.totalBytes() - (SPIFFS.usedBytes() * 1.05)).c_str());
return (SPIFFS.totalBytes() - (SPIFFS.usedBytes() * 1.05) > printsize) ? true : false;
}
Alle Tab-Dateien gehören in ein Verzeichnis mit dem Namen des Haupt-Tabs.
Das Dateisystem SPIFFS des ESP32 muß die Dateien rennen.html, spiffs.html, admin.html und style32.css, die mit dem Esp32 Spiffs Datei Manager geladen werden können, aufnehmen. Dazu im Browser "http://192.168.4.1/spiffs.html" eingeben und den Anweisungen folgen.
Nach dem Upload aller notwendigen Dateien sieht die Anzeige dann so aus:
Durch Klicken auf "rennen.html" mit der Adresse http://192.168.4.1/rennen.html sieht man die noch leere HTML-Seite für die Rennergebnisse:
rennen.html:
<!-- For more information visit: https://fipsok.de -->
<!DOCTYPE HTML>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rennergebnisse</title>
<p id="jsontext"></p>
<script>
function renew() {
fetch('/rennen').then(function (response) {
return response.json();
}).then(function (array) {
var elem = document.querySelector('#F1');
elem.innerHTML = array.rennen[0].F
var elem = document.querySelector('#Z1');
elem.innerHTML = array.rennen[0].Z
var elem = document.querySelector('#F2');
elem.innerHTML = array.rennen[1].F
var elem = document.querySelector('#Z2');
elem.innerHTML = array.rennen[1].Z
var elem = document.querySelector('#F3');
elem.innerHTML = array.rennen[2].F
var elem = document.querySelector('#Z3');
elem.innerHTML = array.rennen[2].Z
var elem = document.querySelector('#F4');
elem.innerHTML = array.rennen[3].F
var elem = document.querySelector('#Z4');
elem.innerHTML = array.rennen[3].Z
var text = JSON.stringify(array);
document.getElementById("jsontext").innerHTML = 'JSON: ' + text;
});
}
document.addEventListener('DOMContentLoaded', renew);
setInterval(renew, 444)
</script>
</head>
<body>
<font color="#000000" face="VERDANA,ARIAL,HELVETICA"><h2>Rennergebnisse</h2>
<table>
<tr><td>Spur</td><td> </td><td>Rundenzeit</td><td> </td><td>Fahrer</td></tr>
<tr><td align=right>1</td><td></td><td align=right id='Z1'>88,188</td><td></td><td id='F1'>Fahrer 1</td></tr>
<tr><td align=right>2</td><td></td><td align=right id='Z2'>88,288</td><td></td><td id='F2'>Fahrer 2</td></tr>
<tr><td align=right>3</td><td></td><td align=right id='Z3'>88,388</td><td></td><td id='F3'>Fahrer 3</td></tr>
<tr><td align=right>4</td><td></td><td align=right id='Z4'>88,488</td><td></td><td id='F4'>Fahrer 4</td></tr>
</table>
</body>
</html>
spiffs.html:
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style32.css">
<title>Esp32 Datei Manager</title>
<script>
var to = JSON.parse(localStorage.getItem('sortBy'));
document.addEventListener('DOMContentLoaded', () => {
list(to);
fs.addEventListener('change', () => {
for (var bytes = 0, j = 0; j < event.target.files.length; j++) bytes += event.target.files[j].size;
for (var out = `${bytes} Byte`, i = 0, circa = bytes / 1024; circa > 1; circa /= 1024) out = circa.toFixed(2) + [' KB', ' MB', ' GB'][i++];
if (bytes > free) {
si.innerHTML = `<li><b> ${out}</b><strong> Ungenügend Speicher frei</strong></li>`;
up.setAttribute('disabled', 'disabled');
}
else {
si.innerHTML = `<li><b>Dateigröße:</b> ${out}</li>`;
up.removeAttribute('disabled');
}
});
btn.addEventListener('click', () => {
if (!confirm(`Wirklich formatieren? Alle Daten gehen verloren.\nDu musst anschließend spiffs.html wieder laden.`)) event.preventDefault();
});
});
function list(arg){
let myList = document.querySelector('main');
fetch('list?sort='+arg).then( (response) => {
return response.json();
}).then((json) => {
myList.innerHTML = '';
for (var i = 0; i < json.length - 1; i++) {
let dir = `<li><a href ="${json[i].name}">${json[i].name}</a><small> ${json[i].size}</small><a href ="${json[i].name}"download="${json[i].name}"> Download </a>`;
if (json[i].name != 'spiffs.html') dir += `or <a href ="?delete=/${json[i].name}">Delete </a>`;
myList.insertAdjacentHTML('beforeend', dir);
}
myList.insertAdjacentHTML('beforeend', `<li><b id="so">${to ? '▼' : '▲'} SPIFFS</b> belegt ${json[i].usedBytes} von ${json[i].totalBytes}`);
free = json[i].freeBytes;
so.addEventListener('click', () => {
list(to=++to%2);
localStorage.setItem('sortBy', JSON.stringify(to));
});
});
}
</script>
</head>
<body>
<h2>ESP32 Datei Manager</h2>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input id="fs" type="file" name="upload[]" multiple>
<input id="up" type="submit" value="Upload" disabled>
</form>
<div>
<span id="si"></span>
<main></main>
</div>
<form action="/format" method="POST"><input id="btn" type="submit" value="Format SPIFFS"></form>
</body>
</html>
admin.html
<!-- For more information visit: https://fipsok.de -->
<!DOCTYPE HTML>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style32.css">
<title>ESP32 Admin</title>
<SCRIPT>
window.addEventListener('load', () => {
document.querySelector('#spiff').addEventListener('click', () => {
window.location = '/spiffs.html';
});
document.querySelector('#home').addEventListener('click', () => {
window.location = '/';
});
document.querySelector('#restart').addEventListener('click', () => {
if (confirm('Bist du sicher!')) re('restart');
});
document.querySelector('#reconnect').addEventListener('click', re.bind(this, 'reconnect'));
document.querySelector('#hostbutton').addEventListener('click', check.bind(this, document.querySelector('input')));
once();
var output = document.querySelector('#note');
function once(arg1, arg2) {
fetch('/admin/once', {
method: 'POST',
body: arg1
}).then( resp => {
return resp.json();
}).then( obj => {
output.innerHTML = '';
output.classList.remove('note');
document.querySelector('form').reset();
if (arg1 == undefined) wait = window.setInterval(renew, 1000);
if (arg2 == 'reconnect') re(arg2);
document.querySelector('#file').innerHTML = obj['File'];
document.querySelector('#build').innerHTML = obj['Build'];
document.querySelector('#local').innerHTML = obj['LocalIP'];
document.querySelector('#host').innerHTML = obj['Hostname'];
document.querySelector('#ssid').innerHTML = obj['SSID'];
document.querySelector('#gateway').innerHTML = obj['GatewayIP'];
document.querySelector('#kanal').innerHTML = obj['Channel'];
document.querySelector('#mac').innerHTML = obj['MacAddress'];
document.querySelector('#subnet').innerHTML = obj['SubnetMask'];
document.querySelector('#bss').innerHTML = obj['BSSID'];
document.querySelector('#client').innerHTML = obj['ClientIP'];
document.querySelector('#dns').innerHTML = obj['DnsIP'];
document.querySelector('#reset1').innerHTML = obj['Reset1'];
document.querySelector('#reset2').innerHTML = obj['Reset2'];
document.querySelector('#cpufreq').innerHTML = obj['CpuFreqMHz'];
document.querySelector('#freeheap').innerHTML = obj['FreeHeap'];
document.querySelector('#csize').innerHTML = obj['ChipSize'];
document.querySelector('#cspeed').innerHTML = obj['ChipSpeed'];
document.querySelector('#cmode').innerHTML = obj['ChipMode'];
document.querySelector('#ide').innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
document.querySelector('#sdk').innerHTML = obj['SdkVersion'];
}).catch(function (err) {
re();
});
}
function renew() {
fetch('admin/renew').then( resp => {
return resp.json();
}).then( array => {
document.querySelector('#runtime').innerHTML = array[0];
document.querySelector('#temp').innerHTML = array[1];
document.querySelector('#rssi').innerHTML = array[2];
});
}
function check(inObj) {
!inObj.checkValidity() ? (output.innerHTML = inObj.validationMessage, output.classList.add('note')) : (once(inObj.value, 'reconnect'));
}
function re(arg) {
window.clearInterval(wait);
fetch(arg);
output.classList.add('note');
if (arg == 'restart') {
output.innerHTML = 'Der Server wird neu gestartet. Die Daten werden in 10 Sekunden neu geladen.';
setTimeout(once, 10000);
} else if (arg == 'reconnect') {
output.innerHTML = 'Die WiFi Verbindung wird neu gestartet. Daten werden in 5 Sekunden neu geladen.';
setTimeout(once, 5000);
} else {
output.innerHTML = 'Es ist ein Verbindungfehler aufgetreten. Es wird versucht neu zu verbinden.';
setTimeout(once, 2000);
}
}
});
</SCRIPT>
</head>
<body>
<h1>ESP32 Admin Page</h1>
<main>
<section id="left">
<span>Runtime ESP:</span>
<span>WiFi RSSI:</span>
<span>CPU Temperatur:</span>
<span>Sketch Name:</span>
<span>Sketch Build:</span>
<span>IP address:</span>
<span>Hostname:</span>
<span>Connected to:</span>
<span>Gateway IP:</span>
<span>Channel:</span>
<span>MacAddress:</span>
<span>SubnetMask:</span>
<span>BSSID:</span>
<span>Client IP:</span>
<span>DnsIP:</span>
<span>Reset CPU 1:</span>
<span>Reset CPU 2:</span>
<span>CPU Freq:</span>
<span>FreeHeap:</span>
<span>FlashSize:</span>
<span>FlashSpeed:</span>
<span>FlashMode:</span>
<span>Arduino IDE Version:</span>
<span>SDK-Version:</span>
</section>
<section>
<data id="runtime">00:00:00</data>
<div>
<data id="rssi"></data>
dBm
</div>
<div>
<data id="temp"></data>
°C
</div>
<data id="file">?</data>
<data id="build">0</data>
<data id="local">0</data>
<data id="host">?</data>
<data id="ssid">?</data>
<data id="gateway">0</data>
<data id="kanal">0</data>
<data id="mac">0</data>
<data id="subnet">0</data>
<data id="bss">0</data>
<data id="client">0</data>
<data id="dns">0</data>
<data id="reset1">0</data>
<data id="reset2">0</data>
<div>
<data id="cpufreq"></data>
MHz
</div>
<data id="freeheap">0</data>
<data id="csize">0</data>
<div>
<data id="cspeed"></data>
MHz
</div>
<data id="cmode">0</data>
<data id="ide">0</data>
<data id="sdk">0</data>
</section>
</main>
<div>
<button class="button" id="spiff">Spiffs</button>
<button class="button" id="home">Startseite</button>
</div>
<div id="note"></div>
<div>
<form><input placeholder=" neuer Hostname" pattern="([A-Za-z0-9-]{1,32})" title="Es dürfen nur Buchstaben
(a-z, A-Z), Ziffern (0-9) und Bindestriche (-) enthalten sein. Maximal 32 Zeichen" required>
<button class="button" type="button" id="hostbutton">Name Senden</button>
</form>
</div>
<div>
<button class="button" id="reconnect">WiFi Reconnect</button>
<button class="button" id="restart">ESP Restart</button>
</div>
</body>
</html>
style32.css:
/*For more information visit: https://fipsok.de*/
body {
font-family: sans-serif;
background-color: #a9a9a9;
display: flex;
flex-flow: column;
align-items: center;
}
h1,h2 {
color: #e1e1e1;
text-shadow: 2px 2px 2px black;
}
li {
background-color: #7cfc00;
list-style-type: none;
margin-bottom: 10px;
padding-right: 5px;
border-top: 3px solid #7cfc00;
border-bottom: 3px solid #7cfc00;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.7);
}
b,a:nth-child(1) {
background-color: #ff4500;
font-weight: bold;
color: white;
text-decoration: none;
padding: 0 5px;
border-top: 3.2px solid #ff4500;
border-bottom: 3.6px solid #ff4500;
text-shadow: 2px 2px 1px black;
}
input {
height: 35px;
font-size: 13px;
}
h1+main {
display: flex;
}
section {
display: flex;
flex-direction: column;
padding: 0.2em;
}
#left {
align-items: flex-end;
text-shadow: 1px 1px 2px #757474;
}
.note {
background-color: salmon;
padding: 0.5em;
margin-top: 1em;
text-align: center;
max-width: 320px;
border-radius: 0.5em;
}
.button {
width: 130px;
height: 40px;
font-size: 16px;
margin-top: 1em;
cursor: pointer;
background-color: #adff2f;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.7);
}
[type=submit] {
height: 40px;
min-width: 70px;
}
[value^=Format] {
background-color: #ddd;
}
[title] {
background-color: silver;
font-size: 16px;
width: 125px;
}
Der ESP32-Empfänger nimmt die vom ESP32-Sender mittels ESP-NOW geschickte Datenstruktur entgegen. Die Anzeige erfolgt mit dem seriellen Monitor und beispielsweise mit einer MAX7219 Matrix. Jede andere Anzeige wäre möglich.
#include <esp_now.h>
#include <WiFi.h>
#include <Adafruit_GFX.h>
#include <SPI.h>
#include <Max72xxPanel.h>
/*
MAX7219 - ESP32
GND - GND
VDD - 3,3V
CLK - 18 (VSPI CLK)
DIN - 23 (VSPI MOSI)
CS - 5 (VSPI CS0)
*/
//Must match the sender structure
const byte MAXZEICHEN = 26;
const byte SPUREN = 4;
struct renn_struct {
uint8_t bed; // Bedeutung des nachfolgenden Textes
char txt[MAXZEICHEN]; // Text
} myData;
class RennAnzeige {
const byte numberOfHorizontalDisplays = 4;
const byte numberOfVerticalDisplays = 1;
const byte pinCS;
Max72xxPanel matrix;
public:
RennAnzeige(const byte pinCS): pinCS(pinCS), matrix(Max72xxPanel(pinCS, numberOfHorizontalDisplays, numberOfVerticalDisplays)) {}
void init() {
matrix.setIntensity(2); // Use a value between 0 and 15 for brightness
matrix.setRotation(0, 1); // The first display is position upside down
matrix.setRotation(1, 1); // The first display is position upside down
matrix.setRotation(2, 1); // The first display is position upside down
matrix.setRotation(3, 1); // The first display is position upside down
matrix.setTextSize(0);
matrix.setCursor(1, 0);
matrix.print("-----");
matrix.write();
}
void ausgabeRZ() {
char * buf = myData.txt;
int vk, nk;
vk = atoi(strtok(buf, ",")); // Erste Zahl - Trenner ist ,
nk = atoi(strtok(NULL, ",")); // zweite Zahl
// Serial.print("vk: "); Serial.print(vk); Serial.print("\tnk: "); Serial.println(nk);
matrix.fillScreen(LOW);
if (vk < 10) {
matrix.setCursor(6, 0);
} else {
matrix.setCursor(0, 0);
}
matrix.print(vk);
matrix.drawPixel(12, 6, HIGH);
matrix.drawPixel(12, 7, HIGH);
matrix.setCursor(14, 0);
if (nk < 100) {
matrix.print("0");
}
if (nk < 10) {
matrix.print("0");
}
matrix.print(nk);
matrix.write();
}
};
RennAnzeige rennAnzeige[] { // pinCS für Anzeige
{5}, {17}, {16}, {4}
};
void serMon() {
Serial.print("bed: 0x"); Serial.print(myData.bed, HEX);
Serial.print("\ttxt: "); Serial.print(myData.txt);
Serial.println();
}
//callback function that will be executed when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
memcpy(&myData, incomingData, sizeof(myData));
// Serial.print("Bytes received: "); Serial.println(len);
serMon();
if (myData.bed == 0x21) rennAnzeige[0].ausgabeRZ();
if (myData.bed == 0x22) rennAnzeige[1].ausgabeRZ();
if (myData.bed == 0x23) rennAnzeige[2].ausgabeRZ();
if (myData.bed == 0x24) rennAnzeige[3].ausgabeRZ();
}
#define CHANNEL 1
// Init ESP Now with fallback
void InitESPNow() {
WiFi.disconnect();
if (esp_now_init() == ESP_OK) {
Serial.println("ESPNow Init Success");
}
else {
Serial.println("ESPNow Init Failed");
// Retry InitESPNow, add a counte and then restart?
// InitESPNow();
// or Simply Restart
ESP.restart();
}
}
// config AP SSID
void configDeviceAP() {
String Prefix = "Slave:";
String Mac = WiFi.macAddress();
String SSID = Prefix + Mac;
String Password = "123456789";
bool result = WiFi.softAP(SSID.c_str(), Password.c_str(), CHANNEL, 0);
if (!result) {
Serial.println("AP Config failed.");
} else {
Serial.println("AP Config Success. Broadcasting with AP: " + String(SSID));
}
}
void setup() {
for (auto &r : rennAnzeige) r.init();
delay(500);
//Initialize Serial Monitor
Serial.begin(115200);
Serial.println("Start ESP-NOW receiver");
Serial.print("ESP Board MAC Address: ");
Serial.println(WiFi.macAddress());
//Set device in AP mode to begin with
WiFi.mode(WIFI_AP);
// configure device AP mode
configDeviceAP();
// This is the mac address of the Slave in AP Mode
Serial.print("AP MAC: "); Serial.println(WiFi.softAPmacAddress());
// Init ESPNow with a fallback logic
InitESPNow();
// Once ESPNow is successfully Init, we will register for recv CB to
// get recv packer info.
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {}
Inbetriebnahme:
- Das ESP32-Empfängerprogramm auf alle beispielsweise vier ESP32-Empfänger übertragen. Zum ersten Testen genügt auch ein Empfänger, den man mittels seriellem Monitor überwacht.
Start ESP-NOW receiver
ESP Board MAC Address: 8C:AA:B5:8C:1C:20
AP Config Success. Broadcasting with AP: Slave:8C:AA:B5:8C:1C:20
AP MAC: 8C:AA:B5:8C:1C:21
ESPNow Init Success
- Das ESP32-Senderprogramm auf den ESP32-Sender übertragen. Nach Reset sollte im seriellen Monitor das Finden mehrerer Access Points gemeldet werden, wobei nur die MAC-Adressen der ESP32-Empfänger wegen der mit "Slave" beginnenden SSID akzeptiert werden. Die Suche erfolgt 5 Sekunden lang nach Reset oder wenn der Taster an paarPin gedrückt wird.
Sketchname: ESP-NOW_one_to_many_sender_server.ino
Build: Fri May 07 10:30:02 2021 IDE: 1.8.13Verbinde dich mit dem Netzwerk "Esp32AP"
Gib die IP 192.168.4.1 im Browser einMac Adresse des AP = A4:CF:12:9A:14:A1
Broadcast IP des AP = 192.168.4.255
ESP-NOW
STA MAC: A4:CF:12:9A:14:A0
ESPNow Init Success
HTTP Server gestartetFound 4 devices
1: Slave:8C:AA:B5:8C:1C:20 [8C:AA:B5:8C:1C:21] (-28)
2: Slave:F0:08:D1:D1:C5:B8 [F0:08:D1:D1:C5:B9] (-36)
3: Slave:24:0A:C4:59:BE:F8 [24:0A:C4:59:BE:F9] (-37)
4: Slave:F0:08:D1:D2:4B:C4 [F0:08:D1:D2:4B:C5] (-52)
4 Slave(s) found, processing..
Processing: 8C:AA:B5:8C:1C:21 Status: Pair success
Processing: F0:8:D1:D1:C5:B9 Status: Pair success
Processing: 24:A:C4:59:BE:F9 Status: Pair success
Processing: F0:8:D1:D2:4B:C5 Status: Pair success
- Das Programm für die Renn-PC-Simulation auf den UNO (oder so) übertragen. GND verbinden ebenso wie TX vom UNO mit RX (GPIO16) vom ESP32-Sender.
Nun sollte der UNO jede Sekunde ein Datenpaket abschicken, der ESP32-Sender dieses verarbeiten und in kleinen Abschnitten bis zum Semikolon per ESP-NOW an alle erkannten ESP-NOW-Empfänger verschicken, die die Rennzeit anzeigen. Bei der Datenübertragung sollte eine LED an GPIO13 flackern. - Läppi oder Händi mit dem Netzwerk (Access Point) des ESP-Senders verbinden, so wie man das auch mit dem Router macht.
- Browser starten und "http://192.168.4.1/rennen.html" eingeben. Die im SPIFFS gespeicherte HTML-Datei wird angezeigt und holt sich mittels JSON aktuelle Renndaten. Die Anzeige sollte wie folgt aussehen:
- Die LED-Matrix zeigt nun die übertragenen Zeiten.
Mein Testaufbau mit einem Empfänger und einer LED-Matrix:
Schlußbemerkungen:
- Dieses Thema beschreibt kein fertiges, ausgereiftes Projekt. Ich habe schließlich keine Rennbahn. Mir geht es um die grundsätzliche Möglichkeit, ESP-NOW mit einer HTML-Seite vom Webserverseite zu kombinieren.
- Im Originalthema war es so gewünscht, aber ob ein dynamisches Einlesen der MAC-Adressen vor jedem Rennen notwendig ist, bezweifle ich. Eine Liste der möglichen MAC-Adressen wäre mir sympathischer, so wie ich es im Originalthema auch vorgeschlagen hatte.
- Das Zeitverhalten wäre noch kritisch zu prüfen. Es wäre viel effektiver, nur die sich ändernden Zeiten zu übertragen, da sich die Fahrernamen während des Rennens nicht ändern dürften.
Ich habe mit diesem Thema eine Menge gelernt und hoffe, der viele Text kann irgendjemandem nützen.