Anleitung: ESP32 mit ESP-NOW als Sender und Webserver als Access Point

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.

grafik

Nach dem Upload aller notwendigen Dateien sieht die Anzeige dann so aus:

grafik

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:

grafik

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>&nbsp;</td><td>Rundenzeit</td><td>&nbsp;</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 ? '&#9660;' : '&#9650;'} 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:

  1. 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

  1. 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.13

Verbinde dich mit dem Netzwerk "Esp32AP"
Gib die IP 192.168.4.1 im Browser ein

Mac 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 gestartet

Found 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

  1. 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.
  2. Läppi oder Händi mit dem Netzwerk (Access Point) des ESP-Senders verbinden, so wie man das auch mit dem Router macht.
    grafik
  3. 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:
  4. Die LED-Matrix zeigt nun die übertragenen Zeiten.

Mein Testaufbau mit einem Empfänger und einer LED-Matrix:

Schlußbemerkungen:

  1. 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.
  2. 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.
  3. 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.

Hallo Agmue,

wow umfangreiches Projekt und eine Menge code

Das hört sich so an als ob du es ausprobierst ob es geht.
Schlussfolgere ich jetzt aus den weiteren Bildern richtig dass es funktioniert
ESP-NOW und parallel zu "aktivem " ESP-NOW eine Anbindung an einen Router über WiFi laufen zu lassen?

viele Grüße Stefan

Eigentlich hatte ich das nicht so empfunden, weil ich mich bei den Tabs von Fips bedienen konnte, aber um sowas dann für andere zu dokumentieren, muß man dann doch ein paar Zeilen mehr schreiben. Das ist mir allerdings erst beim Machen aufgefallen.

Nur theoretisch kann ich sowas nicht, das muß ich schon praktisch probieren.

Deine Schlußfolgerung ist richtig: Ja, es geht, Screenprint und Foto zeigen das!

Spannend war, ob WiFi.mode(WIFI_AP_STA); tut, was ich mir vorgestellt hatte. "STA" für ESP-NOW und "AP" für die Anbindung an Läppi oder Händi. Genau so ein Beispiel mit "WIFI_AP_STA" und one-to-many konnte ich im WWW nicht finden, was für mich Motivation war, etwas darüber zu schreiben.

Hier die übrigen drei Empfänger, die aber nur eine flackernde LED zeigen:

Hi agmue,
ist schon ein interessantes Thema. Ich habe mich da eben ein wenig durchgewuselt. Aktuell nur oberflächlich. Mein Tipp:

Kannst du es nicht in der Rubrik "Geile Projekte" verlinken, dann kann man es scheller wiederfinden.

Bitte gerne :white_check_mark:

Vielen Dank....

@agmue Ich möchte mich zutiefst vor dir verneigen und ganz herzlich bedanken, mit soviel Einsatz wie du ihn in dem Projekt gezeigt und auch umgesetzt hast. Leider ist meine Zeit die ich mit dem neuen Hobby verbringen kann sehr begrenzt. Die Vorbereitungen laufen parallel zu den NRF24. Ich werde in den nächsten Tagen mal den ESP´s deinen Sketch übertragen und berichten ob diese auch so unempfindlich sind wie die NRF´S. Da ich auch @my_xy_projekt möchte ich nicht vor den Kopf stoßen wollte und auch nicht möchte....den auch er hat maßstäblich zum Erfolg beigetragen.

Vielen vielen Dank an euch alle!


Ich habe was gelernt, das ist doch prima!

Bei einem Hobby ist das so und muß das auch so sein, finde ich. Nur kein Streß!

Steckbretter eignen sich nicht für den Dauereinsatz, da korrodieren die Kontakte, Wackelkontakte sind die Folge.

Da freue ich mich drauf :slightly_smiling_face:

Hallo @agmue,
Ohh je, da rächt sich mein Anfängerwissen schon wieder :slight_smile: Bin gerade dabei und wollte den Sender schon mal übertragen.... Nu verstehe ich nicht wie und was ich mit den Tab´s machen muss. Bist du vielleicht so nett und kannst das für Dummies wie mich erklären?
Vielen Dank im voraus.

