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.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()
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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")
|
||||||
|
|
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#
|
-- 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
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 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,20 +201,14 @@ 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:
|
log.info(f"Printer {printer.id} needs to be leveled...")
|
||||||
last_level_date = datetime.fromisoformat(last_level_date)
|
client.gcode(
|
||||||
|
[
|
||||||
if not last_level_date or (
|
"G29",
|
||||||
datetime.utcnow() - last_level_date >= timedelta(hours=24)
|
]
|
||||||
):
|
)
|
||||||
log.info(f"Printer {printer.id} needs to be leveled...")
|
db.update_printer_level_date(pid=printer.id)
|
||||||
client.gcode(
|
|
||||||
[
|
|
||||||
"G29",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
db.update_printer_level_date(pid=printer.id)
|
|
||||||
|
|
||||||
client.select(Path(file.path).name)
|
client.select(Path(file.path).name)
|
||||||
client.start()
|
client.start()
|
||||||
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue