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

View file

@ -92,14 +92,21 @@ def create_file(location: Optional[str] = None):
digest.update(file.filename.encode()) digest.update(file.filename.encode())
sanitized_filename = digest.hexdigest() + ".gcode" sanitized_filename = digest.hexdigest() + ".gcode"
sanitized_path = os.path.join( 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): if os.path.exists(sanitized_path):
return {"error": "file exists already"}, 409 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( 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": 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"))) file = ctx.db.fetch_file(uid=ctx.uid, fid=int(request.form.get("file_id")))
if file: if file:
return send_file( return send_file(
file.path, as_attachment=True, download_name=file.filename file.path,
as_attachment=True,
download_name=file.filename,
) )
else: else:
flash("File not found", category="error") 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# -- name: migration-0002-create-orange-color#
INSERT OR IGNORE INTO filament_color (code, name) VALUES ('#FF6925', 'Orange'); 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^ -- name: create-color^
INSERT OR IGNORE INTO filament_color ( INSERT OR IGNORE INTO filament_color (
code code

View file

@ -44,6 +44,8 @@ SELECT
FROM files f FROM files f
WHERE WHERE
(:uid IS NULL OR user_id = :uid) (: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! -- name: delete-file!

View file

@ -134,8 +134,11 @@ LIMIT 25
-- name: list-mapped-jobs -- name: list-mapped-jobs
SELECT SELECT
* j.*
FROM jobs , js.name
FROM jobs j
INNER JOIN job_statuses js
ON j.status_id = js.id
WHERE WHERE
started_at IS NULL started_at IS NULL
AND printer_id IS NOT 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 pathlib import Path
from pprint import pformat from pprint import pformat
from typing import Callable from typing import Callable
from urllib import parse as urlparse
from cherrypy.process.plugins import Monitor from cherrypy.process.plugins import Monitor
from fastmail import FastMailSMTP from fastmail import FastMailSMTP
from flask import Flask as App, render_template from flask import Flask as App, render_template
from gcode import analyze_gcode_file from gcode import analyze_gcode_file
from octorest import OctoRest as _OR from octorest import OctoRest
from requests import Response from requests import Response
from requests.exceptions import ( from requests.exceptions import (
ConnectionError, ConnectionError,
@ -31,28 +30,6 @@ from requests.exceptions import (
from tentacles.db import Db 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 @cache
def get_client(url, key): def get_client(url, key):
return OctoRest(url=url, apikey=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) db.assign_job(jid=job.id, pid=printer.id)
log.info(f"Mapped job {job.id} to printer {printer.id}") log.info(f"Mapped job {job.id} to printer {printer.id}")
break break
else:
if idle:
log.info(f"Could not map job {job!r}")
def push_jobs(app: App, db: Db) -> None: 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): if client.files_info("local", Path(file.path).name):
client.delete(f"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: except HTTPError as e:
if e.response.status_code == 409: if e.response.status_code == 409:
@ -224,13 +201,7 @@ def push_jobs(app: App, db: Db) -> None:
else: else:
raise raise
# FIXME: Time since last job ended? # Removing objects from the bed can mess things up. Sigh. Level all the time.
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...") log.info(f"Printer {printer.id} needs to be leveled...")
client.gcode( client.gcode(
[ [
@ -297,6 +268,42 @@ def pull_jobs(app: App, db: Db) -> None:
runtime = datetime.utcnow() - start_date runtime = datetime.utcnow() - start_date
if runtime >= timedelta(seconds=15): # 3 polling cycles if runtime >= timedelta(seconds=15): # 3 polling cycles
log.info(f"Job {job.id} has succeeded") 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") db.finish_job(jid=job.id, state="success")
elif (job_state.get("progress", {}).get("completion") or 0.0) < 100.0: elif (job_state.get("progress", {}).get("completion") or 0.0) < 100.0: