This commit is contained in:
Manuel Weiser 2025-02-12 21:10:25 +01:00
commit ec0d7d63de
44 changed files with 8699 additions and 0 deletions

54
.vscode/settings.json vendored Normal file
View 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
View 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
View 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
View File

0
display.h Normal file
View File

3255
docs/ndef.md Normal file

File diff suppressed because it is too large Load Diff

BIN
docs/specification_ndef.pdf Normal file

Binary file not shown.

6
extra_script.py Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

51
html/header.html Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

114
html/rfid.html Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

80
html/spoolman.html Normal file
View 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
View 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
View 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
View 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
View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

2
scripts/buildfs.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
pio run --target buildfs

2
scripts/uploadfs.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
pio run --target uploadfs

471
src/api.cpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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