init
This commit is contained in:
		
							
								
								
									
										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 | ||||||
		Reference in New Issue
	
	Block a user