Compare commits
10 Commits
0166e1a695
...
173b13fcfc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
173b13fcfc | ||
|
|
55eb062d2c | ||
|
|
a768783640 | ||
|
|
2b9cc7283c | ||
|
|
ba1b86a053 | ||
|
|
4a04565878 | ||
|
|
6793a54103 | ||
|
|
60d4393bd2 | ||
|
|
a1c68791bf | ||
| e6a089fd61 |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -31,11 +31,17 @@ jobs:
|
|||||||
cp .pio/build/esp32thing_CI/firmware.bin firmware.bin
|
cp .pio/build/esp32thing_CI/firmware.bin firmware.bin
|
||||||
cp .pio/build/esp32thing_CI/spiffs.bin spiffs.bin
|
cp .pio/build/esp32thing_CI/spiffs.bin spiffs.bin
|
||||||
|
|
||||||
|
- name: Generate tag name
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
TAG="esp32thing-$(date +'%Y%m%d-%H%M%S')-${GITHUB_SHA::7}"
|
||||||
|
echo "tag_name=$TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
name: "esp32thing Firmware ${{ github.ref_name }}"
|
name: "esp32thing Firmware ${{ steps.tag.outputs.tag_name }}"
|
||||||
tag_name: "${{ github.ref_name }}"
|
tag_name: "${{ steps.tag.outputs.tag_name }}"
|
||||||
files: |
|
files: |
|
||||||
firmware.bin
|
firmware.bin
|
||||||
spiffs.bin
|
spiffs.bin
|
||||||
|
|||||||
106
.gitignore
vendored
106
.gitignore
vendored
@@ -1,6 +1,104 @@
|
|||||||
.pio
|
# PlatformIO
|
||||||
|
.pio/
|
||||||
|
.pioenvs/
|
||||||
|
.piolibdeps/
|
||||||
|
.platformio/
|
||||||
|
platformio.ini.bak
|
||||||
|
|
||||||
|
# Build directories
|
||||||
|
build/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node.js (falls du Node.js Tools verwendest)
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
.env
|
.env
|
||||||
.vscode/.browse.c_cpp.db*
|
|
||||||
.vscode/c_cpp_properties.json
|
# IDE specific files
|
||||||
|
.vscode/settings.json
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/ipch
|
.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Compiled files
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Firmware files (optional - falls du sie nicht versionieren willst)
|
||||||
|
# *.bin
|
||||||
|
# *.hex
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Archive files
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# MCP related files (falls nicht benötigt)
|
||||||
|
gitea-mcp.exe
|
||||||
|
gitea-mcp.zip
|
||||||
|
|
||||||
|
# Local configuration files
|
||||||
|
config.local.*
|
||||||
140
API.md
140
API.md
@@ -1,109 +1,93 @@
|
|||||||
# API- und Routenbeschreibung für das AquaMaster-Projekt
|
# API Documentation: AquaMaster Webserver
|
||||||
|
|
||||||
Diese Datei beschreibt alle HTTP-Routen (API und statische Seiten) für das AquaMaster-Projekt. Sie dient als Referenz für Frontend-Entwickler.
|
This document describes all available API routes provided by the webserver in `webserverrouter.h`.
|
||||||
|
All API endpoints return JSON unless otherwise noted.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Statische Seiten
|
## Static Files
|
||||||
|
|
||||||
| Route | Methode | Beschreibung | Antwort (Content-Type) |
|
| Route | Method | Description | Response Type |
|
||||||
|-----------------|---------|-------------------------------------|------------------------|
|
|------------------|--------|------------------------------|--------------|
|
||||||
| `/` | GET | Hauptseite (Timer) | HTML |
|
| `/` | GET | Main page | HTML |
|
||||||
| `/settings` | GET | Einstellungen-Seite | HTML |
|
| `/settings` | GET | Settings page | HTML |
|
||||||
| `/about` | GET | Info-/About-Seite | HTML |
|
| `/rfid` | GET | RFID page | HTML |
|
||||||
| `/` (static) | GET | Statische Dateien (CSS, Bilder, JS) | entspr. MIME-Type |
|
| `/firmware.bin` | GET | Firmware file (SPIFFS) | Binary |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API-Routen
|
## Timer & Data
|
||||||
|
|
||||||
### Timer & Daten
|
| Route | Method | Description | Request Body/Params | Response Example |
|
||||||
|
|-------------------|--------|-------------------------------------|--------------------|------------------|
|
||||||
| Route | Methode | Beschreibung | Body/Parameter | Antwort (Content-Type) |
|
| `/api/data` | GET | Get current timer and status data | – | `{...}` |
|
||||||
|-------------------|---------|-------------------------------------|------------------------|--------------------------------|
|
| `/api/reset-best` | POST | Reset best times | – | `{ "success": true }` |
|
||||||
| `/api/data` | GET | Aktuelle Timerdaten und Status | – | JSON |
|
|
||||||
|
|
||||||
**Beispiel-Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"time1": 12.34,
|
|
||||||
"status1": "running",
|
|
||||||
"time2": 0,
|
|
||||||
"status2": "ready",
|
|
||||||
"best1": 10.12,
|
|
||||||
"best2": 9.87,
|
|
||||||
"learningMode": false,
|
|
||||||
"learningButton": "Start Bahn 1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Bestzeiten
|
## Button Learning
|
||||||
|
|
||||||
| Route | Methode | Beschreibung | Body/Parameter | Antwort (Content-Type) |
|
| Route | Method | Description | Request Body/Params | Response Example |
|
||||||
|----------------------|---------|-------------------------------------|------------------------|--------------------------------|
|
|------------------------|--------|-------------------------------------|--------------------|------------------|
|
||||||
| `/api/reset-best` | POST | Setzt Bestzeiten zurück | – | `{ "success": true }` |
|
| `/api/unlearn-button` | POST | Remove all button assignments | – | `{ "success": true }` |
|
||||||
|
| `/api/start-learning` | POST | Start button learning mode | – | `{ "success": true }` |
|
||||||
|
| `/api/stop-learning` | POST | Stop button learning mode | – | `{ "success": true }` |
|
||||||
|
| `/api/learn/status` | GET | Get learning mode status | – | `{ "active": true, "step": 1 }` |
|
||||||
|
| `/api/buttons/status` | GET | Get button assignment and voltage | – | `{ "lane1Start": true, "lane1StartVoltage": 3.3, ... }` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Button-Lernmodus
|
## Settings
|
||||||
|
|
||||||
| Route | Methode | Beschreibung | Body/Parameter | Antwort (Content-Type) |
|
| Route | Method | Description | Request Body/Params | Response Example |
|
||||||
|--------------------------|---------|-------------------------------------|------------------------|--------------------------------|
|
|----------------------|--------|-------------------------------------|--------------------|------------------|
|
||||||
| `/api/start-learning` | POST | Startet Lernmodus | – | `{ "success": true }` |
|
| `/api/set-max-time` | POST | Set max timer and display time | `maxTime`, `maxTimeDisplay` (form params, seconds) | `{ "success": true }` |
|
||||||
| `/api/stop-learning` | POST | Beendet Lernmodus | – | `{ "success": true }` |
|
| `/api/get-settings` | GET | Get current timer settings | – | `{ "maxTime": 300, "maxTimeDisplay": 20 }` |
|
||||||
| `/api/learn/status` | GET | Status des Lernmodus | – | `{ "active": true, "step": 1 }`|
|
|
||||||
| `/api/unlearn-button` | POST | Löscht alle Button-Zuordnungen | – | `{ "success": true }` |
|
|
||||||
| `/api/buttons/status` | GET | Status der Button-Zuordnung | – | JSON (siehe unten) |
|
|
||||||
|
|
||||||
**Beispiel-Response für `/api/buttons/status`:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"lane1Start": true,
|
|
||||||
"lane1Stop": false,
|
|
||||||
"lane2Start": true,
|
|
||||||
"lane2Stop": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Einstellungen
|
## WiFi Configuration
|
||||||
|
|
||||||
| Route | Methode | Beschreibung | Body/Parameter | Antwort (Content-Type) |
|
| Route | Method | Description | Request Body/Params | Response Example |
|
||||||
|------------------------|---------|-------------------------------------|------------------------|--------------------------------|
|
|-------------------|--------|-------------------------------------|--------------------|------------------|
|
||||||
| `/api/set-max-time` | POST | Setzt max. Laufzeit & max. Anzeigezeit | `maxTime` (Sekunden, optional), `maxTimeDisplay` (Sekunden, optional) als Form-Parameter | `{ "success": true }` oder `{ "success": false }` |
|
| `/api/set-wifi` | POST | Set WiFi SSID and password | `ssid`, `password` (form params) | `{ "success": true }` |
|
||||||
| `/api/get-settings` | GET | Liefert aktuelle Einstellungen | – | `{ "maxTime": 300, "maxTimeDisplay": 20 }` |
|
| `/api/get-wifi` | GET | Get current WiFi SSID and password | – | `{ "ssid": "...", "password": "..." }` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Systeminfo
|
## Location Configuration
|
||||||
|
|
||||||
| Route | Methode | Beschreibung | Antwort (Content-Type) |
|
| Route | Method | Description | Request Body/Params | Response Example |
|
||||||
|-------------------|---------|-------------------------------------|--------------------------------|
|
|----------------------|--------|-------------------------------------|--------------------|------------------|
|
||||||
| `/api/info` | GET | Systeminfos (IP, MAC, Speicher, Lizenz, verbundene Buttons) | JSON (siehe unten) |
|
| `/api/set-location` | POST | Set location name and ID | `id`, `name` (form params) | `{ "success": true }` |
|
||||||
|
| `/api/get-location` | GET | Get current location | – | `{ "locationid": "..." }` |
|
||||||
**Beispiel-Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ip": "192.168.4.1",
|
|
||||||
"channel": 1,
|
|
||||||
"mac": "AA:BB:CC:DD:EE:FF",
|
|
||||||
"freeMemory": 123456,
|
|
||||||
"connectedButtons": 3,
|
|
||||||
"valid": "Ja",
|
|
||||||
"tier": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Hinweise
|
## Button Update & Mode
|
||||||
|
|
||||||
- **Alle API-Routen liefern JSON zurück.**
|
| Route | Method | Description | Request Body/Params | Response Example |
|
||||||
- **POST-Requests erwarten ggf. Form-Parameter (kein JSON-Body).**
|
|----------------------|--------|-------------------------------------|--------------------|------------------|
|
||||||
- **Statische Seiten und Assets werden direkt ausgeliefert.**
|
| `/api/updateButtons` | GET | Trigger MQTT update for buttons | – | `{ "success": true }` |
|
||||||
- **Kein Authentifizierungsverfahren implementiert.**
|
| `/api/set-mode` | POST | Set operational mode | `mode` (form param: "individual" or "wettkampf") | `{ "success": true }` |
|
||||||
|
| `/api/get-mode` | GET | Get current operational mode | – | `{ "mode": "individual" }` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## System Info
|
||||||
|
|
||||||
|
| Route | Method | Description | Request Body/Params | Response Example |
|
||||||
|
|---------------|--------|-------------------------------------|--------------------|------------------|
|
||||||
|
| `/api/info` | GET | Get system info (IP, MAC, memory, license, etc.) | – | `{ "ip": "...", "ipSTA": "...", "channel": 1, "mac": "...", "freeMemory": 123456, "connectedButtons": 3, "isOnline": true, "valid": "Ja", "tier": 1 }` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WebSocket
|
||||||
|
|
||||||
|
| Route | Description |
|
||||||
|
|---------|------------------------------------|
|
||||||
|
| `/ws` | WebSocket endpoint for live updates|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**All API endpoints return JSON unless otherwise noted. POST requests expect form parameters (not JSON body).**
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 The Gitea Authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
245
README.md
245
README.md
@@ -1,28 +1,233 @@
|
|||||||
# Ninjacross Timer ⏱️
|
# Gitea MCP Server
|
||||||
|
|
||||||
Ein präziser, drahtloser Timer für Ninjacross- und Schwimmwettbewerbe. Entwickelt für Trainings- und Wettkampfumgebungen, bei denen Geschwindigkeit, Zuverlässigkeit und Benutzerfreundlichkeit entscheidend sind.
|
[繁體中文](README.zh-tw.md) | [简体中文](README.zh-cn.md)
|
||||||
|
|
||||||
## 🔧 Funktionen
|
**Gitea MCP Server** is an integration plugin designed to connect Gitea with Model Context Protocol (MCP) systems. This allows for seamless command execution and repository management through an MCP-compatible chat interface.
|
||||||
|
|
||||||
- **0.1 s Genauigkeit** bei der Zeitmessung
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}) [](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}&quality=insiders)
|
||||||
- **Drahtlose Kommunikation** über ESP-NOW oder Wi-Fi Mesh
|
|
||||||
- **Mehrere Timer-Zonen** (z.B. Start/Stop für zwei Bahnen)
|
|
||||||
- **Visualisierung in Echtzeit** auf einem zentralen Raspberry Pi Dashboard
|
|
||||||
- **Großanzeige** per 7-Segment-Display oder Browseranzeige
|
|
||||||
- **Einfache Bedienung** über robuste Hardware-Taster
|
|
||||||
- **Erweiterbar** für mehrere Bahnen und Disziplinen
|
|
||||||
|
|
||||||
## 🛠️ Hardware-Komponenten
|
## Table of Contents
|
||||||
|
|
||||||
- ESP32 Mikrocontroller (pro Button oder Sensor ein Gerät)
|
- [Gitea MCP Server](#gitea-mcp-server)
|
||||||
- ESP32 Master mit MQTT Broker (zentrale Steuerung und Webserver)
|
- [Table of Contents](#table-of-contents)
|
||||||
- Taster oder Lichtschranken
|
- [What is Gitea?](#what-is-gitea)
|
||||||
- Optional: 7-Segment-Displays oder HDMI-Display
|
- [What is MCP?](#what-is-mcp)
|
||||||
- Stabile WLAN-Verbindung (z.B. Wi-Fi Mesh)
|
- [🚧 Installation](#-installation)
|
||||||
|
- [Usage with VS Code](#usage-with-vs-code)
|
||||||
|
- [📥 Download the official binary release](#-download-the-official-binary-release)
|
||||||
|
- [🔧 Build from Source](#-build-from-source)
|
||||||
|
- [📁 Add to PATH](#-add-to-path)
|
||||||
|
- [🚀 Usage](#-usage)
|
||||||
|
- [✅ Available Tools](#-available-tools)
|
||||||
|
- [🐛 Debugging](#-debugging)
|
||||||
|
- [🛠 Troubleshooting](#-troubleshooting)
|
||||||
|
|
||||||
## 📦 Aufbau
|
## What is Gitea?
|
||||||
|
|
||||||
|
Gitea is a community-managed lightweight code hosting solution written in Go. It is published under the MIT license. Gitea provides Git hosting including a repository viewer, issue tracking, pull requests, and more.
|
||||||
|
|
||||||
|
## What is MCP?
|
||||||
|
|
||||||
|
Model Context Protocol (MCP) is a protocol that allows for the integration of various tools and systems through a chat interface. It enables seamless command execution and management of repositories, users, and other resources.
|
||||||
|
|
||||||
|
## 🚧 Installation
|
||||||
|
|
||||||
|
### Usage with VS Code
|
||||||
|
|
||||||
|
For quick installation, use one of the one-click install buttons at the top of this README.
|
||||||
|
|
||||||
|
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
|
||||||
|
|
||||||
|
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||||
|
|
||||||
|
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcp": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"type": "promptString",
|
||||||
|
"id": "gitea_token",
|
||||||
|
"description": "Gitea Personal Access Token",
|
||||||
|
"password": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"servers": {
|
||||||
|
"gitea-mcp": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"-i",
|
||||||
|
"--rm",
|
||||||
|
"-e",
|
||||||
|
"GITEA_ACCESS_TOKEN",
|
||||||
|
"docker.gitea.com/gitea-mcp-server"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"GITEA_ACCESS_TOKEN": "${input:gitea_token}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📥 Download the official binary release
|
||||||
|
|
||||||
|
You can download the official release from [official Gitea MCP binary releases](https://gitea.com/gitea/gitea-mcp/releases).
|
||||||
|
|
||||||
|
### 🔧 Build from Source
|
||||||
|
|
||||||
|
You can download the source code by cloning the repository using Git:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.com/gitea/gitea-mcp.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Before building, make sure you have the following installed:
|
||||||
|
|
||||||
|
- make
|
||||||
|
- Golang (Go 1.24 or later recommended)
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📁 Add to PATH
|
||||||
|
|
||||||
|
After installing, copy the binary gitea-mcp to a directory included in your system's PATH. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp gitea-mcp /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Usage
|
||||||
|
|
||||||
|
This example is for Cursor, you can also use plugins in VSCode.
|
||||||
|
To configure the MCP server for Gitea, add the following to your MCP configuration file:
|
||||||
|
|
||||||
|
- **stdio mode**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"command": "gitea-mcp",
|
||||||
|
"args": [
|
||||||
|
"-t",
|
||||||
|
"stdio",
|
||||||
|
"--host",
|
||||||
|
"https://gitea.com"
|
||||||
|
// "--token", "<your personal access token>"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
// "GITEA_HOST": "https://gitea.com",
|
||||||
|
// "GITEA_INSECURE": "true",
|
||||||
|
"GITEA_ACCESS_TOKEN": "<your personal access token>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **sse mode**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"url": "http://localhost:8080/sse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **http mode**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"url": "http://localhost:8080/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Default log path**: `$HOME/.gitea-mcp/gitea-mcp.log`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> You can provide your Gitea host and access token either as command-line arguments or environment variables.
|
||||||
|
> Command-line arguments have the highest priority
|
||||||
|
|
||||||
|
Once everything is set up, try typing the following in your MCP-compatible chatbox:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[ESP32-Startbutton] --\
|
list all my repositories
|
||||||
---> WLAN --> [ESP32 Master] --> [Browseranzeige / Display]
|
```
|
||||||
[ESP32-Stopbutton ] --/
|
|
||||||
|
## ✅ Available Tools
|
||||||
|
|
||||||
|
The Gitea MCP Server supports the following tools:
|
||||||
|
|
||||||
|
| Tool | Scope | Description |
|
||||||
|
| :--------------------------: | :----------: | :------------------------------------------------------: |
|
||||||
|
| get_my_user_info | User | Get the information of the authenticated user |
|
||||||
|
| get_user_orgs | User | Get organizations associated with the authenticated user |
|
||||||
|
| create_repo | Repository | Create a new repository |
|
||||||
|
| fork_repo | Repository | Fork a repository |
|
||||||
|
| list_my_repos | Repository | List all repositories owned by the authenticated user |
|
||||||
|
| create_branch | Branch | Create a new branch |
|
||||||
|
| delete_branch | Branch | Delete a branch |
|
||||||
|
| list_branches | Branch | List all branches in a repository |
|
||||||
|
| create_release | Release | Create a new release in a repository |
|
||||||
|
| delete_release | Release | Delete a release from a repository |
|
||||||
|
| get_release | Release | Get a release |
|
||||||
|
| get_latest_release | Release | Get the latest release in a repository |
|
||||||
|
| list_releases | Release | List all releases in a repository |
|
||||||
|
| create_tag | Tag | Create a new tag |
|
||||||
|
| delete_tag | Tag | Delete a tag |
|
||||||
|
| get_tag | Tag | Get a tag |
|
||||||
|
| list_tags | Tag | List all tags in a repository |
|
||||||
|
| list_repo_commits | Commit | List all commits in a repository |
|
||||||
|
| get_file_content | File | Get the content and metadata of a file |
|
||||||
|
| get_dir_content | File | Get a list of entries in a directory |
|
||||||
|
| create_file | File | Create a new file |
|
||||||
|
| update_file | File | Update an existing file |
|
||||||
|
| delete_file | File | Delete a file |
|
||||||
|
| get_issue_by_index | Issue | Get an issue by its index |
|
||||||
|
| list_repo_issues | Issue | List all issues in a repository |
|
||||||
|
| create_issue | Issue | Create a new issue |
|
||||||
|
| create_issue_comment | Issue | Create a comment on an issue |
|
||||||
|
| edit_issue | Issue | Edit a issue |
|
||||||
|
| edit_issue_comment | Issue | Edit a comment on an issue |
|
||||||
|
| get_issue_comments_by_index | Issue | Get comments of an issue by its index |
|
||||||
|
| get_pull_request_by_index | Pull Request | Get a pull request by its index |
|
||||||
|
| list_repo_pull_requests | Pull Request | List all pull requests in a repository |
|
||||||
|
| create_pull_request | Pull Request | Create a new pull request |
|
||||||
|
| search_users | User | Search for users |
|
||||||
|
| search_org_teams | Organization | Search for teams in an organization |
|
||||||
|
| search_repos | Repository | Search for repositories |
|
||||||
|
| get_gitea_mcp_server_version | Server | Get the version of the Gitea MCP Server |
|
||||||
|
|
||||||
|
## 🐛 Debugging
|
||||||
|
|
||||||
|
To enable debug mode, add the `-d` flag when running the Gitea MCP Server with sse mode:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gitea-mcp -t sse [--port 8080] --token <your personal access token> -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠 Troubleshooting
|
||||||
|
|
||||||
|
If you encounter any issues, here are some common troubleshooting steps:
|
||||||
|
|
||||||
|
1. **Check your PATH**: Ensure that the `gitea-mcp` binary is in a directory included in your system's PATH.
|
||||||
|
2. **Verify dependencies**: Make sure you have all the required dependencies installed, such as `make` and `Golang`.
|
||||||
|
3. **Review configuration**: Double-check your MCP configuration file for any errors or missing information.
|
||||||
|
4. **Consult logs**: Check the logs for any error messages or warnings that can provide more information about the issue.
|
||||||
|
|
||||||
|
Enjoy exploring and managing your Gitea repositories via chat!
|
||||||
|
|||||||
78
THIRD_PARTY_LICENSES.md
Normal file
78
THIRD_PARTY_LICENSES.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
## Third-Party Licenses and Notices
|
||||||
|
|
||||||
|
This project uses third‑party libraries. Below is a summary of their licenses and the key obligations when you distribute firmware/binaries that include them.
|
||||||
|
|
||||||
|
### Summary of obligations
|
||||||
|
|
||||||
|
- Keep original copyright and license notices.
|
||||||
|
- Include this file (or equivalent notices) with any distribution.
|
||||||
|
- For LGPL‑licensed components (ESPAsyncWebServer, AsyncTCP):
|
||||||
|
- You are not required to publish your entire application.
|
||||||
|
- If you distribute binaries, you must provide a way for users to relink the application with a modified version of the LGPL library (e.g., provide object files of your application or the full source code), publish any changes you made to the LGPL libs, and include the LGPL license text.
|
||||||
|
- PrettyOTA has a custom permissive license with attribution and no rebranding without a commercial license.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Dependency overview
|
||||||
|
|
||||||
|
| Library | Version (as configured) | License | Project | License Text |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| ArduinoJson | ^7.4.1 | MIT | [bblanchon/ArduinoJson](https://github.com/bblanchon/ArduinoJson) | [LICENSE](https://github.com/bblanchon/ArduinoJson/blob/v7.4.1/LICENSE.md) |
|
||||||
|
| ESPAsyncWebServer (esp32async) | ^3.7.7 | LGPL‑3.0 | [esp32async/ESPAsyncWebServer](https://github.com/esp32async/ESPAsyncWebServer) | [LICENSE](https://github.com/esp32async/ESPAsyncWebServer/blob/master/LICENSE) |
|
||||||
|
| AsyncTCP (esp32async) | ^3.4.2 | LGPL‑3.0 | [esp32async/AsyncTCP](https://github.com/esp32async/AsyncTCP) | [LICENSE](https://github.com/esp32async/AsyncTCP/blob/master/LICENSE) |
|
||||||
|
| PicoMQTT | ^1.3.0 | MIT | [mlesniew/PicoMQTT](https://github.com/mlesniew/PicoMQTT) | [LICENSE](https://github.com/mlesniew/PicoMQTT/blob/master/LICENSE) |
|
||||||
|
| MFRC522 | ^1.4.12 | MIT | [miguelbalboa/rfid](https://github.com/miguelbalboa/rfid) | [LICENSE](https://github.com/miguelbalboa/rfid/blob/master/LICENSE) |
|
||||||
|
| RTClib | ^2.1.4 | MIT | [adafruit/RTClib](https://github.com/adafruit/RTClib) | [LICENSE](https://github.com/adafruit/RTClib/blob/master/license.txt) |
|
||||||
|
| PrettyOTA (vendored) | included | Custom (see below) | Included in `lib/PrettyOTA` | See “PrettyOTA License” below |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Notes on LGPL‑3.0 components (ESPAsyncWebServer, AsyncTCP)
|
||||||
|
|
||||||
|
If you distribute firmware that statically links these libraries (typical on microcontrollers):
|
||||||
|
|
||||||
|
- Provide a method for relinking your application with a modified version of the LGPL library. Practically, either:
|
||||||
|
- Provide object files (.o/.a) of your application so users can relink against a modified LGPL lib, or
|
||||||
|
- Publish your full application source code (voluntary but simpler), or
|
||||||
|
- Replace these libraries with permissive alternatives to avoid LGPL obligations.
|
||||||
|
- Publish any changes you made to the LGPL libraries themselves.
|
||||||
|
- Include the full text of the LGPL‑3.0 license with your distribution.
|
||||||
|
|
||||||
|
For many teams, the simplest path is to provide application object files or to switch to a permissively licensed HTTP/WebServer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PrettyOTA License (full text)
|
||||||
|
|
||||||
|
The PrettyOTA component is included under the following license. This license imposes attribution requirements and restricts removing/replacing the name/logo without a separate commercial license.
|
||||||
|
|
||||||
|
```
|
||||||
|
# License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Marc Schöndorf
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for private and commercial applications, to alter it and redistribute it, subject to the following conditions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented. You must not
|
||||||
|
claim that you wrote the original software. If you use this Software in a product, acknowledgment in the product documentation or credits is required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||||
|
3. You are not permitted to modify, replace or remove the name "PrettyOTA" or the original logo displayed within the Software's default user interface (if applicable), unless you have obtained a separate commercial license granting you such rights. This restriction applies even when redistributing modified versions of the source code.
|
||||||
|
4. This license notice must not be removed or altered from any source code distribution.
|
||||||
|
|
||||||
|
**Disclaimer:**
|
||||||
|
The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software.
|
||||||
|
|
||||||
|
## Commercial Licensing
|
||||||
|
A separate commercial license is required for specific rights not granted herein, particularly for white-labeling or rebranding (using a different name or logo). Please refer to the README file or contact the copyright holder for details on obtaining a commercial license.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Recommended distribution checklist
|
||||||
|
|
||||||
|
- Include this `THIRD_PARTY_LICENSES.md` with your firmware or product documentation.
|
||||||
|
- Include the full LGPL‑3.0 license text if you distribute firmware using ESPAsyncWebServer/AsyncTCP.
|
||||||
|
- Provide either application object files for relinking or publish source (to satisfy LGPL).
|
||||||
|
- Keep all copyright and license notices intact in UI/docs where applicable (e.g., PrettyOTA attribution).
|
||||||
|
|
||||||
|
|
||||||
3
TODO.md
3
TODO.md
@@ -15,3 +15,6 @@ v2.0
|
|||||||
- ADD option point for location (read from online table and select the location via dropdown) DONE
|
- ADD option point for location (read from online table and select the location via dropdown) DONE
|
||||||
- ADD option to enter a name, age DONE
|
- ADD option to enter a name, age DONE
|
||||||
- ADD upload to a Online Database () DONE
|
- ADD upload to a Online Database () DONE
|
||||||
|
|
||||||
|
|
||||||
|
- Redo Database Backend -> New SQL Server and deploy backend to edge functions?! Maybe host evrythin myself in a VM!
|
||||||
|
|||||||
@@ -355,18 +355,24 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.ready {
|
.status.finished {
|
||||||
background-color: rgba(52, 152, 219, 0.3);
|
background-color: rgba(52, 152, 219, 0.3);
|
||||||
border: 2px solid #3498db;
|
border: 2px solid #3498db;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.running {
|
.status.ready {
|
||||||
background-color: rgba(46, 204, 113, 0.3);
|
background-color: rgba(46, 204, 113, 0.3);
|
||||||
border: 2px solid #2ecc71;
|
border: 2px solid #2ecc71;
|
||||||
animation: pulse 1s infinite;
|
animation: pulse 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.finished {
|
.status.armed {
|
||||||
|
background-color: rgb(197, 194, 0);
|
||||||
|
border: 2px solid #fbff00;
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.running {
|
||||||
background-color: rgba(231, 76, 60, 0.3);
|
background-color: rgba(231, 76, 60, 0.3);
|
||||||
border: 2px solid #e74c3c;
|
border: 2px solid #e74c3c;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,27 +368,49 @@
|
|||||||
s1.textContent = "Standby: Bitte beide Buttons 1x betätigen";
|
s1.textContent = "Standby: Bitte beide Buttons 1x betätigen";
|
||||||
} else {
|
} else {
|
||||||
s1.className = `status ${status1}`;
|
s1.className = `status ${status1}`;
|
||||||
s1.textContent =
|
|
||||||
status1 === "ready"
|
switch (status1) {
|
||||||
? "Bereit"
|
case "ready":
|
||||||
: status1 === "running"
|
s1.textContent = "Bereit";
|
||||||
? "Läuft..."
|
break;
|
||||||
: "Beendet";
|
case "running":
|
||||||
|
s1.textContent = "Läuft...";
|
||||||
|
break;
|
||||||
|
case "finished":
|
||||||
|
s1.textContent = "Beendet";
|
||||||
|
break;
|
||||||
|
case "armed":
|
||||||
|
s1.textContent = "Armiert";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
s1.textContent = "Unbekannter Status";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("time2").textContent = formatTime(display2);
|
document.getElementById("time2").textContent = formatTime(display2);
|
||||||
|
|
||||||
if (!lane2Connected) {
|
if (!lane2Connected) {
|
||||||
s2.className = "status standby";
|
s2.className = "status standby";
|
||||||
s2.textContent = "Standby: Bitte beide 1x betätigen";
|
s2.textContent = "Standby: Bitte beide Buttons 1x betätigen";
|
||||||
} else {
|
} else {
|
||||||
s2.className = `status ${status2}`;
|
s2.className = `status ${status2}`;
|
||||||
s2.textContent =
|
|
||||||
status2 === "ready"
|
switch (status2) {
|
||||||
? "Bereit"
|
case "ready":
|
||||||
: status2 === "running"
|
s2.textContent = "Bereit";
|
||||||
? "Läuft..."
|
break;
|
||||||
: "Beendet";
|
case "running":
|
||||||
|
s2.textContent = "Läuft...";
|
||||||
|
break;
|
||||||
|
case "finished":
|
||||||
|
s2.textContent = "Beendet";
|
||||||
|
break;
|
||||||
|
case "armed":
|
||||||
|
s2.textContent = "Armiert"; // Neuer Status für armiert
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
s2.textContent = "Unbekannter Status";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("best1").textContent =
|
document.getElementById("best1").textContent =
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
// Globale Variablen
|
// Globale Variablen
|
||||||
let rfidData = [];
|
let rfidData = [];
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
let DBUrl = "db.reptilfpv.de:3000";
|
let DBUrl = "ninja.reptilfpv.de:3000";
|
||||||
var APIKey;
|
var APIKey;
|
||||||
|
|
||||||
// Maximales Datum auf heute setzen
|
// Maximales Datum auf heute setzen
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
ageDisplay.style.display = "none";
|
ageDisplay.style.display = "none";
|
||||||
if (age < 0) {
|
if (age < 0) {
|
||||||
showErrorMessage(
|
showErrorMessage(
|
||||||
"Das Geburtsdatum kann nicht in der Zukunft liegen!",
|
"Das Geburtsdatum kann nicht in der Zukunft liegen!"
|
||||||
);
|
);
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
} else {
|
} else {
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
const alter = calculateAge(geburtsdatum);
|
const alter = calculateAge(geburtsdatum);
|
||||||
if (alter < 0) {
|
if (alter < 0) {
|
||||||
showErrorMessage(
|
showErrorMessage(
|
||||||
"Das Geburtsdatum kann nicht in der Zukunft liegen!",
|
"Das Geburtsdatum kann nicht in der Zukunft liegen!"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -217,6 +217,7 @@
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
...(APIKey && { Authorization: `Bearer ${APIKey}` }),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
uid: uid,
|
uid: uid,
|
||||||
@@ -243,13 +244,13 @@
|
|||||||
} else {
|
} else {
|
||||||
// Fehler anzeigen
|
// Fehler anzeigen
|
||||||
showErrorMessage(
|
showErrorMessage(
|
||||||
result.error || "Fehler beim Speichern der Daten",
|
result.error || "Fehler beim Speichern der Daten"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Fehler beim Speichern:", error);
|
console.error("Fehler beim Speichern:", error);
|
||||||
showErrorMessage(
|
showErrorMessage(
|
||||||
"Verbindungsfehler zum Server. Bitte versuchen Sie es später erneut.",
|
"Verbindungsfehler zum Server. Bitte versuchen Sie es später erneut."
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingState(false);
|
setLoadingState(false);
|
||||||
@@ -355,6 +356,7 @@
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
...(APIKey && { Authorization: `Bearer ${APIKey}` }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -394,7 +396,7 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Fehler beim Lesen der UID:", error);
|
console.error("Fehler beim Lesen der UID:", error);
|
||||||
showErrorMessage(
|
showErrorMessage(
|
||||||
"Verbindungsfehler zum RFID Reader. Bitte prüfen Sie die Verbindung.",
|
"Verbindungsfehler zum RFID Reader. Bitte prüfen Sie die Verbindung."
|
||||||
);
|
);
|
||||||
|
|
||||||
// UID Feld rot markieren
|
// UID Feld rot markieren
|
||||||
@@ -412,12 +414,16 @@
|
|||||||
|
|
||||||
async function checkServerStatus() {
|
async function checkServerStatus() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/health");
|
const response = await fetch("/api/health", {
|
||||||
|
headers: {
|
||||||
|
...(APIKey && { Authorization: `Bearer ${APIKey}` }),
|
||||||
|
},
|
||||||
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!data.status || data.status !== "connected") {
|
if (!data.status || data.status !== "connected") {
|
||||||
showErrorMessage(
|
showErrorMessage(
|
||||||
"Server nicht verbunden. Einige Funktionen könnten eingeschränkt sein.",
|
"Server nicht verbunden. Einige Funktionen könnten eingeschränkt sein."
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -437,7 +443,7 @@
|
|||||||
APIKey = data.licence || "";
|
APIKey = data.licence || "";
|
||||||
})
|
})
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
showMessage("Fehler beim Laden der Lizenz", "error"),
|
showMessage("Fehler beim Laden der Lizenz", "error")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
@@ -19,17 +19,17 @@ body {
|
|||||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header::before {
|
.header::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -38,33 +38,33 @@ body {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><defs><radialGradient id="a" cx="50%" cy="40%" r="50%"><stop offset="0%" stop-color="white" stop-opacity="0.1"/><stop offset="100%" stop-color="white" stop-opacity="0"/></radialGradient></defs><rect width="100" height="20" fill="url(%23a)"/></svg>');
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><defs><radialGradient id="a" cx="50%" cy="40%" r="50%"><stop offset="0%" stop-color="white" stop-opacity="0.1"/><stop offset="100%" stop-color="white" stop-opacity="0"/></radialGradient></defs><rect width="100" height="20" fill="url(%23a)"/></svg>');
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h1 {
|
.header h1 {
|
||||||
font-size: 2.5em;
|
font-size: 2.5em;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header p {
|
.header p {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-buttons {
|
.nav-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button {
|
.nav-button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
@@ -75,78 +75,78 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button:hover {
|
.nav-button:hover {
|
||||||
background: #667eea;
|
background: #667eea;
|
||||||
color: white;
|
color: white;
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h2 {
|
.section h2 {
|
||||||
color: #495057;
|
color: #495057;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h2::before {
|
.section h2::before {
|
||||||
content: "";
|
content: "";
|
||||||
width: 4px;
|
width: 4px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input {
|
.form-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 15px;
|
padding: 12px 15px;
|
||||||
border: 2px solid #e9ecef;
|
border: 2px solid #e9ecef;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus {
|
.form-group input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-row {
|
.time-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-input {
|
.time-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-time {
|
.current-time {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -156,15 +156,15 @@ body {
|
|||||||
color: #495057;
|
color: #495057;
|
||||||
border: 2px solid #e9ecef;
|
border: 2px solid #e9ecef;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 15px 25px;
|
padding: 15px 25px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -176,62 +176,99 @@ body {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
|
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
|
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 10px 25px rgba(116, 185, 255, 0.3);
|
box-shadow: 0 10px 25px rgba(116, 185, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-warning {
|
.btn-warning {
|
||||||
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||||
color: #d84315;
|
color: #d84315;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-warning:hover {
|
.btn-warning:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 10px 25px rgba(252, 182, 159, 0.3);
|
box-shadow: 0 10px 25px rgba(252, 182, 159, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
|
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
|
||||||
color: #c62828;
|
color: #c62828;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover {
|
.btn-danger:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 10px 25px rgba(255, 154, 158, 0.3);
|
box-shadow: 0 10px 25px rgba(255, 154, 158, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-disabled {
|
.btn-disabled {
|
||||||
background: #e9ecef !important;
|
background: #e9ecef !important;
|
||||||
color: #6c757d !important;
|
color: #6c757d !important;
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
transform: none !important;
|
transform: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-disabled:hover {
|
.btn-disabled:hover {
|
||||||
transform: none !important;
|
transform: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.restriction-notice {
|
/* Toggle Buttons für Modus-Auswahl */
|
||||||
|
.mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px 25px;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button:not(.active):hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button:first-child {
|
||||||
|
border-right: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restriction-notice {
|
||||||
background: #fff3cd;
|
background: #fff3cd;
|
||||||
color: #856404;
|
color: #856404;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
@@ -240,61 +277,166 @@ body {
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-message {
|
/* Modern Notification Toast */
|
||||||
margin-top: 20px;
|
.notification-toast {
|
||||||
padding: 15px;
|
position: fixed;
|
||||||
border-radius: 10px;
|
top: 24px;
|
||||||
font-weight: 600;
|
right: 24px;
|
||||||
text-align: center;
|
min-width: 320px;
|
||||||
|
max-width: 400px;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
z-index: 99999;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
pointer-events: auto;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.status-success {
|
.notification-toast.show {
|
||||||
background: #d4edda;
|
transform: translateX(0);
|
||||||
color: #155724;
|
opacity: 1;
|
||||||
border: 2px solid #c3e6cb;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.status-error {
|
.notification-icon {
|
||||||
background: #f8d7da;
|
flex-shrink: 0;
|
||||||
color: #721c24;
|
width: 40px;
|
||||||
border: 2px solid #f5c6cb;
|
height: 40px;
|
||||||
}
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
|
}
|
||||||
|
|
||||||
.status-info {
|
.notification-body {
|
||||||
background: #cce7ff;
|
flex: 1;
|
||||||
color: #004085;
|
min-width: 0;
|
||||||
border: 2px solid #b3d9ff;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.learning-mode {
|
.notification-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-right: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Types */
|
||||||
|
.notification-toast.success .notification-icon {
|
||||||
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast.error .notification-icon {
|
||||||
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast.info .notification-icon {
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast.warning .notification-icon {
|
||||||
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOutRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.learning-mode {
|
||||||
display: none;
|
display: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.learning-mode h3 {
|
.learning-mode h3 {
|
||||||
color: #d84315;
|
color: #d84315;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.learning-mode p {
|
.learning-mode p {
|
||||||
color: #bf360c;
|
color: #bf360c;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pulse {
|
.pulse {
|
||||||
animation: pulse 2s infinite;
|
animation: pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
@@ -304,9 +446,47 @@ body {
|
|||||||
100% {
|
100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
.section select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: inherit;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: white;
|
||||||
|
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='M2 0L0 2h4zm0 5L0 3h4z'/></svg>");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 12px center;
|
||||||
|
background-size: 12px;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section select:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section select:disabled {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
.container {
|
.container {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
@@ -332,85 +512,24 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.section select {
|
.mode-toggle {
|
||||||
width: 100%;
|
flex-direction: column;
|
||||||
padding: 12px 16px;
|
gap: 0;
|
||||||
font-size: 16px;
|
}
|
||||||
font-family: inherit;
|
|
||||||
border: 2px solid #e1e5e9;
|
.mode-button:first-child {
|
||||||
border-radius: 8px;
|
border-right: none;
|
||||||
background-color: white;
|
border-bottom: 1px solid #e9ecef;
|
||||||
background-image: url("data:image/svg+xml;charset=US-ASCII,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='%23666' d='M2 0L0 2h4zm0 5L0 3h4z'/></svg>");
|
}
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 12px center;
|
/* Mobile notification bubble adjustments */
|
||||||
background-size: 12px;
|
.notification-bubble {
|
||||||
appearance: none;
|
top: 10px;
|
||||||
-webkit-appearance: none;
|
right: 10px;
|
||||||
-moz-appearance: none;
|
left: 10px;
|
||||||
cursor: pointer;
|
max-width: none;
|
||||||
transition: all 0.3s ease;
|
font-size: 14px;
|
||||||
}
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
.section select:hover {
|
|
||||||
border-color: #007bff;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #007bff;
|
|
||||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section select:disabled {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
color: #6c757d;
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
border-color: #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section select:disabled:hover {
|
|
||||||
border-color: #dee2e6;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Option Styling */
|
|
||||||
.section select option {
|
|
||||||
padding: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: white;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section select option:hover {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section select option:disabled {
|
|
||||||
color: #6c757d;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Group für bessere Abstände */
|
|
||||||
.section .form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section .form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design für kleinere Bildschirme */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.section select {
|
|
||||||
font-size: 16px; /* Verhindert Zoom auf iOS */
|
|
||||||
padding: 14px 16px;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,6 +11,22 @@
|
|||||||
<title>Ninjacross Timer - Einstellungen</title>
|
<title>Ninjacross Timer - Einstellungen</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Modern Notification Toast -->
|
||||||
|
<div id="notificationBubble" class="notification-toast" style="display: none;">
|
||||||
|
<div class="notification-icon">
|
||||||
|
<span id="notificationIcon">✓</span>
|
||||||
|
</div>
|
||||||
|
<div class="notification-body">
|
||||||
|
<div class="notification-title" id="notificationTitle">Erfolg</div>
|
||||||
|
<div class="notification-message" id="notificationText">Bereit</div>
|
||||||
|
</div>
|
||||||
|
<button class="notification-close" onclick="hideNotification()">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -25,9 +41,6 @@
|
|||||||
<a href="/rfid" class="nav-button">📡 RFID</a>
|
<a href="/rfid" class="nav-button">📡 RFID</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Message Container -->
|
|
||||||
<div id="statusMessage" class="status-message"></div>
|
|
||||||
|
|
||||||
<!-- Date & Time Section -->
|
<!-- Date & Time Section -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>🕐 Datum & Uhrzeit</h2>
|
<h2>🕐 Datum & Uhrzeit</h2>
|
||||||
@@ -66,6 +79,30 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mode Selection Section -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>🎯 Modus</h2>
|
||||||
|
<form id="modeForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Modus auswählen:</label>
|
||||||
|
<div class="mode-toggle">
|
||||||
|
<button type="button" class="mode-button active" data-mode="individual" onclick="selectMode('individual')">
|
||||||
|
👤 Individual
|
||||||
|
</button>
|
||||||
|
<button type="button" class="mode-button" data-mode="wettkampf" onclick="selectMode('wettkampf')">
|
||||||
|
🏆 Wettkampf
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
💾 Modus speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Basic Settings Section -->
|
<!-- Basic Settings Section -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>🔧 Grundeinstellungen</h2>
|
<h2>🔧 Grundeinstellungen</h2>
|
||||||
@@ -320,6 +357,7 @@
|
|||||||
loadCurrentTime();
|
loadCurrentTime();
|
||||||
updateCurrentTimeDisplay();
|
updateCurrentTimeDisplay();
|
||||||
loadWifiSettings();
|
loadWifiSettings();
|
||||||
|
loadMode();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Aktuelle Zeit anzeigen (Live-Update)
|
// Aktuelle Zeit anzeigen (Live-Update)
|
||||||
@@ -464,6 +502,60 @@
|
|||||||
saveLicence();
|
saveLicence();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Mode selection function
|
||||||
|
// Remove active class from all mode buttons
|
||||||
|
function selectMode(mode) {
|
||||||
|
document.querySelectorAll('.mode-button').forEach(button => {
|
||||||
|
button.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add active class to selected button
|
||||||
|
document.querySelector(`[data-mode="${mode}"]`).classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode form handler
|
||||||
|
document.getElementById('modeForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const activeButton = document.querySelector('.mode-button.active');
|
||||||
|
const selectedMode = activeButton ? activeButton.getAttribute('data-mode') : 'individual';
|
||||||
|
|
||||||
|
fetch('/api/set-mode', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: 'mode=' + encodeURIComponent(selectedMode)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showMessage('Modus erfolgreich gespeichert!', 'success');
|
||||||
|
} else {
|
||||||
|
showMessage('Fehler beim Speichern des Modus', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => showMessage('Verbindungsfehler', 'error'));
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadMode() {
|
||||||
|
fetch("/api/get-mode")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
const mode = data.mode || "individual";
|
||||||
|
document.querySelectorAll('.mode-button').forEach(button => {
|
||||||
|
button.classList.remove('active');
|
||||||
|
});
|
||||||
|
const btn = document.querySelector(`.mode-button[data-mode="${mode}"]`);
|
||||||
|
if (btn) btn.classList.add('active');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
showMessage("Fehler beim Laden des Modus", "error");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Einstellungen laden
|
// Einstellungen laden
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
fetch("/api/get-settings")
|
fetch("/api/get-settings")
|
||||||
@@ -516,7 +608,7 @@
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
document.getElementById("licencekey").value = data.licence || "";
|
document.getElementById("licencekey").value = data.licence || "";
|
||||||
loadLocations();
|
loadLocationsFromBackend();
|
||||||
})
|
})
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
showMessage("Fehler beim Laden der Lizenz", "error")
|
showMessage("Fehler beim Laden der Lizenz", "error")
|
||||||
@@ -925,16 +1017,7 @@
|
|||||||
|
|
||||||
//location functions
|
//location functions
|
||||||
// Locations laden und Dropdown befüllen
|
// Locations laden und Dropdown befüllen
|
||||||
function loadLocations() {
|
function loadLocationsFromBackend() {
|
||||||
const licence = document.getElementById("licencekey").value; // Get the licence key from the input field
|
|
||||||
fetch("http://db.reptilfpv.de:3000/api/location/", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${licence}`, // Add Bearer token using licenkey
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
const select = document.getElementById("locationSelect");
|
const select = document.getElementById("locationSelect");
|
||||||
|
|
||||||
// Vorhandene Optionen löschen (außer der ersten "Bitte wählen...")
|
// Vorhandene Optionen löschen (außer der ersten "Bitte wählen...")
|
||||||
@@ -942,27 +1025,76 @@
|
|||||||
select.removeChild(select.lastChild);
|
select.removeChild(select.lastChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neue Optionen aus Backend-Response hinzufügen
|
// Fallback: Statische Standorte falls API nicht verfügbar ist
|
||||||
// Neue Optionen aus Backend-Response hinzufügen
|
const staticLocations = [
|
||||||
data.forEach((location) => {
|
{ id: "1", name: "Hauptstandort" },
|
||||||
|
{ id: "2", name: "Standort A" },
|
||||||
|
{ id: "3", name: "Standort B" },
|
||||||
|
{ id: "4", name: "Teststandort" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Versuche zuerst die echte API zu verwenden
|
||||||
|
const licence = document.getElementById("licencekey").value;
|
||||||
|
if (licence && licence.trim() !== "") {
|
||||||
|
fetch("https://ninja.reptilfpv.de/api/v1/private/locations", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${licence}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success && data.data) {
|
||||||
|
// API erfolgreich - verwende echte Daten
|
||||||
|
data.data.forEach((location) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = location.id;
|
option.value = location.id;
|
||||||
option.textContent = location.name;
|
option.textContent = location.name;
|
||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
|
showMessage("Standorte erfolgreich von API geladen", "success");
|
||||||
|
} else {
|
||||||
|
throw new Error("Ungültige API-Response");
|
||||||
|
}
|
||||||
// Aktuell gespeicherten Standort laden
|
// Aktuell gespeicherten Standort laden
|
||||||
loadCurrentLocation();
|
loadSavedLocation();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log("Locations konnten nicht geladen werden:", error);
|
console.log("API nicht verfügbar, verwende statische Daten:", error);
|
||||||
showMessage("Fehler beim Laden der Locations", "error");
|
// API fehlgeschlagen - verwende statische Daten als Fallback
|
||||||
|
staticLocations.forEach((location) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = location.id;
|
||||||
|
option.textContent = location.name;
|
||||||
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
|
showMessage("Standorte geladen (statische Daten - API nicht verfügbar)", "warning");
|
||||||
|
// Aktuell gespeicherten Standort laden
|
||||||
|
loadSavedLocation();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Kein Lizenz-Key - verwende statische Daten
|
||||||
|
staticLocations.forEach((location) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = location.id;
|
||||||
|
option.textContent = location.name;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
showMessage("Standorte geladen (statische Daten - kein Lizenz-Key)", "warning");
|
||||||
|
// Aktuell gespeicherten Standort laden
|
||||||
|
loadSavedLocation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aktuell gespeicherten Standort laden
|
// Aktuell gespeicherten Standort laden
|
||||||
function loadCurrentLocation() {
|
function loadSavedLocation() {
|
||||||
fetch("/api/get-location")
|
fetch("/api/get-local-location")
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.locationId) {
|
if (data.locationId) {
|
||||||
@@ -1035,7 +1167,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Standort an Backend senden
|
// Standort an Backend senden
|
||||||
fetch("/api/set-location", {
|
fetch("/api/set-local-location", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
@@ -1053,18 +1185,90 @@
|
|||||||
.catch((error) => showMessage("Verbindungsfehler", "error"));
|
.catch((error) => showMessage("Verbindungsfehler", "error"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status-Nachricht anzeigen
|
// Moderne Notification anzeigen
|
||||||
function showMessage(message, type) {
|
function showMessage(message, type = 'info') {
|
||||||
const statusDiv = document.getElementById("statusMessage");
|
console.log("showMessage called:", message, type);
|
||||||
statusDiv.textContent = message;
|
const toast = document.getElementById("notificationBubble");
|
||||||
statusDiv.className = `status-message status-${type}`;
|
const icon = document.getElementById("notificationIcon");
|
||||||
statusDiv.style.display = "block";
|
const title = document.getElementById("notificationTitle");
|
||||||
|
const text = document.getElementById("notificationText");
|
||||||
|
|
||||||
|
if (!toast || !icon || !title || !text) {
|
||||||
|
console.error("Notification elements not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (window.notificationTimeout) {
|
||||||
|
clearTimeout(window.notificationTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set content
|
||||||
|
text.textContent = message;
|
||||||
|
|
||||||
|
// Set type-specific styling and content
|
||||||
|
toast.className = "notification-toast";
|
||||||
|
switch(type) {
|
||||||
|
case 'success':
|
||||||
|
toast.classList.add('success');
|
||||||
|
icon.textContent = '✓';
|
||||||
|
title.textContent = 'Erfolg';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
toast.classList.add('error');
|
||||||
|
icon.textContent = '✕';
|
||||||
|
title.textContent = 'Fehler';
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
toast.classList.add('info');
|
||||||
|
icon.textContent = 'ℹ';
|
||||||
|
title.textContent = 'Information';
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
toast.classList.add('warning');
|
||||||
|
icon.textContent = '⚠';
|
||||||
|
title.textContent = 'Warnung';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toast.classList.add('info');
|
||||||
|
icon.textContent = 'ℹ';
|
||||||
|
title.textContent = 'Information';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show toast with animation
|
||||||
|
toast.style.display = "flex";
|
||||||
|
// Force reflow
|
||||||
|
toast.offsetHeight;
|
||||||
|
// Add show class after a small delay to ensure display is set
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
statusDiv.style.display = "none";
|
toast.classList.add('show');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Auto-hide after 5 seconds
|
||||||
|
window.notificationTimeout = setTimeout(() => {
|
||||||
|
hideNotification();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notification verstecken mit Animation
|
||||||
|
function hideNotification() {
|
||||||
|
const toast = document.getElementById("notificationBubble");
|
||||||
|
if (!toast) return;
|
||||||
|
|
||||||
|
// Clear timeout if exists
|
||||||
|
if (window.notificationTimeout) {
|
||||||
|
clearTimeout(window.notificationTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove show class for animation
|
||||||
|
toast.classList.remove('show');
|
||||||
|
|
||||||
|
// Hide after animation completes
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.display = "none";
|
||||||
|
}, 400); // Match CSS transition duration
|
||||||
|
}
|
||||||
|
|
||||||
// System-Info alle 30 Sekunden aktualisieren
|
// System-Info alle 30 Sekunden aktualisieren
|
||||||
setInterval(loadSystemInfo, 30000);
|
setInterval(loadSystemInfo, 30000);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
BIN
lib/PrettyOTA/examples/.DS_Store
vendored
BIN
lib/PrettyOTA/examples/.DS_Store
vendored
Binary file not shown.
BIN
lib/PrettyOTA/examples/callbacks/.DS_Store
vendored
BIN
lib/PrettyOTA/examples/callbacks/.DS_Store
vendored
Binary file not shown.
@@ -12,6 +12,7 @@
|
|||||||
#include "statusled.h"
|
#include "statusled.h"
|
||||||
#include "timesync.h"
|
#include "timesync.h"
|
||||||
#include "webserverrouter.h"
|
#include "webserverrouter.h"
|
||||||
|
#include <gamemodes.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,16 +89,20 @@ void readButtonJSON(const char *topic, const char *payload) {
|
|||||||
// Button-Zuordnung prüfen und entsprechende Aktion ausführen
|
// Button-Zuordnung prüfen und entsprechende Aktion ausführen
|
||||||
if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0 &&
|
if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0 &&
|
||||||
(pressType == 2)) {
|
(pressType == 2)) {
|
||||||
handleStart1(timestamp);
|
// handleStart1(timestamp);
|
||||||
|
triggerAction("start", 2, 1, timestamp);
|
||||||
} else if (memcmp(macBytes.data(), buttonConfigs.stop1.mac, 6) == 0 &&
|
} else if (memcmp(macBytes.data(), buttonConfigs.stop1.mac, 6) == 0 &&
|
||||||
(pressType == 1)) {
|
(pressType == 1)) {
|
||||||
handleStop1(timestamp);
|
// handleStop1(timestamp);
|
||||||
|
triggerAction("stop", 1, 1, timestamp);
|
||||||
} else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0 &&
|
} else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0 &&
|
||||||
(pressType == 2)) {
|
(pressType == 2)) {
|
||||||
handleStart2(timestamp);
|
// handleStart2(timestamp);
|
||||||
|
triggerAction("start", 2, 2, timestamp);
|
||||||
} else if (memcmp(macBytes.data(), buttonConfigs.stop2.mac, 6) == 0 &&
|
} else if (memcmp(macBytes.data(), buttonConfigs.stop2.mac, 6) == 0 &&
|
||||||
(pressType == 1)) {
|
(pressType == 1)) {
|
||||||
handleStop2(timestamp);
|
// handleStop2(timestamp);
|
||||||
|
triggerAction("stop", 1, 2, timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flash status LED to indicate received message
|
// Flash status LED to indicate received message
|
||||||
@@ -339,6 +344,11 @@ void setupMqttServer() {
|
|||||||
handleBatteryTopic(topic, payload);
|
handleBatteryTopic(topic, payload);
|
||||||
} else if (strncmp(topic, "heartbeat/alive/", 16) == 0) {
|
} else if (strncmp(topic, "heartbeat/alive/", 16) == 0) {
|
||||||
handleHeartbeatTopic(topic, payload);
|
handleHeartbeatTopic(topic, payload);
|
||||||
|
} else if (strncmp(topic, "aquacross/competition/toMaster", 30) == 0) {
|
||||||
|
// Handle competition lane messages
|
||||||
|
// payload ist sendMQTTMessage("aquacross/competition/toMaster", "start");
|
||||||
|
startCompetition = (payload != nullptr && strcmp(payload, "start") == 0);
|
||||||
|
runCompetition();
|
||||||
}
|
}
|
||||||
updateStatusLED(3);
|
updateStatusLED(3);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
#include "master.h"
|
#include "master.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
|
#include <preferencemanager.h>
|
||||||
|
|
||||||
const char *BACKEND_SERVER = "http://db.reptilfpv.de:3000";
|
const char *BACKEND_SERVER = "https://ninja.reptilfpv.de";
|
||||||
extern String licence; // Declare licence as an external variable defined elsewhere
|
extern String
|
||||||
String BACKEND_TOKEN = licence; // Use the licence as the token for authentication
|
licence; // Declare licence as an external variable defined elsewhere
|
||||||
|
String BACKEND_TOKEN =
|
||||||
|
licence; // Use the licence as the token for authentication
|
||||||
|
|
||||||
bool backendOnline() {
|
bool backendOnline() {
|
||||||
|
|
||||||
@@ -17,7 +20,7 @@ bool backendOnline() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(String(BACKEND_SERVER) + "/api/health");
|
http.begin(String(BACKEND_SERVER) + "/v1/private/health");
|
||||||
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
|
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
|
||||||
|
|
||||||
int httpCode = http.GET();
|
int httpCode = http.GET();
|
||||||
@@ -53,7 +56,7 @@ UserData checkUser(const String &uid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(String(BACKEND_SERVER) + "/api/users/find");
|
http.begin(String(BACKEND_SERVER) + "/v1/private/users/find");
|
||||||
http.addHeader("Content-Type", "application/json");
|
http.addHeader("Content-Type", "application/json");
|
||||||
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
|
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
|
||||||
|
|
||||||
@@ -95,17 +98,16 @@ bool enterUserData(const String &uid, const String &firstname,
|
|||||||
}
|
}
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(String(BACKEND_SERVER) + "/api/users/insert");
|
http.begin(String(BACKEND_SERVER) + "/v1/private/create-player");
|
||||||
http.addHeader("Content-Type", "application/json");
|
http.addHeader("Content-Type", "application/json");
|
||||||
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
|
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
|
||||||
|
|
||||||
// Create JSON payload
|
// Create JSON payload
|
||||||
StaticJsonDocument<256> requestDoc;
|
StaticJsonDocument<256> requestDoc;
|
||||||
requestDoc["uid"] = uid;
|
requestDoc["rfiduid"] = uid;
|
||||||
requestDoc["vorname"] = firstname;
|
requestDoc["firstname"] = firstname;
|
||||||
requestDoc["nachname"] = lastname;
|
requestDoc["lastname"] = lastname;
|
||||||
requestDoc["geburtsdatum"] = geburtsdatum;
|
requestDoc["birthdate"] = geburtsdatum;
|
||||||
requestDoc["alter"] = alter;
|
|
||||||
|
|
||||||
String requestBody;
|
String requestBody;
|
||||||
serializeJson(requestDoc, requestBody);
|
serializeJson(requestDoc, requestBody);
|
||||||
@@ -133,7 +135,7 @@ JsonDocument getAllLocations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(String(BACKEND_SERVER) + "/api/location/");
|
http.begin(String(BACKEND_SERVER) + "/v1/private/locations");
|
||||||
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
|
http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN);
|
||||||
|
|
||||||
int httpCode = http.GET();
|
int httpCode = http.GET();
|
||||||
@@ -185,5 +187,36 @@ void setupBackendRoutes(AsyncWebServer &server) {
|
|||||||
request->send(200, "application/json", result);
|
request->send(200, "application/json", result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.on("/api/set-local-location", HTTP_POST,
|
||||||
|
[](AsyncWebServerRequest *request) {
|
||||||
|
Serial.println("/api/set-local-location called");
|
||||||
|
String locationId;
|
||||||
|
if (request->hasParam("locationId", true)) {
|
||||||
|
locationId = request->getParam("locationId", true)->value();
|
||||||
|
}
|
||||||
|
if (locationId.length() > 0) {
|
||||||
|
saveLocationIdToPrefs(locationId);
|
||||||
|
DynamicJsonDocument doc(64);
|
||||||
|
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-local-location", HTTP_GET,
|
||||||
|
[](AsyncWebServerRequest *request) {
|
||||||
|
String locationId = getLocationIdFromPrefs();
|
||||||
|
DynamicJsonDocument doc(64);
|
||||||
|
doc["locationId"] = locationId;
|
||||||
|
String result;
|
||||||
|
serializeJson(doc, result);
|
||||||
|
request->send(200, "application/json", result);
|
||||||
|
|
||||||
|
// Andere Logik wie in getBestLocs
|
||||||
|
});
|
||||||
|
|
||||||
// Add more routes as needed
|
// Add more routes as needed
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/debug.h
14
src/debug.h
@@ -7,8 +7,8 @@
|
|||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
|
|
||||||
#include "communication.h"
|
#include "communication.h"
|
||||||
|
#include "gamemodes.h"
|
||||||
|
|
||||||
void setupDebugAPI(AsyncWebServer &server);
|
void setupDebugAPI(AsyncWebServer &server);
|
||||||
|
|
||||||
@@ -16,22 +16,26 @@ void setupDebugAPI(AsyncWebServer &server) {
|
|||||||
|
|
||||||
// DEBUG
|
// DEBUG
|
||||||
server.on("/api/debug/start1", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/api/debug/start1", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
handleStart1(0);
|
// handleStart1(0);
|
||||||
|
IndividualMode("start",2,1,millis());
|
||||||
request->send(200, "text/plain", "handleStart1() called");
|
request->send(200, "text/plain", "handleStart1() called");
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("/api/debug/stop1", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/api/debug/stop1", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
handleStop1(0);
|
// handleStop1(0);
|
||||||
|
IndividualMode("stop",1,1,millis());
|
||||||
request->send(200, "text/plain", "handleStop1() called");
|
request->send(200, "text/plain", "handleStop1() called");
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("/api/debug/start2", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/api/debug/start2", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
handleStart2(0);
|
// handleStart2(0);
|
||||||
|
IndividualMode("start",2,2,millis());
|
||||||
request->send(200, "text/plain", "handleStart2() called");
|
request->send(200, "text/plain", "handleStart2() called");
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("/api/debug/stop2", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/api/debug/stop2", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
handleStop2(0);
|
// handleStop2(0);
|
||||||
|
IndividualMode("stop",1,2,millis());
|
||||||
request->send(200, "text/plain", "handleStop2() called");
|
request->send(200, "text/plain", "handleStop2() called");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
333
src/gamemodes.h
Normal file
333
src/gamemodes.h
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
void publishLaneStatus(int lane, String status);
|
||||||
|
void pushUpdateToFrontend(const String &message);
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <master.h>
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <communication.h>
|
||||||
|
#include <webserverrouter.h>
|
||||||
|
|
||||||
|
void IndividualMode(const char *action, int press, int lane,
|
||||||
|
uint64_t timestamp = 0);
|
||||||
|
void CompetitionMode(const char *action, int press, int lane,
|
||||||
|
uint64_t timestamp = 0);
|
||||||
|
|
||||||
|
void triggerAction(const char *action, int press, int lane,
|
||||||
|
uint64_t _timestamp) {
|
||||||
|
if (gamemode == 0) {
|
||||||
|
Serial.println("Individual Mode aktiv");
|
||||||
|
IndividualMode(action, press, lane, _timestamp);
|
||||||
|
} else if (gamemode == 1) {
|
||||||
|
Serial.println("Wettkampf Mode aktiv");
|
||||||
|
CompetitionMode(action, press, lane, _timestamp);
|
||||||
|
} else {
|
||||||
|
Serial.println("Unbekannter Modus, bitte überprüfen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IndividualMode(const char *action, int press, int lane,
|
||||||
|
uint64_t timestamp) {
|
||||||
|
if (action == "start" && press == 2 && lane == 1) {
|
||||||
|
if (!timerData1.isRunning && timerData1.isReady) {
|
||||||
|
timerData1.isReady = false;
|
||||||
|
timerData1.startTime = (timestamp > 0) ? timestamp : millis();
|
||||||
|
timerData1.localStartTime = millis(); // Set local start time
|
||||||
|
timerData1.isRunning = true;
|
||||||
|
timerData1.endTime = 0;
|
||||||
|
timerData1.isArmed = false; // Reset armed status
|
||||||
|
publishLaneStatus(1, "running");
|
||||||
|
Serial.println("Bahn 1 gestartet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action == "stop" && press == 1 && lane == 1) {
|
||||||
|
if (timerData1.isRunning) {
|
||||||
|
timerData1.endTime = (timestamp > 0) ? timestamp : millis();
|
||||||
|
timerData1.finishedSince = millis(); // Set finished time
|
||||||
|
timerData1.isRunning = false;
|
||||||
|
uint64_t currentTime = timerData1.endTime - timerData1.startTime;
|
||||||
|
|
||||||
|
if (timerData1.bestTime == 0 || currentTime < timerData1.bestTime) {
|
||||||
|
timerData1.bestTime = currentTime;
|
||||||
|
saveBestTimes();
|
||||||
|
}
|
||||||
|
publishLaneStatus(1, "stopped");
|
||||||
|
Serial.println("Bahn 1 gestoppt - Zeit: " + String(currentTime / 1000.0) +
|
||||||
|
"s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action == "start" && press == 2 && lane == 2) {
|
||||||
|
if (!timerData2.isRunning && timerData2.isReady) {
|
||||||
|
timerData2.isReady = false;
|
||||||
|
timerData2.startTime = (timestamp > 0) ? timestamp : millis();
|
||||||
|
timerData2.localStartTime = millis(); // Set local start time
|
||||||
|
timerData2.isRunning = true;
|
||||||
|
timerData2.endTime = 0;
|
||||||
|
timerData2.isArmed = false; // Reset armed status
|
||||||
|
publishLaneStatus(2, "running");
|
||||||
|
Serial.println("Bahn 2 gestartet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action == "stop" && press == 1 && lane == 2) {
|
||||||
|
if (timerData2.isRunning) {
|
||||||
|
timerData2.endTime = (timestamp > 0) ? timestamp : millis();
|
||||||
|
timerData2.finishedSince = millis(); // Set finished time
|
||||||
|
timerData2.isRunning = false;
|
||||||
|
uint64_t currentTime = timerData2.endTime - timerData2.startTime;
|
||||||
|
|
||||||
|
if (timerData2.bestTime == 0 || currentTime < timerData2.bestTime) {
|
||||||
|
timerData2.bestTime = currentTime;
|
||||||
|
saveBestTimes();
|
||||||
|
}
|
||||||
|
publishLaneStatus(2, "stopped");
|
||||||
|
Serial.println("Bahn 2 gestoppt - Zeit: " + String(currentTime / 1000.0) +
|
||||||
|
"s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("Individual Mode Action: %s on Lane %d at %llu\n", action, lane,
|
||||||
|
timestamp);
|
||||||
|
// Implement individual mode logic here
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompetitionMode(const char *action, int press, int lane,
|
||||||
|
uint64_t timestamp) {
|
||||||
|
Serial.printf("Competition Mode Action: %s on Lane %d at %llu\n", action,
|
||||||
|
lane, timestamp);
|
||||||
|
|
||||||
|
int armedAtTime1;
|
||||||
|
int armedAtTime2;
|
||||||
|
int armTimeout = 10000; // Zeit in Millisekunden, die die Bahn armiert bleibt
|
||||||
|
|
||||||
|
if (action == "start" && press == 2 && lane == 1) {
|
||||||
|
if (!timerData1.isRunning && timerData1.isReady) {
|
||||||
|
timerData1.isReady = false;
|
||||||
|
timerData1.isArmed = true; // Set Bahn 1 as armed
|
||||||
|
publishLaneStatus(1, "armed");
|
||||||
|
Serial.println("Bahn 1 armiert");
|
||||||
|
armedAtTime1 = millis(); // Set armed time for Bahn 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action == "start" && press == 2 && lane == 2) {
|
||||||
|
if (!timerData2.isRunning && timerData2.isReady) {
|
||||||
|
timerData2.isReady = false;
|
||||||
|
timerData2.isArmed = true; // Set Bahn 2 as armed
|
||||||
|
publishLaneStatus(2, "armed");
|
||||||
|
Serial.println("Bahn 2 armiert");
|
||||||
|
armedAtTime2 = millis(); // Set armed time for Bahn 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (armedAtTime1 > armedAtTime1 + armTimeout) {
|
||||||
|
timerData1.isArmed = false; // Reset Bahn 1 if armed time exceeded
|
||||||
|
timerData1.isReady = true; // Set Bahn 1 back to ready
|
||||||
|
Serial.println("Bahn 1 automatisch zurückgesetzt (armiert)");
|
||||||
|
publishLaneStatus(1, "ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (armedAtTime2 > armedAtTime2 + armTimeout) {
|
||||||
|
timerData2.isArmed = false; // Reset Bahn 2 if armed time exceeded
|
||||||
|
timerData2.isReady = true; // Set Bahn 2 back to ready
|
||||||
|
Serial.println("Bahn 2 automatisch zurückgesetzt (armiert)");
|
||||||
|
publishLaneStatus(2, "ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timerData1.isArmed && timerData2.isArmed) {
|
||||||
|
sendMQTTMessage("aquacross/competition/toSignal", "armed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((action == "stop" && press == 1 && lane == 1)) {
|
||||||
|
if (timerData1.isRunning) {
|
||||||
|
timerData1.endTime = (timestamp > 0) ? timestamp : millis();
|
||||||
|
timerData1.finishedSince = millis(); // Set finished time
|
||||||
|
timerData1.isRunning = false;
|
||||||
|
uint64_t currentTime1 = timerData1.endTime - timerData1.startTime;
|
||||||
|
|
||||||
|
if (timerData1.bestTime == 0 || currentTime1 < timerData1.bestTime) {
|
||||||
|
timerData1.bestTime = currentTime1;
|
||||||
|
saveBestTimes();
|
||||||
|
}
|
||||||
|
publishLaneStatus(1, "stopped");
|
||||||
|
Serial.println(
|
||||||
|
"Bahn 1 gestoppt - Zeit: " + String(currentTime1 / 1000.0) + "s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action == "stop" && press == 1 && lane == 2) {
|
||||||
|
if (timerData2.isRunning) {
|
||||||
|
timerData2.endTime = (timestamp > 0) ? timestamp : millis();
|
||||||
|
timerData2.finishedSince = millis(); // Set finished time
|
||||||
|
timerData2.isRunning = false;
|
||||||
|
uint64_t currentTime2 = timerData2.endTime - timerData2.startTime;
|
||||||
|
|
||||||
|
if (timerData2.bestTime == 0 || currentTime2 < timerData2.bestTime) {
|
||||||
|
timerData2.bestTime = currentTime2;
|
||||||
|
saveBestTimes();
|
||||||
|
}
|
||||||
|
publishLaneStatus(2, "stopped");
|
||||||
|
Serial.println(
|
||||||
|
"Bahn 2 gestoppt - Zeit: " + String(currentTime2 / 1000.0) + "s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void runCompetition() {
|
||||||
|
if (timerData1.isArmed && timerData2.isArmed && startCompetition) {
|
||||||
|
timerData1.isReady = false;
|
||||||
|
uint64_t startNow = getCurrentTimestampMs();
|
||||||
|
timerData1.startTime = startNow;
|
||||||
|
timerData1.localStartTime = millis(); // Set local start time
|
||||||
|
timerData1.isRunning = true;
|
||||||
|
timerData1.endTime = 0; // Reset end time for Bahn 1
|
||||||
|
timerData1.isArmed = false; // Reset Bahn 1 armed status
|
||||||
|
publishLaneStatus(1, "running");
|
||||||
|
Serial.println("Bahn 1 gestartet");
|
||||||
|
|
||||||
|
timerData2.isReady = false;
|
||||||
|
timerData2.startTime = startNow;
|
||||||
|
timerData2.localStartTime = millis(); // Set local start time
|
||||||
|
timerData2.isRunning = true;
|
||||||
|
timerData2.endTime = 0; // Reset end time for Bahn 2
|
||||||
|
timerData2.isArmed = false; // Reset Bahn 2 armed status
|
||||||
|
publishLaneStatus(2, "running");
|
||||||
|
Serial.println("Bahn 2 gestartet");
|
||||||
|
} else {
|
||||||
|
Serial.println(
|
||||||
|
"Bahn 1 und Bahn 2 müssen armiert sein, um den Wettkampf zu starten.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkAutoReset() {
|
||||||
|
unsigned long currentTime = millis();
|
||||||
|
|
||||||
|
if (gamemode == 0) { // Individual Mode: Bahnen unabhängig zurücksetzen
|
||||||
|
if (!timerData1.isRunning && timerData1.endTime > 0 &&
|
||||||
|
timerData1.finishedSince > 0) {
|
||||||
|
if (currentTime - timerData1.finishedSince > maxTimeDisplay) {
|
||||||
|
timerData1.startTime = 0;
|
||||||
|
timerData1.endTime = 0;
|
||||||
|
timerData1.finishedSince = 0;
|
||||||
|
timerData1.isReady = true;
|
||||||
|
JsonDocument messageDoc;
|
||||||
|
messageDoc["firstname"] = "";
|
||||||
|
messageDoc["lastname"] = "";
|
||||||
|
messageDoc["lane"] = "start1";
|
||||||
|
String message;
|
||||||
|
serializeJson(messageDoc, message);
|
||||||
|
pushUpdateToFrontend(message);
|
||||||
|
publishLaneStatus(1, "ready");
|
||||||
|
Serial.println("Bahn 1 automatisch auf 'Bereit' zurückgesetzt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!timerData2.isRunning && timerData2.endTime > 0 &&
|
||||||
|
timerData2.finishedSince > 0) {
|
||||||
|
if (currentTime - timerData2.finishedSince > maxTimeDisplay) {
|
||||||
|
timerData2.startTime = 0;
|
||||||
|
timerData2.endTime = 0;
|
||||||
|
timerData2.finishedSince = 0;
|
||||||
|
timerData2.isReady = true;
|
||||||
|
JsonDocument messageDoc;
|
||||||
|
messageDoc["firstname"] = "";
|
||||||
|
messageDoc["lastname"] = "";
|
||||||
|
messageDoc["lane"] = "start2";
|
||||||
|
String message;
|
||||||
|
serializeJson(messageDoc, message);
|
||||||
|
pushUpdateToFrontend(message);
|
||||||
|
publishLaneStatus(2, "ready");
|
||||||
|
Serial.println("Bahn 2 automatisch auf 'Bereit' zurückgesetzt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (gamemode ==
|
||||||
|
1) { // Competition Mode: Beide Bahnen gemeinsam zurücksetzen
|
||||||
|
bool bothStopped = !timerData1.isRunning && !timerData2.isRunning &&
|
||||||
|
timerData1.endTime > 0 && timerData2.endTime > 0 &&
|
||||||
|
timerData1.finishedSince > 0 &&
|
||||||
|
timerData2.finishedSince > 0;
|
||||||
|
|
||||||
|
unsigned long latestFinish =
|
||||||
|
timerData1.finishedSince > timerData2.finishedSince
|
||||||
|
? timerData1.finishedSince
|
||||||
|
: timerData2.finishedSince;
|
||||||
|
|
||||||
|
if (bothStopped && (currentTime - latestFinish > maxTimeDisplay)) {
|
||||||
|
// Bahn 1 zurücksetzen
|
||||||
|
timerData1.startTime = 0;
|
||||||
|
timerData1.endTime = 0;
|
||||||
|
timerData1.finishedSince = 0;
|
||||||
|
timerData1.isReady = true;
|
||||||
|
publishLaneStatus(1, "ready");
|
||||||
|
Serial.println("Bahn 1 automatisch auf 'Bereit' zurückgesetzt");
|
||||||
|
|
||||||
|
// Bahn 2 zurücksetzen
|
||||||
|
timerData2.startTime = 0;
|
||||||
|
timerData2.endTime = 0;
|
||||||
|
timerData2.finishedSince = 0;
|
||||||
|
timerData2.isReady = true;
|
||||||
|
publishLaneStatus(2, "ready");
|
||||||
|
Serial.println("Bahn 2 automatisch auf 'Bereit' zurückgesetzt");
|
||||||
|
|
||||||
|
// Optional: Frontend-Update für beide Bahnen
|
||||||
|
for (int lane = 1; lane <= 2; ++lane) {
|
||||||
|
JsonDocument messageDoc;
|
||||||
|
messageDoc["firstname"] = "";
|
||||||
|
messageDoc["lastname"] = "";
|
||||||
|
messageDoc["lane"] = lane == 1 ? "start1" : "start2";
|
||||||
|
String message;
|
||||||
|
serializeJson(messageDoc, message);
|
||||||
|
pushUpdateToFrontend(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getTimerDataJSON() {
|
||||||
|
DynamicJsonDocument doc(1024);
|
||||||
|
|
||||||
|
unsigned long currentTime = millis();
|
||||||
|
// Bahn 1
|
||||||
|
if (timerData1.isRunning) {
|
||||||
|
doc["time1"] = (currentTime - timerData1.localStartTime) / 1000.0;
|
||||||
|
doc["status1"] = "running";
|
||||||
|
} else if (timerData1.endTime > 0) {
|
||||||
|
doc["time1"] = (timerData1.endTime - timerData1.startTime) / 1000.0;
|
||||||
|
doc["status1"] = "finished";
|
||||||
|
} else if (timerData1.isArmed) {
|
||||||
|
doc["time1"] = 0;
|
||||||
|
doc["status1"] = "armed"; // Status für Bahn 1, wenn sie armiert ist
|
||||||
|
} else {
|
||||||
|
doc["time1"] = 0;
|
||||||
|
doc["status1"] = "ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bahn 2
|
||||||
|
if (timerData2.isRunning) {
|
||||||
|
doc["time2"] = (currentTime - timerData2.localStartTime) / 1000.0;
|
||||||
|
doc["status2"] = "running";
|
||||||
|
} else if (timerData2.endTime > 0) {
|
||||||
|
doc["time2"] = (timerData2.endTime - timerData2.startTime) / 1000.0;
|
||||||
|
doc["status2"] = "finished";
|
||||||
|
} else if (timerData2.isArmed) {
|
||||||
|
doc["time2"] = 0;
|
||||||
|
doc["status2"] = "armed"; // Status für Bahn 2, wenn sie armiert ist
|
||||||
|
} else {
|
||||||
|
doc["time2"] = 0;
|
||||||
|
doc["status2"] = "ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beste Zeiten
|
||||||
|
doc["best1"] = timerData1.bestTime / 1000.0;
|
||||||
|
doc["best2"] = timerData2.bestTime / 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;
|
||||||
|
}
|
||||||
253
src/master.cpp
253
src/master.cpp
@@ -16,265 +16,19 @@
|
|||||||
#include <communication.h>
|
#include <communication.h>
|
||||||
#include <databasebackend.h>
|
#include <databasebackend.h>
|
||||||
#include <debug.h>
|
#include <debug.h>
|
||||||
|
#include <gamemodes.h>
|
||||||
#include <licenceing.h>
|
#include <licenceing.h>
|
||||||
#include <rfid.h>
|
#include <rfid.h>
|
||||||
#include <timesync.h>
|
#include <timesync.h>
|
||||||
#include <webserverrouter.h>
|
#include <webserverrouter.h>
|
||||||
#include <wificlass.h>
|
#include <wificlass.h>
|
||||||
|
#include <preferencemanager.h>
|
||||||
|
|
||||||
const char *firmwareversion = "1.0.0"; // Version der Firmware
|
const char *firmwareversion = "1.0.0"; // Version der Firmware
|
||||||
|
|
||||||
void handleStart1(uint64_t timestamp = 0) {
|
// moved to preferencemanager.h
|
||||||
if (!timerData.isRunning1 && timerData.isReady1) {
|
|
||||||
timerData.isReady1 = false;
|
|
||||||
timerData.startTime1 = (timestamp > 0) ? timestamp : millis();
|
|
||||||
timerData.localStartTime1 = millis(); // Set local start time
|
|
||||||
timerData.isRunning1 = true;
|
|
||||||
timerData.endTime1 = 0;
|
|
||||||
publishLaneStatus(1, "running");
|
|
||||||
Serial.println("Bahn 1 gestartet");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleStop1(uint64_t timestamp = 0) {
|
|
||||||
if (timerData.isRunning1) {
|
|
||||||
timerData.endTime1 = (timestamp > 0) ? timestamp : millis();
|
|
||||||
timerData.finishedSince1 = millis(); // Set finished time
|
|
||||||
timerData.isRunning1 = false;
|
|
||||||
unsigned long currentTime = timerData.endTime1 - timerData.startTime1;
|
|
||||||
|
|
||||||
if (timerData.bestTime1 == 0 || currentTime < timerData.bestTime1) {
|
|
||||||
timerData.bestTime1 = currentTime;
|
|
||||||
saveBestTimes();
|
|
||||||
}
|
|
||||||
publishLaneStatus(1, "stopped");
|
|
||||||
Serial.println("Bahn 1 gestoppt - Zeit: " + String(currentTime / 1000.0) +
|
|
||||||
"s");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleStart2(uint64_t timestamp = 0) {
|
|
||||||
if (!timerData.isRunning2 && timerData.isReady2) {
|
|
||||||
timerData.isReady2 = false;
|
|
||||||
timerData.startTime2 = (timestamp > 0) ? timestamp : millis();
|
|
||||||
timerData.localStartTime2 = millis(); // Set local start time
|
|
||||||
timerData.isRunning2 = true;
|
|
||||||
timerData.endTime2 = 0;
|
|
||||||
publishLaneStatus(2, "running");
|
|
||||||
Serial.println("Bahn 2 gestartet");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleStop2(uint64_t timestamp = 0) {
|
|
||||||
if (timerData.isRunning2) {
|
|
||||||
timerData.endTime2 = (timestamp > 0) ? timestamp : millis();
|
|
||||||
timerData.finishedSince2 = millis(); // Set finished time
|
|
||||||
timerData.isRunning2 = false;
|
|
||||||
unsigned long currentTime = timerData.endTime2 - timerData.startTime2;
|
|
||||||
|
|
||||||
if (timerData.bestTime2 == 0 || currentTime < timerData.bestTime2) {
|
|
||||||
timerData.bestTime2 = currentTime;
|
|
||||||
saveBestTimes();
|
|
||||||
}
|
|
||||||
publishLaneStatus(2, "stopped");
|
|
||||||
Serial.println("Bahn 2 gestoppt - Zeit: " + String(currentTime / 1000.0) +
|
|
||||||
"s");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void checkAutoReset() {
|
|
||||||
unsigned long currentTime = millis();
|
|
||||||
|
|
||||||
if (timerData.isRunning1 &&
|
|
||||||
(currentTime - timerData.localStartTime1 > maxTimeBeforeReset)) {
|
|
||||||
timerData.isRunning1 = false;
|
|
||||||
timerData.startTime1 = 0;
|
|
||||||
publishLaneStatus(1, "ready");
|
|
||||||
Serial.println("Bahn 1 automatisch zurückgesetzt");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timerData.isRunning2 &&
|
|
||||||
(currentTime - timerData.localStartTime2 > maxTimeBeforeReset)) {
|
|
||||||
timerData.isRunning2 = false;
|
|
||||||
timerData.startTime2 = 0;
|
|
||||||
publishLaneStatus(2, "ready");
|
|
||||||
Serial.println("Bahn 2 automatisch zurückgesetzt");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Automatischer Reset nach 10 Sekunden "Beendet"
|
|
||||||
if (!timerData.isRunning1 && timerData.endTime1 > 0 &&
|
|
||||||
timerData.finishedSince1 > 0) {
|
|
||||||
if (millis() - timerData.finishedSince1 > maxTimeDisplay) {
|
|
||||||
timerData.startTime1 = 0;
|
|
||||||
timerData.endTime1 = 0;
|
|
||||||
timerData.finishedSince1 = 0;
|
|
||||||
timerData.isReady1 = true; // Zurücksetzen auf "Bereit"
|
|
||||||
|
|
||||||
JsonDocument messageDoc;
|
|
||||||
messageDoc["firstname"] = "";
|
|
||||||
messageDoc["lastname"] = "";
|
|
||||||
messageDoc["lane"] = "start1"; // Add lane information
|
|
||||||
|
|
||||||
String message;
|
|
||||||
serializeJson(messageDoc, message);
|
|
||||||
|
|
||||||
// Push the message to the frontend
|
|
||||||
pushUpdateToFrontend(message);
|
|
||||||
publishLaneStatus(1, "ready");
|
|
||||||
|
|
||||||
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;
|
|
||||||
timerData.isReady2 = true; // Zurücksetzen auf "Bereit"
|
|
||||||
|
|
||||||
JsonDocument messageDoc;
|
|
||||||
messageDoc["firstname"] = "";
|
|
||||||
messageDoc["lastname"] = "";
|
|
||||||
messageDoc["lane"] = "start2"; // Add lane information
|
|
||||||
|
|
||||||
String message;
|
|
||||||
serializeJson(messageDoc, message);
|
|
||||||
|
|
||||||
// Push the message to the frontend
|
|
||||||
pushUpdateToFrontend(message);
|
|
||||||
publishLaneStatus(2, "ready");
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveWifiSettings() {
|
|
||||||
preferences.begin("wifi", false);
|
|
||||||
preferences.putString("ssid", ssidSTA);
|
|
||||||
preferences.putString("password", passwordSTA);
|
|
||||||
preferences.end();
|
|
||||||
delay(500); // Warte 2 Sekunden, bevor der Neustart erfolgt
|
|
||||||
ESP.restart(); // Neustart des ESP32
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadLocationSettings() {
|
|
||||||
preferences.begin("location", true);
|
|
||||||
masterlocation = preferences.getString("location", "");
|
|
||||||
preferences.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveLocationSettings() {
|
|
||||||
preferences.begin("location", false);
|
|
||||||
preferences.putString("location", masterlocation);
|
|
||||||
preferences.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadWifiSettings() {
|
|
||||||
preferences.begin("wifi", true);
|
|
||||||
String ssid = preferences.getString("ssid", "");
|
|
||||||
String password = preferences.getString("password", "");
|
|
||||||
ssidSTA = strdup(ssid.c_str());
|
|
||||||
passwordSTA = strdup(password.c_str());
|
|
||||||
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.localStartTime1) / 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.localStartTime2) / 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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@@ -298,6 +52,7 @@ void setup() {
|
|||||||
loadWifiSettings();
|
loadWifiSettings();
|
||||||
loadLocationSettings();
|
loadLocationSettings();
|
||||||
|
|
||||||
|
|
||||||
setupWifi(); // WiFi initialisieren
|
setupWifi(); // WiFi initialisieren
|
||||||
setupOTA(&server);
|
setupOTA(&server);
|
||||||
|
|
||||||
|
|||||||
47
src/master.h
47
src/master.h
@@ -11,22 +11,28 @@ const char *passwordAP = nullptr;
|
|||||||
char *ssidSTA = nullptr;
|
char *ssidSTA = nullptr;
|
||||||
char *passwordSTA = nullptr;
|
char *passwordSTA = nullptr;
|
||||||
|
|
||||||
// Timer Struktur
|
// Timer Struktur für Bahn 1
|
||||||
struct TimerData {
|
struct TimerData1 {
|
||||||
unsigned long startTime1 = 0;
|
unsigned long startTime = 0;
|
||||||
unsigned long startTime2 = 0;
|
unsigned long localStartTime = 0;
|
||||||
unsigned long localStartTime1 = 0;
|
unsigned long finishedSince = 0;
|
||||||
unsigned long localStartTime2 = 0;
|
unsigned long endTime = 0;
|
||||||
unsigned long finishedSince1 = 0;
|
unsigned long bestTime = 0;
|
||||||
unsigned long finishedSince2 = 0;
|
bool isRunning = false;
|
||||||
unsigned long endTime1 = 0;
|
bool isReady = true; // Status für Bahn 1
|
||||||
unsigned long endTime2 = 0;
|
bool isArmed = false; // Status für Bahn 1 (armiert/nicht armiert)
|
||||||
unsigned long bestTime1 = 0;
|
};
|
||||||
unsigned long bestTime2 = 0;
|
|
||||||
bool isRunning1 = false;
|
// Timer Struktur für Bahn 2
|
||||||
bool isRunning2 = false;
|
struct TimerData2 {
|
||||||
bool isReady1 = true; // Status für Bahn 1
|
unsigned long startTime = 0;
|
||||||
bool isReady2 = true; // Status für Bahn 2
|
unsigned long localStartTime = 0;
|
||||||
|
unsigned long finishedSince = 0;
|
||||||
|
unsigned long endTime = 0;
|
||||||
|
unsigned long bestTime = 0;
|
||||||
|
bool isRunning = false;
|
||||||
|
bool isReady = true; // Status für Bahn 2
|
||||||
|
bool isArmed = false; // Status für Bahn 2 (armiert/nicht armiert)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Button Konfiguration
|
// Button Konfiguration
|
||||||
@@ -48,7 +54,8 @@ struct ButtonConfigs {
|
|||||||
extern const char *firmwareversion;
|
extern const char *firmwareversion;
|
||||||
|
|
||||||
// Globale Variablen
|
// Globale Variablen
|
||||||
TimerData timerData;
|
TimerData1 timerData1;
|
||||||
|
TimerData2 timerData2;
|
||||||
ButtonConfigs buttonConfigs;
|
ButtonConfigs buttonConfigs;
|
||||||
bool learningMode = false;
|
bool learningMode = false;
|
||||||
int learningStep = 0; // 0=Start1, 1=Stop1, 2=Start2, 3=Stop2
|
int learningStep = 0; // 0=Start1, 1=Stop1, 2=Start2, 3=Stop2
|
||||||
@@ -56,15 +63,13 @@ unsigned long maxTimeBeforeReset = 300000; // 5 Minuten default
|
|||||||
unsigned long maxTimeDisplay = 20000; // 20 Sekunden Standard (in ms)
|
unsigned long maxTimeDisplay = 20000; // 20 Sekunden Standard (in ms)
|
||||||
bool wifimodeAP = false; // AP-Modus deaktiviert
|
bool wifimodeAP = false; // AP-Modus deaktiviert
|
||||||
String masterlocation;
|
String masterlocation;
|
||||||
|
int gamemode; // 0=Individual, 1=Wettkampf
|
||||||
|
bool startCompetition = false; // Flag, ob der Timer gestartet wurde
|
||||||
|
|
||||||
// Function Declarations
|
// Function Declarations
|
||||||
void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len);
|
void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len);
|
||||||
void handleLearningMode(const uint8_t *mac);
|
void handleLearningMode(const uint8_t *mac);
|
||||||
void handleStartLearning();
|
void handleStartLearning();
|
||||||
void handleStart1(uint64_t timestamp);
|
|
||||||
void handleStop1(uint64_t timestamp);
|
|
||||||
void handleStart2(uint64_t timestamp);
|
|
||||||
void handleStop2(uint64_t timestamp);
|
|
||||||
void checkAutoReset();
|
void checkAutoReset();
|
||||||
void saveButtonConfig();
|
void saveButtonConfig();
|
||||||
void loadButtonConfig();
|
void loadButtonConfig();
|
||||||
|
|||||||
107
src/preferencemanager.h
Normal file
107
src/preferencemanager.h
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
#include <master.h>
|
||||||
|
#include <licenceing.h>
|
||||||
|
|
||||||
|
// Persist and load button configuration
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist and load best times
|
||||||
|
void saveBestTimes() {
|
||||||
|
preferences.begin("times", false);
|
||||||
|
preferences.putULong("best1", timerData1.bestTime);
|
||||||
|
preferences.putULong("best2", timerData2.bestTime);
|
||||||
|
preferences.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadBestTimes() {
|
||||||
|
preferences.begin("times", true);
|
||||||
|
timerData1.bestTime = preferences.getULong("best1", 0);
|
||||||
|
timerData2.bestTime = preferences.getULong("best2", 0);
|
||||||
|
preferences.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist and load general settings
|
||||||
|
void saveSettings() {
|
||||||
|
preferences.begin("settings", false);
|
||||||
|
preferences.putULong("maxTime", maxTimeBeforeReset);
|
||||||
|
preferences.putULong("maxTimeDisplay", maxTimeDisplay);
|
||||||
|
preferences.putUInt("gamemode", gamemode);
|
||||||
|
preferences.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadSettings() {
|
||||||
|
preferences.begin("settings", true);
|
||||||
|
maxTimeBeforeReset = preferences.getULong("maxTime", 300000);
|
||||||
|
maxTimeDisplay = preferences.getULong("maxTimeDisplay", 20000);
|
||||||
|
gamemode = preferences.getUInt("gamemode", 0);
|
||||||
|
preferences.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist and load WiFi settings
|
||||||
|
void saveWifiSettings() {
|
||||||
|
preferences.begin("wifi", false);
|
||||||
|
preferences.putString("ssid", ssidSTA);
|
||||||
|
preferences.putString("password", passwordSTA);
|
||||||
|
preferences.end();
|
||||||
|
delay(500);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadWifiSettings() {
|
||||||
|
preferences.begin("wifi", true);
|
||||||
|
String ssid = preferences.getString("ssid", "");
|
||||||
|
String password = preferences.getString("password", "");
|
||||||
|
ssidSTA = strdup(ssid.c_str());
|
||||||
|
passwordSTA = strdup(password.c_str());
|
||||||
|
preferences.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist and load location settings
|
||||||
|
void loadLocationSettings() {
|
||||||
|
preferences.begin("location", true);
|
||||||
|
masterlocation = preferences.getString("location", "");
|
||||||
|
preferences.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveLocationSettings() {
|
||||||
|
preferences.begin("location", false);
|
||||||
|
preferences.putString("location", masterlocation);
|
||||||
|
preferences.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Licence helper
|
||||||
|
int checkLicence() {
|
||||||
|
loadLicenceFromPrefs();
|
||||||
|
String id = getUniqueDeviceID();
|
||||||
|
int tier = getLicenseTier(id, licence);
|
||||||
|
return tier;
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveLocationIdToPrefs(const String &locationId) {
|
||||||
|
preferences.begin("locationid", false);
|
||||||
|
preferences.putString("locationid", locationId);
|
||||||
|
preferences.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
String getLocationIdFromPrefs() {
|
||||||
|
preferences.begin("locationid", true);
|
||||||
|
String locationId = preferences.getString("locationid", "");
|
||||||
|
preferences.end();
|
||||||
|
return locationId;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ void sendMQTTMessage(const char *topic, const char *message);
|
|||||||
|
|
||||||
#include "communication.h"
|
#include "communication.h"
|
||||||
#include <buttonassigh.h>
|
#include <buttonassigh.h>
|
||||||
|
#include <gamemodes.h>
|
||||||
#include <wificlass.h>
|
#include <wificlass.h>
|
||||||
|
|
||||||
AsyncWebServer server(80);
|
AsyncWebServer server(80);
|
||||||
@@ -52,8 +53,8 @@ void setupRoutes() {
|
|||||||
|
|
||||||
server.on("/api/reset-best", HTTP_POST, [](AsyncWebServerRequest *request) {
|
server.on("/api/reset-best", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||||
Serial.println("/api/reset-best called");
|
Serial.println("/api/reset-best called");
|
||||||
timerData.bestTime1 = 0;
|
timerData1.bestTime = 0;
|
||||||
timerData.bestTime2 = 0;
|
timerData2.bestTime = 0;
|
||||||
saveBestTimes();
|
saveBestTimes();
|
||||||
DynamicJsonDocument doc(64);
|
DynamicJsonDocument doc(64);
|
||||||
doc["success"] = true;
|
doc["success"] = true;
|
||||||
@@ -280,6 +281,40 @@ void setupRoutes() {
|
|||||||
request->send(200, "application/json", "{\"success\":true}");
|
request->send(200, "application/json", "{\"success\":true}");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.on("/api/set-mode", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||||
|
Serial.println("/api/set-mode called");
|
||||||
|
|
||||||
|
String mode;
|
||||||
|
if (request->hasParam("mode", true)) {
|
||||||
|
mode = request->getParam("mode", true)->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode.length() > 0) {
|
||||||
|
// Speichere den Modus
|
||||||
|
gamemode = mode == "individual" ? 0 : 1;
|
||||||
|
Serial.printf("Operational mode set to: %s\n",
|
||||||
|
gamemode == 0 ? "Individual" : "Wettkampf");
|
||||||
|
// Rückmeldung
|
||||||
|
DynamicJsonDocument doc(64);
|
||||||
|
doc["success"] = true;
|
||||||
|
String result;
|
||||||
|
serializeJson(doc, result);
|
||||||
|
request->send(200, "application/json", result);
|
||||||
|
saveSettings();
|
||||||
|
} else {
|
||||||
|
request->send(400, "application/json",
|
||||||
|
"{\"success\":false,\"error\":\"Modus fehlt\"}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("/api/get-mode", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
|
DynamicJsonDocument doc(32);
|
||||||
|
doc["mode"] = gamemode == 0 ? "individual" : "wettkampf";
|
||||||
|
String result;
|
||||||
|
serializeJson(doc, result);
|
||||||
|
request->send(200, "application/json", result);
|
||||||
|
});
|
||||||
|
|
||||||
// Statische Dateien
|
// Statische Dateien
|
||||||
server.serveStatic("/", SPIFFS, "/");
|
server.serveStatic("/", SPIFFS, "/");
|
||||||
server.begin();
|
server.begin();
|
||||||
|
|||||||
Reference in New Issue
Block a user