From 2d2ee0a41a071a08b0265e0e1ea196ab94497eb8 Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Sun, 1 Jun 2025 11:51:02 +0200 Subject: [PATCH] first commit --- .gitignore | 5 + .vscode/extensions.json | 10 + .vscode/settings.json | 15 + TODO.md | 18 + apientpoints | 47 ++ data/about.html | 480 +++++++++++++++++++ data/index.html | 453 ++++++++++++++++++ data/logo.png | Bin 0 -> 7685 bytes data/settings.html | 998 ++++++++++++++++++++++++++++++++++++++++ include/README | 37 ++ lib/README | 46 ++ platformio.ini | 55 +++ src/communication.h | 44 ++ src/debug.h | 41 ++ src/licenceing.h | 106 +++++ src/master.cpp | 331 +++++++++++++ src/master.h | 64 +++ src/statusled.h | 54 +++ src/timesync.h | 198 ++++++++ src/webserverrouter.h | 169 +++++++ src/wificlass.h | 59 +++ test/README | 11 + 22 files changed, 3241 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 TODO.md create mode 100644 apientpoints create mode 100644 data/about.html create mode 100644 data/index.html create mode 100644 data/logo.png create mode 100644 data/settings.html create mode 100644 include/README create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 src/communication.h create mode 100644 src/debug.h create mode 100644 src/licenceing.h create mode 100644 src/master.cpp create mode 100644 src/master.h create mode 100644 src/statusled.h create mode 100644 src/timesync.h create mode 100644 src/webserverrouter.h create mode 100644 src/wificlass.h create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..981728e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "files.associations": { + "functional": "cpp", + "array": "cpp", + "deque": "cpp", + "list": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "string_view": "cpp", + "initializer_list": "cpp", + "regex": "cpp" + } +} \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..a18f71c --- /dev/null +++ b/TODO.md @@ -0,0 +1,18 @@ +- KOMPLETTES Messageing System auf MQTT Server und Broker umstellen. +- Testen wie zuverlässig das ist + + +- message system überarbeiten. Sehr unzuverlässig mit dem peering +- Uhrzeit abfragen (Eingabe der Zeitzone) DONE +- Sync der Buttons mit echtzeit +- implementierung einer RTC + +v2.0 +- ADD Hotspot Manager to connect to a Wifi +- ADD Licence Management (generate a programm where i can generate keys that get checked agains a private seed in the firmware) +enables the Wifimanager to connect DONE + +- ADD option point for location (read from online table and select the location via dropdown) +- ADD option to enter a name, age +- ADD upload to a Online Database () + diff --git a/apientpoints b/apientpoints new file mode 100644 index 0000000..9e32638 --- /dev/null +++ b/apientpoints @@ -0,0 +1,47 @@ +API-Routen Übersicht +GET /api/data +→ Gibt den aktuellen Timer-Status und Zeiten zurück + +POST /api/reset-best +→ Setzt die besten Zeiten zurück + +POST /api/unlearn-button +→ Verlernt alle Button-Zuordnungen + +POST /api/set-max-time +→ Setzt die maximale Zeit und maxTimeDisplay + +GET /api/get-settings +→ Gibt die aktuellen Einstellungen zurück + +POST /api/start-learning +→ Startet den Anlernmodus + +POST /api/stop-learning +→ Beendet den Anlernmodus + +GET /api/learn/status +→ Gibt den Status des Anlernmodus zurück + +GET /api/buttons/status +→ Gibt den Status der Button-Zuordnungen zurück + +GET /api/info +→ Systeminformationen (IP, MAC, Speicher, verbundene Buttons) + +(aus timesync.h) + +GET /api/time +→ Gibt die aktuelle Systemzeit zurück +POST /api/set-time +→ Setzt die Systemzeit +(aus licenceing.h) + +GET /api/get-licence +→ Gibt den gespeicherten Lizenzschlüssel zurück +POST /api/set-licence +→ Speichert einen neuen Lizenzschlüssel +Statische Dateien: + +/ → index.html +/settings → settings.html \ No newline at end of file diff --git a/data/about.html b/data/about.html new file mode 100644 index 0000000..7b49b14 --- /dev/null +++ b/data/about.html @@ -0,0 +1,480 @@ + + + + + + Über NinjaCross Timer + + + + + + ← Zurück zum Timer + + +
+

🏊‍♀️ Über NinjaCross Timer

+

Der professionelle Zeitmesser für Ninjacross Wettkämpfe

+
+ +
+
+

🎯 Was ist NinjaCross?

+

+ NinjaCross ist ein aufregender Wassersport, der Geschwindigkeit, Technik und Athletik kombiniert. + Teilnehmer durchqueren Schwimmbahnen mit verschiedenen Hindernissen und Herausforderungen, + wobei Zeit und Präzision entscheidend sind. +

+

+ Unser Timer-System wurde speziell entwickelt, um professionelle Wettkämpfe zu unterstützen + und präzise Zeitmessungen für bis zu zwei Bahnen gleichzeitig zu ermöglichen. +

+
+ +
+

⚡ Funktionen

+
+
+

🎲 Dual-Timer

+

Gleichzeitige Zeitmessung für zwei Bahnen mit präziser Synchronisation

+
+
+

📱 Responsive Design

+

Optimiert für alle Geräte - Desktop, Tablet und Smartphone

+
+
+

🏆 Bestzeiten

+

Automatische Verfolgung und Anzeige der besten Tageszeiten

+
+
+

📚 Lernmodus

+

Interaktiver Modus für Training und Schulungszwecke

+
+
+

⚙️ Einfache Bedienung

