Improvement: Add frontend, Many more improvements and small bugfixes

Bump to version 0.1.0
This commit is contained in:
Filip Bednárik 2024-12-15 03:38:50 +01:00
parent 4e1f6be840
commit 32f90cfe84
21 changed files with 2746 additions and 181 deletions

View File

@ -71,10 +71,11 @@ This feature has currently following issues/drawbacks:
- TODO: what about locally run file?
- TODO: test with multiple AMS
- TODO: filament usage in external spool
- Evidently needed GUI improvements
- Code cleanup
- Video showcase
- Docker compose SSL
- Logs
- TODOs
- Bambu Spools have apparently two RFID chips with two distinct UIDs so if you swap the filament to different tray it may fail, needs rework/testing
- Click to resolve issue
- More template components / less copy&paste
- Better Tray naming

View File

@ -1 +1 @@
__version__ = '0.0.2'
__version__ = '0.1.0'

183
app.py
View File

@ -2,73 +2,48 @@ import json
import traceback
import uuid
from flask import Flask, request, render_template_string
from flask import Flask, request, render_template, redirect, url_for
from config import BASE_URL
from config import BASE_URL, AUTO_SPEND
from filament import generate_filament_brand_code, generate_filament_temperatures
from frontend_utils import color_is_dark
from messages import AMS_FILAMENT_SETTING
from mqtt_bambulab import fetchSpools, getLastAMSConfig, publish, getMqttClient, setActiveTray
from spoolman_client import patchExtraTags, getSpoolById
from spoolman_service import augmentTrayDataWithSpoolMan, trayUid
app = Flask(__name__)
@app.route("/spool_info")
def spool_info():
tag_id = request.args.get("tag_id")
try:
tag_id = request.args.get("tag_id")
last_ams_config = getLastAMSConfig()
ams_data = last_ams_config.get("ams", [])
vt_tray_data = last_ams_config.get("vt_tray", {})
last_ams_config = getLastAMSConfig()
ams_data = last_ams_config.get("ams", [])
vt_tray_data = last_ams_config.get("vt_tray", {})
print(ams_data)
print(vt_tray_data)
print(ams_data)
print(vt_tray_data)
if not tag_id:
return "TAG ID is required as a query parameter (e.g., ?tagid=RFID123)"
if not tag_id:
return "TAG ID is required as a query parameter (e.g., ?tagid=RFID123)"
spools = fetchSpools()
current_spool = None
for spool in spools:
if not spool.get("extra", {}).get("tag"):
continue
tag = json.loads(spool["extra"]["tag"])
if tag != tag_id:
continue
current_spool = spool
spools = fetchSpools()
current_spool = None
for spool in spools:
if not spool.get("extra", {}).get("tag"):
continue
tag = json.loads(spool["extra"]["tag"])
if tag != tag_id:
continue
current_spool = spool
# Generate HTML for AMS selection
html = f"""
<h1>Spool information</h1>
"""
if current_spool:
html += f"<p>Current Spool: {current_spool}</p>"
html += """
<h1>AMS</h1>
<ul>
"""
for ams in ams_data:
html += f"<li>AMS {ams['id']} (Humidity: {ams['humidity']}%, Temp: {ams['temp']}°C)<ul>"
for tray in ams["tray"]:
tray_status = f"[{tray['tray_sub_brands']} {tray['tray_color']}]"
html += f"""
<li>
Tray {tray['id']} {tray_status} - Remaining: {tray['remain']}%
<a href="/tray_load?spool_id={current_spool['id']}&tag_id={tag_id}&ams={ams['id']}&tray={tray['id']}">Pick this tray</a>
</li>
"""
html += "</ul></li>"
html += "</ul>"
html += f"""
<h1>External Spool</h1>
<ul>
<li>Tray {vt_tray_data['id']} [{vt_tray_data['tray_sub_brands']} {vt_tray_data['tray_color']}] - Remaining: {vt_tray_data['remain']}%
<a href="/tray_load?spool_id={current_spool['id']}&tag_id={tag_id}&ams={vt_tray_data['id']}&tray=255">Pick this tray</a></li>
</ul>
"""
return render_template_string(html)
# TODO: missing current_spool
return render_template('spool_info.html', tag_id=tag_id, current_spool=current_spool, ams_data=ams_data, vt_tray_data=vt_tray_data, color_is_dark=color_is_dark, AUTO_SPEND=AUTO_SPEND)
except Exception as e:
traceback.print_exc()
return render_template('error.html', exception=str(e))
@app.route("/tray_load")
@ -116,101 +91,63 @@ def tray_load():
print(ams_message)
publish(getMqttClient(), ams_message)
return f"""
<h1>Success</h1>
<p>Updated Spool ID {spool_id} with TAG id {tag_id} to AMS {ams_id}, Tray {tray_id}.</p>
"""
return redirect(url_for('home', success_message=f"Updated Spool ID {spool_id} with TAG id {tag_id} to AMS {ams_id}, Tray {tray_id}."))
except Exception as e:
traceback.print_exc()
return f"<h1>Error</h1><p>{str(e)}</p>"
return render_template('error.html', exception=str(e))
@app.route("/")
def home():
try:
spools = fetchSpools()
last_ams_config = getLastAMSConfig()
ams_data = last_ams_config.get("ams", [])
vt_tray_data = last_ams_config.get("vt_tray", {})
spool_list = fetchSpools()
success_message = request.args.get("success_message")
html = """
<h1>Current AMS Configuration</h1>
"""
html += """
<h1>AMS</h1>
<ul>
"""
issue = False
#TODO: recheck tray ID and external spool ID and extract it to constant
augmentTrayDataWithSpoolMan(spool_list, vt_tray_data, trayUid(vt_tray_data["id"], 255))
issue |= vt_tray_data["issue"]
for ams in ams_data:
html += f"<li>AMS {ams['id']} (Humidity: {ams['humidity']}%, Temp: {ams['temp']}°C)<ul>"
for tray in ams["tray"]:
tray_status = f"[{tray['tray_sub_brands']} {tray['tray_color']}]"
html += f"""
<li>
Tray {tray['id']} {tray_status} - Remaining: {tray['remain']}%
</li>
"""
html += "</ul></li>"
html += "</ul>"
html += f"""
<h1>External Spool</h1>
<ul>
<li>Tray {vt_tray_data['id']} [{vt_tray_data['tray_sub_brands']} {vt_tray_data['tray_color']}] - Remaining: {vt_tray_data['remain']}%</li>
</ul>
"""
html += """
<h1>Add new TAG</h1>
<ul>
"""
for spool in spools:
if not spool.get("extra", {}).get("tag") or spool.get("extra", {}).get("tag") == json.dumps(""):
html += f"<li><a href='/assign_tag?spool_id={spool.get('id')}'>Spool {spool.get('filament').get('vendor').get('name')} - {spool.get('filament').get('name')}</a></li>"
html += "</ul>"
return html
augmentTrayDataWithSpoolMan(spool_list, tray, trayUid(ams["id"], tray["id"]))
issue |= tray["issue"]
return render_template('index.html', success_message=success_message, ams_data=ams_data, vt_tray_data=vt_tray_data, color_is_dark=color_is_dark, AUTO_SPEND=AUTO_SPEND, issue=issue)
except Exception as e:
traceback.print_exc()
return f"<h1>Error</h1><p>{str(e)}</p>"
return render_template('error.html', exception=str(e))
@app.route("/assign_tag")
def assign_tag():
spool_id = request.args.get("spool_id")
try:
spools = fetchSpools()
if not spool_id:
return "spool ID is required as a query parameter (e.g., ?spool_id=1)"
return render_template('assign_tag.html', spools=spools)
except Exception as e:
traceback.print_exc()
return render_template('error.html', exception=str(e))
myuuid = str(uuid.uuid4())
@app.route("/write_tag")
def write_tag():
try:
spool_id = request.args.get("spool_id")
patchExtraTags(spool_id, {}, {
"tag": json.dumps(myuuid),
})
if not spool_id:
return "spool ID is required as a query parameter (e.g., ?spool_id=1)"
return f"""
<html>
<header>
<script type="text/javascript">
function writeNFC(){{
const ndef = new NDEFReader();
document.getElementById("message").textContent="Bring NFC Tag closer to the phone.";
ndef.write({{
records: [{{ recordType: "url", data: "{BASE_URL}/spool_info?tag_id={myuuid}" }}],
}}).then(() => {{
document.getElementById("message").textContent="Message written.";
}}).catch(error => {{
document.getElementById("message").textContent="Write failed :-( try again: " + error + ".";
}});
}};
</script>
</header>
<body>
NFC Write
<button id="write" onclick="writeNFC()">Write</button>
<h1 id="message"></h1>
</body>
</html>
"""
myuuid = str(uuid.uuid4())
patchExtraTags(spool_id, {}, {
"tag": json.dumps(myuuid),
})
return render_template('write_tag.html', myuuid=myuuid, BASE_URL=BASE_URL)
except Exception as e:
traceback.print_exc()
return render_template('error.html', exception=str(e))
@app.route('/', methods=['GET'])
def health():

