Adds new feature to write and read location tags

Location tags can be written via the website. If a location tag is read after reading a spool tag, the location of the spool will be updated in spoolman to the location from the tag.
This commit is contained in:
Jan Philipp Ecker
2025-07-21 21:03:55 +02:00
parent 2920159f32
commit eab937d6ca
7 changed files with 215 additions and 21 deletions

View File

@@ -139,6 +139,18 @@
<p id="nfcInfo" class="nfc-status"></p>
<button id="writeNfcButton" class="btn btn-primary hidden" onclick="writeNfcTag()">Write Tag</button>
</div>
<div class="feature-box">
<h2>Spoolman Locations</h2>
<label for="locationSelect">Location:</label>
<div style="display: flex; justify-content: space-between; align-items: center;">
<select id="locationSelect" class="styled-select">
<option value="">Please choose...</option>
</select>
</div>
<p id="nfcInfoLocation" class="nfc-status"></p>
<button id="writeLocationNfcButton" class="btn btn-primary hidden" onclick="writeLocationNfcTag()">Write Location Tag</button>
</div>
</div>
</div>

View File

@@ -626,11 +626,11 @@ function writeNfcTag() {
// Erstelle das NFC-Datenpaket mit korrekten Datentypen
const nfcData = {
color_hex: selectedSpool.filament.color_hex || "FFFFFF",
type: selectedSpool.filament.material,
min_temp: minTemp,
max_temp: maxTemp,
brand: selectedSpool.filament.vendor.name,
//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
};
@@ -647,8 +647,34 @@ function writeNfcTag() {
}
}
function writeLocationNfcTag() {
const selectedText = document.getElementById("locationSelect").value;
if (selectedText === "Please choose...") {
alert('Please select a location first.');
return;
}
// Erstelle das NFC-Datenpaket mit korrekten Datentypen
const nfcData = {
location: String(selectedText)
};
if (socket?.readyState === WebSocket.OPEN) {
const writeButton = document.getElementById("writeLocationNfcButton");
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");
const writeLocationButton = document.getElementById("writeLocationNfcButton");
if(writeButton.classList.contains("writing")){
writeButton.classList.remove("writing");
writeButton.classList.add(success ? "success" : "error");
writeButton.textContent = success ? "Write success" : "Write failed";
@@ -657,6 +683,20 @@ function handleWriteNfcTagResponse(success) {
writeButton.classList.remove("success", "error");
writeButton.textContent = "Write Tag";
}, 5000);
}
if(writeLocationButton.classList.contains("writing")){
writeLocationButton.classList.remove("writing");
writeLocationButton.classList.add(success ? "success" : "error");
writeLocationButton.textContent = success ? "Write success" : "Write failed";
setTimeout(() => {
writeLocationButton.classList.remove("success", "error");
writeLocationButton.textContent = "Write Location Tag";
}, 5000);
}
}
function showNotification(message, isSuccess) {

View File

@@ -1,6 +1,7 @@
// Globale Variablen
let spoolmanUrl = '';
let spoolsData = [];
let locationData = [];
// Hilfsfunktionen für Datenmanipulation
function processSpoolData(data) {
@@ -133,6 +134,26 @@ function populateVendorDropdown(data, selectedSmId = null) {
}
}
// Dropdown-Funktionen
function populateLocationDropdown(data) {
const locationSelect = document.getElementById("locationSelect");
if (!locationSelect) {
console.error('locationSelect Element nicht gefunden');
return;
}
locationSelect.innerHTML = '<option value="">Bitte wählen...</option>';
// Dropdown mit gefilterten Herstellern befüllen - alphabetisch sortiert
Object.entries(data)
.sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB)) // Sort vendors alphabetically by name
.forEach(([id, name]) => {
const option = document.createElement("option");
option.value = name;
option.textContent = name;
locationSelect.appendChild(option);
});
}
function updateFilamentDropdown(selectedSmId = null) {
const vendorId = document.getElementById("vendorSelect").value;
const dropdownContentInner = document.getElementById("filament-dropdown-content");
@@ -208,6 +229,13 @@ function updateFilamentDropdown(selectedSmId = null) {
}
}
function updateLocationSelect(){
const writeLocationNfcButton = document.getElementById('writeLocationNfcButton');
if(writeLocationNfcButton){
writeLocationNfcButton.classList.remove("hidden");
}
}
function selectFilament(spool) {
const selectedColor = document.getElementById("selected-color");
const selectedText = document.getElementById("selected-filament");
@@ -265,6 +293,14 @@ async function initSpoolman() {
document.dispatchEvent(new CustomEvent('spoolDataLoaded', {
detail: spoolsData
}));
locationData = await fetchLocationData();
document.dispatchEvent(new CustomEvent('locationDataLoaded', {
detail: locationData
}));
} catch (error) {
console.error('Fehler beim Initialisieren von Spoolman:', error);
document.dispatchEvent(new CustomEvent('spoolmanError', {
@@ -292,6 +328,25 @@ async function fetchSpoolData() {
}
}
async function fetchLocationData() {
try {
if (!spoolmanUrl) {
throw new Error('Spoolman URL ist nicht initialisiert');
}
const response = await fetch(`${spoolmanUrl}/api/v1/location`);
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 Location-Daten:', error);
return [];
}
}
// Event Listener
document.addEventListener('DOMContentLoaded', () => {
initSpoolman();
@@ -301,6 +356,11 @@ document.addEventListener('DOMContentLoaded', () => {
vendorSelect.addEventListener('change', () => updateFilamentDropdown());
}
const locationSelect = document.getElementById('locationSelect');
if (locationSelect) {
locationSelect.addEventListener('change', () => updateLocationSelect());
}
const onlyWithoutSmId = document.getElementById('onlyWithoutSmId');
if (onlyWithoutSmId) {
onlyWithoutSmId.addEventListener('change', () => {
@@ -313,6 +373,10 @@ document.addEventListener('DOMContentLoaded', () => {
populateVendorDropdown(event.detail);
});
document.addEventListener('locationDataLoaded', (event) => {
populateLocationDropdown(event.detail);
});
window.onclick = function(event) {
if (!event.target.closest('.custom-dropdown')) {
const dropdowns = document.getElementsByClassName("dropdown-content");
@@ -342,6 +406,7 @@ window.getSpoolData = () => spoolsData;
window.setSpoolData = (data) => { spoolsData = data; };
window.reloadSpoolData = initSpoolman;
window.populateVendorDropdown = populateVendorDropdown;
window.populateLocationDropdown = populateLocationDropdown;
window.updateFilamentDropdown = updateFilamentDropdown;
window.toggleFilamentDropdown = () => {
const content = document.getElementById("filament-dropdown-content");

View File

@@ -971,31 +971,35 @@ input[type="submit"]:disabled,
}
/* Schreib-Button */
#writeNfcButton {
#writeNfcButton, #writeLocationNfcButton {
background-color: #007bff;
color: white;
transition: background-color 0.3s, color 0.3s;
width: 160px;
}
#writeNfcButton.writing {
#writeNfcButton.writing, #writeLocationNfcButton.writing {
background-color: #ffc107;
color: black;
width: 160px;
}
#writeNfcButton.success {
#writeNfcButton.success, #writeLocationNfcButton.success {
background-color: #28a745;
color: white;
width: 160px;
}
#writeNfcButton.error {
#writeNfcButton.error, #writeLocationNfcButton.error {
background-color: #dc3545;
color: white;
width: 160px;
}
#writeLocationNfcButton{
width: 250px;
}
@keyframes dots {
0% { content: ""; }
33% { content: "."; }
@@ -1003,7 +1007,7 @@ input[type="submit"]:disabled,
100% { content: "..."; }
}
#writeNfcButton.writing::after {
#writeNfcButton.writing::after, #writeLocationNfcButton.writing::after {
content: "...";
animation: dots 1s steps(3, end) infinite;
}

View File

@@ -11,6 +11,7 @@ String octoUrl = "";
String octoToken = "";
struct SendToApiParams {
SpoolmanApiRequestType requestType;
String httpType;
String spoolsUrl;
String updatePayload;
@@ -90,6 +91,7 @@ void sendToApi(void *parameter) {
SendToApiParams* params = (SendToApiParams*)parameter;
// Extrahiere die Werte
SpoolmanApiRequestType requestType = params->requestType;
String httpType = params->httpType;
String spoolsUrl = params->spoolsUrl;
String updatePayload = params->updatePayload;
@@ -118,12 +120,15 @@ void sendToApi(void *parameter) {
Serial.print("Fehler beim Parsen der JSON-Antwort: ");
Serial.println(error.c_str());
} else {
if (httpType == "PUT") {
if (requestType == API_REQUEST_SPOOL_WEIGHT_UPDATE) {
uint16_t remaining_weight = doc["remaining_weight"].as<float>();
Serial.print("Aktuelles Gewicht: ");
Serial.println(remaining_weight);
oledShowMessage("Remaining: " + String(remaining_weight) + "g");
}
else if ( requestType == API_REQUEST_SPOOL_LOCATION_UPDATE) {
oledShowMessage("Location updated!");
}
vTaskDelay(3000 / portTICK_PERIOD_MS);
doc.clear();
@@ -178,6 +183,7 @@ bool updateSpoolTagId(String uidString, const char* payload) {
Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren.");
return false;
}
params->requestType = API_REQUEST_SPOOL_TAG_ID_UPDATE;
params->httpType = "PATCH";
params->spoolsUrl = spoolsUrl;
params->updatePayload = updatePayload;
@@ -219,6 +225,7 @@ uint8_t updateSpoolWeight(String spoolId, uint16_t weight) {
Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren.");
return 0;
}
params->requestType = API_REQUEST_SPOOL_WEIGHT_UPDATE;
params->httpType = "PUT";
params->spoolsUrl = spoolsUrl;
params->updatePayload = updatePayload;
@@ -238,6 +245,45 @@ uint8_t updateSpoolWeight(String spoolId, uint16_t weight) {
return 1;
}
uint8_t updateSpoolLocation(String spoolId, String location){
String spoolsUrl = spoolmanUrl + apiUrl + "/spool/" + spoolId;
Serial.print("Update Spule mit URL: ");
Serial.println(spoolsUrl);
// Update Payload erstellen
JsonDocument updateDoc;
updateDoc["location"] = location;
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->requestType = API_REQUEST_SPOOL_LOCATION_UPDATE;
params->httpType = "PATCH";
params->spoolsUrl = spoolsUrl;
params->updatePayload = updatePayload;
// Erstelle die Task
BaseType_t result = xTaskCreate(
sendToApi, // Task-Funktion
"SendToApiTask", // Task-Name
6144, // Stackgröße in Bytes
(void*)params, // Parameter
0, // Priorität
NULL // Task-Handle (nicht benötigt)
);
updateDoc.clear();
return 1;
}
bool updateSpoolOcto(int spoolId) {
String spoolsUrl = octoUrl + "/plugin/Spoolman/selectSpool";
Serial.print("Update Spule in Octoprint mit URL: ");
@@ -257,6 +303,7 @@ bool updateSpoolOcto(int spoolId) {
Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren.");
return false;
}
params->requestType = API_REQUEST_OCTO_SPOOL_UPDATE;
params->httpType = "POST";
params->spoolsUrl = spoolsUrl;
params->updatePayload = updatePayload;
@@ -306,6 +353,7 @@ bool updateSpoolBambuData(String payload) {
Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren.");
return false;
}
params->requestType = API_REQUEST_BAMBU_UPDATE;
params->httpType = "PATCH";
params->spoolsUrl = spoolsUrl;
params->updatePayload = updatePayload;
@@ -510,6 +558,8 @@ bool checkSpoolmanInstance(const String& url) {
return strcmp(status, "healthy") == 0;
}
}
} else {
Serial.println("Error contacting spoolman instance! HTTP Code: " + String(httpCode));
}
http.end();
return false;

View File

@@ -12,6 +12,14 @@ typedef enum {
API_TRANSMITTING
} spoolmanApiStateType;
typedef enum {
API_REQUEST_OCTO_SPOOL_UPDATE,
API_REQUEST_BAMBU_UPDATE,
API_REQUEST_SPOOL_TAG_ID_UPDATE,
API_REQUEST_SPOOL_WEIGHT_UPDATE,
API_REQUEST_SPOOL_LOCATION_UPDATE
} SpoolmanApiRequestType;
extern volatile spoolmanApiStateType spoolmanApiState;
extern bool spoolman_connected;
extern String spoolmanUrl;
@@ -26,6 +34,7 @@ bool checkSpoolmanExtraFields(); // Neue Funktion zum Überprüfen der Extrafeld
JsonDocument fetchSingleSpoolInfo(int spoolId); // API-Funktion für die Webseite
bool 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
uint8_t updateSpoolLocation(String spoolId, String location);
bool initSpoolman(); // Neue Funktion zum Initialisieren von Spoolman
bool updateSpoolBambuData(String payload); // Neue Funktion zum Aktualisieren der Bambu-Daten
bool updateSpoolOcto(int spoolId); // Neue Funktion zum Aktualisieren der Octo-Daten

View File

@@ -218,11 +218,25 @@ bool decodeNdefAndReturnJson(const byte* encodedMessage) {
// Sende die aktualisierten AMS-Daten an alle WebSocket-Clients
Serial.println("JSON-Dokument erfolgreich verarbeitet");
Serial.println(doc.as<String>());
if (doc["sm_id"] != "")
if (doc.containsKey("sm_id") && doc["sm_id"] != "")
{
Serial.println("SPOOL-ID gefunden: " + doc["sm_id"].as<String>());
spoolId = doc["sm_id"].as<String>();
}
else if(doc.containsKey("location") && doc["location"] != "")
{
Serial.println("Location Tag found!");
String location = doc["location"].as<String>();
if(spoolId != ""){
updateSpoolLocation(spoolId, location);
}
else
{
Serial.println("Location update tag scanned without scanning spool before!");
oledShowMessage("No spool scanned before!");
}
}
else
{
Serial.println("Keine SPOOL-ID gefunden.");