+

Intuitive Benutzeroberfläche für schnelle und fehlerfreie Bedienung

+
+
+

🔄 Live-Sync

+

Echtzeitaktualisierung aller Timer-Daten über Backend-Integration

+
+
+
+ +
+

📊 Technische Spezifikationen

+
+
+ 0.01s + Präzision +
+
+ 2 + Bahnen +
+
+ 50ms + Update-Rate +
+
+ 100% + Responsive +
+
+ +

🔧 Technologie-Stack

+
    +
  • Frontend: HTML5, CSS3, Vanilla JavaScript
  • +
  • Design: Responsive Grid Layout, Glassmorphism
  • +
  • Performance: Optimierte Render-Zyklen, Smooth Animations
  • +
  • Kompatibilität: Alle modernen Browser, Mobile-First
  • +
+
+ +
+

👥 Entwicklung

+
+

🚀 Entwickelt mit ❤️ von Carsten Graf

+

+ Dieses Projekt wurde mit Leidenschaft für den NinjaCross-Sport entwickelt, + um Wettkämpfe professioneller und spannender zu gestalten. +

+
+
+ +
+

🎮 Bedienung

+

Grundfunktionen

+
    +
  • Timer starten: Automatische Synchronisation mit Backend-System
  • +
  • Live-Anzeige: Echtzeitaktualisierung aller Zeiten und Status
  • +
  • Bestzeiten: Automatische Speicherung der Tagesrekorde
  • +
  • Lernmodus: Interaktive Anweisungen für neue Benutzer
  • +
+ +

Status-Anzeigen

+
    +
  • Bereit (Blau): Timer ist startbereit
  • +
  • Läuft (Grün): Aktive Zeitmessung mit Pulsation
  • +
  • Beendet (Rot): Zeitmessung abgeschlossen
  • +
+
+ +
+
+

🏁 Bereit für den Wettkampf?

+

+ Starten Sie jetzt mit dem professionellen NinjaCross Timer + und erleben Sie präzise Zeitmessung auf höchstem Niveau! +

+ Timer starten 🚀 +
+
+
+ + + + \ No newline at end of file diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..172b663 --- /dev/null +++ b/data/index.html @@ -0,0 +1,453 @@ + + + + + + NinjaCross Timer + + + + + + ⚙️ + +
+

🏊‍♀️ NinjaCross Timer

+

Professioneller Zeitmesser für Ninjacross Wettkämpfe

+
+ + + +
+
+

🏊‍♀️ Bahn 1

+
00.00
+
Bereit
+
+ +
+

🏊‍♂️ Bahn 2

+
00.00
+
Bereit
+
+
+ +
+

🏆 Beste Zeiten des Tages

+
+ Bahn 1: + --.- +
+
+ Bahn 2: + --.- +
+
+ + + + \ No newline at end of file diff --git a/data/logo.png b/data/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b96bc2ae08f81f608588977253c01875026b6c0c GIT binary patch literal 7685 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 + + + + + Ninjacross Timer - Einstellungen + + + +
+
+

⏱️ Ninjacross Timer

+

Einstellungen & Konfiguration

+
+ +
+ + +
+ +
+

🕐 Datum & Uhrzeit

+
+ Aktuelle Zeit: Laden... +
+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ +
+

🔧 Grundeinstellungen

+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+

🏆 Zeiten verwalten

+
+ +
+
+ +
+

📡 Button-Konfiguration

+
+ + + +
+ +
+

🎯 Anlernmodus aktiv

+

+ Drücken Sie jetzt den Button für: Bahn 1 Start +

+ +
+
+ +
+

🔄 OTA Update

+ +
+ +
+
+ +
+

ℹ️ System-Information

+
+
IP-Adresse: Laden...
+
Kanal: Laden...
+
MAC-Adresse: Laden...
+
Freier Speicher: Laden...
+
Verbundene Buttons: Laden...
+
Lizenz gültig: Laden...
+
Lizenz Level: Laden...
+
+
+ +
+

🔧 Lizenz