23
frontend_utils.py Normal file
View File

@ -0,0 +1,23 @@
def color_is_dark(bg_color):
# Remove '#' if present
color = bg_color[1:] if bg_color.startswith('#') else bg_color
# Extract RGB components
r = int(color[0:2], 16) # Hex to R
g = int(color[2:4], 16) # Hex to G
b = int(color[4:6], 16) # Hex to B
# Convert RGB values to normalized UI colors
uicolors = [r / 255.0, g / 255.0, b / 255.0]
# Apply formula to calculate perceived luminance
c = [
col / 12.92 if col <= 0.03928 else ((col + 0.055) / 1.055) ** 2.4
for col in uicolors
]
# Calculate luminance
luminance = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2])
# Return whether the color is considered "dark"
return luminance <= 0.179

View File

@ -7,12 +7,13 @@ import paho.mqtt.client as mqtt
from config import PRINTER_ID, PRINTER_CODE, PRINTER_IP, AUTO_SPEND
from messages import GET_VERSION, PUSH_ALL
from spoolman_client import fetchSpoolList, patchExtraTags, consumeSpool
from spoolman_service import spendFilaments, setActiveTray, fetchSpools
from tools_3mf import getFilamentsUsageFrom3mf
MQTT_CLIENT = {} # Global variable storing MQTT Client
LAST_AMS_CONFIG = {} # Global variable storing last AMS configuration
def num2letter(num):
return chr(ord("A") + int(num))
@ -28,23 +29,6 @@ def publish(client, msg):
return False
def spendFilaments(filaments_usage):
print(filaments_usage)
ams_usage = {}
for tray_id, usage in filaments_usage:
if tray_id != -1:
#TODO: hardcoded ams_id
if ams_usage.get(f"{PRINTER_ID}_0_{tray_id}"):
ams_usage[f"{PRINTER_ID}_0_{tray_id}"] += float(usage)
else:
ams_usage[f"{PRINTER_ID}_0_{tray_id}"] = float(usage)
for spool in fetchSpools():
#TODO: What if there is a mismatch between AMS and SpoolMan?
if spool.get("extra") and spool.get("extra").get("active_tray") and ams_usage.get(json.loads(spool.get("extra").get("active_tray"))):
consumeSpool(spool["id"], ams_usage.get(json.loads(spool.get("extra").get("active_tray"))))
# Inspired by https://github.com/Donkie/Spoolman/issues/217#issuecomment-2303022970
def on_message(client, userdata, msg):
global LAST_AMS_CONFIG
@ -52,19 +36,20 @@ def on_message(client, userdata, msg):
data = json.loads(msg.payload.decode())
#print(data)
if AUTO_SPEND:
#Prepare AMS spending estimation
if "print" in data and "command" in data["print"] and data["print"]["command"] == "project_file" and "url" in data["print"]:
# Prepare AMS spending estimation
if "print" in data and "command" in data["print"] and data["print"]["command"] == "project_file" and "url" in \
data["print"]:
expected_filaments_usage = getFilamentsUsageFrom3mf(data["print"]["url"])
ams_used = data["print"]["use_ams"]
ams_mapping = data["print"]["ams_mapping"]
if ams_used:
spendFilaments(zip(ams_mapping, expected_filaments_usage))
#Save external spool tray data
# Save external spool tray data
if "print" in data and "vt_tray" in data["print"]:
LAST_AMS_CONFIG["vt_tray"] = data["print"]["vt_tray"]
#Save ams spool data
# Save ams spool data
if "print" in data and "ams" in data["print"] and "ams" in data["print"]["ams"]:
LAST_AMS_CONFIG["ams"] = data["print"]["ams"]["ams"]
for ams in data["print"]["ams"]["ams"]:
@ -72,14 +57,14 @@ def on_message(client, userdata, msg):
for tray in ams["tray"]:
if "tray_sub_brands" in tray:
print(
f" - [{num2letter(ams['id'])}{tray['id']}] {tray['tray_sub_brands']} {tray['tray_color']} ({str(tray['remain']).zfill(3)}%) [[ {tray['tag_uid']} ]]")
f" - [{num2letter(ams['id'])}{tray['id']}] {tray['tray_sub_brands']} {tray['tray_color']} ({str(tray['remain']).zfill(3)}%) [[ {tray['tray_uuid']} ]]")
found = False
for spool in SPOOLS:
for spool in fetchSpools(True):
if not spool.get("extra", {}).get("tag"):
continue
tag = json.loads(spool["extra"]["tag"])
if tag != tray["tag_uid"]:
if tag != tray["tray_uuid"]:
continue
found = True
@ -103,33 +88,6 @@ def on_connect(client, userdata, flags, rc):
publish(client, GET_VERSION)
publish(client, PUSH_ALL)
def setActiveTray(spool_id, spool_extra, ams_id, tray_id):
if spool_extra == None:
spool_extra = {}
if not spool_extra.get("active_tray") or spool_extra.get("active_tray") != json.dumps(
f"{PRINTER_ID}_{ams_id}_{tray_id}"):
patchExtraTags(spool_id, spool_extra, {
"active_tray": json.dumps(f"{PRINTER_ID}_{ams_id}_{tray_id}"),
})
# Remove active tray from inactive spools
for old_spool in SPOOLS:
if spool_id != old_spool["id"] and old_spool["extra"]["active_tray"] == json.dumps(
f"{PRINTER_ID}_{ams_id}_{tray_id}"):
patchExtraTags(old_spool["id"], old_spool["extra"], {"active_tray": json.dumps("")})
else:
print("Skipping set active tray")
# Fetch spools from spoolman
def fetchSpools():
global SPOOLS
SPOOLS = fetchSpoolList()
return SPOOLS
def async_subscribe():
global MQTT_CLIENT
MQTT_CLIENT = mqtt.Client()
@ -158,6 +116,3 @@ def getLastAMSConfig():
def getMqttClient():
global MQTT_CLIENT
return MQTT_CLIENT
SPOOLS = fetchSpools() # Global variable storing latest spool from spoolman

62
spoolman_service.py Normal file
View File

@ -0,0 +1,62 @@
from config import PRINTER_ID
import json
from spoolman_client import consumeSpool, patchExtraTags, fetchSpoolList
def trayUid(ams_id, tray_id):
return f"{PRINTER_ID}_{ams_id}_{tray_id}"
def augmentTrayDataWithSpoolMan(spool_list, tray_data, tray_id):
tray_data["matched"] = False
for spool in spool_list:
if spool.get("extra") and spool["extra"].get("active_tray") and spool["extra"]["active_tray"] == json.dumps(tray_id):
#TODO: check for mismatch
tray_data["remaining_weight"] = spool["remaining_weight"]
tray_data["matched"] = True
break
if tray_data.get("tray_type") and tray_data["tray_type"] != "" and tray_data["matched"] == False:
tray_data["issue"] = True
else:
tray_data["issue"] = False
def spendFilaments(filaments_usage):
print(filaments_usage)
ams_usage = {}
for tray_id, usage in filaments_usage:
if tray_id != -1:
#TODO: hardcoded ams_id
if ams_usage.get(trayUid(0, tray_id)):
ams_usage[trayUid(0, tray_id)] += float(usage)
else:
ams_usage[trayUid(0, tray_id)] = float(usage)
for spool in fetchSpools():
#TODO: What if there is a mismatch between AMS and SpoolMan?
if spool.get("extra") and spool.get("extra").get("active_tray") and ams_usage.get(json.loads(spool.get("extra").get("active_tray"))):
consumeSpool(spool["id"], ams_usage.get(json.loads(spool.get("extra").get("active_tray"))))
def setActiveTray(spool_id, spool_extra, ams_id, tray_id):
if spool_extra == None:
spool_extra = {}
if not spool_extra.get("active_tray") or json.loads(spool_extra.get("active_tray")) != trayUid(ams_id, tray_id):
patchExtraTags(spool_id, spool_extra, {
"active_tray": json.dumps(trayUid(ams_id, tray_id)),
})
# Remove active tray from inactive spools
for old_spool in fetchSpools(cached=True):
if spool_id != old_spool["id"] and old_spool.get("extra") and old_spool["extra"].get("active_tray") and json.loads(old_spool["extra"]["active_tray"]) == trayUid(ams_id, tray_id):
patchExtraTags(old_spool["id"], old_spool["extra"], {"active_tray": json.dumps("")})
else:
print("Skipping set active tray")
# Fetch spools from spoolman
def fetchSpools(cached=False):
global SPOOLS
if not cached:
SPOOLS = fetchSpoolList()
return SPOOLS
SPOOLS = fetchSpools() # Global variable storing latest spool from spoolman

