WIP
This commit is contained in:
parent
89f77bb1fa
commit
e1c9a1773d
13 changed files with 414664 additions and 59 deletions
|
@ -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()
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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")
|
||||
|
|
0
projects/tentacles/src/tentacles/gcode/__init__.py
Normal file
0
projects/tentacles/src/tentacles/gcode/__init__.py
Normal file
138113
projects/tentacles/src/tentacles/gcode/calibrate_abs.gcode
Normal file
138113
projects/tentacles/src/tentacles/gcode/calibrate_abs.gcode
Normal file
File diff suppressed because it is too large
Load diff
138199
projects/tentacles/src/tentacles/gcode/calibrate_petg.gcode
Normal file
138199
projects/tentacles/src/tentacles/gcode/calibrate_petg.gcode
Normal file
File diff suppressed because it is too large
Load diff
138153
projects/tentacles/src/tentacles/gcode/calibrate_pla.gcode
Normal file
138153
projects/tentacles/src/tentacles/gcode/calibrate_pla.gcode
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
<div class="row twelve columns mb-2">
|
||||
{% include "materials_list.html.j2" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -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 %}
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue