This commit is contained in:
Reid D McKenzie 2025-02-05 18:56:46 -07:00
parent 89f77bb1fa
commit e1c9a1773d
13 changed files with 414664 additions and 59 deletions

View file

@ -19,12 +19,17 @@ from tentacles.blueprints import (
)
from tentacles.db import Db
from tentacles.globals import _ctx, Ctx, ctx
from tentacles.workers import *
from tentacles.workers import assign_jobs, Worker
from tentacles import workers
from contextlib import closing
def db_factory(app):
store = Db(app.config.get("db", {}).get("uri"))
store = Db(
Path(
app.config["ROOT_FOLDER"],
app.config.get("db", {}).get("uri"),
)
)
store.connect()
return store
@ -117,6 +122,7 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
if config:
with open(config, "rb") as fp:
app.config.update(tomllib.load(fp))
app.config["ROOT_FOLDER"] = str(Path(config).absolute().parent)
print(app.config)
@ -143,14 +149,69 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
server.subscribe()
# Spawn the worker thread(s)
Worker(cherrypy.engine, app, db_factory, poll_printers, frequency=5).start()
Worker(cherrypy.engine, app, db_factory, analyze_files, frequency=5).start()
Worker(cherrypy.engine, app, db_factory, assign_jobs, frequency=5).start()
Worker(cherrypy.engine, app, db_factory, push_jobs, frequency=5).start()
Worker(cherrypy.engine, app, db_factory, revoke_jobs, frequency=5).start()
Worker(cherrypy.engine, app, db_factory, pull_jobs, frequency=5).start()
Worker(cherrypy.engine, app, db_factory, send_emails, frequency=5).start()
# Worker(cherrypy.engine, app, db_factory, debug_queue, frequency=5).start()
workers.Worker(
cherrypy.engine,
app,
db_factory,
workers.poll_printers,
frequency=5,
).start()
workers.Worker(
cherrypy.engine,
app,
db_factory,
workers.analyze_files,
frequency=5,
).start()
workers.Worker(
cherrypy.engine,
app,
db_factory,
workers.assign_jobs,
frequency=5,
).start()
workers.Worker(
cherrypy.engine,
app,
db_factory,
workers.push_jobs,
frequency=5,
).start()
workers.Worker(
cherrypy.engine,
app,
db_factory,
workers.revoke_jobs,
frequency=5,
).start()
workers.Worker(
cherrypy.engine,
app,
db_factory,
workers.pull_jobs,
frequency=5,
).start()
workers.Worker(
cherrypy.engine,
app,
db_factory,
workers.send_emails,
frequency=5,
).start()
# workers.Worker(
# cherrypy.engine,
# app,
# db_factory,
# workers.debug_queue,
# frequency=5,
# ).start()
# Run the server
cherrypy.engine.start()

View file

@ -92,14 +92,21 @@ def create_file(location: Optional[str] = None):
digest.update(file.filename.encode())
sanitized_filename = digest.hexdigest() + ".gcode"
sanitized_path = os.path.join(
current_app.config["UPLOAD_FOLDER"], sanitized_filename
"$ROOT_FOLDER",
current_app.config["UPLOAD_FOLDER"],
sanitized_filename,
)
if os.path.exists(sanitized_path):
return {"error": "file exists already"}, 409
file.save(sanitized_path)
# FIXME: Explicitly interpolating the path here kinda rots
file.save(
sanitized_path.replace("$ROOT_FOLDER", current_app.config["ROOT_FOLDER"])
)
row = ctx.db.create_file(
uid=ctx.uid, filename=file.filename, path=sanitized_path
uid=ctx.uid,
filename=file.filename,
path=sanitized_path,
)
if request.form.get("print", "").lower() == "true":

View file