2078
static/css/bootstrap-icons.css vendored Normal file

File diff suppressed because it is too large Load Diff

6
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

7
static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

72
templates/assign_tag.html Normal file
View File

@ -0,0 +1,72 @@
{% extends 'base.html' %}
{% block content %}
<!-- Page Title -->
<h1 class="mb-4 text-center">Assign NFC Tag to Spool</h1>
<!-- Empty State -->
{% if spools|length == 0 or not spools %}
<div class="alert alert-info text-center" role="alert">
No spools available to tag at the moment.
</div>
{% else %}
<!-- Spool List -->
<div class="list-group">
{% for spool in spools %}
{% if not spool.extra.get("tag") or spool.extra.get("tag") == "null" %}
<!-- Individual Spool Item -->
<a href="{{ url_for('write_tag', spool_id=spool.id) }}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<!-- Left: Filament Color Badge -->
<div class="me-3">
<span class="badge d-inline-block"
style="background-color: #{{ spool.filament.color_hex }}; width: 20px; height: 50px;">
</span>
</div>
<!-- Middle: Filament Details -->
<div class="flex-grow-1">
<!-- Vendor Name and Material (Row 1) -->
<h6 class="mb-0">{{ spool.filament.vendor.name }} - {{ spool.filament.material }}</h6>
<!-- Filament Name (Row 2) -->
<small class="text-muted">{{ spool.filament.name }}</small>
</div>
<!-- Action Icon -->
<span class="badge bg-primary rounded-pill">
<i class="bi bi-plus-circle"></i> Assign
</span>
</a>
{% endif %}
{% endfor %}
{% for spool in spools %}
{% if spool.extra.get("tag") %}
<!-- Individual Spool Item -->
<a href="{{ url_for('write_tag', spool_id=spool.id) }}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<!-- Left: Filament Color Badge -->
<div class="me-3">
<span class="badge d-inline-block"
style="background-color: #{{ spool.filament.color_hex }}; width: 20px; height: 50px;">
</span>
</div>
<!-- Middle: Filament Details -->
<div class="flex-grow-1">
<!-- Vendor Name and Material (Row 1) -->
<h6 class="mb-0">{{ spool.filament.vendor.name }} - {{ spool.filament.material }}</h6>
<!-- Filament Name (Row 2) -->
<small class="text-muted">{{ spool.filament.name }}</small>
</div>
<!-- Action Icon -->
<span class="badge bg-secondary rounded-pill">
<i class="bi bi-plus-circle"></i> Reassign
</span>
</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endblock %}

