init
This commit is contained in:
commit
ec0d7d63de
54
.vscode/settings.json
vendored
Normal file
54
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"algorithm": "cpp",
|
||||||
|
"vector": "cpp",
|
||||||
|
"cmath": "cpp",
|
||||||
|
"array": "cpp",
|
||||||
|
"atomic": "cpp",
|
||||||
|
"*.tcc": "cpp",
|
||||||
|
"bitset": "cpp",
|
||||||
|
"cctype": "cpp",
|
||||||
|
"clocale": "cpp",
|
||||||
|
"cstdarg": "cpp",
|
||||||
|
"cstddef": "cpp",
|
||||||
|
"cstdint": "cpp",
|
||||||
|
"cstdio": "cpp",
|
||||||
|
"cstdlib": "cpp",
|
||||||
|
"cstring": "cpp",
|
||||||
|
"ctime": "cpp",
|
||||||
|
"cwchar": "cpp",
|
||||||
|
"cwctype": "cpp",
|
||||||
|
"deque": "cpp",
|
||||||
|
"unordered_map": "cpp",
|
||||||
|
"unordered_set": "cpp",
|
||||||
|
"exception": "cpp",
|
||||||
|
"functional": "cpp",
|
||||||
|
"iterator": "cpp",
|
||||||
|
"map": "cpp",
|
||||||
|
"memory": "cpp",
|
||||||
|
"memory_resource": "cpp",
|
||||||
|
"numeric": "cpp",
|
||||||
|
"optional": "cpp",
|
||||||
|
"random": "cpp",
|
||||||
|
"regex": "cpp",
|
||||||
|
"string": "cpp",
|
||||||
|
"string_view": "cpp",
|
||||||
|
"system_error": "cpp",
|
||||||
|
"tuple": "cpp",
|
||||||
|
"type_traits": "cpp",
|
||||||
|
"utility": "cpp",
|
||||||
|
"fstream": "cpp",
|
||||||
|
"initializer_list": "cpp",
|
||||||
|
"iomanip": "cpp",
|
||||||
|
"iosfwd": "cpp",
|
||||||
|
"istream": "cpp",
|
||||||
|
"limits": "cpp",
|
||||||
|
"new": "cpp",
|
||||||
|
"ostream": "cpp",
|
||||||
|
"sstream": "cpp",
|
||||||
|
"stdexcept": "cpp",
|
||||||
|
"streambuf": "cpp",
|
||||||
|
"cinttypes": "cpp",
|
||||||
|
"typeinfo": "cpp"
|
||||||
|
}
|
||||||
|
}
|
162
README.md
Normal file
162
README.md
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
# FilaMan - Filament Management System
|
||||||
|
|
||||||
|
A comprehensive filament management system combining ESP32-based hardware for weight measurement and NFC tag reading/writing with a web interface for managing filament spools in conjunction with Bambu Lab AMS and Spoolman.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
FilaMan is designed to streamline the management of filament spools for 3D printing. The system consists of an ESP32 microcontroller that handles weight measurement and NFC tag operations, and a web interface that allows users to manage filament spools, monitor AMS (Automatic Material System) status, and interact with Spoolman and Bambu Lab printers.
|
||||||
|
|
||||||
|
### ESP32 Hardware Features
|
||||||
|
- **Weight Measurement:** Using a load cell with HX711 amplifier for precise weight tracking.
|
||||||
|
- **NFC Tag Reading/Writing:** PN532 module for reading and writing filament data to NFC tags.
|
||||||
|
- **OLED Display:** Shows current weight, connection status (WiFi, Bambu Lab, Spoolman).
|
||||||
|
- **WiFi Connectivity:** WiFiManager for easy network configuration.
|
||||||
|
- **MQTT Integration:** Connects to Bambu Lab printer for AMS control.
|
||||||
|
- **Data Persistence:** Stores calibration data in EEPROM.
|
||||||
|
- **Watchdog Timer:** Ensures system stability.
|
||||||
|
|
||||||
|
### Web Interface Features
|
||||||
|
- **Real-time Updates:** WebSocket connection for live data updates.
|
||||||
|
- **NFC Tag Management:** Write filament data to NFC tags.
|
||||||
|
- **AMS Integration:**
|
||||||
|
- Display current AMS tray contents.
|
||||||
|
- Assign filaments to AMS slots.
|
||||||
|
- Support for external spool holder.
|
||||||
|
- **Spoolman Integration:**
|
||||||
|
- List available filament spools.
|
||||||
|
- Filter and select filaments.
|
||||||
|
- Update spool weights automatically.
|
||||||
|
- Track NFC tag assignments.
|
||||||
|
|
||||||
|
## Detailed Functionality
|
||||||
|
|
||||||
|
### ESP32 Functionality
|
||||||
|
- **Control and Monitor Print Jobs:** The ESP32 communicates with the Bambu Lab printer to control and monitor print jobs.
|
||||||
|
- **Printer Communication:** Uses MQTT for real-time communication with the printer.
|
||||||
|
- **User Interactions:** The OLED display provides immediate feedback on the system status, including weight measurements and connection status.
|
||||||
|
|
||||||
|
### Web Interface Functionality
|
||||||
|
- **User Interactions:** The web interface allows users to interact with the system, select filaments, write NFC tags, and monitor AMS status.
|
||||||
|
- **UI Elements:** Includes dropdowns for selecting manufacturers and filaments, buttons for writing NFC tags, and real-time status indicators.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- **Software:**
|
||||||
|
- [PlatformIO](https://platformio.org/) in VS Code
|
||||||
|
- [Spoolman](https://github.com/Donkie/Spoolman) instance
|
||||||
|
- Bambu Lab printer (optional for AMS integration)
|
||||||
|
- **Hardware:**
|
||||||
|
- ESP32 Development Board
|
||||||
|
- HX711 Load Cell Amplifier
|
||||||
|
- Load Cell (weight sensor)
|
||||||
|
- OLED Display (128x64 SSD1306)
|
||||||
|
- PN532 NFC Module
|
||||||
|
- Connecting wires
|
||||||
|
|
||||||
|
### Step-by-Step Installation
|
||||||
|
1. **Clone the Repository:**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/FilaMan.git
|
||||||
|
cd FilaMan
|
||||||
|
```
|
||||||
|
2. **Install Dependencies:**
|
||||||
|
```bash
|
||||||
|
pio lib install
|
||||||
|
```
|
||||||
|
3. **Flash the ESP32:**
|
||||||
|
```bash
|
||||||
|
pio run --target upload
|
||||||
|
```
|
||||||
|
4. **Initial Setup:**
|
||||||
|
- Connect to the "FilaMan" WiFi access point.
|
||||||
|
- Configure WiFi settings through the captive portal.
|
||||||
|
- Access the web interface at `http://filaman.local` or the IP address.
|
||||||
|
|
||||||
|
## Hardware Requirements
|
||||||
|
|
||||||
|
### Components
|
||||||
|
- **ESP32 Development Board:** Any ESP32 variant.
|
||||||
|
- **HX711 Load Cell Amplifier:** For weight measurement.
|
||||||
|
- **Load Cell:** Weight sensor.
|
||||||
|
- **OLED Display:** 128x64 SSD1306.
|
||||||
|
- **PN532 NFC Module:** For NFC tag operations.
|
||||||
|
- **Connecting Wires:** For connections.
|
||||||
|
|
||||||
|
### Pin Configuration
|
||||||
|
| Component | ESP32 Pin |
|
||||||
|
|-------------------|-----------|
|
||||||
|
| HX711 DOUT | 16 |
|
||||||
|
| HX711 SCK | 17 |
|
||||||
|
| OLED SDA | 21 |
|
||||||
|
| OLED SCL | 22 |
|
||||||
|
| PN532 IRQ | 32 |
|
||||||
|
| PN532 RESET | 33 |
|
||||||
|
|
||||||
|
## Software Dependencies
|
||||||
|
|
||||||
|
### ESP32 Libraries
|
||||||
|
- `WiFiManager`: Network configuration
|
||||||
|
- `ESPAsyncWebServer`: Web server functionality
|
||||||
|
- `ArduinoJson`: JSON parsing and creation
|
||||||
|
- `PubSubClient`: MQTT communication
|
||||||
|
- `Adafruit_PN532`: NFC functionality
|
||||||
|
- `Adafruit_SSD1306`: OLED display control
|
||||||
|
- `HX711`: Load cell communication
|
||||||
|
|
||||||
|
### External Services
|
||||||
|
- **Bambu Lab Printer:** For AMS integration.
|
||||||
|
- **Spoolman:** For filament management.
|
||||||
|
|
||||||
|
## API Communication
|
||||||
|
|
||||||
|
### Spoolman Integration
|
||||||
|
The system communicates with Spoolman using its REST API for:
|
||||||
|
- Fetching spool information.
|
||||||
|
- Updating spool weights.
|
||||||
|
- Managing NFC tag assignments.
|
||||||
|
|
||||||
|
### Data Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "2.0",
|
||||||
|
"protocol": "openspool",
|
||||||
|
"color_hex": "FFFFFF",
|
||||||
|
"type": "PLA",
|
||||||
|
"min_temp": 200,
|
||||||
|
"max_temp": 220,
|
||||||
|
"brand": "Vendor",
|
||||||
|
"sm_id": "1234"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### Relevant Links
|
||||||
|
- [PlatformIO Documentation](https://docs.platformio.org/)
|
||||||
|
- [Spoolman Documentation](https://github.com/Donkie/Spoolman)
|
||||||
|
- [Bambu Lab Printer Documentation](https://www.bambulab.com/)
|
||||||
|
|
||||||
|
### Tutorials and Examples
|
||||||
|
- [PlatformIO Getting Started](https://docs.platformio.org/en/latest/tutorials/espressif32/arduino_debugging_unit_testing.html)
|
||||||
|
- [ESP32 Web Server Tutorial](https://randomnerdtutorials.com/esp32-web-server-arduino-ide/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Materials
|
||||||
|
|
||||||
|
### Useful Resources
|
||||||
|
- [ESP32 Official Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/)
|
||||||
|
- [Arduino Libraries](https://www.arduino.cc/en/Reference/Libraries)
|
||||||
|
- [NFC Tag Information](https://learn.adafruit.com/adafruit-pn532-rfid-nfc/overview)
|
||||||
|
|
||||||
|
### Community and Support
|
||||||
|
- [PlatformIO Community](https://community.platformio.org/)
|
||||||
|
- [Arduino Forum](https://forum.arduino.cc/)
|
||||||
|
- [ESP32 Forum](https://www.esp32.com/)
|
||||||
|
|
||||||
|
## Availability
|
||||||
|
|
||||||
|
The code can be tested and the application can be downloaded from the [GitHub repository](https://github.com/yourusername/FilaMan).
|
21
ca.cert
Normal file
21
ca.cert
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDZTCCAk2gAwIBAgIUV1FckwXElyek1onFnQ9kL7Bk4N8wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwQjELMAkGA1UEBhMCQ04xIjAgBgNVBAoMGUJCTCBUZWNobm9sb2dpZXMgQ28u
|
||||||
|
LCBMdGQxDzANBgNVBAMMBkJCTCBDQTAeFw0yMjA0MDQwMzQyMTFaFw0zMjA0MDEw
|
||||||
|
MzQyMTFaMEIxCzAJBgNVBAYTAkNOMSIwIAYDVQQKDBlCQkwgVGVjaG5vbG9naWVz
|
||||||
|
IENvLiwgTHRkMQ8wDQYDVQQDDAZCQkwgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||||
|
DwAwggEKAoIBAQDL3pnDdxGOk5Z6vugiT4dpM0ju+3Xatxz09UY7mbj4tkIdby4H
|
||||||
|
oeEdiYSZjc5LJngJuCHwtEbBJt1BriRdSVrF6M9D2UaBDyamEo0dxwSaVxZiDVWC
|
||||||
|
eeCPdELpFZdEhSNTaT4O7zgvcnFsfHMa/0vMAkvE7i0qp3mjEzYLfz60axcDoJLk
|
||||||
|
p7n6xKXI+cJbA4IlToFjpSldPmC+ynOo7YAOsXt7AYKY6Glz0BwUVzSJxU+/+VFy
|
||||||
|
/QrmYGNwlrQtdREHeRi0SNK32x1+bOndfJP0sojuIrDjKsdCLye5CSZIvqnbowwW
|
||||||
|
1jRwZgTBR29Zp2nzCoxJYcU9TSQp/4KZuWNVAgMBAAGjUzBRMB0GA1UdDgQWBBSP
|
||||||
|
NEJo3GdOj8QinsV8SeWr3US+HjAfBgNVHSMEGDAWgBSPNEJo3GdOj8QinsV8SeWr
|
||||||
|
3US+HjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQABlBIT5ZeG
|
||||||
|
fgcK1LOh1CN9sTzxMCLbtTPFF1NGGA13mApu6j1h5YELbSKcUqfXzMnVeAb06Htu
|
||||||
|
3CoCoe+wj7LONTFO++vBm2/if6Jt/DUw1CAEcNyqeh6ES0NX8LJRVSe0qdTxPJuA
|
||||||
|
BdOoo96iX89rRPoxeed1cpq5hZwbeka3+CJGV76itWp35Up5rmmUqrlyQOr/Wax6
|
||||||
|
itosIzG0MfhgUzU51A2P/hSnD3NDMXv+wUY/AvqgIL7u7fbDKnku1GzEKIkfH8hm
|
||||||
|
Rs6d8SCU89xyrwzQ0PR853irHas3WrHVqab3P+qNwR0YirL0Qk7Xt/q3O1griNg2
|
||||||
|
Blbjg3obpHo9
|
||||||
|
-----END CERTIFICATE-----
|
0
display.cpp
Normal file
0
display.cpp
Normal file
3255
docs/ndef.md
Normal file
3255
docs/ndef.md
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/specification_ndef.pdf
Normal file
BIN
docs/specification_ndef.pdf
Normal file
Binary file not shown.
6
extra_script.py
Normal file
6
extra_script.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Import("env")
|
||||||
|
|
||||||
|
# Hook in die Build-Prozesse
|
||||||
|
env.AddPreAction("uploadfs", env.VerboseAction("$PROJECT_DIR/scripts/buildfs.sh", "Building Filesystem Image..."))
|
||||||
|
|
||||||
|
env.AddPreAction("upload", env.VerboseAction("$PROJECT_DIR/scripts/uploadfs.sh", "Uploading Filesystem Image..."))
|
45
gzip_files.py
Normal file
45
gzip_files.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import gzip
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
## gzip files
|
||||||
|
|
||||||
|
def compress_file(input_file, output_file):
|
||||||
|
with open(input_file, 'rb') as f_in:
|
||||||
|
with gzip.open(output_file, 'wb') as f_out:
|
||||||
|
f_out.writelines(f_in)
|
||||||
|
|
||||||
|
def copy_file(input_file, output_file):
|
||||||
|
shutil.copy2(input_file, output_file)
|
||||||
|
|
||||||
|
def should_compress(file):
|
||||||
|
# Komprimiere nur bestimmte Dateitypen
|
||||||
|
return file.endswith(('.js', '.png', '.css'))
|
||||||
|
|
||||||
|
def main(source_dir, target_dir):
|
||||||
|
for root, dirs, files in os.walk(source_dir):
|
||||||
|
rel_path = os.path.relpath(root, source_dir)
|
||||||
|
for file in files:
|
||||||
|
input_file = os.path.join(root, file)
|
||||||
|
output_file_compressed = os.path.join(target_dir, rel_path, file + '.gz')
|
||||||
|
output_file_original = os.path.join(target_dir, rel_path, file)
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(output_file_compressed), exist_ok=True)
|
||||||
|
|
||||||
|
if should_compress(file):
|
||||||
|
compress_file(input_file, output_file_compressed)
|
||||||
|
print(f'Compressed {input_file} to {output_file_compressed}')
|
||||||
|
else:
|
||||||
|
copy_file(input_file, output_file_original)
|
||||||
|
print(f'Copied {input_file} to {output_file_original}')
|
||||||
|
|
||||||
|
def init():
|
||||||
|
source_dir = 'html'
|
||||||
|
target_dir = 'data'
|
||||||
|
|
||||||
|
if os.path.exists(target_dir):
|
||||||
|
shutil.rmtree(target_dir)
|
||||||
|
|
||||||
|
main(source_dir, target_dir)
|
||||||
|
|
||||||
|
init()
|
70
html/bambu_filaments.json
Normal file
70
html/bambu_filaments.json
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"GFU99": "Generic TPU",
|
||||||
|
"GFN99": "Generic PA",
|
||||||
|
"GFN98": "Generic PA-CF",
|
||||||
|
"GFA01": "Bambu PLA Matte",
|
||||||
|
"GFA00": "Bambu PLA Basic",
|
||||||
|
"GFA09": "Bambu PLA Tough",
|
||||||
|
"GFA07": "Bambu PLA Marble",
|
||||||
|
"GFA08": "Bambu PLA Sparkle",
|
||||||
|
"GFA02": "Bambu PLA Metal",
|
||||||
|
"GFA05": "Bambu PLA Silk",
|
||||||
|
"GFS00": "Bambu Support W",
|
||||||
|
"GFL03": "eSUN PLA+",
|
||||||
|
"GFL01": "PolyTerra PLA",
|
||||||
|
"GFL00": "PolyLite PLA",
|
||||||
|
"GFL99": "Generic PLA",
|
||||||
|
"GFL96": "Generic PLA Silk",
|
||||||
|
"GFL98": "Generic PLA-CF",
|
||||||
|
"GFA50": "Bambu PLA-CF",
|
||||||
|
"GFS02": "Bambu Support For PLA",
|
||||||
|
"GFA11": "Bambu PLA Aero",
|
||||||
|
"GFL04": "Overture PLA",
|
||||||
|
"GFL05": "Overture Matte PLA",
|
||||||
|
"GFL95": "Generic PLA High Speed",
|
||||||
|
"GFA12": "Bambu PLA Glow",
|
||||||
|
"GFA13": "Bambu PLA Dynamic",
|
||||||
|
"GFA15": "Bambu PLA Galaxy",
|
||||||
|
"GFS05": "Bambu Support For PLA/PETG",
|
||||||
|
"GFU01": "Bambu TPU 95A",
|
||||||
|
"GFU00": "Bambu TPU 95A HF",
|
||||||
|
"GFG00": "Bambu PETG Basic",
|
||||||
|
"GFT01": "Bambu PET-CF",
|
||||||
|
"GFG99": "Generic PETG",
|
||||||
|
"GFG98": "Generic PETG-CF",
|
||||||
|
"GFG50": "Bambu PETG-CF",
|
||||||
|
"GFG60": "PolyLite PETG",
|
||||||
|
"GFG01": "Bambu PETG Translucent",
|
||||||
|
"GFG97": "Generic PCTG",
|
||||||
|
"GFB00": "Bambu ABS",
|
||||||
|
"GFB99": "Generic ABS",
|
||||||
|
"GFB60": "PolyLite ABS",
|
||||||
|
"GFB50": "Bambu ABS-GF",
|
||||||
|
"GFC00": "Bambu PC",
|
||||||
|
"GFC99": "Generic PC",
|
||||||
|
"GFB98": "Generic ASA",
|
||||||
|
"GFB01": "Bambu ASA",
|
||||||
|
"GFB61": "PolyLite ASA",
|
||||||
|
"GFB02": "Bambu ASA-Aero",
|
||||||
|
"GFS99": "Generic PVA",
|
||||||
|
"GFS04": "Bambu PVA",
|
||||||
|
"GFS01": "Bambu Support G",
|
||||||
|
"GFN03": "Bambu PA-CF",
|
||||||
|
"GFN04": "Bambu PAHT-CF",
|
||||||
|
"GFS03": "Bambu Support For PA/PET",
|
||||||
|
"GFN05": "Bambu PA6-CF",
|
||||||
|
"GFN08": "Bambu PA6-GF",
|
||||||
|
"GFS98": "Generic HIPS",
|
||||||
|
"GFT98": "Generic PPS-CF",
|
||||||
|
"GFT97": "Generic PPS",
|
||||||
|
"GFN97": "Generic PPA-CF",
|
||||||
|
"GFN96": "Generic PPA-GF",
|
||||||
|
"GFP99": "Generic PE",
|
||||||
|
"GFP98": "Generic PE-CF",
|
||||||
|
"GFP97": "Generic PP",
|
||||||
|
"GFP96": "Generic PP-CF",
|
||||||
|
"GFP95": "Generic PP-GF",
|
||||||
|
"GFR99": "Generic EVA",
|
||||||
|
"GFR98": "Generic PHA",
|
||||||
|
"GFS97": "Generic BVOH"
|
||||||
|
}
|
BIN
html/favicon.ico
Normal file
BIN
html/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
51
html/header.html
Normal file
51
html/header.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>FilaMan - Filament Management Tool</title>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<style>
|
||||||
|
.status-container {
|
||||||
|
float: right;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.online { background-color: #2ecc71; }
|
||||||
|
.offline { background-color: #e74c3c; }
|
||||||
|
.ram-status { color: #fff; font-size: 0.8em; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="navbar">
|
||||||
|
<img src="/logo.png" alt="FilaMan Logo" class="logo">
|
||||||
|
<a href="/">Start</a>
|
||||||
|
<a href="/waage">Scale</a>
|
||||||
|
<a href="/spoolman">Spoolman/Bambu</a>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
<div class="status-container">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-dot" id="bambuDot"></span>B
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-dot" id="spoolmanDot"></span>S
|
||||||
|
</div>
|
||||||
|
<div class="ram-status" id="ramStatus"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
37
html/index.html
Normal file
37
html/index.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{{header}}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>FilaMan</h1>
|
||||||
|
<p>Filament Management Tool</p>
|
||||||
|
<p>Your smart solution for <strong>Filament Management</strong> in 3D printing.</p>
|
||||||
|
|
||||||
|
<h2>About FilaMan</h2>
|
||||||
|
<p>
|
||||||
|
FilaMan is a tool designed to simplify filament spool management. It allows you to identify and weigh filament spools,
|
||||||
|
automatically sync data with the self-hosted <a href="https://github.com/Donkie/Spoolman" target="_blank">Spoolman</a> platform,
|
||||||
|
and ensure compatibility with <a href="https://github.com/spuder/OpenSpool" target="_blank">OpenSpool</a> for Bambu printers.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="features">
|
||||||
|
<div class="feature">
|
||||||
|
<h3>Spool Identification</h3>
|
||||||
|
<p>Easily identify filament spools using NFC tags (NTag215 or larger).</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>Automatic Syncing</h3>
|
||||||
|
<p>Seamlessly update spool data with Spoolman for accurate tracking.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>OpenSpool Compatibility</h3>
|
||||||
|
<p>Works with OpenSpool to recognize and activate spools on Bambu printers.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Future Plans</h2>
|
||||||
|
<p>
|
||||||
|
We are working on expanding compatibility to support smaller NFC tags like NTag213
|
||||||
|
and developing custom software to enhance the OpenSpool experience.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
html/logo.png
Normal file
BIN
html/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
114
html/rfid.html
Normal file
114
html/rfid.html
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
{{header}}
|
||||||
|
<div class="connection-status hidden">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span>Connection lost. Trying to reconnect...</span>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="three-column-layout">
|
||||||
|
<!-- Linke Spalte -->
|
||||||
|
<div class="column">
|
||||||
|
<div class="feature-box">
|
||||||
|
<div class="statistics-header">
|
||||||
|
<h2>Statistics</h2>
|
||||||
|
<button id="refreshSpoolman" class="refresh-button">
|
||||||
|
<span>Refresh Spoolman</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="statistics-column">
|
||||||
|
<h3>Spools</h3>
|
||||||
|
<div class="spool-stat" style="display: flex; justify-content: center; align-items: center;">
|
||||||
|
<span class="stat-label">total:</span>
|
||||||
|
<span class="stat-value" id="totalSpools"></span>
|
||||||
|
<div style="width: auto;"></div>
|
||||||
|
<span class="stat-label">without Tag:</span>
|
||||||
|
<span class="stat-value" id="spoolsWithoutTag"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="statistics-grid">
|
||||||
|
<div class="statistics-column">
|
||||||
|
<h3>Overview</h3>
|
||||||
|
<ul class="statistics-list">
|
||||||
|
<li>
|
||||||
|
<span class="stat-label">Manufacturer:</span>
|
||||||
|
<span class="stat-value" id="totalVendors"></span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-label">Weight:</span>
|
||||||
|
<span class="stat-value"><span id="totalWeight"></span> kg</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-label">Length:</span>
|
||||||
|
<span class="stat-value"><span id="totalLength"></span> m</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="statistics-column">
|
||||||
|
<h3>Materials</h3>
|
||||||
|
<ul class="statistics-list" id="materialsList">
|
||||||
|
<!-- Wird dynamisch befüllt -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="feature-box">
|
||||||
|
<div class="nfc-header">
|
||||||
|
<h2>NFC-Tag</h2>
|
||||||
|
<span id="nfcStatusIndicator" class="status-circle"></span>
|
||||||
|
</div>
|
||||||
|
<div class="nfc-status-display"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mittlere Spalte -->
|
||||||
|
<div class="column">
|
||||||
|
<div class="feature-box">
|
||||||
|
<h2>Spoolman Spools</h2>
|
||||||
|
<h2>1. select Manufacturer</h2>
|
||||||
|
<label for="vendorSelect">Manufacturer:</label>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<select id="vendorSelect" class="styled-select">
|
||||||
|
<option value="">Please choose...</option>
|
||||||
|
</select>
|
||||||
|
<label style="margin-left: 10px;">
|
||||||
|
<input type="checkbox" id="onlyWithoutSmId" checked onchange="updateFilamentDropdown()">
|
||||||
|
Only Spools without SM ID
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="filamentSection" class="feature-box hidden">
|
||||||
|
<h2>2. Select Spool</h2>
|
||||||
|
<label>Spool / Filament:</label>
|
||||||
|
<div class="custom-dropdown">
|
||||||
|
<div class="dropdown-button" onclick="toggleFilamentDropdown()">
|
||||||
|
<div class="selected-color" id="selected-color"></div>
|
||||||
|
<span id="selected-filament">Please choose...</span>
|
||||||
|
<span class="dropdown-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-content" id="filament-dropdown-content">
|
||||||
|
<!-- Optionen werden dynamisch hinzugefügt -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p id="nfcInfo" class="nfc-status"></p>
|
||||||
|
<button id="writeNfcButton" class="btn btn-primary hidden" onclick="writeNfcTag()">Write Tag</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rechte Spalte -->
|
||||||
|
<div class="column">
|
||||||
|
<div class="feature-box">
|
||||||
|
<h2>Bambu AMS</h2>
|
||||||
|
<div id="amsDataContainer">
|
||||||
|
<div class="amsData" id="amsData">Wait for AMS-Data...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="spoolman.js"></script>
|
||||||
|
<script src="rfid.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
570
html/rfid.js
Normal file
570
html/rfid.js
Normal file
@ -0,0 +1,570 @@
|
|||||||
|
// WebSocket Variablen
|
||||||
|
let socket;
|
||||||
|
let isConnected = false;
|
||||||
|
const RECONNECT_INTERVAL = 5000;
|
||||||
|
const HEARTBEAT_INTERVAL = 10000;
|
||||||
|
let heartbeatTimer = null;
|
||||||
|
let lastHeartbeatResponse = Date.now();
|
||||||
|
const HEARTBEAT_TIMEOUT = 20000;
|
||||||
|
let reconnectTimer = null;
|
||||||
|
|
||||||
|
// WebSocket Funktionen
|
||||||
|
function startHeartbeat() {
|
||||||
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
||||||
|
|
||||||
|
heartbeatTimer = setInterval(() => {
|
||||||
|
// Prüfe ob zu lange keine Antwort kam
|
||||||
|
if (Date.now() - lastHeartbeatResponse > HEARTBEAT_TIMEOUT) {
|
||||||
|
isConnected = false;
|
||||||
|
updateConnectionStatus();
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||||
|
isConnected = false;
|
||||||
|
updateConnectionStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Sende Heartbeat
|
||||||
|
socket.send(JSON.stringify({ type: 'heartbeat' }));
|
||||||
|
} catch (error) {
|
||||||
|
isConnected = false;
|
||||||
|
updateConnectionStatus();
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, HEARTBEAT_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initWebSocket() {
|
||||||
|
// Clear any existing reconnect timer
|
||||||
|
if (reconnectTimer) {
|
||||||
|
clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn eine existierende Verbindung besteht, diese erst schließen
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket = new WebSocket('ws://' + window.location.host + '/ws');
|
||||||
|
|
||||||
|
socket.onopen = function() {
|
||||||
|
isConnected = true;
|
||||||
|
updateConnectionStatus();
|
||||||
|
startHeartbeat(); // Starte Heartbeat nach erfolgreicher Verbindung
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = function() {
|
||||||
|
isConnected = false;
|
||||||
|
updateConnectionStatus();
|
||||||
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
||||||
|
|
||||||
|
// Nur neue Verbindung versuchen, wenn kein Timer läuft
|
||||||
|
if (!reconnectTimer) {
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
initWebSocket();
|
||||||
|
}, RECONNECT_INTERVAL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = function(error) {
|
||||||
|
isConnected = false;
|
||||||
|
updateConnectionStatus();
|
||||||
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
||||||
|
|
||||||
|
// Bei Fehler Verbindung schließen und neu aufbauen
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = function(event) {
|
||||||
|
lastHeartbeatResponse = Date.now(); // Aktualisiere Zeitstempel bei jeder Server-Antwort
|
||||||
|
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'amsData') {
|
||||||
|
displayAmsData(data.payload);
|
||||||
|
} else if (data.type === 'nfcTag') {
|
||||||
|
updateNfcStatusIndicator(data.payload);
|
||||||
|
} else if (data.type === 'nfcData') {
|
||||||
|
updateNfcData(data.payload);
|
||||||
|
} else if (data.type === 'writeNfcTag') {
|
||||||
|
handleWriteNfcTagResponse(data.success);
|
||||||
|
} else if (data.type === 'heartbeat') {
|
||||||
|
// Optional: Spezifische Behandlung von Heartbeat-Antworten
|
||||||
|
// Update status dots
|
||||||
|
const bambuDot = document.getElementById('bambuDot');
|
||||||
|
const spoolmanDot = document.getElementById('spoolmanDot');
|
||||||
|
const ramStatus = document.getElementById('ramStatus');
|
||||||
|
|
||||||
|
if (bambuDot) {
|
||||||
|
bambuDot.className = 'status-dot ' + (data.bambu_connected ? 'online' : 'offline');
|
||||||
|
}
|
||||||
|
if (spoolmanDot) {
|
||||||
|
spoolmanDot.className = 'status-dot ' + (data.spoolman_connected ? 'online' : 'offline');
|
||||||
|
}
|
||||||
|
if (ramStatus) {
|
||||||
|
ramStatus.textContent = `${data.freeHeap}k`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
isConnected = false;
|
||||||
|
updateConnectionStatus();
|
||||||
|
|
||||||
|
// Nur neue Verbindung versuchen, wenn kein Timer läuft
|
||||||
|
if (!reconnectTimer) {
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
initWebSocket();
|
||||||
|
}, RECONNECT_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus() {
|
||||||
|
const statusElement = document.querySelector('.connection-status');
|
||||||
|
if (!isConnected) {
|
||||||
|
statusElement.classList.remove('hidden');
|
||||||
|
// Verzögerung hinzufügen, damit die CSS-Transition wirken kann
|
||||||
|
setTimeout(() => {
|
||||||
|
statusElement.classList.add('visible');
|
||||||
|
}, 10);
|
||||||
|
} else {
|
||||||
|
statusElement.classList.remove('visible');
|
||||||
|
// Warte auf das Ende der Fade-out Animation bevor wir hidden setzen
|
||||||
|
setTimeout(() => {
|
||||||
|
statusElement.classList.add('hidden');
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
initWebSocket();
|
||||||
|
|
||||||
|
// Event Listener für Checkbox
|
||||||
|
document.getElementById("onlyWithoutSmId").addEventListener("change", function() {
|
||||||
|
const spoolsData = window.getSpoolData();
|
||||||
|
window.populateVendorDropdown(spoolsData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event Listener für Spoolman Events
|
||||||
|
document.addEventListener('spoolDataLoaded', function(event) {
|
||||||
|
window.populateVendorDropdown(event.detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('spoolmanError', function(event) {
|
||||||
|
showNotification(`Spoolman Error: ${event.detail.message}`, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('filamentSelected', function(event) {
|
||||||
|
updateNfcInfo();
|
||||||
|
// Zeige Spool-Buttons wenn ein Filament ausgewählt wurde
|
||||||
|
const selectedText = document.getElementById("selected-filament").textContent;
|
||||||
|
updateSpoolButtons(selectedText !== "Please choose...");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hilfsfunktion für kontrastreiche Textfarbe
|
||||||
|
function getContrastColor(hexcolor) {
|
||||||
|
// Konvertiere Hex zu RGB
|
||||||
|
const r = parseInt(hexcolor.substr(0,2),16);
|
||||||
|
const g = parseInt(hexcolor.substr(2,2),16);
|
||||||
|
const b = parseInt(hexcolor.substr(4,2),16);
|
||||||
|
|
||||||
|
// Berechne Helligkeit (YIQ Formel)
|
||||||
|
const yiq = ((r*299)+(g*587)+(b*114))/1000;
|
||||||
|
|
||||||
|
// Return schwarz oder weiß basierend auf Helligkeit
|
||||||
|
return (yiq >= 128) ? '#000000' : '#FFFFFF';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNfcInfo() {
|
||||||
|
const selectedText = document.getElementById("selected-filament").textContent;
|
||||||
|
const nfcInfo = document.getElementById("nfcInfo");
|
||||||
|
const writeButton = document.getElementById("writeNfcButton");
|
||||||
|
|
||||||
|
if (selectedText === "Please choose...") {
|
||||||
|
nfcInfo.textContent = "No Filament selected";
|
||||||
|
nfcInfo.classList.remove("nfc-success", "nfc-error");
|
||||||
|
writeButton.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finde die ausgewählte Spule in den Daten
|
||||||
|
const selectedSpool = spoolsData.find(spool =>
|
||||||
|
`${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedSpool && selectedSpool.extra.nfc_id) {
|
||||||
|
nfcInfo.textContent = "NFC Tag assigned";
|
||||||
|
nfcInfo.classList.add("nfc-success");
|
||||||
|
nfcInfo.classList.remove("nfc-error");
|
||||||
|
} else {
|
||||||
|
nfcInfo.textContent = "No NFC-Tag assigned";
|
||||||
|
nfcInfo.classList.add("nfc-error");
|
||||||
|
nfcInfo.classList.remove("nfc-success");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSpool) {
|
||||||
|
writeButton.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
writeButton.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayAmsData(amsData) {
|
||||||
|
const amsDataContainer = document.getElementById('amsData');
|
||||||
|
amsDataContainer.innerHTML = '';
|
||||||
|
|
||||||
|
amsData.forEach((ams) => {
|
||||||
|
// Bestimme den Anzeigenamen für das AMS
|
||||||
|
const amsDisplayName = ams.ams_id === 255 ? 'External Spool' : `AMS ${ams.ams_id}`;
|
||||||
|
|
||||||
|
const trayHTML = ams.tray.map(tray => {
|
||||||
|
// Prüfe ob überhaupt Daten vorhanden sind
|
||||||
|
const relevantFields = ['tray_type', 'tray_sub_brands', 'tray_info_idx', 'setting_id'];
|
||||||
|
const hasAnyContent = relevantFields.some(field =>
|
||||||
|
tray[field] !== null &&
|
||||||
|
tray[field] !== undefined &&
|
||||||
|
tray[field] !== '' &&
|
||||||
|
tray[field] !== 'null'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasAnyContent) {
|
||||||
|
return `
|
||||||
|
<div class="tray">
|
||||||
|
<p><b>Tray ${tray.id}</b></p>
|
||||||
|
<p>Empty</p>
|
||||||
|
</div>
|
||||||
|
<hr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur für nicht-leere Trays den Button-HTML erstellen
|
||||||
|
const buttonHtml = `
|
||||||
|
<button class="spool-button" onclick="handleSpoolIn(${ams.ams_id}, ${tray.id})"
|
||||||
|
style="position: absolute; top: 5px; left: 5px;
|
||||||
|
background: none; border: none; padding: 0;
|
||||||
|
cursor: pointer; display: none;">
|
||||||
|
<img src="spool_in.png" alt="Spool In" style="width: 48px; height: 48px;">
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
// Generiere den Type mit Color-Box zusammen
|
||||||
|
const typeWithColor = tray.tray_type ?
|
||||||
|
`<p>Typ: ${tray.tray_type} ${tray.tray_color ? `<span style="
|
||||||
|
background-color: #${tray.tray_color};
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 5px;"></span>` : ''}</p>` : '';
|
||||||
|
|
||||||
|
// Array mit restlichen Tray-Eigenschaften
|
||||||
|
const trayProperties = [
|
||||||
|
{ key: 'tray_sub_brands', label: 'Sub Brands' },
|
||||||
|
{ key: 'tray_info_idx', label: 'Filament Index' },
|
||||||
|
{ key: 'setting_id', label: 'Setting ID' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Nur gültige Felder anzeigen
|
||||||
|
const trayDetails = trayProperties
|
||||||
|
.filter(prop =>
|
||||||
|
tray[prop.key] !== null &&
|
||||||
|
tray[prop.key] !== undefined &&
|
||||||
|
tray[prop.key] !== '' &&
|
||||||
|
tray[prop.key] !== 'null'
|
||||||
|
)
|
||||||
|
.map(prop => `<p>${prop.label}: ${tray[prop.key]}</p>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Temperaturen nur anzeigen, wenn beide nicht 0 sind
|
||||||
|
const tempHTML = (tray.nozzle_temp_min > 0 && tray.nozzle_temp_max > 0)
|
||||||
|
? `<p>Nozzle Temp: ${tray.nozzle_temp_min}°C - ${tray.nozzle_temp_max}°C</p>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Bestimme den Anzeigenamen für das Tray
|
||||||
|
const trayDisplayName = (ams.ams_id === 255) ? 'External' : `Tray ${tray.id}`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="tray" ${tray.tray_color ? `style="border-left: 4px solid #${tray.tray_color};"` : 'style="border-left: 4px solid #007bff;"'}>
|
||||||
|
<div style="position: relative;">
|
||||||
|
${buttonHtml}
|
||||||
|
<p><b>${trayDisplayName}</b></p>
|
||||||
|
${typeWithColor}
|
||||||
|
${trayDetails}
|
||||||
|
${tempHTML}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const amsInfo = `
|
||||||
|
<div class="feature">
|
||||||
|
<h3>${amsDisplayName}:</h3>
|
||||||
|
<div id="trayContainer">
|
||||||
|
${trayHTML}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
amsDataContainer.innerHTML += amsInfo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Funktion zum Anzeigen/Ausblenden der Spool-Buttons
|
||||||
|
function updateSpoolButtons(show) {
|
||||||
|
const spoolButtons = document.querySelectorAll('.spool-button');
|
||||||
|
spoolButtons.forEach(button => {
|
||||||
|
button.style.display = show ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Funktion zum Behandeln des Spool-In-Klicks
|
||||||
|
function handleSpoolIn(amsId, trayId) {
|
||||||
|
// Prüfe WebSocket Verbindung zuerst
|
||||||
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||||
|
showNotification("No active WebSocket connection!", false);
|
||||||
|
console.error("WebSocket not connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole das ausgewählte Filament
|
||||||
|
const selectedText = document.getElementById("selected-filament").textContent;
|
||||||
|
if (selectedText === "Please choose...") {
|
||||||
|
showNotification("Choose Filament first", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finde die ausgewählte Spule in den Daten
|
||||||
|
const selectedSpool = spoolsData.find(spool =>
|
||||||
|
`${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedSpool) {
|
||||||
|
showNotification("Selected Spool not found", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperaturwerte extrahieren
|
||||||
|
let minTemp = "175";
|
||||||
|
let maxTemp = "275";
|
||||||
|
|
||||||
|
if (Array.isArray(selectedSpool.filament.nozzle_temperature) &&
|
||||||
|
selectedSpool.filament.nozzle_temperature.length >= 2) {
|
||||||
|
minTemp = selectedSpool.filament.nozzle_temperature[0];
|
||||||
|
maxTemp = selectedSpool.filament.nozzle_temperature[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle Payload
|
||||||
|
const payload = {
|
||||||
|
type: 'setBambuSpool',
|
||||||
|
payload: {
|
||||||
|
amsId: amsId,
|
||||||
|
trayId: trayId,
|
||||||
|
color: selectedSpool.filament.color_hex || "FFFFFF",
|
||||||
|
nozzle_temp_min: parseInt(minTemp),
|
||||||
|
nozzle_temp_max: parseInt(maxTemp),
|
||||||
|
type: selectedSpool.filament.material,
|
||||||
|
brand: selectedSpool.filament.vendor.name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log("Sende WebSocket Nachricht:", payload);
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket.send(JSON.stringify(payload));
|
||||||
|
showNotification(`Spool set in AMS ${amsId} Tray ${trayId}. Pls wait`, true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Senden der WebSocket Nachricht:", error);
|
||||||
|
showNotification("Fehler beim Senden der Daten", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNfcStatusIndicator(data) {
|
||||||
|
const indicator = document.getElementById('nfcStatusIndicator');
|
||||||
|
|
||||||
|
if (data.found === 0) {
|
||||||
|
// Kein NFC Tag gefunden
|
||||||
|
indicator.className = 'status-circle';
|
||||||
|
} else if (data.found === 1) {
|
||||||
|
// NFC Tag erfolgreich gelesen
|
||||||
|
indicator.className = 'status-circle success';
|
||||||
|
} else {
|
||||||
|
// Fehler beim Lesen
|
||||||
|
indicator.className = 'status-circle error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNfcData(data) {
|
||||||
|
// Den Container für den NFC Status finden
|
||||||
|
const nfcStatusContainer = document.querySelector('.nfc-status-display');
|
||||||
|
|
||||||
|
// Bestehende Daten-Anzeige entfernen falls vorhanden
|
||||||
|
const existingData = nfcStatusContainer.querySelector('.nfc-data');
|
||||||
|
if (existingData) {
|
||||||
|
existingData.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neues div für die Datenanzeige erstellen
|
||||||
|
const nfcDataDiv = document.createElement('div');
|
||||||
|
nfcDataDiv.className = 'nfc-data';
|
||||||
|
|
||||||
|
// Wenn ein Fehler vorliegt oder keine Daten vorhanden sind
|
||||||
|
if (data.error || data.info || !data || Object.keys(data).length === 0) {
|
||||||
|
// Zeige Fehlermeldung oder leere Nachricht
|
||||||
|
if (data.error || data.info) {
|
||||||
|
if (data.error) {
|
||||||
|
nfcDataDiv.innerHTML = `
|
||||||
|
<div class="error-message" style="margin-top: 10px; color: #dc3545;">
|
||||||
|
<p><strong>Error:</strong> ${data.error}</p>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
nfcDataDiv.innerHTML = `
|
||||||
|
<div class="info-message" style="margin-top: 10px; color:rgb(18, 210, 0);">
|
||||||
|
<p><strong>Info:</strong> ${data.info}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
nfcDataDiv.innerHTML = '<div style="margin-top: 10px;"></div>';
|
||||||
|
}
|
||||||
|
nfcStatusContainer.appendChild(nfcDataDiv);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML für die Datenanzeige erstellen
|
||||||
|
let html = `
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<p><strong>Brand:</strong> ${data.brand || 'N/A'}</p>
|
||||||
|
<p><strong>Type:</strong> ${data.type || 'N/A'} ${data.color_hex ? `<span style="
|
||||||
|
background-color: #${data.color_hex};
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 5px;
|
||||||
|
"></span>` : ''}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Spoolman ID anzeigen
|
||||||
|
html += `<p><strong>Spoolman ID:</strong> ${data.sm_id || 'No Spoolman ID'}</p>`;
|
||||||
|
|
||||||
|
// Nur wenn eine sm_id vorhanden ist, aktualisiere die Dropdowns
|
||||||
|
if (data.sm_id) {
|
||||||
|
const matchingSpool = spoolsData.find(spool => spool.id === parseInt(data.sm_id));
|
||||||
|
if (matchingSpool) {
|
||||||
|
// Zuerst Hersteller-Dropdown aktualisieren
|
||||||
|
document.getElementById("vendorSelect").value = matchingSpool.filament.vendor.id;
|
||||||
|
|
||||||
|
// Dann Filament-Dropdown aktualisieren und Spule auswählen
|
||||||
|
updateFilamentDropdown();
|
||||||
|
setTimeout(() => {
|
||||||
|
// Warte kurz bis das Dropdown aktualisiert wurde
|
||||||
|
selectFilament(matchingSpool);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
nfcDataDiv.innerHTML = html;
|
||||||
|
|
||||||
|
|
||||||
|
// Neues div zum Container hinzufügen
|
||||||
|
nfcStatusContainer.appendChild(nfcDataDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeNfcTag() {
|
||||||
|
const selectedText = document.getElementById("selected-filament").textContent;
|
||||||
|
if (selectedText === "Please choose...") {
|
||||||
|
alert('Please select a Spool first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spoolsData = window.getSpoolData();
|
||||||
|
const selectedSpool = spoolsData.find(spool =>
|
||||||
|
`${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedSpool) {
|
||||||
|
alert('Ausgewählte Spule konnte nicht gefunden werden.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperaturwerte korrekt extrahieren
|
||||||
|
let minTemp = "175";
|
||||||
|
let maxTemp = "275";
|
||||||
|
|
||||||
|
if (Array.isArray(selectedSpool.filament.nozzle_temperature) &&
|
||||||
|
selectedSpool.filament.nozzle_temperature.length >= 2) {
|
||||||
|
minTemp = String(selectedSpool.filament.nozzle_temperature[0]);
|
||||||
|
maxTemp = String(selectedSpool.filament.nozzle_temperature[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle das NFC-Datenpaket mit korrekten Datentypen
|
||||||
|
const nfcData = {
|
||||||
|
version: "2.0",
|
||||||
|
protocol: "openspool",
|
||||||
|
color_hex: selectedSpool.filament.color_hex || "FFFFFF",
|
||||||
|
type: selectedSpool.filament.material,
|
||||||
|
min_temp: minTemp,
|
||||||
|
max_temp: maxTemp,
|
||||||
|
brand: selectedSpool.filament.vendor.name,
|
||||||
|
sm_id: String(selectedSpool.id) // Konvertiere zu String
|
||||||
|
};
|
||||||
|
|
||||||
|
if (socket?.readyState === WebSocket.OPEN) {
|
||||||
|
const writeButton = document.getElementById("writeNfcButton");
|
||||||
|
writeButton.classList.add("writing");
|
||||||
|
writeButton.textContent = "Writing";
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: 'writeNfcTag',
|
||||||
|
payload: nfcData
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
alert('Not connected to Server. Please check connection.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWriteNfcTagResponse(success) {
|
||||||
|
const writeButton = document.getElementById("writeNfcButton");
|
||||||
|
writeButton.classList.remove("writing");
|
||||||
|
writeButton.classList.add(success ? "success" : "error");
|
||||||
|
writeButton.textContent = success ? "Write success" : "Write failed";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
writeButton.classList.remove("success", "error");
|
||||||
|
writeButton.textContent = "Write Tag";
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(message, isSuccess) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification ${isSuccess ? 'success' : 'error'}`;
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Nach 3 Sekunden ausblenden
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('fade-out');
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
BIN
html/spool_in.png
Normal file
BIN
html/spool_in.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
80
html/spoolman.html
Normal file
80
html/spoolman.html
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{{header}}
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
if (spoolmanUrl && spoolmanUrl.trim() !== "") {
|
||||||
|
document.getElementById('spoolmanUrl').value = spoolmanUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkSpoolmanInstance() {
|
||||||
|
const url = document.getElementById('spoolmanUrl').value;
|
||||||
|
fetch(`/api/checkSpoolman?url=${encodeURIComponent(url)}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.healthy) {
|
||||||
|
document.getElementById('statusMessage').innerText = 'Spoolman-Instance is availabe and healthy!';
|
||||||
|
} else {
|
||||||
|
document.getElementById('statusMessage').innerText = 'Spoolman-Instance not available.';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
document.getElementById('statusMessage').innerText = 'Error while connecting to Spoolman-Instance: ' + error.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveBambuCredentials() {
|
||||||
|
const ip = document.getElementById('bambuIp').value;
|
||||||
|
const serial = document.getElementById('bambuSerial').value;
|
||||||
|
const code = document.getElementById('bambuCode').value;
|
||||||
|
|
||||||
|
fetch(`/api/bambu?bambu_ip=${encodeURIComponent(ip)}&bambu_serialnr=${encodeURIComponent(serial)}&bambu_accesscode=${encodeURIComponent(code)}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.healthy) {
|
||||||
|
document.getElementById('bambuStatusMessage').innerText = 'Bambu Credentials saved!';
|
||||||
|
// Erstelle und zeige den Reboot-Button
|
||||||
|
const rebootBtn = document.createElement('button');
|
||||||
|
rebootBtn.innerText = 'Reboot now';
|
||||||
|
rebootBtn.className = 'reboot-button';
|
||||||
|
rebootBtn.onclick = () => window.location.href = '/reboot';
|
||||||
|
document.getElementById('bambuStatusMessage').appendChild(rebootBtn);
|
||||||
|
} else {
|
||||||
|
document.getElementById('bambuStatusMessage').innerText = 'Error while saving Bambu Credentials.';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
document.getElementById('bambuStatusMessage').innerText = 'Error while saving: ' + error.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
var spoolmanUrl = "{{spoolmanUrl}}";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h1>Spoolman API URL / Bambu Credentials</h1>
|
||||||
|
<label for="spoolmanUrl">Set URL/IP to your Spoolman-Instanz:</label>
|
||||||
|
<input type="text" id="spoolmanUrl" placeholder="http://ip-or-url-of-your-spoolman-instanz:port">
|
||||||
|
<button onclick="checkSpoolmanInstance()">Save Spoolman URL</button>
|
||||||
|
<p id="statusMessage"></p>
|
||||||
|
|
||||||
|
<h2>Bambu Lab Printer Credentials</h2>
|
||||||
|
<div class="bambu-settings">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="bambuIp">Bambu Drucker IP-Adresse:</label>
|
||||||
|
<input type="text" id="bambuIp" placeholder="192.168.1.xxx" value="{{bambuIp}}">
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="bambuSerial">Drucker Seriennummer:</label>
|
||||||
|
<input type="text" id="bambuSerial" placeholder="BBLXXXXXXXX" value="{{bambuSerial}}">
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="bambuCode">Access Code:</label>
|
||||||
|
<input type="text" id="bambuCode" placeholder="Access Code vom Drucker" value="{{bambuCode}}">
|
||||||
|
</div>
|
||||||
|
<button onclick="saveBambuCredentials()">Save Bambu Credentials</button>
|
||||||
|
<p id="bambuStatusMessage"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
308
html/spoolman.js
Normal file
308
html/spoolman.js
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
// Globale Variablen
|
||||||
|
let spoolmanUrl = '';
|
||||||
|
let spoolsData = [];
|
||||||
|
|
||||||
|
// Hilfsfunktionen für Datenmanipulation
|
||||||
|
function processSpoolData(data) {
|
||||||
|
return data.map(spool => ({
|
||||||
|
id: spool.id,
|
||||||
|
remaining_weight: spool.remaining_weight,
|
||||||
|
remaining_length: spool.remaining_length,
|
||||||
|
filament: spool.filament,
|
||||||
|
extra: spool.extra
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdown-Funktionen
|
||||||
|
function populateVendorDropdown(data, selectedSmId = null) {
|
||||||
|
const vendorSelect = document.getElementById("vendorSelect");
|
||||||
|
if (!vendorSelect) {
|
||||||
|
console.error('vendorSelect Element nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onlyWithoutSmId = document.getElementById("onlyWithoutSmId");
|
||||||
|
if (!onlyWithoutSmId) {
|
||||||
|
console.error('onlyWithoutSmId Element nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate Objekte für alle Hersteller und gefilterte Hersteller
|
||||||
|
const allVendors = {};
|
||||||
|
const filteredVendors = {};
|
||||||
|
|
||||||
|
vendorSelect.innerHTML = '<option value="">Bitte wählen...</option>';
|
||||||
|
|
||||||
|
let vendorIdToSelect = null;
|
||||||
|
let totalSpools = 0;
|
||||||
|
let spoolsWithoutTag = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
let totalLength = 0;
|
||||||
|
// Neues Objekt für Material-Gruppierung
|
||||||
|
const materials = {};
|
||||||
|
|
||||||
|
data.forEach(spool => {
|
||||||
|
if (!spool.filament || !spool.filament.vendor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSpools++;
|
||||||
|
|
||||||
|
// Material zählen und gruppieren
|
||||||
|
if (spool.filament.material) {
|
||||||
|
const material = spool.filament.material.toUpperCase(); // Normalisierung
|
||||||
|
materials[material] = (materials[material] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addiere Gewicht und Länge
|
||||||
|
if (spool.remaining_weight) {
|
||||||
|
totalWeight += spool.remaining_weight;
|
||||||
|
}
|
||||||
|
if (spool.remaining_length) {
|
||||||
|
totalLength += spool.remaining_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Länge gesamt: " + spool.remaining_length);
|
||||||
|
console.log("Gewicht gesamt" + spool.remaining_weight);
|
||||||
|
|
||||||
|
const vendor = spool.filament.vendor;
|
||||||
|
|
||||||
|
const hasValidNfcId = spool.extra &&
|
||||||
|
spool.extra.nfc_id &&
|
||||||
|
spool.extra.nfc_id !== '""' &&
|
||||||
|
spool.extra.nfc_id !== '"\\"\\"\\""';
|
||||||
|
|
||||||
|
if (!hasValidNfcId) {
|
||||||
|
spoolsWithoutTag++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Hersteller sammeln
|
||||||
|
if (!allVendors[vendor.id]) {
|
||||||
|
allVendors[vendor.id] = vendor.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gefilterte Hersteller für Dropdown
|
||||||
|
if (!filteredVendors[vendor.id]) {
|
||||||
|
if (!onlyWithoutSmId.checked || !hasValidNfcId) {
|
||||||
|
filteredVendors[vendor.id] = vendor.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dropdown mit gefilterten Herstellern befüllen
|
||||||
|
Object.entries(filteredVendors).forEach(([id, name]) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = id;
|
||||||
|
option.textContent = name;
|
||||||
|
vendorSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("totalSpools").textContent = totalSpools;
|
||||||
|
document.getElementById("spoolsWithoutTag").textContent = spoolsWithoutTag;
|
||||||
|
// Zeige die Gesamtzahl aller Hersteller an
|
||||||
|
document.getElementById("totalVendors").textContent = Object.keys(allVendors).length;
|
||||||
|
|
||||||
|
// Neue Statistiken hinzufügen
|
||||||
|
document.getElementById("totalWeight").textContent = (totalWeight / 1000).toFixed(2);
|
||||||
|
document.getElementById("totalLength").textContent = (totalLength / 1000).toFixed(2);
|
||||||
|
|
||||||
|
// Material-Statistiken zum DOM hinzufügen
|
||||||
|
const materialsList = document.getElementById("materialsList");
|
||||||
|
materialsList.innerHTML = '';
|
||||||
|
Object.entries(materials)
|
||||||
|
.sort(([,a], [,b]) => b - a) // Sortiere nach Anzahl absteigend
|
||||||
|
.forEach(([material, count]) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = `${material}: ${count} ${count === 1 ? 'Spule' : 'Spulen'}`;
|
||||||
|
materialsList.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vendorIdToSelect) {
|
||||||
|
vendorSelect.value = vendorIdToSelect;
|
||||||
|
updateFilamentDropdown(selectedSmId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilamentDropdown(selectedSmId = null) {
|
||||||
|
const vendorId = document.getElementById("vendorSelect").value;
|
||||||
|
const dropdownContentInner = document.getElementById("filament-dropdown-content");
|
||||||
|
const filamentSection = document.getElementById("filamentSection");
|
||||||
|
const onlyWithoutSmId = document.getElementById("onlyWithoutSmId").checked;
|
||||||
|
const selectedText = document.getElementById("selected-filament");
|
||||||
|
const selectedColor = document.getElementById("selected-color");
|
||||||
|
|
||||||
|
dropdownContentInner.innerHTML = '';
|
||||||
|
selectedText.textContent = "Bitte wählen...";
|
||||||
|
selectedColor.style.backgroundColor = '#FFFFFF';
|
||||||
|
|
||||||
|
if (vendorId) {
|
||||||
|
const filteredFilaments = spoolsData.filter(spool => {
|
||||||
|
const hasValidNfcId = spool.extra &&
|
||||||
|
spool.extra.nfc_id &&
|
||||||
|
spool.extra.nfc_id !== '""' &&
|
||||||
|
spool.extra.nfc_id !== '"\\"\\"\\""';
|
||||||
|
|
||||||
|
return spool.filament.vendor.id == vendorId &&
|
||||||
|
(!onlyWithoutSmId || !hasValidNfcId);
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredFilaments.forEach(spool => {
|
||||||
|
const option = document.createElement("div");
|
||||||
|
option.className = "dropdown-option";
|
||||||
|
option.setAttribute("data-value", spool.filament.id);
|
||||||
|
option.setAttribute("data-nfc-id", spool.extra.nfc_id || "");
|
||||||
|
|
||||||
|
const colorHex = spool.filament.color_hex || 'FFFFFF';
|
||||||
|
option.innerHTML = `
|
||||||
|
<div class="option-color" style="background-color: #${colorHex}"></div>
|
||||||
|
<span>${spool.id} | ${spool.filament.name} (${spool.filament.material})</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
option.onclick = () => selectFilament(spool);
|
||||||
|
dropdownContentInner.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
filamentSection.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
filamentSection.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFilament(spool) {
|
||||||
|
const selectedColor = document.getElementById("selected-color");
|
||||||
|
const selectedText = document.getElementById("selected-filament");
|
||||||
|
const dropdownContent = document.getElementById("filament-dropdown-content");
|
||||||
|
|
||||||
|
selectedColor.style.backgroundColor = `#${spool.filament.color_hex || 'FFFFFF'}`;
|
||||||
|
selectedText.textContent = `${spool.id} | ${spool.filament.name} (${spool.filament.material})`;
|
||||||
|
dropdownContent.classList.remove("show");
|
||||||
|
|
||||||
|
document.dispatchEvent(new CustomEvent('filamentSelected', {
|
||||||
|
detail: spool
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisierung und Event-Handler
|
||||||
|
async function initSpoolman() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/url');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.spoolman_url) {
|
||||||
|
throw new Error('spoolman_url nicht in der Antwort gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
spoolmanUrl = data.spoolman_url;
|
||||||
|
|
||||||
|
const fetchedData = await fetchSpoolData();
|
||||||
|
spoolsData = processSpoolData(fetchedData);
|
||||||
|
|
||||||
|
document.dispatchEvent(new CustomEvent('spoolDataLoaded', {
|
||||||
|
detail: spoolsData
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Initialisieren von Spoolman:', error);
|
||||||
|
document.dispatchEvent(new CustomEvent('spoolmanError', {
|
||||||
|
detail: { message: error.message }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSpoolData() {
|
||||||
|
try {
|
||||||
|
if (!spoolmanUrl) {
|
||||||
|
throw new Error('Spoolman URL ist nicht initialisiert');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${spoolmanUrl}/api/v1/spool`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Spulen-Daten:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Exportiere Funktionen
|
||||||
|
window.getSpoolData = () => spoolsData;
|
||||||
|
window.reloadSpoolData = initSpoolman;
|
||||||
|
window.populateVendorDropdown = populateVendorDropdown;
|
||||||
|
window.updateFilamentDropdown = updateFilamentDropdown;
|
||||||
|
window.toggleFilamentDropdown = () => {
|
||||||
|
const content = document.getElementById("filament-dropdown-content");
|
||||||
|
content.classList.toggle("show");
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Event Listener
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initSpoolman();
|
||||||
|
|
||||||
|
const vendorSelect = document.getElementById('vendorSelect');
|
||||||
|
if (vendorSelect) {
|
||||||
|
vendorSelect.addEventListener('change', () => updateFilamentDropdown());
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlyWithoutSmId = document.getElementById('onlyWithoutSmId');
|
||||||
|
if (onlyWithoutSmId) {
|
||||||
|
onlyWithoutSmId.addEventListener('change', () => {
|
||||||
|
populateVendorDropdown(spoolsData);
|
||||||
|
updateFilamentDropdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('spoolDataLoaded', (event) => {
|
||||||
|
populateVendorDropdown(event.detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (!event.target.closest('.custom-dropdown')) {
|
||||||
|
const dropdowns = document.getElementsByClassName("dropdown-content");
|
||||||
|
for (let dropdown of dropdowns) {
|
||||||
|
dropdown.classList.remove("show");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshButton = document.getElementById('refreshSpoolman');
|
||||||
|
if (refreshButton) {
|
||||||
|
refreshButton.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
refreshButton.disabled = true;
|
||||||
|
refreshButton.textContent = 'Wird aktualisiert...';
|
||||||
|
await initSpoolman();
|
||||||
|
refreshButton.textContent = 'Refresh Spoolman';
|
||||||
|
} finally {
|
||||||
|
refreshButton.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exportiere Funktionen
|
||||||
|
window.getSpoolData = () => spoolsData;
|
||||||
|
window.setSpoolData = (data) => { spoolsData = data; };
|
||||||
|
window.reloadSpoolData = initSpoolman;
|
||||||
|
window.populateVendorDropdown = populateVendorDropdown;
|
||||||
|
window.updateFilamentDropdown = updateFilamentDropdown;
|
||||||
|
window.toggleFilamentDropdown = () => {
|
||||||
|
const content = document.getElementById("filament-dropdown-content");
|
||||||
|
content.classList.toggle("show");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event Listener für Click außerhalb Dropdown
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (!event.target.closest('.custom-dropdown')) {
|
||||||
|
const dropdowns = document.getElementsByClassName("dropdown-content");
|
||||||
|
for (let dropdown of dropdowns) {
|
||||||
|
dropdown.classList.remove("show");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
901
html/style.css
Normal file
901
html/style.css
Normal file
@ -0,0 +1,901 @@
|
|||||||
|
/* Allgemeine Stile */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 40px; /* Anpassen an die Navbar-Höhe */
|
||||||
|
width: auto;
|
||||||
|
margin-right: 15px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigationsleiste */
|
||||||
|
.navbar {
|
||||||
|
background-color: #007bff;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center; /* Zentriert die Navigation */
|
||||||
|
padding: 10px 0;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a {
|
||||||
|
display: inline-block;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background 0.3s, color 0.3s;
|
||||||
|
cursor: pointer !important; /* Wichtig: cursor-Definition für Nav-Links */
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inhalt */
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-top: 20px; /* Platz für die fixe Navbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Überschriften */
|
||||||
|
h1 {
|
||||||
|
color: #007bff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #007bff;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Formulare */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], input[type="submit"] {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
border-color: #007bff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"]:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Statusnachricht */
|
||||||
|
#statusMessage {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #8cc4fd;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.feature {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 10px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.feature h3 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
.feature p {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Karten-Stil für optische Trennung */
|
||||||
|
.card {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Versteckte Elemente */
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown-Stil */
|
||||||
|
.styled-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23007bff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 15px center;
|
||||||
|
background-size: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styled-select:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styled-select:focus {
|
||||||
|
border-color: #007bff;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styled-select:disabled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NFC-Status */
|
||||||
|
.nfc-status {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-success {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Füge diese neuen Styles zu deiner style.css hinzu */
|
||||||
|
|
||||||
|
.three-column-layout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; /* Verhindert Überlauf bei flex-Elementen */
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-box {
|
||||||
|
background: white;
|
||||||
|
padding: 5px 20px 20px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-box h2 {
|
||||||
|
color: #007bff;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-box ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-box ul li {
|
||||||
|
padding: 8px 5px 5px 5px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 95%;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-top: 60px;
|
||||||
|
padding-bottom: 20px;;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tray {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
border-left: 4px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tray p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tray b {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.three-column-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-status-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-circle {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-circle.success {
|
||||||
|
background-color: #28a745;
|
||||||
|
border-color: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-circle.error {
|
||||||
|
background-color: #dc3545;
|
||||||
|
border-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-data {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-data p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-status-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fff3f3;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-message {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fff3f3;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #39d82e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40px 1fr 40px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
grid-column: 2;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-header .status-circle {
|
||||||
|
grid-column: 3;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background-color: #fff3f3;
|
||||||
|
border: 1px solid #dc3545;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 15px auto;
|
||||||
|
color: #dc3545;
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-width: 90%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid rgba(220, 53, 69, 0.2);
|
||||||
|
border-top-color: #dc3545;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.visible .spinner {
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nfc-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center; /* Zentriert das div */
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filament Select Styling */
|
||||||
|
#filamentSelect {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23007bff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 15px center;
|
||||||
|
background-size: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
#filamentSelect:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#filamentSelect:focus {
|
||||||
|
border-color: #007bff;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
#filamentSelect option {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000; /* Standard Textfarbe für alles außer dem Farbblock */
|
||||||
|
}
|
||||||
|
|
||||||
|
#filamentSelect option::first-letter {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filamentSelect option::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color Box im Select */
|
||||||
|
.color-box {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filamentSelect option span {
|
||||||
|
display: inline-block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filamentSelect option span:first-child {
|
||||||
|
margin-right: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filament Select Option Styling */
|
||||||
|
#filamentSelect option span.color-circle {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Dropdown */
|
||||||
|
.custom-dropdown {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: default; /* Container selbst soll normalen Cursor haben */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-button {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-button:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #333;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #007bff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option {
|
||||||
|
padding: 10px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #333;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 25px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.success {
|
||||||
|
background-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.fade-out {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Neue Styles für die Statistiken */
|
||||||
|
.statistics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-column {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-column h3 {
|
||||||
|
color: #007bff;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-list li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 5px 0 5px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design Anpassung */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.statistics-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button:active {
|
||||||
|
background-color: #004494;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spools-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-stat .stat-label {
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-stat .stat-value {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons und klickbare Elemente */
|
||||||
|
button,
|
||||||
|
input[type="submit"],
|
||||||
|
.dropdown-button,
|
||||||
|
.dropdown-option,
|
||||||
|
.refresh-button,
|
||||||
|
.btn,
|
||||||
|
.styled-select,
|
||||||
|
select,
|
||||||
|
a {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled Zustände */
|
||||||
|
button:disabled,
|
||||||
|
input[type="submit"]:disabled,
|
||||||
|
.btn:disabled,
|
||||||
|
.styled-select:disabled {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Schreib-Button */
|
||||||
|
#writeNfcButton {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#writeNfcButton.writing {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: black;
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#writeNfcButton.success {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#writeNfcButton.error {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dots {
|
||||||
|
0% { content: ""; }
|
||||||
|
33% { content: "."; }
|
||||||
|
66% { content: ".."; }
|
||||||
|
100% { content: "..."; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#writeNfcButton.writing::after {
|
||||||
|
content: "...";
|
||||||
|
animation: dots 1s steps(3, end) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-button {
|
||||||
|
background-color: #ff0000;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-button:hover {
|
||||||
|
background-color: #cc0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bambu Settings Erweiterung */
|
||||||
|
.bambu-settings {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bambu-settings .input-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bambu-settings .input-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bambu-settings .input-group input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bambuStatusMessage {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tray {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
99
html/waage.html
Normal file
99
html/waage.html
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
{{header}}
|
||||||
|
<div class="content">
|
||||||
|
<h1>Scale Configuration Page</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Sacle Calibration</h5>
|
||||||
|
<button id="calibrateBtn" class="btn btn-primary">Calibrate Scale</button>
|
||||||
|
<button id="tareBtn" class="btn btn-secondary">Tare Scale</button>
|
||||||
|
<div id="statusMessage" class="mt-3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Neue Kalibrierungskarte -->
|
||||||
|
<div id="calibrationCard" class="card mt-3" style="display: none;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Calibration done</h5>
|
||||||
|
<p>Please follow these steps:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Make sure the scale is empty</li>
|
||||||
|
<li>Have a 500g calibration weight ready</li>
|
||||||
|
<li>Click on "Start Calibration"</li>
|
||||||
|
<li>Follow the further instructions</li>
|
||||||
|
</ol>
|
||||||
|
<ol>
|
||||||
|
<li>Step 1: Empty the scale</li>
|
||||||
|
<li>Step 2: Place the 500g weight on the scale</li>
|
||||||
|
<li>Step 3: Remove weight from Scale</li>
|
||||||
|
</ol>
|
||||||
|
<button id="startCalibrationBtn" class="btn btn-danger">Start Calibration</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let ws = null;
|
||||||
|
const statusMessage = document.getElementById('statusMessage');
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
ws = new WebSocket(`ws://${window.location.hostname}/ws`);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket verbunden');
|
||||||
|
statusMessage.innerHTML = 'Scale connected';
|
||||||
|
enableButtons(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket getrennt');
|
||||||
|
statusMessage.innerHTML = 'Scale connection lost';
|
||||||
|
enableButtons(false);
|
||||||
|
setTimeout(connectWebSocket, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'scale') {
|
||||||
|
if (data.payload === 'success') {
|
||||||
|
statusMessage.innerHTML = 'Well done';
|
||||||
|
statusMessage.className = 'alert alert-success';
|
||||||
|
} else if (data.payload === 'error') {
|
||||||
|
statusMessage.innerHTML = 'Error while action';
|
||||||
|
statusMessage.className = 'alert alert-danger';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableButtons(enabled) {
|
||||||
|
document.getElementById('calibrateBtn').disabled = !enabled;
|
||||||
|
document.getElementById('tareBtn').disabled = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('calibrateBtn').addEventListener('click', () => {
|
||||||
|
// Kalibrierungskarte anzeigen
|
||||||
|
document.getElementById('calibrationCard').style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('startCalibrationBtn').addEventListener('click', () => {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'scale',
|
||||||
|
payload: 'calibrate'
|
||||||
|
}));
|
||||||
|
// Optional: Kalibrierungskarte nach dem Start ausblenden
|
||||||
|
document.getElementById('calibrationCard').style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('tareBtn').addEventListener('click', () => {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'scale',
|
||||||
|
payload: 'tare'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocket-Verbindung beim Laden der Seite initiieren
|
||||||
|
connectWebSocket();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
html/wifi.html
Normal file
12
html/wifi.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{{header}}
|
||||||
|
<div class="content">
|
||||||
|
<h1>WiFi Configuration Page</h1>
|
||||||
|
<form action="/setToken" method="post">
|
||||||
|
<label for="deviceToken">Device Token:</label><br>
|
||||||
|
<input type="text" id="deviceToken" name="deviceToken"><br>
|
||||||
|
<input type="submit" value="Set Token">
|
||||||
|
</form>
|
||||||
|
<p>Configure your WiFi settings here.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
img/doc-esp32-pinout-reference-wroom-devkit.png
Normal file
BIN
img/doc-esp32-pinout-reference-wroom-devkit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 255 KiB |
2
scripts/buildfs.sh
Executable file
2
scripts/buildfs.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
pio run --target buildfs
|
2
scripts/uploadfs.sh
Executable file
2
scripts/uploadfs.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
pio run --target uploadfs
|
471
src/api.cpp
Normal file
471
src/api.cpp
Normal file
@ -0,0 +1,471 @@
|
|||||||
|
#include "api.h"
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "commonFS.h"
|
||||||
|
|
||||||
|
bool spoolman_connected = false;
|
||||||
|
String spoolmanUrl = "";
|
||||||
|
|
||||||
|
struct SendToApiParams {
|
||||||
|
String httpType;
|
||||||
|
String spoolsUrl;
|
||||||
|
String updatePayload;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Spoolman Data
|
||||||
|
{
|
||||||
|
"version":"1.0",
|
||||||
|
"protocol":"openspool",
|
||||||
|
"color_hex":"AF7933",
|
||||||
|
"type":"ABS",
|
||||||
|
"min_temp":175,
|
||||||
|
"max_temp":275,
|
||||||
|
"brand":"Overture"
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilaMan Data
|
||||||
|
{
|
||||||
|
"version":"1.0",
|
||||||
|
"protocol":"openspool",
|
||||||
|
"color_hex":"AF7933",
|
||||||
|
"type":"ABS",
|
||||||
|
"min_temp":175,
|
||||||
|
"max_temp":275,
|
||||||
|
"brand":"Overture",
|
||||||
|
"sm_id":
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
JsonDocument fetchSpoolsForWebsite() {
|
||||||
|
HTTPClient http;
|
||||||
|
String spoolsUrl = spoolmanUrl + apiUrl + "/spool";
|
||||||
|
|
||||||
|
Serial.print("Rufe Spool-Daten von: ");
|
||||||
|
Serial.println(spoolsUrl);
|
||||||
|
|
||||||
|
http.begin(spoolsUrl);
|
||||||
|
int httpCode = http.GET();
|
||||||
|
|
||||||
|
JsonDocument filteredDoc;
|
||||||
|
if (httpCode == HTTP_CODE_OK) {
|
||||||
|
String payload = http.getString();
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, payload);
|
||||||
|
if (error) {
|
||||||
|
Serial.print("Fehler beim Parsen der JSON-Antwort: ");
|
||||||
|
Serial.println(error.c_str());
|
||||||
|
} else {
|
||||||
|
JsonArray spools = doc.as<JsonArray>();
|
||||||
|
JsonArray filteredSpools = filteredDoc.to<JsonArray>();
|
||||||
|
|
||||||
|
for (JsonObject spool : spools) {
|
||||||
|
JsonObject filteredSpool = filteredSpools.createNestedObject();
|
||||||
|
filteredSpool["extra"]["nfc_id"] = spool["extra"]["nfc_id"];
|
||||||
|
|
||||||
|
JsonObject filament = filteredSpool.createNestedObject("filament");
|
||||||
|
filament["sm_id"] = spool["id"];
|
||||||
|
filament["id"] = spool["filament"]["id"];
|
||||||
|
filament["name"] = spool["filament"]["name"];
|
||||||
|
filament["material"] = spool["filament"]["material"];
|
||||||
|
filament["color_hex"] = spool["filament"]["color_hex"];
|
||||||
|
filament["nozzle_temperature"] = spool["filament"]["extra"]["nozzle_temperature"]; // [190,230]
|
||||||
|
filament["price_meter"] = spool["filament"]["extra"]["price_meter"];
|
||||||
|
filament["price_gramm"] = spool["filament"]["extra"]["price_gramm"];
|
||||||
|
|
||||||
|
JsonObject vendor = filament.createNestedObject("vendor");
|
||||||
|
vendor["id"] = spool["filament"]["vendor"]["id"];
|
||||||
|
vendor["name"] = spool["filament"]["vendor"]["name"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.print("Fehler beim Abrufen der Spool-Daten. HTTP-Code: ");
|
||||||
|
Serial.println(httpCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
http.end();
|
||||||
|
return filteredDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument fetchAllSpoolsInfo() {
|
||||||
|
HTTPClient http;
|
||||||
|
String spoolsUrl = spoolmanUrl + apiUrl + "/spool";
|
||||||
|
|
||||||
|
Serial.print("Rufe Spool-Daten von: ");
|
||||||
|
Serial.println(spoolsUrl);
|
||||||
|
|
||||||
|
http.begin(spoolsUrl);
|
||||||
|
int httpCode = http.GET();
|
||||||
|
|
||||||
|
JsonDocument filteredDoc;
|
||||||
|
if (httpCode == HTTP_CODE_OK) {
|
||||||
|
String payload = http.getString();
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, payload);
|
||||||
|
if (error) {
|
||||||
|
Serial.print("Fehler beim Parsen der JSON-Antwort: ");
|
||||||
|
Serial.println(error.c_str());
|
||||||
|
} else {
|
||||||
|
JsonArray spools = doc.as<JsonArray>();
|
||||||
|
JsonArray filteredSpools = filteredDoc.to<JsonArray>();
|
||||||
|
|
||||||
|
for (JsonObject spool : spools) {
|
||||||
|
JsonObject filteredSpool = filteredSpools.createNestedObject();
|
||||||
|
filteredSpool["price"] = spool["price"];
|
||||||
|
filteredSpool["remaining_weight"] = spool["remaining_weight"];
|
||||||
|
filteredSpool["used_weight"] = spool["used_weight"];
|
||||||
|
filteredSpool["extra"]["nfc_id"] = spool["extra"]["nfc_id"];
|
||||||
|
|
||||||
|
JsonObject filament = filteredSpool.createNestedObject("filament");
|
||||||
|
filament["id"] = spool["filament"]["id"];
|
||||||
|
filament["name"] = spool["filament"]["name"];
|
||||||
|
filament["material"] = spool["filament"]["material"];
|
||||||
|
filament["density"] = spool["filament"]["density"];
|
||||||
|
filament["diameter"] = spool["filament"]["diameter"];
|
||||||
|
filament["spool_weight"] = spool["filament"]["spool_weight"];
|
||||||
|
filament["color_hex"] = spool["filament"]["color_hex"];
|
||||||
|
|
||||||
|
JsonObject vendor = filament.createNestedObject("vendor");
|
||||||
|
vendor["id"] = spool["filament"]["vendor"]["id"];
|
||||||
|
vendor["name"] = spool["filament"]["vendor"]["name"];
|
||||||
|
|
||||||
|
JsonObject extra = filament.createNestedObject("extra");
|
||||||
|
extra["nozzle_temperature"] = spool["filament"]["extra"]["nozzle_temperature"];
|
||||||
|
extra["price_gramm"] = spool["filament"]["extra"]["price_gramm"];
|
||||||
|
extra["price_meter"] = spool["filament"]["extra"]["price_meter"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.print("Fehler beim Abrufen der Spool-Daten. HTTP-Code: ");
|
||||||
|
Serial.println(httpCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
http.end();
|
||||||
|
return filteredDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendToApi(void *parameter) {
|
||||||
|
SendToApiParams* params = (SendToApiParams*)parameter;
|
||||||
|
|
||||||
|
// Extrahiere die Werte
|
||||||
|
String httpType = params->httpType;
|
||||||
|
String spoolsUrl = params->spoolsUrl;
|
||||||
|
String updatePayload = params->updatePayload;
|
||||||
|
|
||||||
|
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(spoolsUrl);
|
||||||
|
http.addHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
|
int httpCode = http.PUT(updatePayload);
|
||||||
|
if (httpType == "PATCH") httpCode = http.PATCH(updatePayload);
|
||||||
|
|
||||||
|
if (httpCode == HTTP_CODE_OK) {
|
||||||
|
Serial.println("Gewicht der Spule erfolgreich aktualisiert");
|
||||||
|
} else {
|
||||||
|
Serial.println("Fehler beim Aktualisieren des Gewichts der Spule");
|
||||||
|
oledShowMessage("Spoolman update failed");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
// Speicher freigeben
|
||||||
|
delete params;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t updateSpoolTagId(String uidString, const char* payload) {
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, payload);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
Serial.print("Fehler beim JSON-Parsing: ");
|
||||||
|
Serial.println(error.c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Überprüfe, ob die erforderlichen Felder vorhanden sind
|
||||||
|
if (!doc.containsKey("sm_id") || doc["sm_id"] == "") {
|
||||||
|
Serial.println("Keine Spoolman-ID gefunden.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
String spoolsUrl = spoolmanUrl + apiUrl + "/spool/" + doc["sm_id"].as<String>();
|
||||||
|
Serial.print("Update Spule mit URL: ");
|
||||||
|
Serial.println(spoolsUrl);
|
||||||
|
|
||||||
|
// Update Payload erstellen
|
||||||
|
JsonDocument updateDoc;
|
||||||
|
updateDoc["extra"]["nfc_id"] = "\""+uidString+"\"";
|
||||||
|
|
||||||
|
String updatePayload;
|
||||||
|
serializeJson(updateDoc, updatePayload);
|
||||||
|
Serial.print("Update Payload: ");
|
||||||
|
Serial.println(updatePayload);
|
||||||
|
|
||||||
|
SendToApiParams* params = new SendToApiParams();
|
||||||
|
if (params == nullptr) {
|
||||||
|
Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
params->httpType = "PATCH";
|
||||||
|
params->spoolsUrl = spoolsUrl;
|
||||||
|
params->updatePayload = updatePayload;
|
||||||
|
|
||||||
|
// Erstelle die Task
|
||||||
|
BaseType_t result = xTaskCreate(
|
||||||
|
sendToApi, // Task-Funktion
|
||||||
|
"SendToApiTask", // Task-Name
|
||||||
|
4096, // Stackgröße in Bytes
|
||||||
|
(void*)params, // Parameter
|
||||||
|
0, // Priorität
|
||||||
|
NULL // Task-Handle (nicht benötigt)
|
||||||
|
);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t updateSpoolWeight(String spoolId, uint16_t weight) {
|
||||||
|
String spoolsUrl = spoolmanUrl + apiUrl + "/spool/" + spoolId + "/measure";
|
||||||
|
Serial.print("Update Spule mit URL: ");
|
||||||
|
Serial.println(spoolsUrl);
|
||||||
|
|
||||||
|
// Update Payload erstellen
|
||||||
|
JsonDocument updateDoc;
|
||||||
|
updateDoc["weight"] = weight;
|
||||||
|
|
||||||
|
String updatePayload;
|
||||||
|
serializeJson(updateDoc, updatePayload);
|
||||||
|
Serial.print("Update Payload: ");
|
||||||
|
Serial.println(updatePayload);
|
||||||
|
|
||||||
|
SendToApiParams* params = new SendToApiParams();
|
||||||
|
if (params == nullptr) {
|
||||||
|
Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
params->httpType = "PUT";
|
||||||
|
params->spoolsUrl = spoolsUrl;
|
||||||
|
params->updatePayload = updatePayload;
|
||||||
|
|
||||||
|
// Erstelle die Task
|
||||||
|
BaseType_t result = xTaskCreate(
|
||||||
|
sendToApi, // Task-Funktion
|
||||||
|
"SendToApiTask", // Task-Name
|
||||||
|
4096, // Stackgröße in Bytes
|
||||||
|
(void*)params, // Parameter
|
||||||
|
0, // Priorität
|
||||||
|
NULL // Task-Handle (nicht benötigt)
|
||||||
|
);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #### Spoolman init
|
||||||
|
bool checkSpoolmanExtraFields() {
|
||||||
|
HTTPClient http;
|
||||||
|
String checkUrls[] = {
|
||||||
|
spoolmanUrl + apiUrl + "/field/spool",
|
||||||
|
spoolmanUrl + apiUrl + "/field/filament"
|
||||||
|
};
|
||||||
|
|
||||||
|
String spoolExtra[] = {
|
||||||
|
"nfc_id"
|
||||||
|
};
|
||||||
|
|
||||||
|
String filamentExtra[] = {
|
||||||
|
"nozzle_temperature",
|
||||||
|
"price_meter",
|
||||||
|
"price_gramm",
|
||||||
|
"bambu_setting_id",
|
||||||
|
"bambu_idx"
|
||||||
|
};
|
||||||
|
|
||||||
|
String spoolExtraFields[] = {
|
||||||
|
"{\"name\": \"NFC ID\","
|
||||||
|
"\"key\": \"nfc_id\","
|
||||||
|
"\"field_type\": \"text\"}"
|
||||||
|
};
|
||||||
|
|
||||||
|
String filamentExtraFields[] = {
|
||||||
|
"{\"name\": \"Nozzle Temp\","
|
||||||
|
"\"unit\": \"°C\","
|
||||||
|
"\"field_type\": \"integer_range\","
|
||||||
|
"\"default_value\": \"[190,230]\","
|
||||||
|
"\"key\": \"nozzle_temperature\"}",
|
||||||
|
|
||||||
|
"{\"name\": \"Price/m\","
|
||||||
|
"\"unit\": \"€\","
|
||||||
|
"\"field_type\": \"float\","
|
||||||
|
"\"key\": \"price_meter\"}",
|
||||||
|
|
||||||
|
"{\"name\": \"Price/g\","
|
||||||
|
"\"unit\": \"€\","
|
||||||
|
"\"field_type\": \"float\","
|
||||||
|
"\"key\": \"price_gramm\"}",
|
||||||
|
|
||||||
|
"{\"name\": \"Bambu Setting ID\","
|
||||||
|
"\"field_type\": \"text\","
|
||||||
|
"\"key\": \"bambu_setting_id\"}",
|
||||||
|
|
||||||
|
"{\"name\": \"Bambu IDX\","
|
||||||
|
"\"field_type\": \"text\","
|
||||||
|
"\"key\": \"bambu_idx\"}"
|
||||||
|
};
|
||||||
|
|
||||||
|
Serial.println("Überprüfe Extrafelder...");
|
||||||
|
|
||||||
|
int urlLength = sizeof(checkUrls) / sizeof(checkUrls[0]);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < urlLength; i++) {
|
||||||
|
Serial.println();
|
||||||
|
Serial.println("-------- Prüfe Felder für "+checkUrls[i]+" --------");
|
||||||
|
http.begin(checkUrls[i]);
|
||||||
|
int httpCode = http.GET();
|
||||||
|
|
||||||
|
if (httpCode == HTTP_CODE_OK) {
|
||||||
|
String payload = http.getString();
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, payload);
|
||||||
|
if (!error) {
|
||||||
|
String* extraFields;
|
||||||
|
String* extraFieldData;
|
||||||
|
u16_t extraLength;
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
extraFields = spoolExtra;
|
||||||
|
extraFieldData = spoolExtraFields;
|
||||||
|
extraLength = sizeof(spoolExtra) / sizeof(spoolExtra[0]);
|
||||||
|
} else {
|
||||||
|
extraFields = filamentExtra;
|
||||||
|
extraFieldData = filamentExtraFields;
|
||||||
|
extraLength = sizeof(filamentExtra) / sizeof(filamentExtra[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t s = 0; s < extraLength; s++) {
|
||||||
|
bool found = false;
|
||||||
|
for (JsonObject field : doc.as<JsonArray>()) {
|
||||||
|
if (field.containsKey("key") && field["key"] == extraFields[s]) {
|
||||||
|
Serial.println("Feld gefunden: " + extraFields[s]);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
Serial.println("Feld nicht gefunden: " + extraFields[s]);
|
||||||
|
|
||||||
|
// Extrafeld hinzufügen
|
||||||
|
http.begin(checkUrls[i] + "/" + extraFields[s]);
|
||||||
|
http.addHeader("Content-Type", "application/json");
|
||||||
|
int httpCode = http.POST(extraFieldData[s]);
|
||||||
|
|
||||||
|
if (httpCode > 0) {
|
||||||
|
// Antwortscode und -nachricht abrufen
|
||||||
|
String response = http.getString();
|
||||||
|
//Serial.println("HTTP-Code: " + String(httpCode));
|
||||||
|
//Serial.println("Antwort: " + response);
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fehler beim Senden der Anfrage
|
||||||
|
Serial.println("Fehler beim Senden der Anfrage: " + String(http.errorToString(httpCode)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
http.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("-------- ENDE Prüfe Felder --------");
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkSpoolmanInstance(const String& url) {
|
||||||
|
HTTPClient http;
|
||||||
|
String healthUrl = url + apiUrl + "/health";
|
||||||
|
|
||||||
|
Serial.print("Überprüfe Spoolman-Instanz unter: ");
|
||||||
|
Serial.println(healthUrl);
|
||||||
|
|
||||||
|
http.begin(healthUrl);
|
||||||
|
int httpCode = http.GET();
|
||||||
|
|
||||||
|
if (httpCode > 0) {
|
||||||
|
if (httpCode == HTTP_CODE_OK) {
|
||||||
|
oledShowMessage("Spoolman available");
|
||||||
|
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
String payload = http.getString();
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, payload);
|
||||||
|
if (!error && doc.containsKey("status")) {
|
||||||
|
const char* status = doc["status"];
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
if (!checkSpoolmanExtraFields()) {
|
||||||
|
Serial.println("Fehler beim Überprüfen der Extrafelder.");
|
||||||
|
|
||||||
|
oledShowMessage("Spoolman Error creating Extrafields");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
spoolman_connected = true;
|
||||||
|
return strcmp(status, "healthy") == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool saveSpoolmanUrl(const String& url) {
|
||||||
|
if (!checkSpoolmanInstance(url)) return false;
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
doc["url"] = url;
|
||||||
|
Serial.print("Speichere URL in Datei: ");
|
||||||
|
Serial.println(url);
|
||||||
|
if (!saveJsonValue("/spoolman_url.json", doc)) {
|
||||||
|
Serial.println("Fehler beim Speichern der Spoolman-URL.");
|
||||||
|
}
|
||||||
|
spoolmanUrl = url;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String loadSpoolmanUrl() {
|
||||||
|
JsonDocument doc;
|
||||||
|
if (loadJsonValue("/spoolman_url.json", doc) && doc.containsKey("url")) {
|
||||||
|
return doc["url"].as<String>();
|
||||||
|
}
|
||||||
|
Serial.println("Keine gültige Spoolman-URL gefunden.");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool initSpoolman() {
|
||||||
|
spoolmanUrl = loadSpoolmanUrl();
|
||||||
|
spoolmanUrl.trim();
|
||||||
|
if (spoolmanUrl == "") {
|
||||||
|
Serial.println("Keine Spoolman-URL gefunden.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = checkSpoolmanInstance(spoolmanUrl);
|
||||||
|
if (!success) {
|
||||||
|
Serial.println("Spoolman nicht erreichbar.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
oledShowTopRow();
|
||||||
|
return true;
|
||||||
|
}
|
24
src/api.h
Normal file
24
src/api.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#ifndef API_H
|
||||||
|
#define API_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESPAsyncWebServer.h> // Include for AsyncWebServerRequest
|
||||||
|
#include "website.h"
|
||||||
|
#include "display.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
extern bool spoolman_connected;
|
||||||
|
extern String spoolmanUrl;
|
||||||
|
|
||||||
|
bool checkSpoolmanInstance(const String& url);
|
||||||
|
bool saveSpoolmanUrl(const String& url);
|
||||||
|
String loadSpoolmanUrl(); // Neue Funktion zum Laden der URL
|
||||||
|
bool checkSpoolmanExtraFields(); // Neue Funktion zum Überprüfen der Extrafelder
|
||||||
|
JsonDocument fetchSpoolsForWebsite(); // API-Funktion für die Webseite
|
||||||
|
JsonDocument fetchAllSpoolsInfo();
|
||||||
|
void sendAmsData(AsyncWebSocketClient *client); // Neue Funktion zum Senden von AMS-Daten
|
||||||
|
uint8_t updateSpoolTagId(String uidString, const char* payload); // Neue Funktion zum Aktualisieren eines Spools
|
||||||
|
uint8_t updateSpoolWeight(String spoolId, uint16_t weight); // Neue Funktion zum Aktualisieren des Gewichts
|
||||||
|
bool initSpoolman(); // Neue Funktion zum Initialisieren von Spoolman
|
||||||
|
|
||||||
|
#endif
|
486
src/bambu.cpp
Normal file
486
src/bambu.cpp
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
#include "bambu.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
#include <SSLClient.h>
|
||||||
|
#include "bambu_cert.h"
|
||||||
|
#include "website.h"
|
||||||
|
#include "nfc.h"
|
||||||
|
#include "commonFS.h"
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "display.h"
|
||||||
|
|
||||||
|
WiFiClient espClient;
|
||||||
|
SSLClient sslClient(&espClient);
|
||||||
|
PubSubClient client(sslClient);
|
||||||
|
|
||||||
|
TaskHandle_t BambuMqttTask;
|
||||||
|
|
||||||
|
String report_topic = "";
|
||||||
|
//String request_topic = "";
|
||||||
|
const char* bambu_username = "bblp";
|
||||||
|
const char* bambu_ip = nullptr;
|
||||||
|
const char* bambu_accesscode = nullptr;
|
||||||
|
const char* bambu_serialnr = nullptr;
|
||||||
|
bool bambu_connected = false;
|
||||||
|
|
||||||
|
// Globale Variablen für AMS-Daten
|
||||||
|
int ams_count = 0;
|
||||||
|
String amsJsonData; // Speichert das fertige JSON für WebSocket-Clients
|
||||||
|
AMSData ams_data[MAX_AMS]; // Definition des Arrays
|
||||||
|
|
||||||
|
bool saveBambuCredentials(const String& bambu_ip, const String& bambu_serialnr, const String& bambu_accesscode) {
|
||||||
|
JsonDocument doc;
|
||||||
|
doc["bambu_ip"] = bambu_ip;
|
||||||
|
doc["bambu_accesscode"] = bambu_accesscode;
|
||||||
|
doc["bambu_serialnr"] = bambu_serialnr;
|
||||||
|
|
||||||
|
if (!saveJsonValue("/bambu_credentials.json", doc)) {
|
||||||
|
Serial.println("Fehler beim Speichern der Bambu-Credentials.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||||
|
if (!setupMqtt()) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadBambuCredentials() {
|
||||||
|
JsonDocument doc;
|
||||||
|
if (loadJsonValue("/bambu_credentials.json", doc) && doc.containsKey("bambu_ip")) {
|
||||||
|
// Temporäre Strings für die Werte
|
||||||
|
String ip = doc["bambu_ip"].as<String>();
|
||||||
|
String code = doc["bambu_accesscode"].as<String>();
|
||||||
|
String serial = doc["bambu_serialnr"].as<String>();
|
||||||
|
|
||||||
|
ip.trim();
|
||||||
|
code.trim();
|
||||||
|
serial.trim();
|
||||||
|
|
||||||
|
// Dynamische Speicherallokation für die globalen Pointer
|
||||||
|
bambu_ip = strdup(ip.c_str());
|
||||||
|
bambu_accesscode = strdup(code.c_str());
|
||||||
|
bambu_serialnr = strdup(serial.c_str());
|
||||||
|
|
||||||
|
report_topic = "device/" + String(bambu_serialnr) + "/report";
|
||||||
|
//request_topic = "device/" + String(bambu_serialnr) + "/request";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Serial.println("Keine gültigen Bambu-Credentials gefunden.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String findFilamentIdx(String brand, String type) {
|
||||||
|
// JSON-Dokument für die Filament-Daten erstellen
|
||||||
|
JsonDocument doc;
|
||||||
|
|
||||||
|
// Laden der bambu_filaments.json
|
||||||
|
if (!loadJsonValue("/bambu_filaments.json", doc)) {
|
||||||
|
Serial.println("Fehler beim Laden der Filament-Daten");
|
||||||
|
return "GFL99"; // Fallback auf Generic PLA
|
||||||
|
}
|
||||||
|
|
||||||
|
String searchKey;
|
||||||
|
|
||||||
|
// 1. Suche nach Brand + Type Kombination
|
||||||
|
if (brand == "Bambu" || brand == "Bambulab") {
|
||||||
|
searchKey = "Bambu " + type;
|
||||||
|
} else if (brand == "PolyLite") {
|
||||||
|
searchKey = "PolyLite " + type;
|
||||||
|
} else if (brand == "eSUN") {
|
||||||
|
searchKey = "eSUN " + type;
|
||||||
|
} else if (brand == "Overture") {
|
||||||
|
searchKey = "Overture " + type;
|
||||||
|
} else if (brand == "PolyTerra") {
|
||||||
|
searchKey = "PolyTerra " + type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Durchsuche alle Einträge nach der Brand + Type Kombination
|
||||||
|
for (JsonPair kv : doc.as<JsonObject>()) {
|
||||||
|
if (kv.value().as<String>() == searchKey) {
|
||||||
|
return kv.key().c_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Wenn nicht gefunden, suche nach Generic + Type
|
||||||
|
searchKey = "Generic " + type;
|
||||||
|
for (JsonPair kv : doc.as<JsonObject>()) {
|
||||||
|
if (kv.value().as<String>() == searchKey) {
|
||||||
|
return kv.key().c_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Wenn immer noch nichts gefunden, gebe GFL99 zurück (Generic PLA)
|
||||||
|
return "GFL99";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendMqttMessage(String payload) {
|
||||||
|
Serial.println("Sending MQTT message");
|
||||||
|
Serial.println(payload);
|
||||||
|
if (client.publish(report_topic.c_str(), payload.c_str()))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setBambuSpool(String payload) {
|
||||||
|
/* payload
|
||||||
|
//// set Spool
|
||||||
|
{
|
||||||
|
"print": {
|
||||||
|
"sequence_id": 0,
|
||||||
|
"command": "ams_filament_setting",
|
||||||
|
"ams_id": 0, // AMS ID 0-3 oder externe Spule 255
|
||||||
|
"tray_id": 0, // Tray ID 0-3 oder externe Spule 254
|
||||||
|
"tray_color": "000000FF",
|
||||||
|
"nozzle_temp_min": 170,
|
||||||
|
"nozzle_temp_max": 200,
|
||||||
|
"tray_type": "PETG",
|
||||||
|
"setting_id": "",
|
||||||
|
"tray_info_idx": "GFG99"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//// Remove Spool
|
||||||
|
{
|
||||||
|
"print":{
|
||||||
|
"ams_id":255,
|
||||||
|
"command":"ams_filament_setting",
|
||||||
|
"nozzle_temp_max": 0,
|
||||||
|
"nozzle_temp_min": 0,
|
||||||
|
"sequence_id": 0,
|
||||||
|
"setting_id": "",
|
||||||
|
"tray_color": "FFFFFFFF",
|
||||||
|
"tray_id": 254,
|
||||||
|
"tray_info_idx": "",
|
||||||
|
"tray_type": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Serial.println("Setting spool");
|
||||||
|
|
||||||
|
// Parse the JSON
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, payload);
|
||||||
|
if (error) {
|
||||||
|
Serial.print("Error parsing JSON: ");
|
||||||
|
Serial.println(error.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int amsId = doc["amsId"];
|
||||||
|
int trayId = doc["trayId"];
|
||||||
|
String color = doc["color"].as<String>();
|
||||||
|
color.toUpperCase();
|
||||||
|
int minTemp = doc["nozzle_temp_min"];
|
||||||
|
int maxTemp = doc["nozzle_temp_max"];
|
||||||
|
String type = doc["type"].as<String>();
|
||||||
|
String brand = doc["brand"].as<String>();
|
||||||
|
String tray_info_idx = findFilamentIdx(brand, type);
|
||||||
|
|
||||||
|
doc.clear();
|
||||||
|
|
||||||
|
doc["print"]["sequence_id"] = 0;
|
||||||
|
doc["print"]["command"] = "ams_filament_setting";
|
||||||
|
doc["print"]["ams_id"] = amsId < 200 ? amsId-1 : 255;
|
||||||
|
doc["print"]["tray_id"] = trayId < 200 ? trayId-1 : 254;
|
||||||
|
doc["print"]["tray_color"] = color.length() == 8 ? color : color+"FF";
|
||||||
|
doc["print"]["nozzle_temp_min"] = minTemp;
|
||||||
|
doc["print"]["nozzle_temp_max"] = maxTemp;
|
||||||
|
doc["print"]["tray_type"] = type;
|
||||||
|
doc["print"]["setting_id"] = "";
|
||||||
|
doc["print"]["tray_info_idx"] = tray_info_idx;
|
||||||
|
|
||||||
|
// Serialize the JSON
|
||||||
|
String output;
|
||||||
|
serializeJson(doc, output);
|
||||||
|
|
||||||
|
if (sendMqttMessage(output)) {
|
||||||
|
Serial.println("Spool successfully set");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.println("Failed to set spool");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// init
|
||||||
|
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
|
||||||
|
String message;
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
message += (char)payload[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON-Dokument parsen
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, message);
|
||||||
|
if (error) {
|
||||||
|
Serial.print("Fehler beim Parsen des JSON: ");
|
||||||
|
Serial.println(error.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen, ob "print->upgrade_state" und "print.ams.ams" existieren
|
||||||
|
if (doc["print"].containsKey("upgrade_state")) {
|
||||||
|
// Prüfen ob AMS-Daten vorhanden sind
|
||||||
|
if (!doc["print"].containsKey("ams") || !doc["print"]["ams"].containsKey("ams")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray amsArray = doc["print"]["ams"]["ams"].as<JsonArray>();
|
||||||
|
|
||||||
|
// Prüfe ob sich die AMS-Daten geändert haben
|
||||||
|
bool hasChanges = false;
|
||||||
|
|
||||||
|
// Vergleiche jedes AMS und seine Trays
|
||||||
|
for (int i = 0; i < amsArray.size() && !hasChanges; i++) {
|
||||||
|
JsonObject amsObj = amsArray[i];
|
||||||
|
int amsId = amsObj["id"].as<uint8_t>();
|
||||||
|
JsonArray trayArray = amsObj["tray"].as<JsonArray>();
|
||||||
|
|
||||||
|
// Finde das entsprechende AMS in unseren Daten
|
||||||
|
int storedIndex = -1;
|
||||||
|
for (int k = 0; k < ams_count; k++) {
|
||||||
|
if (ams_data[k].ams_id == amsId) {
|
||||||
|
storedIndex = k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storedIndex == -1) {
|
||||||
|
hasChanges = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vergleiche die Trays
|
||||||
|
for (int j = 0; j < trayArray.size() && j < 4 && !hasChanges; j++) {
|
||||||
|
JsonObject trayObj = trayArray[j];
|
||||||
|
if (trayObj["tray_info_idx"].as<String>() != ams_data[storedIndex].trays[j].tray_info_idx ||
|
||||||
|
trayObj["tray_type"].as<String>() != ams_data[storedIndex].trays[j].tray_type ||
|
||||||
|
trayObj["tray_color"].as<String>() != ams_data[storedIndex].trays[j].tray_color) {
|
||||||
|
hasChanges = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe die externe Spule
|
||||||
|
if (!hasChanges && doc["print"].containsKey("vt_tray")) {
|
||||||
|
JsonObject vtTray = doc["print"]["vt_tray"];
|
||||||
|
bool foundExternal = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < ams_count; i++) {
|
||||||
|
if (ams_data[i].ams_id == 255) {
|
||||||
|
foundExternal = true;
|
||||||
|
if (vtTray["tray_info_idx"].as<String>() != ams_data[i].trays[0].tray_info_idx ||
|
||||||
|
vtTray["tray_type"].as<String>() != ams_data[i].trays[0].tray_type ||
|
||||||
|
vtTray["tray_color"].as<String>() != ams_data[i].trays[0].tray_color) {
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundExternal) hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChanges) return;
|
||||||
|
|
||||||
|
// Fortfahren mit der bestehenden Verarbeitung, da Änderungen gefunden wurden
|
||||||
|
ams_count = amsArray.size();
|
||||||
|
|
||||||
|
// Restlicher bestehender Code...
|
||||||
|
for (int i = 0; i < ams_count && i < 16; i++) {
|
||||||
|
JsonObject amsObj = amsArray[i];
|
||||||
|
JsonArray trayArray = amsObj["tray"].as<JsonArray>();
|
||||||
|
|
||||||
|
ams_data[i].ams_id = i; // Setze die AMS-ID
|
||||||
|
for (int j = 0; j < trayArray.size() && j < 4; j++) { // Annahme: Maximal 4 Trays pro AMS
|
||||||
|
JsonObject trayObj = trayArray[j];
|
||||||
|
|
||||||
|
ams_data[i].trays[j].id = trayObj["id"].as<uint8_t>();
|
||||||
|
ams_data[i].trays[j].tray_info_idx = trayObj["tray_info_idx"].as<String>();
|
||||||
|
ams_data[i].trays[j].tray_type = trayObj["tray_type"].as<String>();
|
||||||
|
ams_data[i].trays[j].tray_sub_brands = trayObj["tray_sub_brands"].as<String>();
|
||||||
|
ams_data[i].trays[j].tray_color = trayObj["tray_color"].as<String>();
|
||||||
|
ams_data[i].trays[j].nozzle_temp_min = trayObj["nozzle_temp_min"].as<int>();
|
||||||
|
ams_data[i].trays[j].nozzle_temp_max = trayObj["nozzle_temp_max"].as<int>();
|
||||||
|
ams_data[i].trays[j].setting_id = trayObj["setting_id"].as<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Serial.println("----------------");
|
||||||
|
//Serial.println();
|
||||||
|
|
||||||
|
// Sende die aktualisierten AMS-Daten an alle WebSocket-Clients
|
||||||
|
sendAmsData(nullptr);
|
||||||
|
|
||||||
|
// Verarbeite erst die normalen AMS-Daten
|
||||||
|
for (int i = 0; i < amsArray.size() && i < 16; i++) {
|
||||||
|
JsonObject amsObj = amsArray[i];
|
||||||
|
JsonArray trayArray = amsObj["tray"].as<JsonArray>();
|
||||||
|
|
||||||
|
ams_data[i].ams_id = amsObj["id"].as<uint8_t>();
|
||||||
|
for (int j = 0; j < trayArray.size() && j < 4; j++) {
|
||||||
|
JsonObject trayObj = trayArray[j];
|
||||||
|
ams_data[i].trays[j].id = trayObj["id"].as<uint8_t>();
|
||||||
|
ams_data[i].trays[j].tray_info_idx = trayObj["tray_info_idx"].as<String>();
|
||||||
|
// ... weitere Tray-Daten ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setze ams_count auf die Anzahl der normalen AMS
|
||||||
|
ams_count = amsArray.size();
|
||||||
|
|
||||||
|
// Wenn externe Spule vorhanden, füge sie hinzu
|
||||||
|
if (doc["print"].containsKey("vt_tray")) {
|
||||||
|
JsonObject vtTray = doc["print"]["vt_tray"];
|
||||||
|
int extIdx = ams_count; // Index für externe Spule
|
||||||
|
ams_data[extIdx].ams_id = 255; // Spezielle ID für externe Spule
|
||||||
|
ams_data[extIdx].trays[0].id = 254; // Spezielle ID für externes Tray
|
||||||
|
ams_data[extIdx].trays[0].tray_info_idx = vtTray["tray_info_idx"].as<String>();
|
||||||
|
ams_data[extIdx].trays[0].tray_type = vtTray["tray_type"].as<String>();
|
||||||
|
ams_data[extIdx].trays[0].tray_sub_brands = vtTray["tray_sub_brands"].as<String>();
|
||||||
|
ams_data[extIdx].trays[0].tray_color = vtTray["tray_color"].as<String>();
|
||||||
|
ams_data[extIdx].trays[0].nozzle_temp_min = vtTray["nozzle_temp_min"].as<int>();
|
||||||
|
ams_data[extIdx].trays[0].nozzle_temp_max = vtTray["nozzle_temp_max"].as<int>();
|
||||||
|
ams_data[extIdx].trays[0].setting_id = vtTray["setting_id"].as<String>();
|
||||||
|
ams_count++; // Erhöhe ams_count für die externe Spule
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sende die aktualisierten AMS-Daten
|
||||||
|
sendAmsData(nullptr);
|
||||||
|
|
||||||
|
// Erstelle JSON für WebSocket-Clients
|
||||||
|
JsonDocument wsDoc;
|
||||||
|
JsonArray wsArray = wsDoc.to<JsonArray>();
|
||||||
|
|
||||||
|
for (int i = 0; i < ams_count; i++) {
|
||||||
|
JsonObject amsObj = wsArray.createNestedObject();
|
||||||
|
amsObj["ams_id"] = ams_data[i].ams_id;
|
||||||
|
|
||||||
|
JsonArray trays = amsObj.createNestedArray("tray");
|
||||||
|
int maxTrays = (ams_data[i].ams_id == 255) ? 1 : 4;
|
||||||
|
|
||||||
|
for (int j = 0; j < maxTrays; j++) {
|
||||||
|
JsonObject trayObj = trays.createNestedObject();
|
||||||
|
trayObj["id"] = ams_data[i].trays[j].id;
|
||||||
|
trayObj["tray_info_idx"] = ams_data[i].trays[j].tray_info_idx;
|
||||||
|
trayObj["tray_type"] = ams_data[i].trays[j].tray_type;
|
||||||
|
trayObj["tray_sub_brands"] = ams_data[i].trays[j].tray_sub_brands;
|
||||||
|
trayObj["tray_color"] = ams_data[i].trays[j].tray_color;
|
||||||
|
trayObj["nozzle_temp_min"] = ams_data[i].trays[j].nozzle_temp_min;
|
||||||
|
trayObj["nozzle_temp_max"] = ams_data[i].trays[j].nozzle_temp_max;
|
||||||
|
trayObj["setting_id"] = ams_data[i].trays[j].setting_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeJson(wsArray, amsJsonData);
|
||||||
|
sendAmsData(nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reconnect() {
|
||||||
|
// Loop until we're reconnected
|
||||||
|
while (!client.connected()) {
|
||||||
|
Serial.print("Attempting MQTT connection...");
|
||||||
|
|
||||||
|
// Attempt to connect
|
||||||
|
if (client.connect(bambu_serialnr, bambu_username, bambu_accesscode)) {
|
||||||
|
Serial.println("... re-connected");
|
||||||
|
// ... and resubscribe
|
||||||
|
client.subscribe(report_topic.c_str());
|
||||||
|
bambu_connected = true;
|
||||||
|
oledShowTopRow();
|
||||||
|
} else {
|
||||||
|
Serial.print("failed, rc=");
|
||||||
|
Serial.print(client.state());
|
||||||
|
Serial.println(" try again in 5 seconds");
|
||||||
|
bambu_connected = false;
|
||||||
|
oledShowTopRow();
|
||||||
|
// Wait 5 seconds before retrying
|
||||||
|
delay(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mqtt_loop(void * parameter) {
|
||||||
|
oledShowMessage("Bambu Connected");
|
||||||
|
bambu_connected = true;
|
||||||
|
oledShowTopRow();
|
||||||
|
for(;;) {
|
||||||
|
if (pauseBambuMqttTask) {
|
||||||
|
vTaskDelay(10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.connected()) {
|
||||||
|
reconnect();
|
||||||
|
yield();
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
vTaskDelay(100);
|
||||||
|
}
|
||||||
|
client.loop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setupMqtt() {
|
||||||
|
// Wenn Bambu Daten vorhanden
|
||||||
|
bool success = loadBambuCredentials();
|
||||||
|
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
Serial.println("Failed to load Bambu credentials");
|
||||||
|
oledShowMessage("Bambu Credentials Missing");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success && bambu_ip != "" && bambu_accesscode != "" && bambu_serialnr != "") {
|
||||||
|
sslClient.setCACert(root_ca);
|
||||||
|
sslClient.setInsecure();
|
||||||
|
client.setServer(bambu_ip, 8883);
|
||||||
|
|
||||||
|
// Verbinden mit dem MQTT-Server
|
||||||
|
if (client.connect(bambu_serialnr, bambu_username, bambu_accesscode)) {
|
||||||
|
client.setCallback(mqtt_callback);
|
||||||
|
client.setBufferSize(5120);
|
||||||
|
// Optional: Topic abonnieren
|
||||||
|
client.subscribe(report_topic.c_str());
|
||||||
|
//client.subscribe(request_topic.c_str());
|
||||||
|
Serial.println("MQTT-Client initialisiert");
|
||||||
|
|
||||||
|
oledShowTopRow();
|
||||||
|
|
||||||
|
xTaskCreatePinnedToCore(
|
||||||
|
mqtt_loop, /* Function to implement the task */
|
||||||
|
"BambuMqtt", /* Name of the task */
|
||||||
|
10000, /* Stack size in words */
|
||||||
|
NULL, /* Task input parameter */
|
||||||
|
mqttTaskPrio, /* Priority of the task */
|
||||||
|
&BambuMqttTask, /* Task handle. */
|
||||||
|
mqttTaskCore); /* Core where the task should run */
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Serial.println("Fehler: Konnte sich nicht beim MQTT-Server anmelden");
|
||||||
|
oledShowMessage("Bambu Connection Failed");
|
||||||
|
oledShowTopRow();
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.println("Fehler: Keine MQTT-Daten vorhanden");
|
||||||
|
oledShowMessage("Bambu Credentials Missing");
|
||||||
|
oledShowTopRow();
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
37
src/bambu.h
Normal file
37
src/bambu.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#ifndef BAMBU_H
|
||||||
|
#define BAMBU_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
struct TrayData {
|
||||||
|
uint8_t id;
|
||||||
|
String tray_info_idx;
|
||||||
|
String tray_type;
|
||||||
|
String tray_sub_brands;
|
||||||
|
String tray_color;
|
||||||
|
int nozzle_temp_min;
|
||||||
|
int nozzle_temp_max;
|
||||||
|
String setting_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define MAX_AMS 17 // 16 normale AMS + 1 externe Spule
|
||||||
|
extern String amsJsonData; // Für die vorbereiteten JSON-Daten
|
||||||
|
|
||||||
|
struct AMSData {
|
||||||
|
uint8_t ams_id;
|
||||||
|
TrayData trays[4]; // Annahme: Maximal 4 Trays pro AMS
|
||||||
|
};
|
||||||
|
|
||||||
|
extern bool bambu_connected;
|
||||||
|
|
||||||
|
extern int ams_count;
|
||||||
|
extern AMSData ams_data[MAX_AMS];
|
||||||
|
|
||||||
|
bool loadBambuCredentials();
|
||||||
|
bool saveBambuCredentials(const String& bambu_ip, const String& bambu_serialnr, const String& bambu_accesscode);
|
||||||
|
bool setupMqtt();
|
||||||
|
void mqtt_loop(void * parameter);
|
||||||
|
bool setBambuSpool(String payload);
|
||||||
|
|
||||||
|
#endif
|
45
src/bambu_cert.h
Normal file
45
src/bambu_cert.h
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
const char root_ca[] PROGMEM =
|
||||||
|
"-----BEGIN CERTIFICATE-----\n"
|
||||||
|
"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\n"
|
||||||
|
"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n"
|
||||||
|
"b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\n"
|
||||||
|
"MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\n"
|
||||||
|
"b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\n"
|
||||||
|
"ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n"
|
||||||
|
"9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\n"
|
||||||
|
"IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\n"
|
||||||
|
"VOujw5H5SNz/0egwLX0tdHA234gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n"
|
||||||
|
"93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\n"
|
||||||
|
"jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\n"
|
||||||
|
"AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\n"
|
||||||
|
"A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\n"
|
||||||
|
"U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\n"
|
||||||
|
"N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\n"
|
||||||
|
"o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n"
|
||||||
|
"5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\n"
|
||||||
|
"rqXRfboQnoZsG4q5WTP468SQvvG5\n"
|
||||||
|
"-----END CERTIFICATE-----";
|
||||||
|
|
||||||
|
/*
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDZTCCAk2gAwIBAgIUV1FckwXElyek1onFnQ9kL7Bk4N8wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwQjELMAkGA1UEBhMCQ04xIjAgBgNVBAoMGUJCTCBUZWNobm9sb2dpZXMgQ28u
|
||||||
|
LCBMdGQxDzANBgNVBAMMBkJCTCBDQTAeFw0yMjA0MDQwMzQyMTFaFw0zMjA0MDEw
|
||||||
|
MzQyMTFaMEIxCzAJBgNVBAYTAkNOMSIwIAYDVQQKDBlCQkwgVGVjaG5vbG9naWVz
|
||||||
|
IENvLiwgTHRkMQ8wDQYDVQQDDAZCQkwgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||||
|
DwAwggEKAoIBAQDL3pnDdxGOk5Z6vugiT4dpM0ju+3Xatxz09UY7mbj4tkIdby4H
|
||||||
|
oeEdiYSZjc5LJngJuCHwtEbBJt1BriRdSVrF6M9D2UaBDyamEo0dxwSaVxZiDVWC
|
||||||
|
eeCPdELpFZdEhSNTaT4O7zgvcnFsfHMa/0vMAkvE7i0qp3mjEzYLfz60axcDoJLk
|
||||||
|
p7n6xKXI+cJbA4IlToFjpSldPmC+ynOo7YAOsXt7AYKY6Glz0BwUVzSJxU+/+VFy
|
||||||
|
/QrmYGNwlrQtdREHeRi0SNK32x1+bOndfJP0sojuIrDjKsdCLye5CSZIvqnbowwW
|
||||||
|
1jRwZgTBR29Zp2nzCoxJYcU9TSQp/4KZuWNVAgMBAAGjUzBRMB0GA1UdDgQWBBSP
|
||||||
|
NEJo3GdOj8QinsV8SeWr3US+HjAfBgNVHSMEGDAWgBSPNEJo3GdOj8QinsV8SeWr
|
||||||
|
3US+HjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQABlBIT5ZeG
|
||||||
|
fgcK1LOh1CN9sTzxMCLbtTPFF1NGGA13mApu6j1h5YELbSKcUqfXzMnVeAb06Htu
|
||||||
|
3CoCoe+wj7LONTFO++vBm2/if6Jt/DUw1CAEcNyqeh6ES0NX8LJRVSe0qdTxPJuA
|
||||||
|
BdOoo96iX89rRPoxeed1cpq5hZwbeka3+CJGV76itWp35Up5rmmUqrlyQOr/Wax6
|
||||||
|
itosIzG0MfhgUzU51A2P/hSnD3NDMXv+wUY/AvqgIL7u7fbDKnku1GzEKIkfH8hm
|
||||||
|
Rs6d8SCU89xyrwzQ0PR853irHas3WrHVqab3P+qNwR0YirL0Qk7Xt/q3O1griNg2
|
||||||
|
Blbjg3obpHo9
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
*/
|
56
src/commonFS.cpp
Normal file
56
src/commonFS.cpp
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#include "commonFS.h"
|
||||||
|
|
||||||
|
bool saveJsonValue(const char* filename, const JsonDocument& doc) {
|
||||||
|
File file = SPIFFS.open(filename, "w");
|
||||||
|
if (!file) {
|
||||||
|
Serial.print("Fehler beim Öffnen der Datei zum Schreiben: ");
|
||||||
|
Serial.println(filename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
if (serializeJson(doc, file) == 0) {
|
||||||
|
Serial.println("Fehler beim Serialisieren von JSON.");
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadJsonValue(const char* filename, JsonDocument& doc) {
|
||||||
|
File file = SPIFFS.open(filename, "r");
|
||||||
|
if (!file) {
|
||||||
|
Serial.print("Fehler beim Öffnen der Datei zum Lesen: ");
|
||||||
|
Serial.println(filename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DeserializationError error = deserializeJson(doc, file);
|
||||||
|
file.close();
|
||||||
|
if (error) {
|
||||||
|
Serial.print("Fehler beim Deserialisieren von JSON: ");
|
||||||
|
Serial.println(error.f_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool initializeSPIFFS() {
|
||||||
|
// Erster Versuch
|
||||||
|
if (SPIFFS.begin(true)) {
|
||||||
|
Serial.println("SPIFFS mounted successfully.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatierung versuchen
|
||||||
|
Serial.println("Failed to mount SPIFFS. Formatting...");
|
||||||
|
SPIFFS.format();
|
||||||
|
|
||||||
|
// Zweiter Versuch nach Formatierung
|
||||||
|
if (SPIFFS.begin(true)) {
|
||||||
|
Serial.println("SPIFFS formatted and mounted successfully.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("SPIFFS initialization failed completely.");
|
||||||
|
return false;
|
||||||
|
}
|
12
src/commonFS.h
Normal file
12
src/commonFS.h
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#ifndef COMMONFS_H
|
||||||
|
#define COMMONFS_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <SPIFFS.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
bool saveJsonValue(const char* filename, const JsonDocument& doc);
|
||||||
|
bool loadJsonValue(const char* filename, JsonDocument& doc);
|
||||||
|
bool initializeSPIFFS();
|
||||||
|
|
||||||
|
#endif
|
54
src/config.cpp
Normal file
54
src/config.cpp
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// ################### Config Bereich Start
|
||||||
|
// ***** PN532 (RFID)
|
||||||
|
//#define PN532_SCK 18
|
||||||
|
//#define PN532_MOSI 23
|
||||||
|
//#define PN532_SS 5
|
||||||
|
//#define PN532_MISO 19
|
||||||
|
const uint8_t PN532_IRQ = 32;
|
||||||
|
const uint8_t PN532_RESET = 33;
|
||||||
|
// ***** PN532
|
||||||
|
|
||||||
|
// ***** HX711 (Waage)
|
||||||
|
// HX711 circuit wiring
|
||||||
|
const uint8_t LOADCELL_DOUT_PIN = 16; //16;
|
||||||
|
const uint8_t LOADCELL_SCK_PIN = 17; //17;
|
||||||
|
const uint8_t calVal_eepromAdress = 0;
|
||||||
|
const uint16_t SCALE_LEVEL_WEIGHT = 500;
|
||||||
|
uint16_t defaultScaleCalibrationValue = 430;
|
||||||
|
// ***** HX711
|
||||||
|
|
||||||
|
// ***** Display
|
||||||
|
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
|
||||||
|
// On an ESP32: 21(SDA), 22(SCL)
|
||||||
|
const int8_t OLED_RESET = -1; // Reset pin # (or -1 if sharing Arduino reset pin)
|
||||||
|
const uint8_t SCREEN_ADDRESS = 0x3C; ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
|
||||||
|
const uint8_t SCREEN_WIDTH = 128; // OLED display width, in pixels
|
||||||
|
const uint8_t SCREEN_HEIGHT = 64; // OLED display height, in pixels
|
||||||
|
const uint8_t OLED_TOP_START = 0;
|
||||||
|
const uint8_t OLED_TOP_END = 16;
|
||||||
|
const uint8_t OLED_DATA_START = 17;
|
||||||
|
const uint8_t OLED_DATA_END = SCREEN_HEIGHT;
|
||||||
|
// ***** Display
|
||||||
|
|
||||||
|
// ***** Webserver
|
||||||
|
const uint8_t webserverPort = 80;
|
||||||
|
// ***** Webserver
|
||||||
|
|
||||||
|
// ***** API
|
||||||
|
const char* apiUrl = "/api/v1";
|
||||||
|
// ***** API
|
||||||
|
|
||||||
|
// ***** Task Prios
|
||||||
|
uint8_t rfidTaskCore = 1;
|
||||||
|
uint8_t rfidTaskPrio = 1;
|
||||||
|
|
||||||
|
uint8_t rfidWriteTaskPrio = 1;
|
||||||
|
|
||||||
|
uint8_t mqttTaskCore = 1;
|
||||||
|
uint8_t mqttTaskPrio = 1;
|
||||||
|
|
||||||
|
uint8_t scaleTaskCore = 0;
|
||||||
|
uint8_t scaleTaskPrio = 1;
|
||||||
|
// ***** Task Prios
|
48
src/config.h
Normal file
48
src/config.h
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#ifndef CONFIG_H
|
||||||
|
#define CONFIG_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
extern const uint8_t PN532_IRQ;
|
||||||
|
extern const uint8_t PN532_RESET;
|
||||||
|
|
||||||
|
extern const uint8_t LOADCELL_DOUT_PIN;
|
||||||
|
extern const uint8_t LOADCELL_SCK_PIN;
|
||||||
|
extern const uint8_t calVal_eepromAdress;
|
||||||
|
extern const uint16_t SCALE_LEVEL_WEIGHT;
|
||||||
|
|
||||||
|
extern const int8_t OLED_RESET;
|
||||||
|
extern const uint8_t SCREEN_ADDRESS;
|
||||||
|
extern const uint8_t SCREEN_WIDTH;
|
||||||
|
extern const uint8_t SCREEN_HEIGHT;
|
||||||
|
extern const uint8_t OLED_TOP_START;
|
||||||
|
extern const uint8_t OLED_TOP_END;
|
||||||
|
extern const uint8_t OLED_DATA_START;
|
||||||
|
extern const uint8_t OLED_DATA_END;
|
||||||
|
|
||||||
|
extern const char* apiUrl;
|
||||||
|
extern const uint8_t webserverPort;
|
||||||
|
|
||||||
|
extern const unsigned char wifi_on[];
|
||||||
|
extern const unsigned char wifi_off[];
|
||||||
|
extern const unsigned char cloud_on[];
|
||||||
|
extern const unsigned char cloud_off[];
|
||||||
|
|
||||||
|
extern const unsigned char icon_failed[];
|
||||||
|
extern const unsigned char icon_success[];
|
||||||
|
extern const unsigned char icon_transfer[];
|
||||||
|
extern const unsigned char icon_loading[];
|
||||||
|
|
||||||
|
extern uint8_t rfidTaskCore;
|
||||||
|
extern uint8_t rfidTaskPrio;
|
||||||
|
|
||||||
|
extern uint8_t rfidWriteTaskPrio;
|
||||||
|
|
||||||
|
extern uint8_t mqttTaskCore;
|
||||||
|
extern uint8_t mqttTaskPrio;
|
||||||
|
|
||||||
|
extern uint8_t scaleTaskCore;
|
||||||
|
extern uint8_t scaleTaskPrio;
|
||||||
|
|
||||||
|
extern uint16_t defaultScaleCalibrationValue;
|
||||||
|
#endif
|
225
src/display.cpp
Normal file
225
src/display.cpp
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
#include "display.h"
|
||||||
|
#include "api.h"
|
||||||
|
#include <vector>
|
||||||
|
#include "icons.h"
|
||||||
|
|
||||||
|
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
|
||||||
|
|
||||||
|
bool wifiOn = false;
|
||||||
|
|
||||||
|
void setupDisplay() {
|
||||||
|
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
|
||||||
|
Serial.println(F("SSD1306 allocation failed"));
|
||||||
|
for (;;); // Stoppe das Programm, wenn das Display nicht initialisiert werden kann
|
||||||
|
}
|
||||||
|
display.setTextColor(WHITE);
|
||||||
|
display.clearDisplay();
|
||||||
|
display.display();
|
||||||
|
|
||||||
|
// Show initial display buffer contents on the screen --
|
||||||
|
// the library initializes this with an Adafruit splash screen.
|
||||||
|
display.setTextColor(WHITE);
|
||||||
|
display.display();
|
||||||
|
delay(1000); // Pause for 2 seconds
|
||||||
|
oledShowTopRow();
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void oledclearline() {
|
||||||
|
int x, y;
|
||||||
|
for (y = 0; y < 16; y++) {
|
||||||
|
for (x = 0; x < SCREEN_WIDTH; x++) {
|
||||||
|
display.drawPixel(x, y, BLACK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void oledcleardata() {
|
||||||
|
int x, y;
|
||||||
|
for (y = OLED_DATA_START; y < OLED_DATA_END; y++) {
|
||||||
|
for (x = 0; x < SCREEN_WIDTH; x++) {
|
||||||
|
display.drawPixel(x, y, BLACK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
int oled_center_h(String text) {
|
||||||
|
int16_t x1, y1;
|
||||||
|
uint16_t w, h;
|
||||||
|
display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
|
||||||
|
return (SCREEN_WIDTH - w) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oled_center_v(String text) {
|
||||||
|
int16_t x1, y1;
|
||||||
|
uint16_t w, h;
|
||||||
|
display.getTextBounds(text, 0, OLED_DATA_START, &x1, &y1, &w, &h);
|
||||||
|
// Zentrierung nur im Datenbereich zwischen OLED_DATA_START und OLED_DATA_END
|
||||||
|
return OLED_DATA_START + ((OLED_DATA_END - OLED_DATA_START - h) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<String> splitTextIntoLines(String text, uint8_t textSize) {
|
||||||
|
std::vector<String> lines;
|
||||||
|
display.setTextSize(textSize);
|
||||||
|
|
||||||
|
// Text in Wörter aufteilen
|
||||||
|
std::vector<String> words;
|
||||||
|
int pos = 0;
|
||||||
|
while (pos < text.length()) {
|
||||||
|
// Überspringe Leerzeichen am Anfang
|
||||||
|
while (pos < text.length() && text[pos] == ' ') pos++;
|
||||||
|
|
||||||
|
// Finde nächstes Leerzeichen
|
||||||
|
int nextSpace = text.indexOf(' ', pos);
|
||||||
|
if (nextSpace == -1) {
|
||||||
|
// Letztes Wort
|
||||||
|
if (pos < text.length()) {
|
||||||
|
words.push_back(text.substring(pos));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Wort hinzufügen
|
||||||
|
words.push_back(text.substring(pos, nextSpace));
|
||||||
|
pos = nextSpace + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wörter zu Zeilen zusammenfügen
|
||||||
|
String currentLine = "";
|
||||||
|
for (size_t i = 0; i < words.size(); i++) {
|
||||||
|
String testLine = currentLine;
|
||||||
|
if (currentLine.length() > 0) testLine += " ";
|
||||||
|
testLine += words[i];
|
||||||
|
|
||||||
|
// Prüfe ob diese Kombination auf die Zeile passt
|
||||||
|
int16_t x1, y1;
|
||||||
|
uint16_t lineWidth, h;
|
||||||
|
display.getTextBounds(testLine, 0, OLED_DATA_START, &x1, &y1, &lineWidth, &h);
|
||||||
|
|
||||||
|
if (lineWidth <= SCREEN_WIDTH) {
|
||||||
|
// Passt noch in diese Zeile
|
||||||
|
currentLine = testLine;
|
||||||
|
} else {
|
||||||
|
// Neue Zeile beginnen
|
||||||
|
if (currentLine.length() > 0) {
|
||||||
|
lines.push_back(currentLine);
|
||||||
|
currentLine = words[i];
|
||||||
|
} else {
|
||||||
|
// Ein einzelnes Wort ist zu lang
|
||||||
|
lines.push_back(words[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Letzte Zeile hinzufügen
|
||||||
|
if (currentLine.length() > 0) {
|
||||||
|
lines.push_back(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println(lines.size());
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
void oledShowMultilineMessage(String message, uint8_t size) {
|
||||||
|
std::vector<String> lines;
|
||||||
|
int maxLines = 3; // Maximale Anzahl Zeilen für size 2
|
||||||
|
|
||||||
|
// Erste Prüfung mit aktueller Größe
|
||||||
|
lines = splitTextIntoLines(message, size);
|
||||||
|
|
||||||
|
// Wenn mehr als maxLines Zeilen, reduziere Textgröße
|
||||||
|
if (lines.size() > maxLines && size > 1) {
|
||||||
|
size = 1;
|
||||||
|
lines = splitTextIntoLines(message, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ausgabe
|
||||||
|
display.setTextSize(size);
|
||||||
|
int lineHeight = size * 8;
|
||||||
|
int totalHeight = lines.size() * lineHeight;
|
||||||
|
int startY = OLED_DATA_START + ((OLED_DATA_END - OLED_DATA_START - totalHeight) / 2);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < lines.size(); i++) {
|
||||||
|
display.setCursor(oled_center_h(lines[i]), startY + (i * lineHeight));
|
||||||
|
display.print(lines[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void oledShowMessage(String message, uint8_t size) {
|
||||||
|
oledcleardata();
|
||||||
|
display.setTextSize(size);
|
||||||
|
display.setTextWrap(false);
|
||||||
|
|
||||||
|
// Prüfe ob Text in eine Zeile passt
|
||||||
|
int16_t x1, y1;
|
||||||
|
uint16_t textWidth, h;
|
||||||
|
display.getTextBounds(message, 0, 0, &x1, &y1, &textWidth, &h);
|
||||||
|
|
||||||
|
// Text passt in eine Zeile?
|
||||||
|
if (textWidth <= SCREEN_WIDTH) {
|
||||||
|
display.setCursor(oled_center_h(message), oled_center_v(message));
|
||||||
|
display.print(message);
|
||||||
|
display.display();
|
||||||
|
} else {
|
||||||
|
oledShowMultilineMessage(message, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void oledShowTopRow() {
|
||||||
|
oledclearline();
|
||||||
|
|
||||||
|
if (bambu_connected == 1) {
|
||||||
|
display.drawBitmap(50, 0, bitmap_bambu_on , 16, 16, WHITE);
|
||||||
|
} else {
|
||||||
|
display.drawBitmap(50, 0, bitmap_off , 16, 16, WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spoolman_connected == 1) {
|
||||||
|
display.drawBitmap(80, 0, bitmap_spoolman_on , 16, 16, WHITE);
|
||||||
|
} else {
|
||||||
|
display.drawBitmap(80, 0, bitmap_off , 16, 16, WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wifiOn == 1) {
|
||||||
|
display.drawBitmap(107, 0, wifi_on , 16, 16, WHITE);
|
||||||
|
} else {
|
||||||
|
display.drawBitmap(107, 0, wifi_off , 16, 16, WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void oledShowIcon(const char* icon) {
|
||||||
|
oledcleardata();
|
||||||
|
|
||||||
|
uint16_t iconSize = OLED_DATA_END-OLED_DATA_START;
|
||||||
|
uint16_t iconStart = (SCREEN_WIDTH - iconSize) / 2;
|
||||||
|
|
||||||
|
if (icon == "failed") {
|
||||||
|
display.drawBitmap(iconStart, OLED_DATA_START, icon_failed , iconSize, iconSize, WHITE);
|
||||||
|
}
|
||||||
|
else if (icon == "success") {
|
||||||
|
display.drawBitmap(iconStart, OLED_DATA_START, icon_success , iconSize, iconSize, WHITE);
|
||||||
|
}
|
||||||
|
else if (icon == "transfer") {
|
||||||
|
display.drawBitmap(iconStart, OLED_DATA_START, icon_transfer , iconSize, iconSize, WHITE);
|
||||||
|
}
|
||||||
|
else if (icon == "loading") {
|
||||||
|
display.drawBitmap(iconStart, OLED_DATA_START, icon_loading , iconSize, iconSize, WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void oledShowWeight(uint16_t weight) {
|
||||||
|
// Display Gewicht
|
||||||
|
oledcleardata();
|
||||||
|
display.setTextSize(3);
|
||||||
|
display.setCursor(oled_center_h(String(weight)+" g"), OLED_DATA_START+10);
|
||||||
|
display.print(weight);
|
||||||
|
display.print(" g");
|
||||||
|
display.display();
|
||||||
|
}
|
24
src/display.h
Normal file
24
src/display.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#ifndef DISPLAY_H
|
||||||
|
#define DISPLAY_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Adafruit_GFX.h>
|
||||||
|
#include <Adafruit_SSD1306.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
|
||||||
|
extern Adafruit_SSD1306 display;
|
||||||
|
extern bool wifiOn;
|
||||||
|
|
||||||
|
void setupDisplay();
|
||||||
|
void oledclearline();
|
||||||
|
void oledcleardata();
|
||||||
|
int oled_center_h(String text);
|
||||||
|
int oled_center_v(String text);
|
||||||
|
|
||||||
|
void oledShowWeight(uint16_t weight);
|
||||||
|
void oledShowMessage(String message, uint8_t size = 2);
|
||||||
|
void oledShowTopRow();
|
||||||
|
void oledShowIcon(const char* icon);
|
||||||
|
|
||||||
|
#endif
|
126
src/icons.h
Normal file
126
src/icons.h
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Create Icons
|
||||||
|
https://javl.github.io/image2cpp/
|
||||||
|
Size: 47x47
|
||||||
|
BG Color: Black
|
||||||
|
Invert: True
|
||||||
|
Ohters in default
|
||||||
|
*/
|
||||||
|
|
||||||
|
const unsigned char wifi_on [] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x1f, 0xf8, 0x78, 0x1e, 0x60, 0x06, 0x07, 0xe0,
|
||||||
|
0x0f, 0xf0, 0x08, 0x10, 0x00, 0x00, 0x03, 0xc0, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char wifi_off [] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x1b, 0xf0, 0x3d, 0xfc, 0x7e, 0x1e, 0x63, 0x06, 0x07, 0xa0,
|
||||||
|
0x1f, 0xd8, 0x08, 0x60, 0x01, 0xb0, 0x03, 0xd8, 0x01, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'off', 16x16px
|
||||||
|
const unsigned char bitmap_off [] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'spoolman_on', 16x16px
|
||||||
|
const unsigned char bitmap_spoolman_on [] PROGMEM = {
|
||||||
|
0x03, 0xc0, 0x08, 0xf0, 0x20, 0xfc, 0x00, 0xfc, 0x40, 0xfe, 0x70, 0xf0, 0xf8, 0xc1, 0xff, 0xc1,
|
||||||
|
0xff, 0xc1, 0xf9, 0xc1, 0x70, 0xf0, 0x40, 0xfe, 0x00, 0xfc, 0x20, 0xfc, 0x08, 0xf0, 0x03, 0xc0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'bambu_on', 16x16px
|
||||||
|
const unsigned char bitmap_bambu_on [] PROGMEM = {
|
||||||
|
0x3e, 0x7c, 0x3e, 0x7c, 0x3e, 0x7c, 0x3e, 0x7c, 0x3e, 0x7c, 0x3e, 0x1c, 0x3e, 0x00, 0x3e, 0x40,
|
||||||
|
0x30, 0x78, 0x00, 0x7c, 0x06, 0x7c, 0x1e, 0x7c, 0x3e, 0x7c, 0x3e, 0x7c, 0x3e, 0x7c, 0x3e, 0x7c
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'failed', 47x47px
|
||||||
|
const unsigned char icon_failed [] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfc, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff,
|
||||||
|
0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x1f, 0xfc, 0x7f, 0xf0, 0x00, 0x00, 0x7f,
|
||||||
|
0x80, 0x03, 0xf8, 0x00, 0x00, 0xfe, 0x00, 0x00, 0xfe, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x3f, 0x00,
|
||||||
|
0x03, 0xf0, 0x00, 0x00, 0x1f, 0x80, 0x07, 0xc0, 0x00, 0x00, 0x0f, 0x80, 0x07, 0x80, 0x00, 0x00,
|
||||||
|
0x07, 0xc0, 0x0f, 0x80, 0x00, 0x00, 0x03, 0xe0, 0x1f, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x1e, 0x03,
|
||||||
|
0x80, 0x07, 0x80, 0xf0, 0x3e, 0x03, 0xc0, 0x07, 0x80, 0xf0, 0x3c, 0x03, 0xe0, 0x0f, 0x80, 0x78,
|
||||||
|
0x3c, 0x01, 0xf0, 0x1f, 0x00, 0x78, 0x78, 0x00, 0xf8, 0x3e, 0x00, 0x38, 0x78, 0x00, 0x7c, 0x7c,
|
||||||
|
0x00, 0x3c, 0x78, 0x00, 0x3c, 0xf8, 0x00, 0x3c, 0x78, 0x00, 0x3f, 0xf0, 0x00, 0x3c, 0x70, 0x00,
|
||||||
|
0x1f, 0xf0, 0x00, 0x3c, 0x70, 0x00, 0x0f, 0xe0, 0x00, 0x3c, 0xf0, 0x00, 0x07, 0xc0, 0x00, 0x1c,
|
||||||
|
0xf0, 0x00, 0x0f, 0xe0, 0x00, 0x3c, 0x70, 0x00, 0x1f, 0xf0, 0x00, 0x3c, 0x78, 0x00, 0x3f, 0xf0,
|
||||||
|
0x00, 0x3c, 0x78, 0x00, 0x3e, 0xf8, 0x00, 0x3c, 0x78, 0x00, 0x7c, 0x7c, 0x00, 0x3c, 0x78, 0x00,
|
||||||
|
0xf8, 0x3e, 0x00, 0x3c, 0x3c, 0x01, 0xf0, 0x1f, 0x00, 0x78, 0x3c, 0x03, 0xe0, 0x0f, 0x80, 0x78,
|
||||||
|
0x3e, 0x03, 0xc0, 0x0f, 0x80, 0xf0, 0x1e, 0x03, 0x80, 0x07, 0x80, 0xf0, 0x1f, 0x00, 0x00, 0x00,
|
||||||
|
0x01, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x07, 0x80, 0x00, 0x00, 0x07, 0xc0, 0x07, 0xc0,
|
||||||
|
0x00, 0x00, 0x0f, 0xc0, 0x03, 0xf0, 0x00, 0x00, 0x1f, 0x80, 0x01, 0xf8, 0x00, 0x00, 0x3f, 0x00,
|
||||||
|
0x00, 0xfe, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x7f, 0x80, 0x03, 0xfc, 0x00, 0x00, 0x1f, 0xf8, 0x3f,
|
||||||
|
0xf0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00,
|
||||||
|
0x7f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'loading', 47x47px
|
||||||
|
const unsigned char icon_loading [] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfc, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe,
|
||||||
|
0x00, 0x00, 0x00, 0x3f, 0xf9, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0x83, 0xf8, 0x38, 0x00, 0x00, 0xff,
|
||||||
|
0x07, 0xf0, 0x7c, 0x00, 0x00, 0xfc, 0x07, 0xe0, 0x7e, 0x00, 0x01, 0xf8, 0x0f, 0xc0, 0x3f, 0x00,
|
||||||
|
0x03, 0xf0, 0x07, 0x80, 0x1f, 0x00, 0x03, 0xe0, 0x03, 0x00, 0x1f, 0x80, 0x07, 0xe0, 0x00, 0x00,
|
||||||
|
0x0f, 0x80, 0x07, 0xc0, 0x00, 0x00, 0x0f, 0xc0, 0x07, 0xc0, 0x00, 0x00, 0x07, 0xc0, 0x0f, 0x80,
|
||||||
|
0x00, 0x00, 0x07, 0xc0, 0x0f, 0x80, 0x00, 0x00, 0x03, 0xc0, 0x0f, 0x80, 0x00, 0x00, 0x03, 0xe0,
|
||||||
|
0x0f, 0x80, 0x00, 0x00, 0x03, 0xe0, 0x0f, 0x80, 0x00, 0x00, 0x03, 0xe0, 0x0f, 0x80, 0x00, 0x00,
|
||||||
|
0x03, 0xe0, 0x0f, 0x80, 0x00, 0x00, 0x03, 0xe0, 0x0f, 0x80, 0x00, 0x00, 0x03, 0xe0, 0x0f, 0x80,
|
||||||
|
0x00, 0x00, 0x03, 0xc0, 0x0f, 0x80, 0x00, 0x00, 0x07, 0xc0, 0x07, 0xc0, 0x00, 0x00, 0x07, 0xc0,
|
||||||
|
0x07, 0xc0, 0x00, 0x00, 0x07, 0xc0, 0x07, 0xe0, 0x00, 0x00, 0x0f, 0x80, 0x03, 0xe0, 0x00, 0x00,
|
||||||
|
0x1f, 0x80, 0x03, 0xf0, 0x00, 0x00, 0x1f, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x3f, 0x00, 0x01, 0xfc,
|
||||||
|
0x00, 0x00, 0x7e, 0x00, 0x00, 0xfe, 0x00, 0x01, 0xfc, 0x00, 0x00, 0x7f, 0x80, 0x07, 0xf8, 0x00,
|
||||||
|
0x00, 0x3f, 0xf0, 0x3f, 0xf0, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xff,
|
||||||
|
0xc0, 0x00, 0x00, 0x01, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfc, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'success', 47x47px
|
||||||
|
const unsigned char icon_success [] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfc, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff,
|
||||||
|
0x80, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x1f, 0xfc, 0x3f, 0xf0, 0x00, 0x00, 0x7f,
|
||||||
|
0x80, 0x03, 0xfc, 0x00, 0x00, 0xfe, 0x00, 0x00, 0xfe, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x3f, 0x00,
|
||||||
|
0x03, 0xf0, 0x00, 0x00, 0x1f, 0x80, 0x03, 0xe0, 0x00, 0x00, 0x07, 0xc0, 0x07, 0xc0, 0x00, 0x00,
|
||||||
|
0x03, 0xc0, 0x0f, 0x80, 0x00, 0x00, 0x01, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x1e, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0xf0, 0x1e, 0x00, 0x00, 0x00, 0xc0, 0xf8, 0x3c, 0x00, 0x00, 0x01, 0xe0, 0x78,
|
||||||
|
0x3c, 0x00, 0x00, 0x03, 0xe0, 0x78, 0x38, 0x00, 0x00, 0x03, 0xc0, 0x3c, 0x78, 0x00, 0x00, 0x07,
|
||||||
|
0xc0, 0x3c, 0x78, 0x00, 0x00, 0x0f, 0x80, 0x3c, 0x78, 0x00, 0x00, 0x1f, 0x00, 0x1c, 0x78, 0x00,
|
||||||
|
0x00, 0x1e, 0x00, 0x1c, 0x78, 0x0f, 0x00, 0x3e, 0x00, 0x1e, 0x78, 0x0f, 0x80, 0x7c, 0x00, 0x1e,
|
||||||
|
0x78, 0x0f, 0xc0, 0xf8, 0x00, 0x1e, 0x78, 0x07, 0xe0, 0xf0, 0x00, 0x1c, 0x78, 0x03, 0xf1, 0xf0,
|
||||||
|
0x00, 0x1c, 0x78, 0x01, 0xfb, 0xe0, 0x00, 0x3c, 0x78, 0x00, 0xff, 0xc0, 0x00, 0x3c, 0x38, 0x00,
|
||||||
|
0x7f, 0x80, 0x00, 0x3c, 0x3c, 0x00, 0x3f, 0x80, 0x00, 0x78, 0x3c, 0x00, 0x1f, 0x00, 0x00, 0x78,
|
||||||
|
0x1e, 0x00, 0x0e, 0x00, 0x00, 0xf8, 0x1e, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x0f, 0x00, 0x00, 0x00,
|
||||||
|
0x01, 0xf0, 0x0f, 0x80, 0x00, 0x00, 0x01, 0xe0, 0x07, 0xc0, 0x00, 0x00, 0x03, 0xc0, 0x03, 0xe0,
|
||||||
|
0x00, 0x00, 0x07, 0xc0, 0x03, 0xf0, 0x00, 0x00, 0x1f, 0x80, 0x01, 0xf8, 0x00, 0x00, 0x3f, 0x00,
|
||||||
|
0x00, 0xfe, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x7f, 0x80, 0x03, 0xfc, 0x00, 0x00, 0x1f, 0xfc, 0x3f,
|
||||||
|
0xf0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00,
|
||||||
|
0x7f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'transfer', 47x47px
|
||||||
|
const unsigned char icon_transfer [] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xe0, 0x01,
|
||||||
|
0xfe, 0x00, 0x00, 0x3f, 0xe0, 0x07, 0xff, 0x80, 0x00, 0xff, 0xc0, 0x0f, 0xff, 0xc0, 0x01, 0xfb,
|
||||||
|
0xc0, 0x1f, 0x03, 0xe0, 0x03, 0xe3, 0x80, 0x3c, 0x00, 0xf0, 0x07, 0xc3, 0x80, 0x78, 0x00, 0x78,
|
||||||
|
0x0f, 0x80, 0x00, 0x70, 0x00, 0x78, 0x0f, 0x00, 0x00, 0x70, 0x00, 0x38, 0x1e, 0x00, 0x00, 0xf0,
|
||||||
|
0x00, 0x3c, 0x1c, 0x00, 0x00, 0xe0, 0x00, 0x3c, 0x3c, 0x00, 0x00, 0xe0, 0x00, 0x1c, 0x38, 0x00,
|
||||||
|
0x00, 0xe0, 0x00, 0x3c, 0x38, 0x00, 0x00, 0xf0, 0x00, 0x3c, 0x38, 0x00, 0x00, 0x70, 0x00, 0x38,
|
||||||
|
0x38, 0x00, 0x00, 0x78, 0x00, 0x78, 0x38, 0x00, 0x00, 0x78, 0x00, 0x78, 0x30, 0x00, 0x00, 0x3c,
|
||||||
|
0x00, 0xf0, 0x00, 0x00, 0x00, 0x1f, 0x03, 0xe0, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00,
|
||||||
|
0x00, 0x07, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x01, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfc, 0x00,
|
||||||
|
0x00, 0x00, 0x1f, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x1e, 0x00, 0x00, 0x18, 0x38, 0x00,
|
||||||
|
0x0e, 0x00, 0x00, 0x1c, 0x38, 0x00, 0x0e, 0x00, 0x00, 0x1c, 0x38, 0x00, 0x0e, 0x00, 0x00, 0x3c,
|
||||||
|
0x38, 0x00, 0x0e, 0x00, 0x00, 0x3c, 0x38, 0x00, 0x0e, 0x00, 0x00, 0x38, 0x38, 0x00, 0x0e, 0x00,
|
||||||
|
0x00, 0x38, 0x38, 0x00, 0x0e, 0x00, 0x00, 0x78, 0x38, 0x00, 0x0e, 0x00, 0x00, 0x70, 0x38, 0x00,
|
||||||
|
0x0e, 0x00, 0x00, 0xf0, 0x38, 0x00, 0x0e, 0x00, 0x01, 0xe0, 0x38, 0x00, 0x0e, 0x01, 0x83, 0xe0,
|
||||||
|
0x38, 0x00, 0x0e, 0x03, 0x87, 0xc0, 0x3c, 0x00, 0x1e, 0x03, 0x9f, 0x80, 0x1f, 0x80, 0xfc, 0x07,
|
||||||
|
0xff, 0x00, 0x1f, 0xff, 0xf8, 0x07, 0xfc, 0x00, 0x07, 0xff, 0xf0, 0x0f, 0xf0, 0x00, 0x01, 0xff,
|
||||||
|
0xc0, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
191
src/main.cpp
Normal file
191
src/main.cpp
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <DNSServer.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
#include <ESPmDNS.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "website.h"
|
||||||
|
#include "api.h"
|
||||||
|
#include "display.h"
|
||||||
|
#include "bambu.h"
|
||||||
|
#include "nfc.h"
|
||||||
|
#include "scale.h"
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
|
#include "commonFS.h"
|
||||||
|
|
||||||
|
// ***** WIFI initialisieren
|
||||||
|
WiFiManager wm;
|
||||||
|
bool wm_nonblocking = false;
|
||||||
|
void initWiFi();
|
||||||
|
// ################### Functions
|
||||||
|
|
||||||
|
// ##### SETUP #####
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
// Initialize SPIFFS
|
||||||
|
initializeSPIFFS();
|
||||||
|
|
||||||
|
// Start Display
|
||||||
|
setupDisplay();
|
||||||
|
|
||||||
|
// WiFiManager
|
||||||
|
initWiFi();
|
||||||
|
|
||||||
|
// Webserver
|
||||||
|
Serial.println("Starte Webserver");
|
||||||
|
setupWebserver(server);
|
||||||
|
|
||||||
|
// Spoolman API
|
||||||
|
// api.cpp
|
||||||
|
initSpoolman();
|
||||||
|
|
||||||
|
// Bambu MQTT
|
||||||
|
// bambu.cpp
|
||||||
|
setupMqtt();
|
||||||
|
|
||||||
|
// mDNS
|
||||||
|
Serial.println("Starte MDNS");
|
||||||
|
if (!MDNS.begin("filaman")) { // Set the hostname to "esp32.local"
|
||||||
|
Serial.println("Error setting up MDNS responder!");
|
||||||
|
while(1) {
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serial.println("mDNS responder started");
|
||||||
|
|
||||||
|
startNfc();
|
||||||
|
|
||||||
|
start_scale();
|
||||||
|
|
||||||
|
// WDT initialisieren mit 10 Sekunden Timeout
|
||||||
|
bool panic = true; // Wenn true, löst ein WDT-Timeout einen System-Panik aus
|
||||||
|
esp_task_wdt_init(10, panic);
|
||||||
|
|
||||||
|
// Aktuellen Task (loopTask) zum Watchdog hinzufügen
|
||||||
|
esp_task_wdt_add(NULL);
|
||||||
|
|
||||||
|
// Optional: Andere Tasks zum Watchdog hinzufügen, falls nötig
|
||||||
|
// esp_task_wdt_add(task_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
unsigned long lastWeightReadTime = 0;
|
||||||
|
const unsigned long weightReadInterval = 1000; // 1 second
|
||||||
|
uint8_t weightSend = 0;
|
||||||
|
int16_t lastWeight = 0;
|
||||||
|
uint8_t wifiErrorCounter = 0;
|
||||||
|
|
||||||
|
// ##### PROGRAM START #####
|
||||||
|
void loop() {
|
||||||
|
// Überprüfe den WLAN-Status
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
wifiErrorCounter++;
|
||||||
|
wifiOn = false;
|
||||||
|
} else {
|
||||||
|
wifiErrorCounter = 0;
|
||||||
|
wifiOn = true;
|
||||||
|
}
|
||||||
|
if (wifiErrorCounter > 20) ESP.restart();
|
||||||
|
|
||||||
|
unsigned long currentMillis = millis();
|
||||||
|
|
||||||
|
// Falls WifiManager im nicht blockenden Modus ist
|
||||||
|
//if(wm_nonblocking) wm.process();
|
||||||
|
|
||||||
|
// Ausgabe der Waage auf Display
|
||||||
|
if (pauseMainTask == 0 && weight != lastWeight && hasReadRfidTag == 0)
|
||||||
|
{
|
||||||
|
(weight < 0) ? oledShowMessage("!! -1") : oledShowWeight(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn Timer abgelaufen und nicht gerade ein RFID-Tag geschrieben wird
|
||||||
|
if (currentMillis - lastWeightReadTime >= weightReadInterval && hasReadRfidTag < 3)
|
||||||
|
{
|
||||||
|
lastWeightReadTime = currentMillis;
|
||||||
|
|
||||||
|
// Prüfen ob die Waage korrekt genullt ist
|
||||||
|
if ((weight > 0 && weight < 5) || weight < 0)
|
||||||
|
{
|
||||||
|
scale_tare_counter++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scale_tare_counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob das Gewicht gleich bleibt und dann senden
|
||||||
|
if (weight == lastWeight && weight > 5)
|
||||||
|
{
|
||||||
|
weigthCouterToApi++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
weigthCouterToApi = 0;
|
||||||
|
weightSend = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// reset weight counter after writing tag
|
||||||
|
if (currentMillis - lastWeightReadTime >= weightReadInterval && hasReadRfidTag > 1)
|
||||||
|
{
|
||||||
|
weigthCouterToApi = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastWeight = weight;
|
||||||
|
|
||||||
|
// Wenn ein Tag mit SM id erkannte wurde und der Waage Counter anspricht an SM Senden
|
||||||
|
if (spoolId != "" && weigthCouterToApi > 5 && weightSend == 0 && hasReadRfidTag == 1) {
|
||||||
|
oledShowIcon("loading");
|
||||||
|
if (updateSpoolWeight(spoolId, weight))
|
||||||
|
{
|
||||||
|
oledShowIcon("success");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
weightSend = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
oledShowIcon("failed");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield();
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ##### Funktionen für Konfiguration #####
|
||||||
|
void initWiFi() {
|
||||||
|
WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP
|
||||||
|
|
||||||
|
if(wm_nonblocking) wm.setConfigPortalBlocking(false);
|
||||||
|
wm.setConfigPortalTimeout(320); // Portal nach 5min schließen
|
||||||
|
|
||||||
|
oledShowTopRow();
|
||||||
|
oledShowMessage("WiFi Setup");
|
||||||
|
|
||||||
|
bool res;
|
||||||
|
// res = wm.autoConnect(); // auto generated AP name from chipid
|
||||||
|
res = wm.autoConnect("FilaMan"); // anonymous ap
|
||||||
|
// res = wm.autoConnect("spoolman","password"); // password protected ap
|
||||||
|
|
||||||
|
if(!res) {
|
||||||
|
Serial.println("Failed to connect or hit timeout");
|
||||||
|
// ESP.restart();
|
||||||
|
oledShowTopRow();
|
||||||
|
oledShowMessage("WiFi not connected Check Portal");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
wifiOn = true;
|
||||||
|
|
||||||
|
//if you get here you have connected to the WiFi
|
||||||
|
Serial.println("connected...yeey :)");
|
||||||
|
Serial.print("IP address: ");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
|
||||||
|
oledShowTopRow();
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ##### Funktionen für Konfiguration Ende #####
|
503
src/nfc.cpp
Normal file
503
src/nfc.cpp
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
#include "nfc.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Adafruit_PN532.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "config.h"
|
||||||
|
#include "website.h"
|
||||||
|
#include "api.h"
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
|
|
||||||
|
//Adafruit_PN532 nfc(PN532_SCK, PN532_MISO, PN532_MOSI, PN532_SS);
|
||||||
|
Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET);
|
||||||
|
|
||||||
|
TaskHandle_t RfidReaderTask;
|
||||||
|
|
||||||
|
JsonDocument rfidData;
|
||||||
|
String spoolId = "";
|
||||||
|
String nfcJsonData = "";
|
||||||
|
volatile bool pauseBambuMqttTask = false;
|
||||||
|
|
||||||
|
volatile uint8_t hasReadRfidTag = 0;
|
||||||
|
// 0 = nicht gelesen
|
||||||
|
// 1 = erfolgreich gelesen
|
||||||
|
// 2 = fehler beim Lesen
|
||||||
|
// 3 = schreiben
|
||||||
|
// 4 = fehler beim Schreiben
|
||||||
|
// 5 = erfolgreich geschrieben
|
||||||
|
// 6 = reading
|
||||||
|
// ***** PN532
|
||||||
|
|
||||||
|
|
||||||
|
// ##### Funktionen für RFID #####
|
||||||
|
void payloadToJson(uint8_t *data) {
|
||||||
|
const char* startJson = strchr((char*)data, '{');
|
||||||
|
const char* endJson = strrchr((char*)data, '}');
|
||||||
|
|
||||||
|
if (startJson && endJson && endJson > startJson) {
|
||||||
|
String jsonString = String(startJson, endJson - startJson + 1);
|
||||||
|
//Serial.print("Bereinigter JSON-String: ");
|
||||||
|
//Serial.println(jsonString);
|
||||||
|
|
||||||
|
// JSON-Dokument verarbeiten
|
||||||
|
JsonDocument doc; // Passen Sie die Größe an den JSON-Inhalt an
|
||||||
|
DeserializationError error = deserializeJson(doc, jsonString);
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
const char* version = doc["version"];
|
||||||
|
const char* protocol = doc["protocol"];
|
||||||
|
const char* color_hex = doc["color_hex"];
|
||||||
|
const char* type = doc["type"];
|
||||||
|
int min_temp = doc["min_temp"];
|
||||||
|
int max_temp = doc["max_temp"];
|
||||||
|
const char* brand = doc["brand"];
|
||||||
|
|
||||||
|
Serial.println();
|
||||||
|
Serial.println("-----------------");
|
||||||
|
Serial.println("JSON-Parsed Data:");
|
||||||
|
Serial.println(version);
|
||||||
|
Serial.println(protocol);
|
||||||
|
Serial.println(color_hex);
|
||||||
|
Serial.println(type);
|
||||||
|
Serial.println(min_temp);
|
||||||
|
Serial.println(max_temp);
|
||||||
|
Serial.println(brand);
|
||||||
|
Serial.println("-----------------");
|
||||||
|
Serial.println();
|
||||||
|
} else {
|
||||||
|
Serial.print("deserializeJson() failed: ");
|
||||||
|
Serial.println(error.f_str());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.println("Kein gültiger JSON-Inhalt gefunden oder fehlerhafte Formatierung.");
|
||||||
|
//writeJsonToTag("{\"version\":\"1.0\",\"protocol\":\"NFC\",\"color_hex\":\"#FFFFFF\",\"type\":\"Example\",\"min_temp\":10,\"max_temp\":30,\"brand\":\"BrandName\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool formatNdefTag() {
|
||||||
|
uint8_t ndefInit[] = { 0x03, 0x00, 0xFE }; // NDEF Initialisierungsnachricht
|
||||||
|
bool success = true;
|
||||||
|
int pageOffset = 4; // Startseite für NDEF-Daten auf NTAG2xx
|
||||||
|
|
||||||
|
Serial.println();
|
||||||
|
Serial.println("Formatiere NDEF-Tag...");
|
||||||
|
|
||||||
|
// Schreibe die Initialisierungsnachricht auf die ersten Seiten
|
||||||
|
for (int i = 0; i < sizeof(ndefInit); i += 4) {
|
||||||
|
if (!nfc.ntag2xx_WritePage(pageOffset + (i / 4), &ndefInit[i])) {
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t ntag2xx_WriteNDEF(const char *payload) {
|
||||||
|
/*
|
||||||
|
if (!formatNdefTag()) {
|
||||||
|
Serial.println("Fehler beim Formatieren des NDEF-Tags.");
|
||||||
|
hasReadRfidTag = 2;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
uint8_t tagSize = 240; // 144 bytes is maximum for NTAG213
|
||||||
|
Serial.print("Tag Size: ");Serial.println(tagSize);
|
||||||
|
|
||||||
|
uint8_t pageBuffer[4] = {0, 0, 0, 0};
|
||||||
|
Serial.println("Beginne mit dem Schreiben der NDEF-Nachricht...");
|
||||||
|
|
||||||
|
// Figure out how long the string is
|
||||||
|
uint8_t len = strlen(payload);
|
||||||
|
Serial.print("Länge der Payload: ");
|
||||||
|
Serial.println(len);
|
||||||
|
|
||||||
|
Serial.print("Payload: ");Serial.println(payload);
|
||||||
|
|
||||||
|
// Setup the record header
|
||||||
|
// See NFCForum-TS-Type-2-Tag_1.1.pdf for details
|
||||||
|
uint8_t pageHeader[21] = {
|
||||||
|
/* NDEF Message TLV - JSON Record */
|
||||||
|
0x03, /* Tag Field (0x03 = NDEF Message) */
|
||||||
|
(uint8_t)(len+3+16), /* Payload Length (including NDEF header) */
|
||||||
|
0xD2, /* NDEF Record Header (TNF=0x2:MIME Media + SR + ME + MB) */
|
||||||
|
0x10, /* Type Length for the record type indicator */
|
||||||
|
(uint8_t)(len), /* Payload len */
|
||||||
|
'a', 'p', 'p', 'l', 'i', 'c', 'a', 't', 'i', 'o', 'n', '/', 'j', 's', 'o', 'n'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure the URI payload will fit in dataLen (include 0xFE trailer)
|
||||||
|
if ((len < 1) || (len + 1 > (tagSize - sizeof(pageHeader))))
|
||||||
|
{
|
||||||
|
Serial.println();
|
||||||
|
Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||||
|
Serial.println("Fehler: Die Nutzlast passt nicht in die Datenlänge.");
|
||||||
|
Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||||
|
Serial.println();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Serial.println();
|
||||||
|
//Serial.print("Header Size: ");Serial.println(sizeof(pageHeader));
|
||||||
|
|
||||||
|
// Kombiniere Header und Payload
|
||||||
|
int totalSize = sizeof(pageHeader) + len;
|
||||||
|
uint8_t* combinedData = (uint8_t*) malloc(totalSize);
|
||||||
|
if (combinedData == NULL)
|
||||||
|
{
|
||||||
|
Serial.println("Fehler: Nicht genug Speicher vorhanden.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Überprüfe die Kombination von Header und Payload
|
||||||
|
/*
|
||||||
|
Serial.print("Header: ");
|
||||||
|
for (int i = 0; i < sizeof(pageHeader); i++) {
|
||||||
|
Serial.print(pageHeader[i], HEX);
|
||||||
|
Serial.print(" ");
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
Serial.print("Payload: ");
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
Serial.print(payload[i], HEX);
|
||||||
|
Serial.print(" ");
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Kombiniere Header und Payload
|
||||||
|
memcpy(combinedData, pageHeader, sizeof(pageHeader));
|
||||||
|
memcpy(&combinedData[sizeof(pageHeader)], payload, len);
|
||||||
|
|
||||||
|
// Überprüfe die Kombination von Header und Payload
|
||||||
|
/*
|
||||||
|
Serial.print("Kombinierte Daten: ");
|
||||||
|
for (int i = 0; i < totalSize; i++) {
|
||||||
|
Serial.print(combinedData[i], HEX);
|
||||||
|
Serial.print(" ");
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Schreibe die Seiten
|
||||||
|
uint8_t a = 0;
|
||||||
|
uint8_t i = 0;
|
||||||
|
while (totalSize > 0) {
|
||||||
|
memset(pageBuffer, 0, 4);
|
||||||
|
int bytesToWrite = (totalSize < 4) ? totalSize : 4;
|
||||||
|
memcpy(pageBuffer, combinedData + a, bytesToWrite);
|
||||||
|
|
||||||
|
// Überprüfe die Schreibung der Seiten
|
||||||
|
/*
|
||||||
|
Serial.print("Seite ");
|
||||||
|
Serial.print(i);
|
||||||
|
Serial.print(": ");
|
||||||
|
for (int j = 0; j < bytesToWrite; j++) {
|
||||||
|
Serial.print(pageBuffer[j], HEX);
|
||||||
|
Serial.print(" ");
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
*/
|
||||||
|
uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; // Buffer to store the returned UID
|
||||||
|
uint8_t uidLength;
|
||||||
|
nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 500);
|
||||||
|
//Serial.print("Schreibe Seite: ");Serial.println(i);
|
||||||
|
|
||||||
|
if (!(nfc.ntag2xx_WritePage(4+i, pageBuffer)))
|
||||||
|
{
|
||||||
|
Serial.println("Fehler beim Schreiben der Seite.");
|
||||||
|
free(combinedData);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Serial.print("Seite geschrieben: ");Serial.println(i);
|
||||||
|
|
||||||
|
yield();
|
||||||
|
//esp_task_wdt_reset();
|
||||||
|
|
||||||
|
i++;
|
||||||
|
a += 4;
|
||||||
|
totalSize -= bytesToWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the NDEF message is properly terminated
|
||||||
|
memset(pageBuffer, 0, 4);
|
||||||
|
pageBuffer[0] = 0xFE; // NDEF record footer
|
||||||
|
if (!(nfc.ntag2xx_WritePage(4+i, pageBuffer)))
|
||||||
|
{
|
||||||
|
Serial.println("Fehler beim Schreiben des End-Bits.");
|
||||||
|
free(combinedData);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("NDEF-Nachricht erfolgreich geschrieben.");
|
||||||
|
free(combinedData);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool decodeNdefAndReturnJson(const byte* encodedMessage) {
|
||||||
|
byte typeLength = encodedMessage[3];
|
||||||
|
byte payloadLength = encodedMessage[4];
|
||||||
|
|
||||||
|
nfcJsonData = "";
|
||||||
|
|
||||||
|
for (int i = 2; i < payloadLength+2; i++)
|
||||||
|
{
|
||||||
|
nfcJsonData += (char)encodedMessage[3 + typeLength + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON-Dokument verarbeiten
|
||||||
|
JsonDocument doc; // Passen Sie die Größe an den JSON-Inhalt an
|
||||||
|
DeserializationError error = deserializeJson(doc, nfcJsonData);
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
nfcJsonData = "";
|
||||||
|
Serial.println("Fehler beim Verarbeiten des JSON-Dokuments");
|
||||||
|
Serial.print("deserializeJson() failed: ");
|
||||||
|
Serial.println(error.f_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Sende die aktualisierten AMS-Daten an alle WebSocket-Clients
|
||||||
|
Serial.println("JSON-Dokument erfolgreich verarbeitet");
|
||||||
|
Serial.println(doc.as<String>());
|
||||||
|
if (doc["sm_id"] != "")
|
||||||
|
{
|
||||||
|
Serial.println("SPOOL-ID gefunden: " + doc["sm_id"].as<String>());
|
||||||
|
spoolId = doc["sm_id"].as<String>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.println("Keine SPOOL-ID gefunden.");
|
||||||
|
spoolId = "";
|
||||||
|
oledShowMessage("Unknown Spool");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeJsonToTag(void *parameter) {
|
||||||
|
const char* payload = (const char*)parameter;
|
||||||
|
|
||||||
|
// Gib die erstellte NDEF-Message aus
|
||||||
|
Serial.println("Erstelle NDEF-Message...");
|
||||||
|
hasReadRfidTag = 3;
|
||||||
|
vTaskSuspend(RfidReaderTask);
|
||||||
|
//pauseBambuMqttTask = true;
|
||||||
|
// aktualisieren der Website wenn sich der Status ändert
|
||||||
|
sendNfcData(nullptr);
|
||||||
|
|
||||||
|
// Wait 10sec for tag
|
||||||
|
uint8_t success = 0;
|
||||||
|
String uidString = "";
|
||||||
|
for (uint16_t i = 0; i < 20; i++) {
|
||||||
|
uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; // Buffer to store the returned UID
|
||||||
|
uint8_t uidLength;
|
||||||
|
success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 500);
|
||||||
|
if (success) {
|
||||||
|
for (uint8_t i = 0; i < uidLength; i++) {
|
||||||
|
uidString += String(uid[i], HEX);
|
||||||
|
if (i < uidLength - 1) {
|
||||||
|
uidString += ":"; // Optional: Trennzeichen hinzufügen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundNfcTag(nullptr, success);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 0) oledShowMessage("Waiting for NFC-Tag");
|
||||||
|
|
||||||
|
yield();
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
oledShowIcon("transfer");
|
||||||
|
// Schreibe die NDEF-Message auf den Tag
|
||||||
|
success = ntag2xx_WriteNDEF(payload);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
Serial.println("NDEF-Message erfolgreich auf den Tag geschrieben");
|
||||||
|
//oledShowMessage("NFC-Tag written");
|
||||||
|
oledShowIcon("success");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
hasReadRfidTag = 5;
|
||||||
|
// aktualisieren der Website wenn sich der Status ändert
|
||||||
|
sendNfcData(nullptr);
|
||||||
|
vTaskResume(RfidReaderTask);
|
||||||
|
pauseBambuMqttTask = false;
|
||||||
|
updateSpoolTagId(uidString, payload);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.println("Fehler beim Schreiben der NDEF-Message auf den Tag");
|
||||||
|
oledShowIcon("failed");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
hasReadRfidTag = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.println("Fehler: Kein Tag zu schreiben gefunden.");
|
||||||
|
oledShowMessage("No NFC-Tag found");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
hasReadRfidTag = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWriteResult(nullptr, success);
|
||||||
|
sendNfcData(nullptr);
|
||||||
|
|
||||||
|
vTaskResume(RfidReaderTask);
|
||||||
|
pauseBambuMqttTask = false;
|
||||||
|
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startWriteJsonToTag(const char* payload) {
|
||||||
|
char* payloadCopy = strdup(payload);
|
||||||
|
|
||||||
|
// Erstelle die Task
|
||||||
|
xTaskCreate(
|
||||||
|
writeJsonToTag, // Task-Funktion
|
||||||
|
"WriteJsonToTagTask", // Task-Name
|
||||||
|
4096, // Stackgröße in Bytes
|
||||||
|
(void*)payloadCopy, // Parameter
|
||||||
|
rfidWriteTaskPrio, // Priorität
|
||||||
|
NULL // Task-Handle (nicht benötigt)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void scanRfidTask(void * parameter) {
|
||||||
|
Serial.println("RFID Task gestartet");
|
||||||
|
for(;;) {
|
||||||
|
// Wenn geschrieben wird Schleife aussetzen
|
||||||
|
if (hasReadRfidTag != 3)
|
||||||
|
{
|
||||||
|
uint8_t success;
|
||||||
|
uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; // Buffer to store the returned UID
|
||||||
|
uint8_t uidLength;
|
||||||
|
|
||||||
|
success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 1000);
|
||||||
|
|
||||||
|
foundNfcTag(nullptr, success);
|
||||||
|
|
||||||
|
if (success && hasReadRfidTag != 1)
|
||||||
|
{
|
||||||
|
// Display some basic information about the card
|
||||||
|
Serial.println("Found an ISO14443A card");
|
||||||
|
|
||||||
|
hasReadRfidTag = 6;
|
||||||
|
|
||||||
|
oledShowIcon("transfer");
|
||||||
|
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
if (uidLength == 7)
|
||||||
|
{
|
||||||
|
uint8_t data[256];
|
||||||
|
|
||||||
|
// We probably have an NTAG2xx card (though it could be Ultralight as well)
|
||||||
|
Serial.println("Seems to be an NTAG2xx tag (7 byte UID)");
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < 45; i++) {
|
||||||
|
/*
|
||||||
|
if (i < uidLength) {
|
||||||
|
uidString += String(uid[i], HEX);
|
||||||
|
if (i < uidLength - 1) {
|
||||||
|
uidString += ":"; // Optional: Trennzeichen hinzufügen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (!nfc.mifareclassic_ReadDataBlock(i, data + (i - 4) * 4))
|
||||||
|
{
|
||||||
|
break; // Stop if reading fails
|
||||||
|
}
|
||||||
|
// Check for NDEF message end
|
||||||
|
if (data[(i - 4) * 4] == 0xFE)
|
||||||
|
{
|
||||||
|
break; // End of NDEF message
|
||||||
|
}
|
||||||
|
|
||||||
|
yield();
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decodeNdefAndReturnJson(data))
|
||||||
|
{
|
||||||
|
oledShowMessage("NFC-Tag unknown");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
hasReadRfidTag = 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hasReadRfidTag = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.println("This doesn't seem to be an NTAG2xx tag (UUID length != 7 bytes)!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success && hasReadRfidTag > 0)
|
||||||
|
{
|
||||||
|
hasReadRfidTag = 0;
|
||||||
|
//uidString = "";
|
||||||
|
nfcJsonData = "";
|
||||||
|
Serial.println("Tag entfernt");
|
||||||
|
oledShowWeight(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// aktualisieren der Website wenn sich der Status ändert
|
||||||
|
sendNfcData(nullptr);
|
||||||
|
}
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void startNfc() {
|
||||||
|
nfc.begin(); // Beginne Kommunikation mit RFID Leser
|
||||||
|
delay(1000);
|
||||||
|
unsigned long versiondata = nfc.getFirmwareVersion(); // Lese Versionsnummer der Firmware aus
|
||||||
|
if (! versiondata) { // Wenn keine Antwort kommt
|
||||||
|
Serial.println("Kann kein RFID Board finden !"); // Sende Text "Kann kein..." an seriellen Monitor
|
||||||
|
//delay(5000);
|
||||||
|
//ESP.restart();
|
||||||
|
oledShowMessage("No RFID Board found");
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Serial.print("Chip PN5 gefunden"); Serial.println((versiondata >> 24) & 0xFF, HEX); // Sende Text und Versionsinfos an seriellen
|
||||||
|
Serial.print("Firmware ver. "); Serial.print((versiondata >> 16) & 0xFF, DEC); // Monitor, wenn Antwort vom Board kommt
|
||||||
|
Serial.print('.'); Serial.println((versiondata >> 8) & 0xFF, DEC); //
|
||||||
|
|
||||||
|
nfc.SAMConfig();
|
||||||
|
// Set the max number of retry attempts to read from a card
|
||||||
|
// This prevents us from waiting forever for a card, which is
|
||||||
|
// the default behaviour of the PN532.
|
||||||
|
//nfc.setPassiveActivationRetries(0x7F);
|
||||||
|
//nfc.setPassiveActivationRetries(0xFF);
|
||||||
|
|
||||||
|
BaseType_t result = xTaskCreatePinnedToCore(
|
||||||
|
scanRfidTask, /* Function to implement the task */
|
||||||
|
"RfidReader", /* Name of the task */
|
||||||
|
10000, /* Stack size in words */
|
||||||
|
NULL, /* Task input parameter */
|
||||||
|
rfidTaskPrio, /* Priority of the task */
|
||||||
|
&RfidReaderTask, /* Task handle. */
|
||||||
|
rfidTaskCore); /* Core where the task should run */
|
||||||
|
|
||||||
|
if (result != pdPASS) {
|
||||||
|
Serial.println("Fehler beim Erstellen des RFID Tasks");
|
||||||
|
} else {
|
||||||
|
Serial.println("RFID Task erfolgreich erstellt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/nfc.h
Normal file
16
src/nfc.h
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#ifndef NFC_H
|
||||||
|
#define NFC_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
void startNfc();
|
||||||
|
void scanRfidTask(void * parameter);
|
||||||
|
void startWriteJsonToTag(const char* payload);
|
||||||
|
|
||||||
|
extern TaskHandle_t RfidReaderTask;
|
||||||
|
extern String nfcJsonData;
|
||||||
|
extern String spoolId;
|
||||||
|
extern volatile uint8_t hasReadRfidTag;
|
||||||
|
extern volatile bool pauseBambuMqttTask;
|
||||||
|
|
||||||
|
#endif
|
214
src/scale.cpp
Normal file
214
src/scale.cpp
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
#include "nfc.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "config.h"
|
||||||
|
#include "HX711.h"
|
||||||
|
#include <EEPROM.h>
|
||||||
|
#include "display.h"
|
||||||
|
#include "nfc.h"
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
|
|
||||||
|
HX711 scale;
|
||||||
|
|
||||||
|
TaskHandle_t ScaleTask;
|
||||||
|
|
||||||
|
int16_t weight = 0;
|
||||||
|
|
||||||
|
uint8_t weigthCouterToApi = 0;
|
||||||
|
uint8_t scale_tare_counter = 0;
|
||||||
|
uint8_t pauseMainTask = 0;
|
||||||
|
|
||||||
|
// ##### Funktionen für Waage #####
|
||||||
|
uint8_t tareScale() {
|
||||||
|
Serial.println("Tare scale");
|
||||||
|
scale.tare();
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scale_loop(void * parameter) {
|
||||||
|
Serial.println("++++++++++++++++++++++++++++++");
|
||||||
|
Serial.println("Scale Loop started");
|
||||||
|
Serial.println("++++++++++++++++++++++++++++++");
|
||||||
|
for(;;) {
|
||||||
|
if (scale.is_ready())
|
||||||
|
{
|
||||||
|
// Waage nochmal Taren, wenn zu lange Abweichung
|
||||||
|
if (scale_tare_counter >= 5)
|
||||||
|
{
|
||||||
|
scale.tare();
|
||||||
|
scale_tare_counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
weight = round(scale.get_units());
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100)); // Verzögerung, um die CPU nicht zu überlasten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_scale() {
|
||||||
|
Serial.println("Prüfe Calibration Value");
|
||||||
|
long calibrationValue; // calibration value (see example file "Calibration.ino")
|
||||||
|
//calibrationValue = 696.0; // uncomment this if you want to set the calibration value in the sketch
|
||||||
|
|
||||||
|
EEPROM.begin(512);
|
||||||
|
EEPROM.get(calVal_eepromAdress, calibrationValue); // uncomment this if you want to fetch the calibration value from eeprom
|
||||||
|
|
||||||
|
//calibrationValue = EEPROM.read(calVal_eepromAdress);
|
||||||
|
|
||||||
|
Serial.print("Read Scale Calibration Value ");
|
||||||
|
Serial.println(calibrationValue);
|
||||||
|
|
||||||
|
scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
|
||||||
|
|
||||||
|
if (isnan(calibrationValue) || calibrationValue < 1) calibrationValue = defaultScaleCalibrationValue;
|
||||||
|
|
||||||
|
oledShowMessage("Scale Tare Please remove all");
|
||||||
|
for (uint16_t i = 0; i < 2000; i++) {
|
||||||
|
yield();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale.wait_ready_timeout(1000))
|
||||||
|
{
|
||||||
|
scale.set_scale(calibrationValue); // this value is obtained by calibrating the scale with known weights; see the README for details
|
||||||
|
scale.tare();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display Gewicht
|
||||||
|
oledShowWeight(0);
|
||||||
|
|
||||||
|
Serial.println("starte Scale Task");
|
||||||
|
BaseType_t result = xTaskCreatePinnedToCore(
|
||||||
|
scale_loop, /* Function to implement the task */
|
||||||
|
"ScaleLoop", /* Name of the task */
|
||||||
|
10000, /* Stack size in words */
|
||||||
|
NULL, /* Task input parameter */
|
||||||
|
scaleTaskPrio, /* Priority of the task */
|
||||||
|
&ScaleTask, /* Task handle. */
|
||||||
|
scaleTaskCore); /* Core where the task should run */
|
||||||
|
|
||||||
|
if (result != pdPASS) {
|
||||||
|
Serial.println("Fehler beim Erstellen des ScaleLoop-Tasks");
|
||||||
|
} else {
|
||||||
|
Serial.println("ScaleLoop-Task erfolgreich erstellt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t calibrate_scale() {
|
||||||
|
long newCalibrationValue;
|
||||||
|
|
||||||
|
//vTaskSuspend(RfidReaderTask);
|
||||||
|
vTaskDelete(RfidReaderTask);
|
||||||
|
pauseBambuMqttTask = true;
|
||||||
|
pauseMainTask = 1;
|
||||||
|
|
||||||
|
if (scale.wait_ready_timeout(1000))
|
||||||
|
{
|
||||||
|
scale.set_scale();
|
||||||
|
oledShowMessage("Step 1 empty Scale");
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < 5000; i++) {
|
||||||
|
yield();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
scale.tare();
|
||||||
|
Serial.println("Tare done...");
|
||||||
|
Serial.print("Place a known weight on the scale...");
|
||||||
|
|
||||||
|
oledShowMessage("Step 2 Place the weight");
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < 5000; i++) {
|
||||||
|
yield();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
long newCalibrationValue = scale.get_units(10);
|
||||||
|
Serial.print("Result: ");
|
||||||
|
Serial.println(newCalibrationValue);
|
||||||
|
|
||||||
|
newCalibrationValue = newCalibrationValue/SCALE_LEVEL_WEIGHT;
|
||||||
|
|
||||||
|
if (newCalibrationValue > 0)
|
||||||
|
{
|
||||||
|
Serial.print("New calibration value has been set to: ");
|
||||||
|
Serial.println(newCalibrationValue);
|
||||||
|
Serial.print("Save this value to EEPROM adress ");
|
||||||
|
Serial.println(calVal_eepromAdress);
|
||||||
|
|
||||||
|
//EEPROM.put(calVal_eepromAdress, newCalibrationValue);
|
||||||
|
EEPROM.put(calVal_eepromAdress, newCalibrationValue);
|
||||||
|
EEPROM.commit();
|
||||||
|
|
||||||
|
EEPROM.get(calVal_eepromAdress, newCalibrationValue);
|
||||||
|
//newCalibrationValue = EEPROM.read(calVal_eepromAdress);
|
||||||
|
|
||||||
|
Serial.print("Read Value ");
|
||||||
|
Serial.println(newCalibrationValue);
|
||||||
|
|
||||||
|
Serial.println("End calibration, revome weight");
|
||||||
|
|
||||||
|
oledShowMessage("Remove weight");
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < 2000; i++) {
|
||||||
|
yield();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
oledShowMessage("Calibration done");
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < 2000; i++) {
|
||||||
|
yield();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
//ESP.restart();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Serial.println("Calibration value is invalid. Please recalibrate.");
|
||||||
|
|
||||||
|
oledShowMessage("Calibration ERROR Try again");
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < 50000; i++) {
|
||||||
|
yield();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.println("HX711 not found.");
|
||||||
|
|
||||||
|
oledShowMessage("HX711 not found");
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < 30000; i++) {
|
||||||
|
yield();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
oledShowMessage("Scale Ready");
|
||||||
|
|
||||||
|
|
||||||
|
Serial.println("starte Scale Task");
|
||||||
|
start_scale();
|
||||||
|
|
||||||
|
pauseBambuMqttTask = false;
|
||||||
|
pauseMainTask = 0;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
18
src/scale.h
Normal file
18
src/scale.h
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#ifndef SCALE_H
|
||||||
|
#define SCALE_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "HX711.h"
|
||||||
|
|
||||||
|
|
||||||
|
void start_scale();
|
||||||
|
uint8_t calibrate_scale();
|
||||||
|
uint8_t tareScale();
|
||||||
|
|
||||||
|
extern HX711 scale;
|
||||||
|
extern int16_t weight;
|
||||||
|
extern uint8_t weigthCouterToApi;
|
||||||
|
extern uint8_t scale_tare_counter;
|
||||||
|
extern uint8_t pauseMainTask;
|
||||||
|
|
||||||
|
#endif
|
334
src/website.cpp
Normal file
334
src/website.cpp
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
#include "website.h"
|
||||||
|
#include "commonFS.h"
|
||||||
|
#include "api.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
//#include <AsyncWebSocket.h>
|
||||||
|
#include "bambu.h"
|
||||||
|
#include "nfc.h"
|
||||||
|
#include "scale.h"
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
|
|
||||||
|
// Cache-Control Header definieren
|
||||||
|
#define CACHE_CONTROL "max-age=31536000" // Cache für 1 Jahr
|
||||||
|
|
||||||
|
AsyncWebServer server(webserverPort);
|
||||||
|
AsyncWebSocket ws("/ws");
|
||||||
|
|
||||||
|
uint8_t lastSuccess = 0;
|
||||||
|
uint8_t lastHasReadRfidTag = 0;
|
||||||
|
|
||||||
|
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
|
||||||
|
if (type == WS_EVT_CONNECT) {
|
||||||
|
Serial.println("Neuer Client verbunden!");
|
||||||
|
// Sende die AMS-Daten an den neuen Client
|
||||||
|
sendAmsData(client);
|
||||||
|
sendNfcData(client);
|
||||||
|
foundNfcTag(client, hasReadRfidTag);
|
||||||
|
sendWriteResult(client, 0);
|
||||||
|
} else if (type == WS_EVT_DISCONNECT) {
|
||||||
|
Serial.println("Client getrennt.");
|
||||||
|
} else if (type == WS_EVT_DATA) {
|
||||||
|
String message = String((char*)data);
|
||||||
|
JsonDocument doc;
|
||||||
|
deserializeJson(doc, message);
|
||||||
|
|
||||||
|
if (doc["type"] == "heartbeat") {
|
||||||
|
// Sende Heartbeat-Antwort
|
||||||
|
ws.text(client->id(), "{"
|
||||||
|
"\"type\":\"heartbeat\","
|
||||||
|
"\"freeHeap\":" + String(ESP.getFreeHeap()/1024) + ","
|
||||||
|
"\"bambu_connected\":" + String(bambu_connected) + ","
|
||||||
|
"\"spoolman_connected\":" + String(spoolman_connected) + ""
|
||||||
|
"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (doc["type"] == "writeNfcTag") {
|
||||||
|
if (doc.containsKey("payload")) {
|
||||||
|
// Versuche NFC-Daten zu schreiben
|
||||||
|
String payloadString;
|
||||||
|
serializeJson(doc["payload"], payloadString);
|
||||||
|
startWriteJsonToTag(payloadString.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (doc["type"] == "scale") {
|
||||||
|
uint8_t success = 0;
|
||||||
|
if (doc["payload"] == "tare") {
|
||||||
|
success = tareScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc["payload"] == "calibrate") {
|
||||||
|
success = calibrate_scale();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
ws.textAll("{\"type\":\"scale\",\"payload\":\"success\"}");
|
||||||
|
} else {
|
||||||
|
ws.textAll("{\"type\":\"scale\",\"payload\":\"error\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (doc["type"] == "setBambuSpool") {
|
||||||
|
Serial.println(doc["payload"].as<String>());
|
||||||
|
setBambuSpool(doc["payload"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
Serial.println("Unbekannter WebSocket-Typ: " + doc["type"].as<String>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion zum Laden und Ersetzen des Headers in einer HTML-Datei
|
||||||
|
String loadHtmlWithHeader(const char* filename) {
|
||||||
|
if (!SPIFFS.exists(filename) || !SPIFFS.exists("/header.html")) {
|
||||||
|
Serial.println("Fehler: Datei nicht gefunden!");
|
||||||
|
return "Fehler: Datei nicht gefunden!";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lade den Header
|
||||||
|
File headerFile = SPIFFS.open("/header.html", "r");
|
||||||
|
String header = headerFile.readString();
|
||||||
|
headerFile.close();
|
||||||
|
|
||||||
|
// Lade die Hauptdatei
|
||||||
|
File file = SPIFFS.open(filename, "r");
|
||||||
|
String html = file.readString();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// Ersetze den Platzhalter mit dem Header
|
||||||
|
html.replace("{{header}}", header);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendWriteResult(AsyncWebSocketClient *client, uint8_t success) {
|
||||||
|
// Sende Erfolg/Misserfolg an alle Clients
|
||||||
|
String response = "{\"type\":\"writeNfcTag\",\"success\":" + String(success ? "1" : "0") + "}";
|
||||||
|
ws.textAll(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void foundNfcTag(AsyncWebSocketClient *client, uint8_t success) {
|
||||||
|
if (success == lastSuccess) return;
|
||||||
|
ws.textAll("{\"type\":\"nfcTag\", \"payload\":{\"found\": " + String(success) + "}}");
|
||||||
|
sendNfcData(nullptr);
|
||||||
|
lastSuccess = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendNfcData(AsyncWebSocketClient *client) {
|
||||||
|
if (lastHasReadRfidTag == hasReadRfidTag) return;
|
||||||
|
if (hasReadRfidTag == 0) {
|
||||||
|
ws.textAll("{\"type\":\"nfcData\", \"payload\":{}}");
|
||||||
|
}
|
||||||
|
else if (hasReadRfidTag == 1) {
|
||||||
|
ws.textAll("{\"type\":\"nfcData\", \"payload\":" + nfcJsonData + "}");
|
||||||
|
}
|
||||||
|
else if (hasReadRfidTag == 2)
|
||||||
|
{
|
||||||
|
ws.textAll("{\"type\":\"nfcData\", \"payload\":{\"error\":\"Empty Tag or Data not readable\"}}");
|
||||||
|
}
|
||||||
|
else if (hasReadRfidTag == 3)
|
||||||
|
{
|
||||||
|
ws.textAll("{\"type\":\"nfcData\", \"payload\":{\"info\":\"Schreibe Tag...\"}}");
|
||||||
|
}
|
||||||
|
else if (hasReadRfidTag == 4)
|
||||||
|
{
|
||||||
|
ws.textAll("{\"type\":\"nfcData\", \"payload\":{\"error\":\"Error writing to Tag\"}}");
|
||||||
|
}
|
||||||
|
else if (hasReadRfidTag == 5)
|
||||||
|
{
|
||||||
|
ws.textAll("{\"type\":\"nfcData\", \"payload\":{\"info\":\"Tag erfolgreich geschrieben\"}}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ws.textAll("{\"type\":\"nfcData\", \"payload\":{\"error\":\"Something went wrong\"}}");
|
||||||
|
}
|
||||||
|
lastHasReadRfidTag = hasReadRfidTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendAmsData(AsyncWebSocketClient *client) {
|
||||||
|
if (ams_count > 0) {
|
||||||
|
ws.textAll("{\"type\":\"amsData\", \"payload\":" + amsJsonData + "}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupWebserver(AsyncWebServer &server) {
|
||||||
|
// Lade die Spoolman-URL beim Booten
|
||||||
|
spoolmanUrl = loadSpoolmanUrl();
|
||||||
|
Serial.print("Geladene Spoolman-URL: ");
|
||||||
|
Serial.println(spoolmanUrl);
|
||||||
|
|
||||||
|
// Route für die Startseite
|
||||||
|
server.on("/about", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("Anfrage für / erhalten");
|
||||||
|
String html = loadHtmlWithHeader("/index.html");
|
||||||
|
request->send(200, "text/html", html);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für Waage
|
||||||
|
server.on("/waage", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("Anfrage für /waage erhalten");
|
||||||
|
String html = loadHtmlWithHeader("/waage.html");
|
||||||
|
request->send(200, "text/html", html);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für RFID
|
||||||
|
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("Anfrage für /rfid erhalten");
|
||||||
|
String html = loadHtmlWithHeader("/rfid.html");
|
||||||
|
request->send(200, "text/html", html);
|
||||||
|
Serial.println("RFID-Seite gesendet");
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Neue API-Route für das Abrufen der Spool-Daten
|
||||||
|
server.on("/api/spools", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("API-Aufruf: /api/spools");
|
||||||
|
JsonDocument spoolsData = fetchSpoolsForWebsite();
|
||||||
|
String response;
|
||||||
|
serializeJson(spoolsData, response);
|
||||||
|
request->send(200, "application/json", response);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
server.on("/api/url", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("API-Aufruf: /api/url");
|
||||||
|
String jsonResponse = "{\"spoolman_url\": \"" + String(spoolmanUrl) + "\"}";
|
||||||
|
request->send(200, "application/json", jsonResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für WiFi
|
||||||
|
server.on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("Anfrage für /wifi erhalten");
|
||||||
|
String html = loadHtmlWithHeader("/wifi.html");
|
||||||
|
request->send(200, "text/html", html);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für Spoolman Setting
|
||||||
|
server.on("/spoolman", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("Anfrage für /spoolman erhalten");
|
||||||
|
String html = loadHtmlWithHeader("/spoolman.html");
|
||||||
|
html.replace("{{spoolmanUrl}}", spoolmanUrl);
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
if (loadJsonValue("/bambu_credentials.json", doc) && doc.containsKey("bambu_ip")) {
|
||||||
|
html.replace("{{bambuIp}}", doc["bambu_ip"].as<String>() ? doc["bambu_ip"].as<String>() : "");
|
||||||
|
html.replace("{{bambuSerial}}", doc["bambu_serialnr"].as<String>() ? doc["bambu_serialnr"].as<String>() : "");
|
||||||
|
html.replace("{{bambuCode}}", doc["bambu_accesscode"].as<String>() ? doc["bambu_accesscode"].as<String>() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
request->send(200, "text/html", html);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für das Überprüfen der Spoolman-Instanz
|
||||||
|
server.on("/api/checkSpoolman", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
if (!request->hasParam("url")) {
|
||||||
|
request->send(400, "application/json", "{\"success\": false, \"error\": \"Missing URL parameter\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = request->getParam("url")->value();
|
||||||
|
url.trim();
|
||||||
|
|
||||||
|
bool healthy = saveSpoolmanUrl(url);
|
||||||
|
String jsonResponse = "{\"healthy\": " + String(healthy ? "true" : "false") + "}";
|
||||||
|
|
||||||
|
request->send(200, "application/json", jsonResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für das Überprüfen der Spoolman-Instanz
|
||||||
|
server.on("/api/bambu", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
if (!request->hasParam("bambu_ip") || !request->hasParam("bambu_serialnr") || !request->hasParam("bambu_accesscode")) {
|
||||||
|
request->send(400, "application/json", "{\"success\": false, \"error\": \"Missing parameter\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String bambu_ip = request->getParam("bambu_ip")->value();
|
||||||
|
String bambu_serialnr = request->getParam("bambu_serialnr")->value();
|
||||||
|
String bambu_accesscode = request->getParam("bambu_accesscode")->value();
|
||||||
|
bambu_ip.trim();
|
||||||
|
bambu_serialnr.trim();
|
||||||
|
bambu_accesscode.trim();
|
||||||
|
|
||||||
|
bool success = saveBambuCredentials(bambu_ip, bambu_serialnr, bambu_accesscode);
|
||||||
|
|
||||||
|
request->send(200, "application/json", "{\"healthy\": " + String(success ? "true" : "false") + "}");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für das Überprüfen der Spoolman-Instanz
|
||||||
|
server.on("/reboot", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
ESP.restart();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für das Laden der CSS-Datei
|
||||||
|
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("Lade style.css");
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/style.css.gz", "text/css");
|
||||||
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
|
response->addHeader("Cache-Control", CACHE_CONTROL);
|
||||||
|
request->send(response);
|
||||||
|
Serial.println("style.css gesendet");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für das Logo
|
||||||
|
server.on("/logo.png", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/logo.png.gz", "image/png");
|
||||||
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
|
response->addHeader("Cache-Control", CACHE_CONTROL);
|
||||||
|
request->send(response);
|
||||||
|
Serial.println("logo.png gesendet");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für Favicon
|
||||||
|
server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/favicon.ico", "image/x-icon");
|
||||||
|
response->addHeader("Cache-Control", CACHE_CONTROL);
|
||||||
|
request->send(response);
|
||||||
|
Serial.println("favicon.ico gesendet");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für spool_in.png
|
||||||
|
server.on("/spool_in.png", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/spool_in.png.gz", "image/png");
|
||||||
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
|
response->addHeader("Cache-Control", CACHE_CONTROL);
|
||||||
|
request->send(response);
|
||||||
|
Serial.println("spool_in.png gesendet");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route für JavaScript Dateien
|
||||||
|
server.on("/spoolman.js", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("Anfrage für /spoolman.js erhalten");
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/spoolman.js.gz", "text/javascript");
|
||||||
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
|
response->addHeader("Cache-Control", CACHE_CONTROL);
|
||||||
|
request->send(response);
|
||||||
|
Serial.println("Spoolman.js gesendet");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("/rfid.js", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
Serial.println("Anfrage für /rfid.js erhalten");
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse(SPIFFS,"/rfid.js.gz", "text/javascript");
|
||||||
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
|
response->addHeader("Cache-Control", CACHE_CONTROL);
|
||||||
|
request->send(response);
|
||||||
|
Serial.println("RFID.js gesendet");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fehlerbehandlung für nicht gefundene Seiten
|
||||||
|
server.onNotFound([](AsyncWebServerRequest *request){
|
||||||
|
Serial.print("404 - Nicht gefunden: ");
|
||||||
|
Serial.println(request->url());
|
||||||
|
request->send(404, "text/plain", "Seite nicht gefunden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocket-Route
|
||||||
|
ws.onEvent(onWsEvent);
|
||||||
|
server.addHandler(&ws);
|
||||||
|
ws.enable(true);
|
||||||
|
|
||||||
|
// Starte den Webserver
|
||||||
|
server.begin();
|
||||||
|
Serial.println("Webserver gestartet");
|
||||||
|
}
|
26
src/website.h
Normal file
26
src/website.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#ifndef WEBSITE_H
|
||||||
|
#define WEBSITE_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include "commonFS.h"
|
||||||
|
#include "api.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <AsyncWebSocket.h>
|
||||||
|
#include "bambu.h"
|
||||||
|
#include "nfc.h"
|
||||||
|
#include "scale.h"
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
|
|
||||||
|
extern String spoolmanUrl;
|
||||||
|
extern AsyncWebServer server;
|
||||||
|
extern AsyncWebSocket ws;
|
||||||
|
|
||||||
|
void setupWebserver(AsyncWebServer &server);
|
||||||
|
void sendAmsData(AsyncWebSocketClient *client);
|
||||||
|
void sendNfcData(AsyncWebSocketClient *client);
|
||||||
|
void foundNfcTag(AsyncWebSocketClient *client, uint8_t success);
|
||||||
|
void sendWriteResult(AsyncWebSocketClient *client, uint8_t success);
|
||||||
|
|
||||||
|
#endif
|
Loading…
x
Reference in New Issue
Block a user