Nichts. Befinden sich mehrere *.ino Dateien im Verzeichnis, zeigt die IDE diese in Tabs, früher Karteikartenreiter genannt, an. Das Zusammenbasteln macht dann die IDE, was meist funktioniert.

Falls es bei den Tabs Reihenfolgeabhängigkeiten geben sollte, kann man im Hinterkopf behalten, dass die Tabs (außer dem Haupttab) in alphabetischer Reihenfolge beabeitet werden.

Gruß Tommy

Nun ja, wer lesen kann ist klar im Vorteil. Hab´s dann gestern doch noch hinbekommen. Wenn´s einem in den Finger kribbelt .... :slight_smile:

@Tommy56
Danke für den Tipp, halte ich im Hinterkopf, für den Fall das ich irgendwann vielleicht auchmal so große Projekte selber habe bzw. selber schreibe.

Der Aufbau ist auch nur für Testzwecke, halt ordentlich verkabelt um es durch die Gegend tragen zu können ohne das etwas auseinander fällt.

Hier ein kleiner Vorbericht, der zusammenbau des Sketches war zunächst für mich als Anfänger durch Nichtwissen was Tab´s sind und wie diese erstellt werden zunächst Haare raufend. Mein Ehrgeiz es auszuprobieren lies mich so einiges lesen und verstehen, somit lief gestern Abend bereit der AP und die Renndaten standen zu später Stunde auf meinem Handy. :slight_smile:

Der heutige Nachmittag lies mich 2 Empfänger in Betrieb nehmen und siehe da, die Daten würden auch hier angezeigt.Das automatische verbinden funktioniert grandios.Dabei ist es vollkommen egal was ich zuerst einschalte.Die Geschwindigkeit zu den Empfängern mit der Matrix-Anzeige perfekt, keine Aussetzer oder verschlucken von Rundenzeiten selbst bei einer Datenfolge <100ms .

Anders sieht es beim Handy aus, hier werden gern mal Rundenzeiten übersprungen bis hin zum ganzen fehlen der Rundenzeit. Die Darstellung des grünen Menü´s mit Bottom funktioniert bis jetzt bei mir noch nicht. Vielleicht habe ich da beim kopieren was vergessen, werde es die Tage noch mal probieren.

Resultat bis Dato: Absolut geiles Projekt in Funktion und Ausführung !!!
Danke agmue

Das ist bei mir genau so. Ich habe da nicht weiter geforscht, da Du ja NRF24 nehmen möchtest.

Was meinst Du damit?

Nun, wenn ich sehe was mit dem ESP geht, bin ich wieder hin und her gerissen..... Wie sieht es den hier mit den Slave´s aus ? ich habe mal gelesen das hier nur 6 Stück gehen würden, stimmt das?

Na ja, etaws grünes hat es ja.... :wink:
Grünes Menü

So, für mich war es für heute, muss mal heute früher ins Bett. :wink:
Gut Nächtle euch allen, und bleibt gesund.

Ach so, die SPIFFS-Seite braucht Stylesheets für das Grün :slightly_smiling_face:

Wie schon früher geschrieben, bleibe bei NRF24. Wenn das gut funktioniert, kannst Du nochmal über die zusätzliche Anzeige auf Händis nachdenken. Es bliebe ja auch noch auszuprobieren, was passiert, wenn viele Zuschauer ihr Händi zücken. Wäre ja blöd, wenn die die Anzeige auf der Rennstrecke in die Knie zwingen. Daher getrennte Systeme!

Das habe ich auch gelesen.

Aus der Sicht hast Du vollkommen recht, finde es halt schade das Deine ganze Arbeit dabei auf der Strecke bleibt. Nun ja zumindest wird später ein Teil davon benutzt wofür ich Dir heute schon unendlich dankbar bin.

Auch wenn ich es so komplett nicht nutzen kann. Geiles Projekt was Du da gezaubert hast.
Hut ab !!!

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.