72
templates/base.html Normal file
View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>OpenSpoolMan</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/bootstrap-icons.css') }}" rel="stylesheet">
<style>
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.bd-mode-toggle .dropdown-menu .active .bi {
display: block !important;
}
</style>
</head>
<body>
<header class="p-1 mb-3 border-bottom">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="{{ url_for('home') }}" class="d-flex align-items-center mb-2 mb-lg-0 link-body-emphasis text-decoration-none">
<img width="40" height="40" alt="OpenSpoolMan Logo" src="{{ url_for('static', filename='logo.png') }}"/>
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a href="{{ url_for('home') }}" class="nav-link px-2 link-body-emphasis">Home</a></li>
<li><a href="{{ url_for('assign_tag') }}" class="nav-link px-2 link-body-emphasis">Assign NFC Tag</a></li>
</ul>
</div>
</div>
</header>
<main class="container">
{% if success_message %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<strong>Success!</strong> {{ success_message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% block content %}{% endblock %}
</main>
<div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
<ul class="nav col-md-12 justify-content-end list-unstyled d-flex">
<li class="ms-3"><a class="link-body-emphasis" href="https://github.com/drndos/openspoolman">
<i class="bi bi-github"></i>
</a></li>
</ul>
</footer>
</div>
<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
<script>
;(function () {
const htmlElement = document.querySelector("html")
if (htmlElement.getAttribute("data-bs-theme") === 'auto') {
function updateTheme() {
document.querySelector("html").setAttribute("data-bs-theme",
window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
updateTheme()
}
})()
</script>
</body>
</html>

6
templates/error.html Normal file
View File

@ -0,0 +1,6 @@
{% extends 'base.html' %}
{% block content %}
<h1>Error</h1>
<p>{{ exception }}</p>
{% endblock %}

138
templates/index.html Normal file
View File

@ -0,0 +1,138 @@
{% extends 'base.html' %}
{% block content %}
<h1 class="mb-4">Info</h1>
{% if issue %}
<div class="card border-warning shadow-sm mb-4">
<div class="card-header bg-warning text-dark fw-bold">
Warning
</div>
<div class="card-body">
<h5 class="card-title">There is a mismatch between printer and SpoolMan</h5>
<p class="card-text">TODO: To fix the issue click on the tray with the red exclamation mark <i class="bi bi-exclamation-circle text-danger me-2"></i></p>
</div>
</div>
{% endif %}
<!-- AMS and External Spool Row -->
<div class="row">
<!-- External Spool -->
<div class="col-md-2 mb-4">
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0">External Spool</h5>
</div>
<div class="card-body text-center">
<!-- Tray ID -->
<h6 class="text-uppercase mb-1">
{% if vt_tray_data.issue %}
<i class="bi bi-exclamation-circle text-danger me-2"></i>
{% endif %}
{% if not vt_tray_data.tray_type %}
Empty
{% endif %}
Tray {{ vt_tray_data.id }}
</h6>
<!-- Tray Sub-Brand and Type -->
<div class="small text-muted mb-2">
{{ vt_tray_data.tray_type }}
{% if vt_tray_data.tray_sub_brands %}
<br/>
{{ vt_tray_data.tray_sub_brands }}
{% endif %}
</div>
<!-- Badge with Dynamic Colors -->
<span class="badge d-inline-block p-2"
style="background-color: #{{ vt_tray_data.tray_color }};
color: {% if color_is_dark(vt_tray_data.tray_color) %}#FFFFFF{% else %}#000000{% endif %}">
#{{ vt_tray_data.tray_color | upper }}
</span>
<!-- Remaining Percentage -->
<div class="mt-2">
Remaining:
{% if AUTO_SPEND and vt_tray_data.matched %}
<span class="fw-bold">{{ vt_tray_data.remaining_weight|round }}g</span>
{% else %}
<span class="fw-bold">{{ vt_tray_data.remain }}%</span>
{% endif %}
</div>
</div>
</div>
</div>
{% for ams in ams_data %}
<div class="col-md-4 mb-4">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">AMS {{ ams.id }}</h5>
<span class="text-muted small">Humidity: {{ ams.humidity }}%, Temp: {{ ams.temp }}°C</span>
</div>
<div class="card-body">
<div class="row">
{% for tray in ams.tray %}
<div class="col-6 mb-3">
<div class="border rounded p-2 text-center">
<!-- Tray ID -->
<h6 class="text-uppercase mb-1">
{% if tray.issue %}
<i class="bi bi-exclamation-circle text-danger me-2"></i>
{% endif %}
{% if not tray.tray_type %}
Empty
{% endif %}
Tray {{ tray.id }}
</h6>
<!-- Tray Sub-Brand and Type -->
<div class="small text-muted mb-2">
{{ tray.tray_type }}
{% if tray.tray_sub_brands %}
<br/>
{{ tray.tray_sub_brands }}
{% endif %}
</div>
{% if tray.tray_color %}
<!-- Badge with Dynamic Colors -->
<span class="badge d-inline-block p-2"
style="background-color: #{{ tray.tray_color }};
color: {% if color_is_dark(tray.tray_color) %}#FFFFFF{% else %}#000000{% endif %}">
#{{ tray.tray_color | upper }}
</span>
{% endif %}
<!-- Remaining Percentage -->
<div class="mt-2">
Remaining:
{% if AUTO_SPEND and tray.matched %}
<span class="fw-bold">{{ tray.remaining_weight|round }}g</span>
{% else %}
<span class="fw-bold">{{ tray.remain }}%</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Instruction Section -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title mb-3">
<i class="bi bi-info-circle text-info me-2"></i> Instructions
</h5>
<ul class="list-unstyled">
<li>Assign NFC Tags to your spools
</li>
<li>Load the spool with NFC tag to your AMS and bring your phone close to the NFC tag, open the URL.</li>
<li>Choose the tray you just put the spool in.</li>
</ul>
</div>
</div>
{% endblock %}

144
templates/spool_info.html Normal file
View File

@ -0,0 +1,144 @@
{% extends 'base.html' %}
{% block content %}
<h1 class="mb-4">Spool Info</h1>
<div class="card mb-3 shadow-sm">
<div class="card-header fw-bold">
Spool Details
</div>
<div class="card-body">
<!-- Filament Color Badge and Name -->
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge d-inline-block"
style="background-color: #{{ current_spool.filament.color_hex }}; width: 40px; height: 40px; border-radius: 5px;">
</span>
</div>
<div>
<h5 class="card-title mb-0">{{ current_spool.filament.name }} - {{ current_spool.filament.material }}</h5>
<small class="text-muted">{{ current_spool.filament.vendor.name }}</small>
</div>
</div>
<!-- Filament Details -->
<div class="row mb-2">
<div class="col-6">
<p class="mb-1"><strong>Remaining Weight:</strong> {{ current_spool.remaining_weight|round(2) }}g</p>
<p class="mb-1"><strong>Remaining Length:</strong> {{ current_spool.remaining_length|round(0) }}mm</p>
</div>
<div class="col-6">
<p class="mb-1"><strong>Nozzle Temp:</strong> {{ current_spool.filament.extra.nozzle_temperature }}</p>
</div>
</div>
</div>
<div class="card-footer text-muted">
<small>Registered: {{ current_spool.registered }} | Last Used: {{ current_spool.last_used }}</small>
</div>
</div>
<h1 class="mb-4">Pick Tray</h1>
<!-- AMS and External Spool Row -->
<div class="row">
<!-- External Spool -->
<div class="col-md-2 mb-4">
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0">External Spool</h5>
</div>
<div class="card-body text-center">
<div class="card">
<!-- Tray ID -->
<div class="card-header d-flex justify-content-between align-items-center">
{% if vt_tray_data.issue %}
<i class="bi bi-exclamation-circle text-danger me-2"></i>
{% endif %}
{% if not vt_tray_data.tray_type %}
Empty
{% endif %}
Tray {{ vt_tray_data.id }}
</div>
<div class="card-body">
<!-- Tray Sub-Brand and Type -->
<div class="small text-muted mb-2">
{{ vt_tray_data.tray_type }}
{% if vt_tray_data.tray_sub_brands %}
<br/>
{{ vt_tray_data.tray_sub_brands }}
{% endif %}
</div>
<!-- Badge with Dynamic Colors -->
<span class="badge d-inline-block p-2"
style="background-color: #{{ vt_tray_data.tray_color }};
color: {% if color_is_dark(vt_tray_data.tray_color) %}#FFFFFF{% else %}#000000{% endif %}">
#{{ vt_tray_data.tray_color | upper }}
</span>
</div>
<div class="card-footer">
<a class="btn btn-primary"
href="{{ url_for('tray_load', spool_id=current_spool['id'], tag_id=tag_id,ams=vt_tray_data['id'], tray='255') }}">Pick
this tray</a>
</div>
</div>
</div>
</div>
</div>
{% for ams in ams_data %}
<div class="col-md-4 mb-4">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">AMS {{ ams.id }}</h5>
<span class="text-muted small">Humidity: {{ ams.humidity }}%, Temp: {{ ams.temp }}°C</span>
</div>
<div class="card-body">
<div class="row">
{% for tray in ams.tray %}
<div class="col-6 mb-3">
<div class="card">
<!-- Tray ID -->
<div class="card-header d-flex justify-content-between align-items-center">
{% if tray.issue %}
<i class="bi bi-exclamation-circle text-danger me-2"></i>
{% endif %}
{% if not tray.tray_type %}
Empty
{% endif %}
Tray {{ tray.id }}
</div>
<div class="card-body">
<!-- Tray Sub-Brand and Type -->
<div class="small text-muted mb-2">
{{ tray.tray_type }}
{% if tray.tray_sub_brands %}
<br/>
{{ tray.tray_sub_brands }}
{% endif %}
</div>
{% if tray.tray_color %}
<!-- Badge with Dynamic Colors -->
<span class="badge d-inline-block p-2"
style="background-color: #{{ tray.tray_color }};
color: {% if color_is_dark(tray.tray_color) %}#FFFFFF{% else %}#000000{% endif %}">
#{{ tray.tray_color | upper }}
</span>
{% endif %}
</div>
<div class="card-footer">
<a class="btn btn-primary"
href="{{ url_for('tray_load', spool_id=current_spool['id'], tag_id=tag_id,ams=ams['id'], tray=tray['id']) }}">Pick
this tray</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

62
templates/write_tag.html Normal file
View File

@ -0,0 +1,62 @@
{% extends 'base.html' %}
{% block content %}
<!-- Page Title -->
<div class="text-center mb-4">
<h1 class="fw-bold">NFC Write Process</h1>
<p class="text-muted">Follow the steps below to write data to your NFC tag.</p>
</div>
<!-- Instruction Section -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title mb-3">
<i class="bi bi-info-circle text-info me-2"></i> Instructions
</h5>
<ul class="list-unstyled">
<li><i class="bi bi-check-circle-fill text-success me-2"></i> Attach NFC tag to spool so you can reach it with the
top of your phone.
</li>
<li><i class="bi bi-wifi text-primary me-2"></i> Allow NFC usage if prompted.</li>
<li><i class="bi bi-phone text-secondary me-2"></i> Bring the tag close to your phone when prompted.</li>
</ul>
</div>
</div>
<!-- Action Button -->
<div class="text-center mb-3">
<button id="write" class="btn btn-lg btn-primary shadow" onclick="writeNFC()">
<i class="bi bi-nfc me-2"></i> Start Writing NFC Tag
</button>
</div>
<!-- Message Display -->
<div id="message" class="alert alert-secondary text-center" role="alert">
Press "Start Writing NFC Tag" to begin.
</div>
<script type="text/javascript">
function writeNFC() {
// Update user message
const messageBox = document.getElementById("message");
messageBox.className = "alert alert-info text-center";
messageBox.textContent = "Bring NFC Tag closer to the phone...";
if ('NDEFReader' in window) {
const ndef = new NDEFReader();
// Attempt to write the NFC tag
ndef.write({
records: [{recordType: "url", data: "{{ BASE_URL }}/spool_info?tag_id={{ myuuid }}"}],
}).then(() => {
messageBox.className = "alert alert-success text-center";
messageBox.textContent = "✅ Message successfully written to the NFC tag!";
}).catch(error => {
messageBox.className = "alert alert-danger text-center";
messageBox.textContent = "❌ Write failed. Please try again: " + error;
});
} else {
messageBox.className = "alert alert-danger text-center";
messageBox.textContent = "Your browser or device does not support NFC writing. Try Android Phone with Chrome browser.";
}
}
</script>
{% endblock %}