@ -46,7 +46,9 @@ def manipulate_files():
file = ctx.db.fetch_file(uid=ctx.uid, fid=int(request.form.get("file_id")))
if file:
return send_file(
file.path, as_attachment=True, download_name=file.filename
file.path,
as_attachment=True,
download_name=file.filename,
)
else:
flash("File not found", category="error")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,21 @@ INSERT OR IGNORE INTO filament_color (code, name) VALUES ('#FFFFFF', 'White');
-- name: migration-0002-create-orange-color#
INSERT OR IGNORE INTO filament_color (code, name) VALUES ('#FF6925', 'Orange');
-- name: migration-0003-create-yellow-color#
INSERT OR IGNORE INTO filament_color (code, name) VALUES ('#F8F036', 'Yellow');
-- name: migration-0004-create-red-color#
INSERT OR IGNORE INTO filament_color (code, name) VALUES ('#E90402', 'Red');
-- name: migration-0005-create-blue-color#
INSERT OR IGNORE INTO filament_color (code, name) VALUES ('#5268CE', 'Blue (dark)');
-- name: migration-0005-create-light-blue-color#
INSERT OR IGNORE INTO filament_color (code, name) VALUES ('#1BA9EB', 'Blue (light)');
-- name: migration-0006-create-powder-blue-color#
INSERT OR IGNORE INTO filament_color (code, name) VALUES ('#99D1D9', 'Blue (baby)');
-- name: create-color^
INSERT OR IGNORE INTO filament_color (
code

View file

@ -44,6 +44,8 @@ SELECT
FROM files f
WHERE
(:uid IS NULL OR user_id = :uid)
ORDER BY
(SELECT MAX(finished_at) FROM jobs WHERE file_id = f.id AND finished_at IS NOT NULL) DESC
;
-- name: delete-file!

View file

@ -134,8 +134,11 @@ LIMIT 25
-- name: list-mapped-jobs
SELECT
*
FROM jobs
j.*
, js.name
FROM jobs j
INNER JOIN job_statuses js
ON j.status_id = js.id
WHERE
started_at IS NULL
AND printer_id IS NOT NULL

View file

@ -0,0 +1,6 @@
{% extends "base.html.j2" %}
{% block content %}
<div class="row twelve columns mb-2">
{% include "materials_list.html.j2" %}
</div>
{% endblock %}

View file

@ -0,0 +1,37 @@
{% import "macros.html.j2" as macros %}
<h2>Materials</h2>
{% with files = ctx.db.list_files(uid=None if ctx.is_admin else ctx.uid) %}
{% if files %}
{% for file in files %}
<div class="material row u-flex">
<div class="details eight columns u-flex u-flex-wrap u-overflow-elipsis">
<div class="file-name u-flex u-flex-break">
<label for="filename">File</label>
<span name="filename">{{ file.filename }}</span>
</div>
<div class="file-sucesses u-flex">
<label>Successes</label>
<span>{{ file.print_successes }}</span>
</div>
<div class="file-failures u-flex">
<label>Failures</label>
<span>{{ file.print_failures }}</span>
</div>
{% if file.user_id != ctx.uid %}
<div class="file-user u-flex">
<label>Owner</label>
{{ ctx.db.fetch_user(uid=file.user_id).name }}
</div>
{% endif %}
</div>
<div class="controls u-flex u-ml-auto u-mv-auto">
{{ macros.download_file(file.id) }}
{{ macros.start_job(file.id) }}
{{ macros.delete_file(file.id) }}
</div>
</div>
{% endfor %}
{% else %}
You don't have any files. Upload something!
{% endif %}
{% endwith %}

View file

@ -15,13 +15,12 @@ import os
from pathlib import Path
from pprint import pformat
from typing import Callable
from urllib import parse as urlparse
from cherrypy.process.plugins import Monitor
from fastmail import FastMailSMTP
from flask import Flask as App, render_template
from gcode import analyze_gcode_file
from octorest import OctoRest as _OR
from octorest import OctoRest
from requests import Response
from requests.exceptions import (
ConnectionError,
@ -31,28 +30,6 @@ from requests.exceptions import (
from tentacles.db import Db
class OctoRest(_OR):
def _get(self, path, params=None):
url = urlparse.urljoin(self.url, path)
response = self.session.get(url, params=params, timeout=(1.0, 1.0))
self._check_response(response)
return response.json()
def _check_response(self, response: Response):
response.raise_for_status()
return response
def files_info(self, *args, **kwargs):
try:
return super().files_info(*args, **kwargs)
except HTTPError as e:
if e.response.status_code == 404:
return {}
else:
raise e
@cache
def get_client(url, key):
return OctoRest(url=url, apikey=key)
@ -184,9 +161,6 @@ def assign_jobs(app: App, db: Db) -> None:
db.assign_job(jid=job.id, pid=printer.id)
log.info(f"Mapped job {job.id} to printer {printer.id}")
break
else:
if idle:
log.info(f"Could not map job {job!r}")
def push_jobs(app: App, db: Db) -> None:
@ -216,7 +190,10 @@ def push_jobs(app: App, db: Db) -> None:
if client.files_info("local", Path(file.path).name):
client.delete(f"local/{Path(file.path).name}")
client.upload(file.path)
# FIXME: Explicitly interpolating the path here kinda rots
client.upload(
file.path.replace("$ROOT_FOLDER", app.config["ROOT_FOLDER"])
)
except HTTPError as e:
if e.response.status_code == 409:
@ -224,20 +201,14 @@ def push_jobs(app: App, db: Db) -> None:
else:
raise
# FIXME: Time since last job ended?
if last_level_date := printer.last_level_date:
last_level_date = datetime.fromisoformat(last_level_date)
if not last_level_date or (
datetime.utcnow() - last_level_date >= timedelta(hours=24)
):
log.info(f"Printer {printer.id} needs to be leveled...")
client.gcode(
[
"G29",
]
)
db.update_printer_level_date(pid=printer.id)
# Removing objects from the bed can mess things up. Sigh. Level all the time.
log.info(f"Printer {printer.id} needs to be leveled...")
client.gcode(
[
"G29",
]
)
db.update_printer_level_date(pid=printer.id)
client.select(Path(file.path).name)
client.start()
@ -297,6 +268,42 @@ def pull_jobs(app: App, db: Db) -> None:
runtime = datetime.utcnow() - start_date
if runtime >= timedelta(seconds=15): # 3 polling cycles
log.info(f"Job {job.id} has succeeded")
# Attempt to automatically clear the bed
if False:
log.info(f"Attempting to clear bed of {printer.id} ({printer.url})")
client.gcode(
"""\
G90 ; Absolute motion coordinates
G1 X155 Y310 F9000 ; Traverse to the back of the bed without lowering
M118 A1 action:notification Cooling bed
M104 S0 ; Turn off the hotend if it's on
M190 R45 ; Cool the bed
M190 R40 ; Cool the bed
M190 R35 ; Cool the bed
M190 R30 ; Cool the bed
M140 S0 ; Turn off the bed
M118 A1 action:notification Bed cooled
M118 A1 action:notification Bed clear start
G1 X155 Y310 Z0.5 F9000 ; Lower the head to a push height
G1 X155 Y0 F9000 ; Traverse the head to the front of the bed to knock off any objects
G1 X155 Y200 Z250 F9000 ; Return the head to the presentation state
M118 A1 action:notification Bed clear end
"""
)
client.gcode(
"""\
M118 A1 action:notification Present bed start
G1 X150 Y200 Z250 F9000 ; Return the head to the presentation state
M118 A1 action:notification Present bed end
"""
)
# And having dispatched that gcode mark the job done
# FIXME: There should be some more state sync here....
db.finish_job(jid=job.id, state="success")
elif (job_state.get("progress", {}).get("completion") or 0.0) < 100.0: