Log filament usage to FilamentDB when starting a job
- filamentdb.get_ams_slots(): parses /ams/ HTML to extract slot, material,
color, weight_remaining_g, roll_id for each loaded AMS slot
- filamentdb.find_matching_ams_roll(): filters by material+color (case-insensitive),
picks the roll with least filament remaining
- filamentdb.log_print_on_roll(): POSTs to /rolls/{id}/prints
- start_job: after setting job to printing, looks up AMS, computes weight
(math.ceil of estimate), logs against matching roll, writes result to job log
- Verified parser against live AMS: PLA Black → slot 3 (30g) over slot 1 (1000g)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -64,6 +66,70 @@ def spool_counts() -> dict[tuple[str, str], int]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_ams_html(html: str) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Parse the /ams/ HTML page to extract slot info.
|
||||||
|
Each card yields: slot, material, color, weight_remaining_g, roll_id (numeric).
|
||||||
|
"""
|
||||||
|
slots = []
|
||||||
|
for card in re.split(r'class="card ams-card"', html)[1:]:
|
||||||
|
slot_m = re.search(r'<h4>Slot (\d+)</h4>', card)
|
||||||
|
matcol_m = re.search(
|
||||||
|
r'<strong>.*?</strong><br>\s*(.*?)<br>\s*(.*?)\s*</p>', card, re.DOTALL
|
||||||
|
)
|
||||||
|
weight_m = re.search(r'~(\d+)g', card)
|
||||||
|
roll_id_m = re.search(r'action="/rolls/(\d+)/prints"', card)
|
||||||
|
if slot_m and matcol_m and weight_m and roll_id_m:
|
||||||
|
slots.append({
|
||||||
|
"slot": int(slot_m.group(1)),
|
||||||
|
"material": matcol_m.group(1).strip(),
|
||||||
|
"color": matcol_m.group(2).strip(),
|
||||||
|
"weight_remaining_g": int(weight_m.group(1)),
|
||||||
|
"roll_id": int(roll_id_m.group(1)),
|
||||||
|
})
|
||||||
|
return slots
|
||||||
|
|
||||||
|
|
||||||
|
def get_ams_slots() -> list[dict]:
|
||||||
|
"""Return currently loaded AMS slots (live, not cached — state changes often)."""
|
||||||
|
try:
|
||||||
|
r = httpx.get(f"{_base()}/ams/", timeout=5, verify=False)
|
||||||
|
r.raise_for_status()
|
||||||
|
return _parse_ams_html(r.text)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("FilamentDB AMS endpoint unreachable")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def find_matching_ams_roll(material: str, color: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
Find the best roll in AMS1/slots 1-4 matching material+color.
|
||||||
|
If multiple match, pick the one with least filament remaining (use it up first).
|
||||||
|
"""
|
||||||
|
matches = [
|
||||||
|
s for s in get_ams_slots()
|
||||||
|
if s["material"].lower() == material.lower()
|
||||||
|
and s["color"].lower() == color.lower()
|
||||||
|
]
|
||||||
|
return min(matches, key=lambda s: s["weight_remaining_g"]) if matches else None
|
||||||
|
|
||||||
|
|
||||||
|
def log_print_on_roll(roll_id: int, weight_g: int, note: str) -> bool:
|
||||||
|
"""POST a print log entry to FilamentDB for the given roll."""
|
||||||
|
try:
|
||||||
|
r = httpx.post(
|
||||||
|
f"{_base()}/rolls/{roll_id}/prints",
|
||||||
|
data={"weight_used_g": weight_g, "note": note, "redirect_to": ""},
|
||||||
|
timeout=5,
|
||||||
|
verify=False,
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
logger.warning("FilamentDB print log failed for roll %s", roll_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def available_colors(material: str | None = None) -> list[str]:
|
def available_colors(material: str | None = None) -> list[str]:
|
||||||
"""Unique color names in stock, optionally filtered by material."""
|
"""Unique color names in stock, optionally filtered by material."""
|
||||||
rows = get_stock()
|
rows = get_stock()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import uuid as _uuid
|
import uuid as _uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -21,8 +22,9 @@ from app.models.printer_type import PrinterType
|
|||||||
from app.models.pricing import PricingConfig
|
from app.models.pricing import PricingConfig
|
||||||
from app.models.todo import Todo
|
from app.models.todo import Todo
|
||||||
from app.constants import COLORS, MATERIALS
|
from app.constants import COLORS, MATERIALS
|
||||||
from app.estimation import estimate
|
from app.estimation import estimate # also used in start_job
|
||||||
from app.filamentdb import available_colors, available_materials, get_stock, spool_counts
|
from app.filamentdb import (available_colors, available_materials, find_matching_ams_roll,
|
||||||
|
get_stock, log_print_on_roll, spool_counts)
|
||||||
from app.stl import get_stl_dir, list_stl_files
|
from app.stl import get_stl_dir, list_stl_files
|
||||||
from app.stl_analysis import analyse, get_cache_map # noqa: F401 used in job_detail route
|
from app.stl_analysis import analyse, get_cache_map # noqa: F401 used in job_detail route
|
||||||
|
|
||||||
@@ -325,6 +327,29 @@ def start_job(job_id: int, printer_id: int = Form(...), db: Session = Depends(ge
|
|||||||
printer.status = PrinterStatus.printing
|
printer.status = PrinterStatus.printing
|
||||||
|
|
||||||
db.add(JobLog(job_id=job_id, message=f"Job started on {printer.name}"))
|
db.add(JobLog(job_id=job_id, message=f"Job started on {printer.name}"))
|
||||||
|
|
||||||
|
# Log filament usage against matching AMS roll in FilamentDB
|
||||||
|
if job.job_material and job.job_color:
|
||||||
|
stl = db.query(StlCache).filter_by(filename=job.file_name).first() if job.file_name else None
|
||||||
|
if stl and stl.volume_ccm:
|
||||||
|
weight_g = math.ceil(estimate(stl.volume_ccm, job.job_material)["weight_g"])
|
||||||
|
roll = find_matching_ams_roll(job.job_material, job.job_color)
|
||||||
|
if roll:
|
||||||
|
note = (f"POPS {job.job_uuid[:8] if job.job_uuid else job.id}: "
|
||||||
|
f"{job.file_name or job.name} on {printer.name}")
|
||||||
|
if log_print_on_roll(roll["roll_id"], weight_g, note):
|
||||||
|
db.add(JobLog(job_id=job_id, message=(
|
||||||
|
f"FilamentDB: logged {weight_g} g against roll #{roll['roll_id']} "
|
||||||
|
f"(AMS slot {roll['slot']} — {roll['material']} {roll['color']}, "
|
||||||
|
f"{roll['weight_remaining_g']} g remaining, least-full roll selected)"
|
||||||
|
)))
|
||||||
|
else:
|
||||||
|
db.add(JobLog(job_id=job_id, message=f"FilamentDB: print log failed for roll #{roll['roll_id']}"))
|
||||||
|
else:
|
||||||
|
db.add(JobLog(job_id=job_id, message=(
|
||||||
|
f"FilamentDB: no {job.job_material} {job.job_color} roll found in AMS"
|
||||||
|
)))
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
|
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user