Improvement: Add frontend, Many more improvements and small bugfixes
Bump to version 0.1.0
This commit is contained in:
parent
4e1f6be840
commit
32f90cfe84
@ -71,10 +71,11 @@ This feature has currently following issues/drawbacks:
|
|||||||
- TODO: what about locally run file?
|
- TODO: what about locally run file?
|
||||||
- TODO: test with multiple AMS
|
- TODO: test with multiple AMS
|
||||||
- TODO: filament usage in external spool
|
- TODO: filament usage in external spool
|
||||||
- Evidently needed GUI improvements
|
|
||||||
- Code cleanup
|
- Code cleanup
|
||||||
- Video showcase
|
- Video showcase
|
||||||
- Docker compose SSL
|
- Docker compose SSL
|
||||||
- Logs
|
- Logs
|
||||||
- TODOs
|
- 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
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = '0.0.2'
|
__version__ = '0.1.0'
|
||||||
|
183
app.py
183
app.py
@ -2,73 +2,48 @@ import json
|
|||||||
import traceback
|
import traceback
|
||||||
import uuid
|
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 filament import generate_filament_brand_code, generate_filament_temperatures
|
||||||
|
from frontend_utils import color_is_dark
|
||||||
from messages import AMS_FILAMENT_SETTING
|
from messages import AMS_FILAMENT_SETTING
|
||||||
from mqtt_bambulab import fetchSpools, getLastAMSConfig, publish, getMqttClient, setActiveTray
|
from mqtt_bambulab import fetchSpools, getLastAMSConfig, publish, getMqttClient, setActiveTray
|
||||||
from spoolman_client import patchExtraTags, getSpoolById
|
from spoolman_client import patchExtraTags, getSpoolById
|
||||||
|
from spoolman_service import augmentTrayDataWithSpoolMan, trayUid
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/spool_info")
|
@app.route("/spool_info")
|
||||||
def spool_info():
|
def spool_info():
|
||||||
tag_id = request.args.get("tag_id")
|
try:
|
||||||
|
tag_id = request.args.get("tag_id")
|
||||||
|
|
||||||
last_ams_config = getLastAMSConfig()
|
last_ams_config = getLastAMSConfig()
|
||||||
ams_data = last_ams_config.get("ams", [])
|
ams_data = last_ams_config.get("ams", [])
|
||||||
vt_tray_data = last_ams_config.get("vt_tray", {})
|
vt_tray_data = last_ams_config.get("vt_tray", {})
|
||||||
|
|
||||||
print(ams_data)
|
print(ams_data)
|
||||||
print(vt_tray_data)
|
print(vt_tray_data)
|
||||||
|
|
||||||
if not tag_id:
|
if not tag_id:
|
||||||
return "TAG ID is required as a query parameter (e.g., ?tagid=RFID123)"
|
return "TAG ID is required as a query parameter (e.g., ?tagid=RFID123)"
|
||||||
|
|
||||||
spools = fetchSpools()
|
spools = fetchSpools()
|
||||||
current_spool = None
|
current_spool = None
|
||||||
for spool in spools:
|
for spool in spools:
|
||||||
if not spool.get("extra", {}).get("tag"):
|
if not spool.get("extra", {}).get("tag"):
|
||||||
continue
|
continue
|
||||||
tag = json.loads(spool["extra"]["tag"])
|
tag = json.loads(spool["extra"]["tag"])
|
||||||
if tag != tag_id:
|
if tag != tag_id:
|
||||||
continue
|
continue
|
||||||
current_spool = spool
|
current_spool = spool
|
||||||
|
|
||||||
# Generate HTML for AMS selection
|
# TODO: missing current_spool
|
||||||
html = f"""
|
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)
|
||||||
<h1>Spool information</h1>
|
except Exception as e:
|
||||||
"""
|
traceback.print_exc()
|
||||||
if current_spool:
|
return render_template('error.html', exception=str(e))
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/tray_load")
|
@app.route("/tray_load")
|
||||||
@ -116,101 +91,63 @@ def tray_load():
|
|||||||
print(ams_message)
|
print(ams_message)
|
||||||
publish(getMqttClient(), ams_message)
|
publish(getMqttClient(), ams_message)
|
||||||
|
|
||||||
return f"""
|
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}."))
|
||||||
<h1>Success</h1>
|
|
||||||
<p>Updated Spool ID {spool_id} with TAG id {tag_id} to AMS {ams_id}, Tray {tray_id}.</p>
|
|
||||||
"""
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return f"<h1>Error</h1><p>{str(e)}</p>"
|
return render_template('error.html', exception=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def home():
|
def home():
|
||||||
try:
|
try:
|
||||||
spools = fetchSpools()
|
|
||||||
|
|
||||||
last_ams_config = getLastAMSConfig()
|
last_ams_config = getLastAMSConfig()
|
||||||
ams_data = last_ams_config.get("ams", [])
|
ams_data = last_ams_config.get("ams", [])
|
||||||
vt_tray_data = last_ams_config.get("vt_tray", {})
|
vt_tray_data = last_ams_config.get("vt_tray", {})
|
||||||
|
spool_list = fetchSpools()
|
||||||
|
success_message = request.args.get("success_message")
|
||||||
|
|
||||||
html = """
|
issue = False
|
||||||
<h1>Current AMS Configuration</h1>
|
#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))
|
||||||
html += """
|
issue |= vt_tray_data["issue"]
|
||||||
<h1>AMS</h1>
|
|
||||||
<ul>
|
|
||||||
"""
|
|
||||||
|
|
||||||
for ams in ams_data:
|
for ams in ams_data:
|
||||||
html += f"<li>AMS {ams['id']} (Humidity: {ams['humidity']}%, Temp: {ams['temp']}°C)<ul>"
|
|
||||||
for tray in ams["tray"]:
|
for tray in ams["tray"]:
|
||||||
tray_status = f"[{tray['tray_sub_brands']} {tray['tray_color']}]"
|
augmentTrayDataWithSpoolMan(spool_list, tray, trayUid(ams["id"], tray["id"]))
|
||||||
html += f"""
|
issue |= tray["issue"]
|
||||||
<li>
|
|
||||||
Tray {tray['id']} {tray_status} - Remaining: {tray['remain']}%
|
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)
|
||||||
</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
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return f"<h1>Error</h1><p>{str(e)}</p>"
|
return render_template('error.html', exception=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/assign_tag")
|
@app.route("/assign_tag")
|
||||||
def assign_tag():
|
def assign_tag():
|
||||||
spool_id = request.args.get("spool_id")
|
try:
|
||||||
|
spools = fetchSpools()
|
||||||
|
|
||||||
if not spool_id:
|
return render_template('assign_tag.html', spools=spools)
|
||||||
return "spool ID is required as a query parameter (e.g., ?spool_id=1)"
|
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, {}, {
|
if not spool_id:
|
||||||
"tag": json.dumps(myuuid),
|
return "spool ID is required as a query parameter (e.g., ?spool_id=1)"
|
||||||
})
|
|
||||||
|
|
||||||
return f"""
|
myuuid = str(uuid.uuid4())
|
||||||
<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>
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
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'])
|
@app.route('/', methods=['GET'])
|
||||||
def health():
|
def health():
|
||||||
|
23
frontend_utils.py
Normal file
23
frontend_utils.py
Normal 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
|
@ -7,12 +7,13 @@ import paho.mqtt.client as mqtt
|
|||||||
|
|
||||||
from config import PRINTER_ID, PRINTER_CODE, PRINTER_IP, AUTO_SPEND
|
from config import PRINTER_ID, PRINTER_CODE, PRINTER_IP, AUTO_SPEND
|
||||||
from messages import GET_VERSION, PUSH_ALL
|
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
|
from tools_3mf import getFilamentsUsageFrom3mf
|
||||||
|
|
||||||
MQTT_CLIENT = {} # Global variable storing MQTT Client
|
MQTT_CLIENT = {} # Global variable storing MQTT Client
|
||||||
LAST_AMS_CONFIG = {} # Global variable storing last AMS configuration
|
LAST_AMS_CONFIG = {} # Global variable storing last AMS configuration
|
||||||
|
|
||||||
|
|
||||||
def num2letter(num):
|
def num2letter(num):
|
||||||
return chr(ord("A") + int(num))
|
return chr(ord("A") + int(num))
|
||||||
|
|
||||||
@ -28,23 +29,6 @@ def publish(client, msg):
|
|||||||
return False
|
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
|
# Inspired by https://github.com/Donkie/Spoolman/issues/217#issuecomment-2303022970
|
||||||
def on_message(client, userdata, msg):
|
def on_message(client, userdata, msg):
|
||||||
global LAST_AMS_CONFIG
|
global LAST_AMS_CONFIG
|
||||||
@ -52,19 +36,20 @@ def on_message(client, userdata, msg):
|
|||||||
data = json.loads(msg.payload.decode())
|
data = json.loads(msg.payload.decode())
|
||||||
#print(data)
|
#print(data)
|
||||||
if AUTO_SPEND:
|
if AUTO_SPEND:
|
||||||
#Prepare AMS spending estimation
|
# Prepare AMS spending estimation
|
||||||
if "print" in data and "command" in data["print"] and data["print"]["command"] == "project_file" and "url" in data["print"]:
|
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"])
|
expected_filaments_usage = getFilamentsUsageFrom3mf(data["print"]["url"])
|
||||||
ams_used = data["print"]["use_ams"]
|
ams_used = data["print"]["use_ams"]
|
||||||
ams_mapping = data["print"]["ams_mapping"]
|
ams_mapping = data["print"]["ams_mapping"]
|
||||||
if ams_used:
|
if ams_used:
|
||||||
spendFilaments(zip(ams_mapping, expected_filaments_usage))
|
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"]:
|
if "print" in data and "vt_tray" in data["print"]:
|
||||||
LAST_AMS_CONFIG["vt_tray"] = data["print"]["vt_tray"]
|
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"]:
|
if "print" in data and "ams" in data["print"] and "ams" in data["print"]["ams"]:
|
||||||
LAST_AMS_CONFIG["ams"] = data["print"]["ams"]["ams"]
|
LAST_AMS_CONFIG["ams"] = data["print"]["ams"]["ams"]
|
||||||
for ams in 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"]:
|
for tray in ams["tray"]:
|
||||||
if "tray_sub_brands" in tray:
|
if "tray_sub_brands" in tray:
|
||||||
print(
|
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
|
found = False
|
||||||
for spool in SPOOLS:
|
for spool in fetchSpools(True):
|
||||||
if not spool.get("extra", {}).get("tag"):
|
if not spool.get("extra", {}).get("tag"):
|
||||||
continue
|
continue
|
||||||
tag = json.loads(spool["extra"]["tag"])
|
tag = json.loads(spool["extra"]["tag"])
|
||||||
if tag != tray["tag_uid"]:
|
if tag != tray["tray_uuid"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
found = True
|
found = True
|
||||||
@ -103,33 +88,6 @@ def on_connect(client, userdata, flags, rc):
|
|||||||
publish(client, GET_VERSION)
|
publish(client, GET_VERSION)
|
||||||
publish(client, PUSH_ALL)
|
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():
|
def async_subscribe():
|
||||||
global MQTT_CLIENT
|
global MQTT_CLIENT
|
||||||
MQTT_CLIENT = mqtt.Client()
|
MQTT_CLIENT = mqtt.Client()
|
||||||
@ -158,6 +116,3 @@ def getLastAMSConfig():
|
|||||||
def getMqttClient():
|
def getMqttClient():
|
||||||
global MQTT_CLIENT
|
global MQTT_CLIENT
|
||||||
return MQTT_CLIENT
|
return MQTT_CLIENT
|
||||||
|
|
||||||
|
|
||||||
SPOOLS = fetchSpools() # Global variable storing latest spool from spoolman
|
|
||||||
|
62
spoolman_service.py
Normal file
62
spoolman_service.py
Normal 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
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
6
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap.min.css.map
Normal file
1
static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
static/css/fonts/bootstrap-icons.woff
Normal file
BIN
static/css/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
static/css/fonts/bootstrap-icons.woff2
Normal file
BIN
static/css/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
BIN
static/favicon.ico
Normal file
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
7
static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/bootstrap.min.js.map
Normal file
1
static/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
static/logo.png
Normal file
BIN
static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 379 KiB |
72
templates/assign_tag.html
Normal file
72
templates/assign_tag.html
Normal 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
72
templates/base.html
Normal 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
6
templates/error.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>{{ exception }}</p>
|
||||||
|
{% endblock %}
|
138
templates/index.html
Normal file
138
templates/index.html
Normal 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
144
templates/spool_info.html
Normal 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
62
templates/write_tag.html
Normal 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 %}
|
Loading…
x
Reference in New Issue
Block a user