RFID message ins backend geht, websocket fürs frontend

This commit is contained in:
Carsten Graf
2025-06-08 00:38:00 +02:00
parent e5c4094cfa
commit c35e857904
11 changed files with 311 additions and 98 deletions

View File

@@ -150,6 +150,35 @@ html {
color: #fff; color: #fff;
} }
.swimmer-name {
font-size: clamp(1.5rem, 3.5vw, 2.2rem);
font-weight: bold;
margin-bottom: clamp(15px, 2vh, 25px);
padding: clamp(8px, 1.5vh, 12px) clamp(12px, 2vw, 18px);
background: rgba(255, 255, 255, 0.2);
border-radius: 15px;
border: 2px solid rgba(255, 255, 255, 0.3);
color: #fff;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(5px);
animation: fadeIn 0.5s ease-in;
text-align: center;
word-wrap: break-word;
line-height: 1.2;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.time-display { .time-display {
font-size: clamp(3rem, 9vw, 10rem); font-size: clamp(3rem, 9vw, 10rem);
font-weight: bold; font-weight: bold;
@@ -280,6 +309,11 @@ html {
body { body {
padding: 10px; padding: 10px;
} }
.swimmer-name {
font-size: clamp(1.2rem, 4vw, 1.8rem);
margin-bottom: clamp(10px, 1.5vh, 20px);
}
} }
@media (max-width: 480px) { @media (max-width: 480px) {
@@ -299,4 +333,9 @@ html {
.timer-container { .timer-container {
padding: 0 2vw; padding: 0 2vw;
} }
.swimmer-name {
font-size: clamp(1rem, 4vw, 1.5rem);
padding: clamp(6px, 1vh, 10px) clamp(8px, 1.5vw, 12px);
}
} }

View File

@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="index.css" /> <link rel="stylesheet" href="index.css" />
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<title>NinjaCross Timer</title> <title>NinjaCross Timer</title>
@@ -29,12 +29,14 @@
<div class="timer-container"> <div class="timer-container">
<div class="lane"> <div class="lane">
<div id="name1" class="swimmer-name" style="display: none;"></div>
<h2>🏊‍♀️ Bahn 1</h2> <h2>🏊‍♀️ Bahn 1</h2>
<div id="time1" class="time-display">00.00</div> <div id="time1" class="time-display">00.00</div>
<div id="status1" class="status ready">Bereit</div> <div id="status1" class="status ready">Bereit</div>
</div> </div>
<div class="lane"> <div class="lane">
<div id="name2" class="swimmer-name" style="display: none;"></div>
<h2>🏊‍♂️ Bahn 2</h2> <h2>🏊‍♂️ Bahn 2</h2>
<div id="time2" class="time-display">00.00</div> <div id="time2" class="time-display">00.00</div>
<div id="status2" class="status ready">Bereit</div> <div id="status2" class="status ready">Bereit</div>
@@ -64,6 +66,37 @@
let lastSync = Date.now(); let lastSync = Date.now();
let learningMode = false; let learningMode = false;
let learningButton = ""; let learningButton = "";
let name1 = "";
let name2 = "";
const ws = new WebSocket(`ws://${window.location.host}/ws`);
// Handle WebSocket events
ws.onopen = () => {
console.log("WebSocket connected");
};
ws.onclose = () => {
console.log("WebSocket disconnected");
};
ws.onmessage = (event) => {
console.log("WebSocket message received:", event.data);
try {
const data = JSON.parse(event.data);
if (data.firstname && data.lastname && data.lane) {
if (data.lane === "start1") {
name1 = `${data.firstname} ${data.lastname}`;
} else if (data.lane === "start2") {
name2 = `${data.firstname} ${data.lastname}`;
}
updateDisplay();
}
} catch (error) {
console.error("Error processing WebSocket message:", error);
}
};
function formatTime(seconds) { function formatTime(seconds) {
if (seconds === 0) return "00.00"; if (seconds === 0) return "00.00";
@@ -83,7 +116,6 @@
display2 += (now - lastSync) / 1000; display2 += (now - lastSync) / 1000;
} }
document.getElementById("time1").textContent = formatTime(display1); document.getElementById("time1").textContent = formatTime(display1);
const s1 = document.getElementById("status1"); const s1 = document.getElementById("status1");
s1.className = `status ${status1}`; s1.className = `status ${status1}`;
@@ -109,6 +141,24 @@
document.getElementById("best2").textContent = document.getElementById("best2").textContent =
best2 > 0 ? formatTime(best2) + "s" : "--.-"; best2 > 0 ? formatTime(best2) + "s" : "--.-";
// Namen anzeigen/verstecken - verbesserte Logik
const name1Element = document.getElementById("name1");
const name2Element = document.getElementById("name2");
if (name1 && name1.trim() !== "") {
name1Element.textContent = name1;
name1Element.style.display = "block";
} else {
name1Element.style.display = "none";
}
if (name2 && name2.trim() !== "") {
name2Element.textContent = name2;
name2Element.style.display = "block";
} else {
name2Element.style.display = "none";
}
// Lernmodus // Lernmodus
const learningDisplay = document.getElementById("learning-display"); const learningDisplay = document.getElementById("learning-display");
if (learningMode) { if (learningMode) {
@@ -139,7 +189,7 @@
); );
} }
// Sync with backend every 2 seconds // Sync with backend every 1 second
setInterval(syncFromBackend, 1000); setInterval(syncFromBackend, 1000);
// Smooth update every 50ms // Smooth update every 50ms

BIN
data/pictures/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="rfid.css" /> <link rel="stylesheet" href="rfid.css" />
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<title>RFID Daten Eingabe</title> <title>RFID Daten Eingabe</title>

View File

@@ -4,6 +4,7 @@
<!-- Meta Tags --> <!-- Meta Tags -->
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<!-- Stylesheets --> <!-- Stylesheets -->
<link rel="stylesheet" href="settings.css" /> <link rel="stylesheet" href="settings.css" />

View File

@@ -11,6 +11,8 @@
#include "helper.h" #include "helper.h"
#include <debug.h> #include <debug.h>
#include <map> #include <map>
#include <databasebackend.h>
#include <webserverrouter.h>
struct TimestampData { struct TimestampData {
uint64_t lastMessageTimestamp; // Timestamp from the device uint64_t lastMessageTimestamp; // Timestamp from the device
@@ -35,52 +37,6 @@ typedef struct {
PicoMQTT::Server mqtt; PicoMQTT::Server mqtt;
void processHeartbeat(const char* topic, const char* payload) {
String macAddress = String(topic).substring(15);
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, payload);
if (error) {
Serial.printf("JSON parsing failed for MAC %s: %s\n", macAddress.c_str(), error.c_str());
return;
}
uint64_t messageTimestamp = doc["timestamp"] | 0;
uint64_t currentLocalTime = getCurrentTimestampMs();
// Update timestamps for current device
if (deviceTimestamps.count(macAddress) > 0) {
TimestampData& data = deviceTimestamps[macAddress];
uint64_t messageDiff = messageTimestamp - data.lastMessageTimestamp;
uint64_t localDiff = currentLocalTime - data.lastLocalTimestamp;
data.drift = localDiff - messageDiff;
}
// Calculate drift relative to all other devices
Serial.printf("\nDrift analysis for device %s:\n", macAddress.c_str());
Serial.println("----------------------------------------");
for (const auto& device : deviceTimestamps) {
if (device.first != macAddress) { // Skip comparing to self
int64_t timeDiff = messageTimestamp - device.second.lastMessageTimestamp;
int64_t relativeDrift = timeDiff - (currentLocalTime - device.second.lastLocalTimestamp);
Serial.printf("Relative to %s:\n", device.first.c_str());
Serial.printf(" Time difference: %lld ms\n", timeDiff);
Serial.printf(" Relative drift: %lld ms\n", relativeDrift);
Serial.println("----------------------------------------");
}
}
// Update stored timestamps for current device
deviceTimestamps[macAddress] = {
messageTimestamp,
currentLocalTime,
deviceTimestamps[macAddress].drift
};
}
void readButtonJSON(const char * topic, const char * payload) { void readButtonJSON(const char * topic, const char * payload) {
if(strcmp(topic, "aquacross/button/press") == 0){ if(strcmp(topic, "aquacross/button/press") == 0){
@@ -133,22 +89,99 @@ void readButtonJSON(const char * topic, const char * payload) {
} }
} }
void readRFIDfromButton(const char * topic, const char * payload) {
// Create a JSON document to hold the button press data
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
const char* mac = doc["buttonmac"] | "unknown";
const char* uid = doc["uid"] | "unknown";
Serial.printf("RFID Read from Button:\n");
Serial.printf(" Button MAC: %s\n", mac);
Serial.printf(" UID: %s\n", uid);
// Convert buttonmac to byte array for comparison
auto macBytes = macStringToBytes(mac);
// Check if the buttonmac matches buttonConfigs.start1.mac
if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0) {
// Fetch user data
UserData userData = checkUser(uid);
if (userData.exists) {
// Log user data
Serial.printf("User found for start1: %s %s, Alter: %d\n",
userData.firstname.c_str(),
userData.lastname.c_str(),
userData.alter);
// Create JSON message to send to the frontend
StaticJsonDocument<128> messageDoc;
messageDoc["firstname"] = userData.firstname;
messageDoc["lastname"] = userData.lastname;
messageDoc["lane"] = "start1"; // Add lane information
String message;
serializeJson(messageDoc, message);
// Push the message to the frontend
pushUpdateToFrontend(message);
Serial.printf("Pushed user data for start1 to frontend: %s\n", message.c_str());
} else {
Serial.println("User not found for UID: " + String(uid));
}
}
// Check if the buttonmac matches buttonConfigs.start2.mac
else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0) {
// Fetch user data
UserData userData = checkUser(uid);
if (userData.exists) {
// Log user data
Serial.printf("User found for start2: %s %s, Alter: %d\n",
userData.firstname.c_str(),
userData.lastname.c_str(),
userData.alter);
// Create JSON message to send to the frontend
StaticJsonDocument<128> messageDoc;
messageDoc["firstname"] = userData.firstname;
messageDoc["lastname"] = userData.lastname;
messageDoc["lane"] = "start2"; // Add lane information
String message;
serializeJson(messageDoc, message);
// Push the message to the frontend
pushUpdateToFrontend(message);
Serial.printf("Pushed user data for start2 to frontend: %s\n", message.c_str());
} else {
Serial.println("User not found for UID: " + String(uid));
}
} else {
Serial.println("Button MAC does not match start1.mac or start2.mac");
}
} else {
Serial.println("Failed to parse RFID JSON");
}
}
void setupMqttServer() { void setupMqttServer() {
// Set up the MQTT server with the desired port // Set up the MQTT server with the desired port
// Subscribe to a topic pattern and attach a callback // Subscribe to a topic pattern and attach a callback
mqtt.subscribe("#", [](const char * topic, const char * payload) { mqtt.subscribe("#", [](const char * topic, const char * payload) {
if (strncmp(topic, "heartbeat/alive/", 15) == 0) { //Message received callback
processHeartbeat(topic, payload); //Serial.printf("Received message on topic '%s': %s\n", topic, payload);
} else if (strcmp(topic, "aquacross/button/press") == 0) { if (strcmp(topic, "aquacross/button/press") == 0) {
readButtonJSON(topic, payload); readButtonJSON(topic, payload);
} else if (strncmp(topic, "aquacross/button/rfid/", 22) == 0) {
readRFIDfromButton(topic, payload);
// Handle RFID read messages
} }
updateStatusLED(3); updateStatusLED(3);
}); });
// Add the button subscription
// Start the MQTT server // Start the MQTT server
mqtt.begin(); mqtt.begin();
@@ -168,6 +201,7 @@ void loopMqttServer() {
mqtt.publish("sync/time", timeStr); mqtt.publish("sync/time", timeStr);
lastPublish = millis(); lastPublish = millis();
} }
} }
void sendMQTTMessage(const char * topic, const char * message) { void sendMQTTMessage(const char * topic, const char * message) {
@@ -187,3 +221,5 @@ void sendMQTTJSONMessage(const char * topic, const JsonDocument & doc) {
Serial.printf("Published JSON message to topic '%s': %s\n", topic, jsonString.c_str()); Serial.printf("Published JSON message to topic '%s': %s\n", topic, jsonString.c_str());
} }

View File

@@ -1,3 +1,4 @@
#pragma once
#include <Arduino.h> #include <Arduino.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include "master.h" #include "master.h"
@@ -17,34 +18,76 @@ bool backendOnline() {
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN); http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
int httpCode = http.GET(); int httpCode = http.GET();
bool isOnline = (httpCode == HTTP_CODE_OK);
if (httpCode == HTTP_CODE_OK) { if (isOnline) {
return true;
Serial.println("Database server connection successful"); Serial.println("Database server connection successful");
} else { } else {
return false;
Serial.printf("Database server connection failed, error: %d\n", httpCode); Serial.printf("Database server connection failed, error: %d\n", httpCode);
} }
http.end(); http.end();
return isOnline;
} }
bool userExists(const String& userId) { struct UserData {
String uid;
String firstname;
String lastname;
int alter;
bool exists;
};
// UserData checkUser(const String& uid) is defined only once to avoid redefinition errors.
UserData checkUser(const String& uid) {
UserData userData = {"", "", "", 0, false};
if (!backendOnline()) { if (!backendOnline()) {
Serial.println("No internet connection, cannot check user existence."); Serial.println("No internet connection, cannot check user.");
return false; return userData;
} }
HTTPClient http; HTTPClient http;
http.begin(String(BACKEND_SERVER) + "/api/users/" + userId); http.begin(String(BACKEND_SERVER) + "/api/users/find");
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN); http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
//Post request to check if user exists // Create JSON payload
int httpCode = http.POST(""); StaticJsonDocument<200> requestDoc;
requestDoc["uid"] = uid;
String requestBody;
serializeJson(requestDoc, requestBody);
int httpCode = http.POST(requestBody);
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
StaticJsonDocument<512> responseDoc;
DeserializationError error = deserializeJson(responseDoc, payload);
if (!error) {
userData.uid = responseDoc["uid"].as<String>();
userData.firstname = responseDoc["firstname"].as<String>();
userData.lastname = responseDoc["lastname"].as<String>();
userData.alter = responseDoc["alter"] | 0;
userData.exists = true;
}
} else {
Serial.printf("User check failed, HTTP code: %d\n", httpCode);
}
http.end();
return userData;
}
// Keep this for backward compatibility
bool userExists(const String& uid) {
return checkUser(uid).exists;
} }
void setupBackendRoutes(AsyncWebServer& server) { void setupBackendRoutes(AsyncWebServer& server) {
server.on("/api/health", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/api/health", HTTP_GET, [](AsyncWebServerRequest *request) {
DynamicJsonDocument doc(64); DynamicJsonDocument doc(64);

View File

@@ -163,9 +163,25 @@ void saveWifiSettings() {
void loadWifiSettings() { void loadWifiSettings() {
preferences.begin("wifi", true); preferences.begin("wifi", true);
ssidSTA = preferences.getString("ssid", "").c_str();
passwordSTA = preferences.getString("password", "").c_str(); // Speicher freigeben, falls bereits zugewiesen
if (ssidSTA) {
free(ssidSTA);
}
if (passwordSTA) {
free(passwordSTA);
}
// Neue Werte laden und dynamisch zuweisen
String ssid = preferences.getString("ssid", "");
String password = preferences.getString("password", "");
ssidSTA = strdup(ssid.c_str());
passwordSTA = strdup(password.c_str());
preferences.end(); preferences.end();
// Debug-Ausgabe
Serial.printf("WLAN-Einstellungen geladen: SSID=%s, Passwort=%s\n", ssidSTA, passwordSTA);
} }
int checkLicence() { int checkLicence() {
@@ -179,7 +195,6 @@ String getTimerDataJSON() {
DynamicJsonDocument doc(1024); DynamicJsonDocument doc(1024);
unsigned long currentTime = millis(); unsigned long currentTime = millis();
// Bahn 1 // Bahn 1
if (timerData.isRunning1) { if (timerData.isRunning1) {
doc["time1"] = (currentTime - timerData.startTime1) / 1000.0; doc["time1"] = (currentTime - timerData.startTime1) / 1000.0;
@@ -234,7 +249,7 @@ void setup() {
setupTimeAPI(server); setupTimeAPI(server);
setupLicenceAPI(server); setupLicenceAPI(server);
setupDebugAPI(server); setupDebugAPI(server);
setupBackendRoutes(server); setupBackendRoutes(server);// Speichere WLAN-Einstellungen, falls noch nicht vorhanden
// Gespeicherte Daten laden // Gespeicherte Daten laden
@@ -246,6 +261,7 @@ void setup() {
setupOTA(&server); setupOTA(&server);
setupRoutes(); setupRoutes();
setupWebSocket();
setupLED(); setupLED();
setupMqttServer(); // MQTT Server initialisieren setupMqttServer(); // MQTT Server initialisieren
@@ -255,4 +271,5 @@ void setup() {
void loop() { void loop() {
checkAutoReset(); checkAutoReset();
loopMqttServer(); // MQTT Server in der Loop aufrufen loopMqttServer(); // MQTT Server in der Loop aufrufen
loopWebSocket();
} }

View File

@@ -1,3 +1,4 @@
#pragma once
#include <Arduino.h> #include <Arduino.h>
#include <WebServer.h> #include <WebServer.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
@@ -21,10 +22,6 @@ struct User {
unsigned long timestamp; unsigned long timestamp;
}; };
// Array für Benutzer (max 100 Benutzer)
User users[100];
int userCount = 0;
void setupRFID() { void setupRFID() {
// SPI und RFID initialisieren // SPI und RFID initialisieren

View File

@@ -1,18 +1,25 @@
#pragma once
#include <Arduino.h> #include <Arduino.h>
#include "master.h" #include "master.h"
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <AsyncWebSocket.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <SPIFFS.h> #include <SPIFFS.h>
#include <esp_wifi.h> #include <esp_wifi.h>
#include <buttonassigh.h> #include <buttonassigh.h>
#include <wificlass.h>
AsyncWebServer server(80); AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
void setupRoutes(){ void setupRoutes(){
// Web Server Routes // Web Server Routes
// Attach WebSocket to the server
server.addHandler(&ws);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html"); request->send(SPIFFS, "/index.html", "text/html");
}); });
@@ -176,13 +183,14 @@ void setupRoutes(){
password = request->getParam("password", true)->value(); password = request->getParam("password", true)->value();
} }
if (ssid.length() > 0) { if (ssid.length() > 0) {
// Hier speichern wir die neuen Werte (z.B. in Preferences oder global) // Speicher freigeben, bevor neue Werte zugewiesen werden
// Beispiel: strcpy(ssidSTA, ssid.c_str()); free(ssidSTA);
// Beispiel: strcpy(passwordSTA, password.c_str()); free(passwordSTA);
// In deinem Projekt ggf. persistent speichern!
// Hier als global (unsicher, nach Neustart verloren!): // Neue Werte zuweisen
ssidSTA = strdup(ssid.c_str()); ssidSTA = strdup(ssid.c_str());
passwordSTA = strdup(password.c_str()); passwordSTA = strdup(password.c_str());
// Rückmeldung // Rückmeldung
DynamicJsonDocument doc(64); DynamicJsonDocument doc(64);
doc["success"] = true; doc["success"] = true;
@@ -211,3 +219,24 @@ void setupRoutes(){
Serial.println("Web Server gestartet"); Serial.println("Web Server gestartet");
} }
void setupWebSocket() {
ws.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
if (type == WS_EVT_CONNECT) {
Serial.printf("WebSocket client connected: %u\n", client->id());
} else if (type == WS_EVT_DISCONNECT) {
Serial.printf("WebSocket client disconnected: %u\n", client->id());
} else if (type == WS_EVT_DATA) {
// Handle incoming WebSocket messages if needed
Serial.printf("WebSocket message received: %s\n", (char *)data);
}
});
}
void pushUpdateToFrontend(const String &message) {
ws.textAll(message); // Send the message to all connected clients
}
void loopWebSocket() {
ws.cleanupClients(); // Clean up disconnected clients
}

View File

@@ -13,8 +13,8 @@ String uniqueSSID;
const char* ssidAP; const char* ssidAP;
const char* passwordAP = nullptr; const char* passwordAP = nullptr;
const char* ssidSTA = "Obiwlankenobi"; char* ssidSTA = strdup("Obiwlankenobi");
const char* passwordSTA = "Delfine1!"; char* passwordSTA = strdup("Delfine1!");
PrettyOTA OTAUpdates; PrettyOTA OTAUpdates;