+
+
+ + +
+
+ +
+
+
+
+
+ + + + diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..49d0090 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,55 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = esp32dev + +[env] +platform = https://github.com/platformio/platform-espressif32.git +framework = arduino +lib_deps = esp32async/ESPAsyncWebServer@^3.7.7 +lib_compat_mode = strict + +[env:wemos_d1_mini32] +board = wemos_d1_mini32 +monitor_speed = 115200 +lib_deps = + bblanchon/ArduinoJson@^7.4.1 + esp32async/ESPAsyncWebServer@^3.7.7 + lostincompilation/PrettyOTA@^1.1.3 + esp32async/AsyncTCP@^3.4.2 + mlesniew/PicoMQTT@^1.3.0 + +[env:wemos_d1_mini32_OTA] +board = wemos_d1_mini32 +monitor_speed = 115200 +lib_deps = + bblanchon/ArduinoJson@^7.4.1 + esp32async/ESPAsyncWebServer@^3.7.7 + lostincompilation/PrettyOTA@^1.1.3 + esp32async/AsyncTCP@^3.4.2 + mlesniew/PicoMQTT@^1.3.0 +upload_protocol = espota +upload_port = 192.168.1.94 + +[env:esp32dev] +board = esp32dev +monitor_speed = 115200 +build_flags = + -DBOARD_HAS_PSRAM + -mfix-esp32-psram-cache-issue +targets = uploadfs +board_build.psram = disabled +lib_deps = + bblanchon/ArduinoJson@^7.4.1 + esp32async/ESPAsyncWebServer@^3.7.7 + lostincompilation/PrettyOTA@^1.1.3 + esp32async/AsyncTCP@^3.4.2 + mlesniew/PicoMQTT@^1.3.0 diff --git a/src/communication.h b/src/communication.h new file mode 100644 index 0000000..b631005 --- /dev/null +++ b/src/communication.h @@ -0,0 +1,44 @@ +#include +#include "master.h" + +#include + +#include +#include "timesync.h" + + +// Datenstruktur für ESP-NOW Nachrichten +// Datenstruktur für ESP-NOW Nachrichten +typedef struct { + uint8_t messageType; + uint8_t buttonId; + int buttonPressed; + uint32_t timestamp; + char messageId[33]; // 32 hex chars + null terminator for 128-bit ID +} ButtonMessage; + +PicoMQTT::Server mqtt; + + +void setupMqttServer() { + // Set up the MQTT server with the desired port + // Subscribe to a topic pattern and attach a callback + mqtt.subscribe("#", [](const char * topic, const char * payload) { + Serial.printf("Received message in topic '%s': %s\n", topic, payload); + }); + + // Start the MQTT server + mqtt.begin(); +} + +void loopMqttServer() { + // Handle incoming MQTT messages + mqtt.loop(); + + // Optionally, you can publish a message periodically + static unsigned long lastPublish = 0; + if (millis() - lastPublish > 5000) { // Publish every 5 seconds + mqtt.publish("test/topic", "Hello from ESP32!"); + lastPublish = millis(); + } +} diff --git a/src/debug.h b/src/debug.h new file mode 100644 index 0000000..508aff5 --- /dev/null +++ b/src/debug.h @@ -0,0 +1,41 @@ +// Zeit-bezogene Variablen und Includes +#pragma once +#include +#include +#include +#include +#include +#include + + + +void setupDebugAPI(AsyncWebServer& server); + + +void setupDebugAPI(AsyncWebServer& server) { + +//DEBUG +server.on("/api/debug/start1", HTTP_GET, [](AsyncWebServerRequest *request){ + handleStart1(); + request->send(200, "text/plain", "handleStart1() called"); +}); + +server.on("/api/debug/stop1", HTTP_GET, [](AsyncWebServerRequest *request){ + handleStop1(); + request->send(200, "text/plain", "handleStop1() called"); +}); + +server.on("/api/debug/start2", HTTP_GET, [](AsyncWebServerRequest *request){ + handleStart2(); + request->send(200, "text/plain", "handleStart2() called"); +}); + +server.on("/api/debug/stop2", HTTP_GET, [](AsyncWebServerRequest *request){ + handleStop2(); + request->send(200, "text/plain", "handleStop2() called"); +}); + + +Serial.println("Debug-API initialisiert"); +} +//DEBUG END \ No newline at end of file diff --git a/src/licenceing.h b/src/licenceing.h new file mode 100644 index 0000000..d06f0da --- /dev/null +++ b/src/licenceing.h @@ -0,0 +1,106 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "mbedtls/md.h" + +const char* secret = "542ff224606c61fb3024e22f76ef9ac8"; + +// Preferences für persistente Speicherung +Preferences preferences; + +String licence; + +//Prototype für Funktionen +String getUniqueDeviceID(); +String hmacSHA256(const String& key, const String& message); +bool checkLicense(const String& deviceID, const String& licenseKey); +void setupLicenceAPI(AsyncWebServer& server); +void saveLicenceToPrefs(); +void loadLicenceFromPrefs(); + + +String getUniqueDeviceID() { + uint8_t mac[6]; + esp_wifi_get_mac(WIFI_IF_STA, mac); // Use STA MAC for uniqueness + char id[13]; + sprintf(id, "%02X%02X%02X%02X%02X%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return String(id); +} + +String hmacSHA256(const String& key, const String& message) { + byte hmacResult[32]; + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + + mbedtls_md_init(&ctx); + const mbedtls_md_info_t* md_info = mbedtls_md_info_from_type(md_type); + mbedtls_md_setup(&ctx, md_info, 1); + mbedtls_md_hmac_starts(&ctx, (const unsigned char*)key.c_str(), key.length()); + mbedtls_md_hmac_update(&ctx, (const unsigned char*)message.c_str(), message.length()); + mbedtls_md_hmac_finish(&ctx, hmacResult); + mbedtls_md_free(&ctx); + + String result = ""; + for (int i = 0; i < 32; i++) { + char buf[3]; + sprintf(buf, "%02X", hmacResult[i]); + result += buf; + } + return result; +} + +int getLicenseTier(const String& deviceID, const String& licenseKey) { + for (int tier = 1; tier <= 4; ++tier) { + String data = deviceID + ":" + String(tier); + String expected = hmacSHA256(secret, data); + if (licenseKey.equalsIgnoreCase(expected)) { + return tier; // Found matching tier + } + } + return 0; // No valid tier found +} + +void setupLicenceAPI(AsyncWebServer& server) { + + server.on("/api/get-licence", HTTP_GET, [](AsyncWebServerRequest *request){ + Serial.println("Received request to get licence"); + loadLicenceFromPrefs(); + String deviceID = getUniqueDeviceID(); + int tier = getLicenseTier(deviceID, licence); + String json = "{\"licence\":\"" + licence + "\"," + "\"valid\":" + String(tier > 0 ? "true" : "false") + + ",\"tier\":" + String(tier) + "}"; + request->send(200, "application/json", json); + }); + + server.on("/api/set-licence", HTTP_POST, [](AsyncWebServerRequest *request){ + Serial.println("Received request to set licence"); + if (request->hasParam("licence", true)) { + licence = request->getParam("licence", true)->value(); + Serial.println("Received request to set licence " + licence); + saveLicenceToPrefs(); // eigene Funktion + request->send(200, "application/json", "{\"success\":true}"); + } else { + request->send(400, "application/json", "{\"success\":false}"); + } + }); + + Serial.println("Licence API setup complete"); +} + +void saveLicenceToPrefs() { + preferences.begin("key", false); + preferences.putString("key", licence); + preferences.end(); +} + +void loadLicenceFromPrefs() { + preferences.begin("key", true); + licence = preferences.getString("key", ""); + preferences.end(); +} \ No newline at end of file diff --git a/src/master.cpp b/src/master.cpp new file mode 100644 index 0000000..c00f3dd --- /dev/null +++ b/src/master.cpp @@ -0,0 +1,331 @@ +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +#include +#include "master.h" +// Aquacross Timer - ESP32 Master (Webserver + ESP-NOW + Anlernmodus) +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +const char* firmwareversion = "1.0.0"; // Version der Firmware + + + +void handleLearningMode(const uint8_t* mac) { + // Prüfen ob MAC bereits einem anderen Button zugewiesen ist + if (buttonConfigs.start1.isAssigned && memcmp(buttonConfigs.start1.mac, mac, 6) == 0) { + Serial.println("Diese MAC ist bereits zugewiesen - wird ignoriert"); + return; + } + if (buttonConfigs.stop1.isAssigned && memcmp(buttonConfigs.stop1.mac, mac, 6) == 0) { + Serial.println("Diese MAC ist bereits zugewiesen - wird ignoriert"); + return; + } + if (buttonConfigs.start2.isAssigned && memcmp(buttonConfigs.start2.mac, mac, 6) == 0) { + Serial.println("Diese MAC ist bereits zugewiesen - wird ignoriert"); + return; + } + if (buttonConfigs.stop2.isAssigned && memcmp(buttonConfigs.stop2.mac, mac, 6) == 0) { + Serial.println("Diese MAC ist bereits zugewiesen - wird ignoriert"); + return; + } + + // MAC ist noch nicht zugewiesen, normal fortfahren + switch(learningStep) { + case 0: // Start1 + memcpy(buttonConfigs.start1.mac, mac, 6); + buttonConfigs.start1.isAssigned = true; + Serial.println("Start1 Button zugewiesen"); + break; + case 1: // Stop1 + memcpy(buttonConfigs.stop1.mac, mac, 6); + buttonConfigs.stop1.isAssigned = true; + Serial.println("Stop1 Button zugewiesen"); + break; + case 2: // Start2 + memcpy(buttonConfigs.start2.mac, mac, 6); + buttonConfigs.start2.isAssigned = true; + Serial.println("Start2 Button zugewiesen"); + break; + case 3: // Stop2 + memcpy(buttonConfigs.stop2.mac, mac, 6); + buttonConfigs.stop2.isAssigned = true; + Serial.println("Stop2 Button zugewiesen"); + break; + } + + learningStep++; + if (learningStep >= 4) { + learningMode = false; + learningStep = 0; + saveButtonConfig(); + Serial.println("Lernmodus beendet!"); + } +} + +void handleStartLearning() { + learningMode = true; + + // Count assigned buttons and set appropriate learning step + int assignedButtons = 0; + if (buttonConfigs.start1.isAssigned) assignedButtons++; + if (buttonConfigs.stop1.isAssigned) assignedButtons++; + if (buttonConfigs.start2.isAssigned) assignedButtons++; + if (buttonConfigs.stop2.isAssigned) assignedButtons++; + + learningStep = assignedButtons; + + Serial.printf("Learning mode started - %d buttons already assigned, continuing at step %d\n", + assignedButtons, learningStep); +} + +void handleLearningStatus() { + DynamicJsonDocument doc(256); + doc["active"] = learningMode; + doc["step"] = learningStep; + + String response; + serializeJson(doc, response); +} + +void unlearnButton() { + + memset(buttonConfigs.start1.mac, 0, 6); + buttonConfigs.start1.isAssigned = false; + memset(buttonConfigs.stop1.mac, 0, 6); + buttonConfigs.stop1.isAssigned = false; + memset(buttonConfigs.start2.mac, 0, 6); + buttonConfigs.start2.isAssigned = false; + memset(buttonConfigs.stop2.mac, 0, 6); + buttonConfigs.stop2.isAssigned = false; + + saveButtonConfig(); + Serial.println("Buttons wurden verlernt."); +} + +void handleStart1() { + if (!timerData.isRunning1) { + timerData.startTime1 = millis(); + timerData.isRunning1 = true; + timerData.endTime1 = 0; + Serial.println("Bahn 1 gestartet"); + } +} + +void handleStop1() { + if (timerData.isRunning1) { + timerData.endTime1 = millis(); + timerData.isRunning1 = false; + unsigned long currentTime = timerData.endTime1 - timerData.startTime1; + + if (timerData.bestTime1 == 0 || currentTime < timerData.bestTime1) { + timerData.bestTime1 = currentTime; + saveBestTimes(); + } + timerData.finishedSince1 = millis(); + Serial.println("Bahn 1 gestoppt - Zeit: " + String(currentTime/1000.0) + "s"); + } +} + +void handleStart2() { + if (!timerData.isRunning2) { + timerData.startTime2 = millis(); + timerData.isRunning2 = true; + timerData.endTime2 = 0; + Serial.println("Bahn 2 gestartet"); + } +} + +void handleStop2() { + if (timerData.isRunning2) { + timerData.endTime2 = millis(); + timerData.isRunning2 = false; + unsigned long currentTime = timerData.endTime2 - timerData.startTime2; + + if (timerData.bestTime2 == 0 || currentTime < timerData.bestTime2) { + timerData.bestTime2 = currentTime; + saveBestTimes(); + } + timerData.finishedSince2 = millis(); + Serial.println("Bahn 2 gestoppt - Zeit: " + String(currentTime/1000.0) + "s"); + } +} + +void checkAutoReset() { + unsigned long currentTime = millis(); + + if (timerData.isRunning1 && (currentTime - timerData.startTime1 > maxTimeBeforeReset)) { + timerData.isRunning1 = false; + timerData.startTime1 = 0; + Serial.println("Bahn 1 automatisch zurückgesetzt"); + } + + if (timerData.isRunning2 && (currentTime - timerData.startTime2 > maxTimeBeforeReset)) { + timerData.isRunning2 = false; + timerData.startTime2 = 0; + Serial.println("Bahn 2 automatisch zurückgesetzt"); + } + + // Automatischer Reset nach 10 Sekunden "Beendet" + if (!timerData.isRunning1 && timerData.endTime1 > 0 && timerData.finishedSince1 > 0) { + if (currentTime - timerData.finishedSince1 > maxTimeDisplay) { + timerData.startTime1 = 0; + timerData.endTime1 = 0; + timerData.finishedSince1 = 0; + Serial.println("Bahn 1 automatisch auf 'Bereit' zurückgesetzt"); + } + } + + if (!timerData.isRunning2 && timerData.endTime2 > 0 && timerData.finishedSince2 > 0) { + if (currentTime - timerData.finishedSince2 > maxTimeDisplay) { + timerData.startTime2 = 0; + timerData.endTime2 = 0; + timerData.finishedSince2 = 0; + Serial.println("Bahn 2 automatisch auf 'Bereit' zurückgesetzt"); + } + } +} + +void saveButtonConfig() { + preferences.begin("buttons", false); + preferences.putBytes("config", &buttonConfigs, sizeof(buttonConfigs)); + preferences.end(); +} + +void loadButtonConfig() { + preferences.begin("buttons", true); + size_t schLen = preferences.getBytesLength("config"); + if (schLen == sizeof(buttonConfigs)) { + preferences.getBytes("config", &buttonConfigs, schLen); + } + preferences.end(); +} + +void saveBestTimes() { + preferences.begin("times", false); + preferences.putULong("best1", timerData.bestTime1); + preferences.putULong("best2", timerData.bestTime2); + preferences.end(); +} + +void loadBestTimes() { + preferences.begin("times", true); + timerData.bestTime1 = preferences.getULong("best1", 0); + timerData.bestTime2 = preferences.getULong("best2", 0); + preferences.end(); +} + +void saveSettings() { + preferences.begin("settings", false); + preferences.putULong("maxTime", maxTimeBeforeReset); + preferences.putULong("maxTimeDisplay", maxTimeDisplay); + preferences.end(); +} + +void loadSettings() { + preferences.begin("settings", true); + maxTimeBeforeReset = preferences.getULong("maxTime", 300000); + maxTimeDisplay = preferences.getULong("maxTimeDisplay", 20000); + preferences.end(); +} + +int checkLicence() { + loadLicenceFromPrefs(); + String id = getUniqueDeviceID(); + int tier = getLicenseTier(id, licence); // licence = stored or entered key + return tier; +} + +String getTimerDataJSON() { + DynamicJsonDocument doc(1024); + + unsigned long currentTime = millis(); + + // Bahn 1 + if (timerData.isRunning1) { + doc["time1"] = (currentTime - timerData.startTime1) / 1000.0; + doc["status1"] = "running"; + } else if (timerData.endTime1 > 0) { + doc["time1"] = (timerData.endTime1 - timerData.startTime1) / 1000.0; + doc["status1"] = "finished"; + } else { + doc["time1"] = 0; + doc["status1"] = "ready"; + } + + // Bahn 2 + if (timerData.isRunning2) { + doc["time2"] = (currentTime - timerData.startTime2) / 1000.0; + doc["status2"] = "running"; + } else if (timerData.endTime2 > 0) { + doc["time2"] = (timerData.endTime2 - timerData.startTime2) / 1000.0; + doc["status2"] = "finished"; + } else { + doc["time2"] = 0; + doc["status2"] = "ready"; + } + + // Beste Zeiten + doc["best1"] = timerData.bestTime1 / 1000.0; + doc["best2"] = timerData.bestTime2 / 1000.0; + + // Lernmodus + doc["learningMode"] = learningMode; + if (learningMode) { + String buttons[] = {"Start Bahn 1", "Stop Bahn 1", "Start Bahn 2", "Stop Bahn 2"}; + doc["learningButton"] = buttons[learningStep]; + } + + String result; + serializeJson(doc, result); + return result; +} + + +void setup() { + Serial.begin(115200); + + if (!SPIFFS.begin(true)) { + Serial.println("SPIFFS Mount Failed"); + return; + } + + //setup external libararies + setupTimeAPI(server); + setupLicenceAPI(server); + setupDebugAPI(server); + + + // Gespeicherte Daten laden + loadButtonConfig(); + loadBestTimes(); + loadSettings(); + + setupWifi(); // WiFi initialisieren + setupOTA(&server); + + setupRoutes(); + + setupMqttServer(); // MQTT Server initialisieren + + +} + +void loop() { + checkAutoReset(); + loopMqttServer(); // MQTT Server in der Loop aufrufen + delay(100); +} diff --git a/src/master.h b/src/master.h new file mode 100644 index 0000000..839788b --- /dev/null +++ b/src/master.h @@ -0,0 +1,64 @@ +#pragma once +#include +#include +#include +#include +#include + + +// Timer Struktur +struct TimerData { + unsigned long startTime1 = 0; + unsigned long startTime2 = 0; + unsigned long endTime1 = 0; + unsigned long endTime2 = 0; + unsigned long bestTime1 = 0; + unsigned long bestTime2 = 0; + bool isRunning1 = false; + bool isRunning2 = false; + unsigned long finishedSince1 = 0; + unsigned long finishedSince2 = 0; +}; + +// Button Konfiguration +struct ButtonConfig { + uint8_t mac[6]; + bool isAssigned = false; +}; + +struct ButtonConfigs { + ButtonConfig start1; + ButtonConfig stop1; + ButtonConfig start2; + ButtonConfig stop2; +}; + +extern const char* firmwareversion; + +// Globale Variablen +TimerData timerData; +ButtonConfigs buttonConfigs; +bool learningMode = false; +int learningStep = 0; // 0=Start1, 1=Stop1, 2=Start2, 3=Stop2 +unsigned long maxTimeBeforeReset = 300000; // 5 Minuten default +unsigned long maxTimeDisplay = 20000; // 20 Sekunden Standard (in ms) +bool wifimodeAP = false; // AP-Modus deaktiviert + +//Function Declarations +void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len); +void handleLearningMode(const uint8_t* mac); +void handleStartLearning(); +void handleStart1(); +void handleStop1(); +void handleStart2(); +void handleStop2(); +void checkAutoReset(); +void saveButtonConfig(); +void loadButtonConfig(); +void saveBestTimes(); +void loadBestTimes(); +void saveSettings(); +void loadSettings(); +void unlearnButton(); +int checkLicence(); +String getTimerDataJSON(); \ No newline at end of file diff --git a/src/statusled.h b/src/statusled.h new file mode 100644 index 0000000..751a2e5 --- /dev/null +++ b/src/statusled.h @@ -0,0 +1,54 @@ +#include + + +#define LED_PIN LED_BUILTIN + +// Status LED +unsigned long lastLedBlink = 0; +bool ledState = false; + + +void updateStatusLED(int blinkPattern) { + unsigned long currentTime = millis(); + + switch (blinkPattern) { + case 0: // Suche Master - Langsames Blinken + if (currentTime - lastLedBlink > 1000) { + ledState = !ledState; + digitalWrite(LED_PIN, ledState); + lastLedBlink = currentTime; + } + break; + + case 1: // Verbunden - Kurzes Blinken alle 3 Sekunden + if (currentTime - lastLedBlink > 3000) { + digitalWrite(LED_PIN, HIGH); + delay(100); + digitalWrite(LED_PIN, LOW); + lastLedBlink = currentTime; + } + break; + + case 2: // Button gedrückt - Schnelles Blinken 3x + static int blinkCount = 0; + if (currentTime - lastLedBlink > 100) { + ledState = !ledState; + digitalWrite(LED_PIN, ledState); + lastLedBlink = currentTime; + blinkCount++; + + if (blinkCount >= 6) { // 3 komplette Blinks + blinkCount = 0; + blinkPattern = 1; // Zurück zu verbunden + } + } + + case 3: // Flash bei Empfang - Einmaliges kurzes Blinken + { + digitalWrite(LED_PIN, HIGH); + delay(100); + digitalWrite(LED_PIN, LOW); + } + break; + } +} diff --git a/src/timesync.h b/src/timesync.h new file mode 100644 index 0000000..8b3c22f --- /dev/null +++ b/src/timesync.h @@ -0,0 +1,198 @@ +// Zeit-bezogene Variablen und Includes +#pragma once +#include +#include +#include +#include +#include + +// Globale Zeitvariablen +struct timeval tv; +struct timezone tz; +time_t now; +struct tm timeinfo; + +void setupTimeAPI(AsyncWebServer& server); +String getCurrentTimeJSON(); +bool setSystemTime(long timestamp); + +// Hilfsfunktionen für Zeit-Management +String getCurrentTimeJSON() { + gettimeofday(&tv, &tz); + now = tv.tv_sec; + + StaticJsonDocument<200> doc; + doc["timestamp"] = (long)now; + doc["success"] = true; + + // Zusätzliche Zeitinformationen + gmtime_r(&now, &timeinfo); + char timeStr[64]; + strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo); + doc["formatted"] = String(timeStr); + doc["year"] = timeinfo.tm_year + 1900; + doc["month"] = timeinfo.tm_mon + 1; + doc["day"] = timeinfo.tm_mday; + doc["hour"] = timeinfo.tm_hour; + doc["minute"] = timeinfo.tm_min; + doc["second"] = timeinfo.tm_sec; + + String response; + serializeJson(doc, response); + return response; +} + +bool setSystemTime(long timestamp) { + struct timeval tv; + tv.tv_sec = timestamp; + tv.tv_usec = 0; + + if (settimeofday(&tv, NULL) == 0) { + Serial.println("Zeit erfolgreich gesetzt: " + String(timestamp)); + return true; + } else { + Serial.println("Fehler beim Setzen der Zeit"); + return false; + } +} + +void setupTimeAPI(AsyncWebServer& server) { + +// API-Endpunkt: Aktuelle Zeit abrufen +server.on("/api/time", HTTP_GET, [](AsyncWebServerRequest *request){ + String response = getCurrentTimeJSON(); + request->send(200, "application/json", response); +}); + +// API-Endpunkt: Zeit setzen +server.on("/api/set-time", HTTP_POST, [](AsyncWebServerRequest *request){ + StaticJsonDocument<100> doc; + + if (request->hasParam("timestamp", true)) { + String timestampStr = request->getParam("timestamp", true)->value(); + long timestamp = timestampStr.toInt(); + + if (timestamp > 0) { + bool success = setSystemTime(timestamp); + + doc["success"] = success; + if (success) { + doc["message"] = "Zeit erfolgreich gesetzt"; + doc["timestamp"] = timestamp; + } else { + doc["message"] = "Fehler beim Setzen der Zeit"; + } + } else { + doc["success"] = false; + doc["message"] = "Ungültiger Timestamp"; + } + } else { + doc["success"] = false; + doc["message"] = "Timestamp-Parameter fehlt"; + } + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); +}); + +// Alternative Implementierung für manuelle Datum/Zeit-Eingabe +server.on("/api/set-datetime", HTTP_POST, [](AsyncWebServerRequest *request){ + StaticJsonDocument<150> doc; + + if (request->hasParam("year", true) && + request->hasParam("month", true) && + request->hasParam("day", true) && + request->hasParam("hour", true) && + request->hasParam("minute", true) && + request->hasParam("second", true)) { + + struct tm timeinfo; + timeinfo.tm_year = request->getParam("year", true)->value().toInt() - 1900; + timeinfo.tm_mon = request->getParam("month", true)->value().toInt() - 1; + timeinfo.tm_mday = request->getParam("day", true)->value().toInt(); + timeinfo.tm_hour = request->getParam("hour", true)->value().toInt(); + timeinfo.tm_min = request->getParam("minute", true)->value().toInt(); + timeinfo.tm_sec = request->getParam("second", true)->value().toInt(); + + time_t timestamp = mktime(&timeinfo); + + if (timestamp != -1) { + bool success = setSystemTime(timestamp); + + doc["success"] = success; + if (success) { + doc["message"] = "Zeit erfolgreich gesetzt"; + doc["timestamp"] = (long)timestamp; + } else { + doc["message"] = "Fehler beim Setzen der Zeit"; + } + } else { + doc["success"] = false; + doc["message"] = "Ungültiges Datum/Zeit"; + } + } else { + doc["success"] = false; + doc["message"] = "Datum/Zeit-Parameter fehlen"; + } + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); +}); + +// Erweiterte Zeit-Informationen (optional) +server.on("/api/time/info", HTTP_GET, [](AsyncWebServerRequest *request){ + gettimeofday(&tv, &tz); + now = tv.tv_sec; + gmtime_r(&now, &timeinfo); + + StaticJsonDocument<400> doc; + doc["timestamp"] = (long)now; + doc["uptime"] = millis(); + + // Formatierte Zeitstrings + char buffer[64]; + strftime(buffer, sizeof(buffer), "%Y-%m-%d", &timeinfo); + doc["date"] = String(buffer); + + strftime(buffer, sizeof(buffer), "%H:%M:%S", &timeinfo); + doc["time"] = String(buffer); + + strftime(buffer, sizeof(buffer), "%A", &timeinfo); + doc["weekday"] = String(buffer); + + strftime(buffer, sizeof(buffer), "%B", &timeinfo); + doc["month_name"] = String(buffer); + + // Zusätzliche Infos + doc["day_of_year"] = timeinfo.tm_yday + 1; + doc["week_of_year"] = (timeinfo.tm_yday + 7 - timeinfo.tm_wday) / 7; + doc["is_dst"] = timeinfo.tm_isdst; + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); +}); +Serial.println("Zeit-API initialisiert"); +} + +// Hilfsfunktion: Zeit-Validierung +bool isValidDateTime(int year, int month, int day, int hour, int minute, int second) { + if (year < 2020 || year > 2099) return false; + if (month < 1 || month > 12) return false; + if (day < 1 || day > 31) return false; + if (hour < 0 || hour > 23) return false; + if (minute < 0 || minute > 59) return false; + if (second < 0 || second > 59) return false; + + // Erweiterte Validierung für Monatstage + int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + + // Schaltjahr-Prüfung + if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { + daysInMonth[1] = 29; + } + + return day <= daysInMonth[month - 1]; +} diff --git a/src/webserverrouter.h b/src/webserverrouter.h new file mode 100644 index 0000000..bda2ed9 --- /dev/null +++ b/src/webserverrouter.h @@ -0,0 +1,169 @@ +#include +#include "master.h" +#include +#include +#include +#include + +AsyncWebServer server(80); + +void setupRoutes(){ + // Web Server Routes + + // SPIFFS initialisieren + + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(SPIFFS, "/index.html", "text/html"); + }); + + server.on("/settings", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(SPIFFS, "/settings.html", "text/html"); + }); + + server.on("/about", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(SPIFFS, "/about.html", "text/html"); + }); + + server.on("/api/data", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, "application/json", getTimerDataJSON()); + }); + + server.on("/api/reset-best", HTTP_POST, [](AsyncWebServerRequest *request){ + Serial.println("/api/reset-best called"); + timerData.bestTime1 = 0; + timerData.bestTime2 = 0; + saveBestTimes(); + DynamicJsonDocument doc(64); + doc["success"] = true; + String result; + serializeJson(doc, result); + request->send(200, "application/json", result); + }); + + server.on("/api/unlearn-button", HTTP_POST, [](AsyncWebServerRequest *request){ + Serial.println("/api/unlearn-button called"); + unlearnButton(); + request->send(200, "application/json", "{\"success\":true}"); + +}); + + + server.on("/api/set-max-time", HTTP_POST, [](AsyncWebServerRequest *request){ + Serial.println("/api/set-max-time called"); + bool changed = false; + if (request->hasParam("maxTime", true)) { + maxTimeBeforeReset = request->getParam("maxTime", true)->value().toInt() * 1000; + changed = true; + } + if (request->hasParam("maxTimeDisplay", true)) { + maxTimeDisplay = request->getParam("maxTimeDisplay", true)->value().toInt() * 1000; + changed = true; + } + if (changed) { + saveSettings(); + DynamicJsonDocument doc(32); + doc["success"] = true; + String result; + serializeJson(doc, result); + request->send(200, "application/json", result); + } else { + request->send(400, "application/json", "{\"success\":false}"); + } + }); + + server.on("/api/get-settings", HTTP_GET, [](AsyncWebServerRequest *request){ + Serial.println("/api/get-settings called"); + DynamicJsonDocument doc(256); + doc["maxTime"] = maxTimeBeforeReset / 1000; + doc["maxTimeDisplay"] = maxTimeDisplay / 1000; + String result; + serializeJson(doc, result); + request->send(200, "application/json", result); + }); + + server.on("/api/start-learning", HTTP_POST, [](AsyncWebServerRequest *request){ + Serial.println("/api/start-learning called"); + learningMode = true; + learningStep = 0; + DynamicJsonDocument doc(64); + doc["success"] = true; + String result; + serializeJson(doc, result); + Serial.println("Learning mode started"); + request->send(200, "application/json", result); +}); + + server.on("/api/stop-learning", HTTP_POST, [](AsyncWebServerRequest *request){ + Serial.println("/api/stop-learning called"); + learningMode = false; + learningStep = 0; + DynamicJsonDocument doc(64); + doc["success"] = true; + String result; + serializeJson(doc, result); + Serial.println("Learning mode stopped"); + request->send(200, "application/json", result); + }); + + server.on("/api/learn/status", HTTP_GET, [](AsyncWebServerRequest *request){ + DynamicJsonDocument doc(256); + doc["active"] = learningMode; + doc["step"] = learningStep; + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); + }); + + server.on("/api/buttons/status", HTTP_GET, [](AsyncWebServerRequest *request){ + DynamicJsonDocument doc(128); + doc["lane1Start"] = buttonConfigs.start1.isAssigned; + doc["lane1Stop"] = buttonConfigs.stop1.isAssigned; + doc["lane2Start"] = buttonConfigs.start2.isAssigned; + doc["lane2Stop"] = buttonConfigs.stop2.isAssigned; + String result; + serializeJson(doc, result); + request->send(200, "application/json", result); + }); + + server.on("/api/info", HTTP_GET, [](AsyncWebServerRequest *request){ + DynamicJsonDocument doc(256); + + // IP address + IPAddress ip = WiFi.softAPIP(); + doc["ip"] = ip.toString(); + doc["channel"] = WiFi.channel(); + + // MAC address + uint8_t mac[6]; + esp_wifi_get_mac(WIFI_IF_STA, mac); + char macStr[18]; + sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + doc["mac"] = macStr; + + // Free memory + doc["freeMemory"] = ESP.getFreeHeap(); + + // Connected buttons (count assigned) + int connected = 0; + if (buttonConfigs.start1.isAssigned) connected++; + if (buttonConfigs.stop1.isAssigned) connected++; + if (buttonConfigs.start2.isAssigned) connected++; + if (buttonConfigs.stop2.isAssigned) connected++; + doc["connectedButtons"] = connected; + + doc["valid"] = checkLicence() > 0 ? "Ja" : "Nein"; + doc["tier"] = checkLicence() ; + + + String result; + serializeJson(doc, result); + request->send(200, "application/json", result); +}); + +// Statische Dateien + server.serveStatic("/", SPIFFS, "/"); + server.begin(); + Serial.println("Web Server gestartet"); + +} \ No newline at end of file diff --git a/src/wificlass.h b/src/wificlass.h new file mode 100644 index 0000000..24c0d2c --- /dev/null +++ b/src/wificlass.h @@ -0,0 +1,59 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "master.h" +#include "licenceing.h" + +const char* ssidAP = "AquaCross-Timer"; +const char* passwordAP = "aquacross123"; + +const char* ssidSTA = "Obiwlankenobi"; +const char* passwordSTA = "Delfine1!"; + +PrettyOTA OTAUpdates; + + +void setupWifi() { + + WiFi.mode(WIFI_MODE_APSTA); + WiFi.softAP(ssidAP, passwordAP); + WiFi.begin(ssidSTA, passwordSTA); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(""); + Serial.println("Verbunden mit WLAN!"); + Serial.print("IP-Adresse: "); + Serial.println(WiFi.localIP()); + + Serial.println("WiFi AP gestartet"); + Serial.print("IP Adresse: "); + Serial.println(WiFi.softAPIP()); + Serial.println("PrettyOTA can be accessed at: http://" + WiFi.softAPIP().toString() + "/update"); + +} + +void setupOTA(AsyncWebServer *server) { + // Initialize PrettyOTA + OTAUpdates.Begin(server); + + // Set unique Hardware-ID for your hardware/board + OTAUpdates.SetHardwareID("AquaCross-Master"); + + // Set firmware version to 1.0.0 + OTAUpdates.SetAppVersion(firmwareversion); + + // Set current build time and date + PRETTY_OTA_SET_CURRENT_BUILD_TIME_AND_DATE(); +} + + + +// WiFi als Access Point + \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html