Format, add run on printer, start working on calibration
This commit is contained in:
parent
036465a50b
commit
9910b72d05
16 changed files with 173 additions and 81 deletions
|
@ -1,10 +1,10 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import smtplib
|
||||
from email import encoders
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
import smtplib
|
||||
|
||||
|
||||
class FastMailSMTP(smtplib.SMTP_SSL):
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from attrs import define
|
||||
|
||||
|
||||
OPTION_PATTERN = re.compile("; (?P<key>[a-z0-9_]+) = (?P<value>.*?)\n")
|
||||
|
||||
|
||||
|
|
|
@ -2,14 +2,20 @@
|
|||
|
||||
"""The core app entrypoint."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
from contextlib import closing
|
||||
from datetime import datetime, timedelta
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
from shutil import copyfile
|
||||
|
||||
import cherrypy
|
||||
import click
|
||||
import tomllib
|
||||
from flask import Flask, request
|
||||
|
||||
from tentacles import workers
|
||||
from tentacles.blueprints import (
|
||||
admin_ui,
|
||||
api,
|
||||
|
@ -18,11 +24,7 @@ from tentacles.blueprints import (
|
|||
user_ui,
|
||||
)
|
||||
from tentacles.db import Db
|
||||
from tentacles.globals import _ctx, Ctx, ctx
|
||||
from tentacles import workers
|
||||
from contextlib import closing
|
||||
from tentacles.blueprints.util import salt
|
||||
|
||||
from tentacles.globals import Ctx, _ctx, ctx
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -136,6 +138,28 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
|
|||
with closing(db_factory(app)) as db:
|
||||
db.migrate()
|
||||
|
||||
# Register embedded gcode files as usable files
|
||||
with closing(db_factory(app)) as db:
|
||||
for f in files("tentacles.gcode").iterdir():
|
||||
print(type(f), repr(f))
|
||||
if f.is_file() and f.name.endswith(".gcode"):
|
||||
sanitized_path = os.path.join(
|
||||
"$ROOT_FOLDER",
|
||||
app.config["UPLOAD_FOLDER"],
|
||||
f.name,
|
||||
)
|
||||
real_path = sanitized_path.replace(
|
||||
"$ROOT_FOLDER", app.config["ROOT_FOLDER"]
|
||||
)
|
||||
if not os.path.exists(real_path):
|
||||
copyfile(f, real_path)
|
||||
db.create_file(
|
||||
uid=None,
|
||||
filename=f.name,
|
||||
path=sanitized_path,
|
||||
)
|
||||
log.info("Registered calibration script %s", f.name)
|
||||
|
||||
# Configuring cherrypy is kinda awful
|
||||
cherrypy.server.unsubscribe()
|
||||
server = cherrypy._cpserver.Server()
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
import logging
|
||||
|
||||
from .api import requires_admin
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
flash,
|
||||
|
@ -11,8 +9,10 @@ from flask import (
|
|||
render_template,
|
||||
request,
|
||||
)
|
||||
|
||||
from tentacles.globals import ctx
|
||||
|
||||
from .api import requires_admin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BLUEPRINT = Blueprint("admin", __name__)
|
||||
|
@ -96,9 +96,9 @@ def handle_add_printer():
|
|||
flash("Printer created")
|
||||
return redirect("/admin/printers")
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
log.exception("Failed to create printer")
|
||||
flash(f"Unable to create printer", category="error")
|
||||
flash("Unable to create printer", category="error")
|
||||
|
||||
return render_template("printers.html.j2")
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
"""API endpoints supporting the 'ui'."""
|
||||
|
||||
from hashlib import sha3_256
|
||||
import os
|
||||
from hashlib import sha3_256
|
||||
from typing import Optional
|
||||
|
||||
from flask import Blueprint, current_app, request
|
||||
from tentacles.globals import ctx
|
||||
|
||||
from tentacles.globals import ctx
|
||||
|
||||
BLUEPRINT = Blueprint("api", __name__, url_prefix="/api")
|
||||
|
||||
|
@ -96,12 +96,11 @@ def create_file(location: Optional[str] = None):
|
|||
current_app.config["UPLOAD_FOLDER"],
|
||||
sanitized_filename,
|
||||
)
|
||||
if os.path.exists(sanitized_path):
|
||||
return {"error": "file exists already"}, 409
|
||||
|
||||
real_path = sanitized_path.replace(
|
||||
"$ROOT_FOLDER", current_app.config["ROOT_FOLDER"]
|
||||
)
|
||||
if os.path.exists(real_path):
|
||||
return {"error": "file exists already"}, 409
|
||||
|
||||
print(file.filename, real_path)
|
||||
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from .api import create_file
|
||||
from .util import requires_auth
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
flash,
|
||||
|
@ -14,8 +11,11 @@ from flask import (
|
|||
request,
|
||||
send_file,
|
||||
)
|
||||
|
||||
from tentacles.globals import ctx
|
||||
|
||||
from .api import create_file
|
||||
from .util import requires_auth
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BLUEPRINT = Blueprint("files", __name__)
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
import logging
|
||||
|
||||
from .util import requires_auth
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
flash,
|
||||
|
@ -11,14 +9,19 @@ from flask import (
|
|||
render_template,
|
||||
request,
|
||||
)
|
||||
|
||||
from tentacles.globals import ctx
|
||||
|
||||
from .util import requires_auth
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BLUEPRINT = Blueprint("jobs", __name__)
|
||||
|
||||
|
||||
def maybe(f, x):
|
||||
if x == "None":
|
||||
return None
|
||||
|
||||
if x is not None:
|
||||
return f(x)
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .util import is_logged_in, salt
|
||||
from datetime import timedelta
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
|
@ -14,8 +12,10 @@ from flask import (
|
|||
render_template,
|
||||
request,
|
||||
)
|
||||
|
||||
from tentacles.globals import ctx
|
||||
|
||||
from .util import is_logged_in, salt
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BLUEPRINT = Blueprint("user", __name__)
|
||||
|
@ -121,7 +121,7 @@ def post_register():
|
|||
|
||||
return render_template("register.html.j2")
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
log.exception("Error encountered while registering a user...")
|
||||
|
||||
flash("Unable to register that username", category="error")
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
import logging
|
||||
|
||||
from flask import current_app, flash, redirect
|
||||
from tentacles.globals import ctx
|
||||
|
||||
from tentacles.globals import ctx
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta
|
||||
from hashlib import sha3_256
|
||||
from importlib.resources import files
|
||||
from inspect import signature
|
||||
import logging
|
||||
import sqlite3
|
||||
from time import sleep
|
||||
from types import GeneratorType, new_class
|
||||
from typing import Optional
|
||||
|
@ -18,7 +18,6 @@ from aiosql.aiosql import (
|
|||
from aiosql.queries import Queries
|
||||
from aiosql.query_loader import QueryLoader
|
||||
|
||||
|
||||
_sqlite = get_adapter("sqlite3")
|
||||
_loader = QueryLoader(_sqlite, None)
|
||||
_queries = Queries(_sqlite, kwargs_only=False)
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
from contextvars import ContextVar
|
||||
|
||||
from attrs import define
|
||||
from tentacles.db import Db
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from tentacles.db import Db
|
||||
|
||||
|
||||
@define
|
||||
class Ctx:
|
||||
|
|
|
@ -35,6 +35,12 @@ ALTER TABLE jobs ADD COLUMN color_id INTEGER DEFAULT (NULL);
|
|||
-- name: migration-0003-jobs-add-continuous#
|
||||
ALTER TABLE jobs ADD COLUMN continuous BOOLEAN DEFAULT (FALSE);
|
||||
|
||||
-- name: migration-0004-jobs-add-mapped-at#
|
||||
ALTER TABLE jobs ADD COLUMN mapped_at TEXT;
|
||||
|
||||
-- name: migration-0005-jobs-add-mapped-at#
|
||||
ALTER TABLE jobs ADD COLUMN requested_at TEXT;
|
||||
|
||||
-- name: create-job^
|
||||
INSERT INTO jobs (
|
||||
user_id
|
||||
|
@ -42,6 +48,7 @@ INSERT INTO jobs (
|
|||
, color_id
|
||||
, printer_id
|
||||
, continuous
|
||||
, requested_at
|
||||
)
|
||||
VALUES (
|
||||
:uid
|
||||
|
@ -49,6 +56,7 @@ VALUES (
|
|||
, :cid
|
||||
, :pid
|
||||
, :cont
|
||||
, datetime('now')
|
||||
)
|
||||
RETURNING
|
||||
*
|
||||
|
@ -125,15 +133,59 @@ ORDER BY
|
|||
, id
|
||||
;
|
||||
|
||||
-- name: list-live-jobs
|
||||
SELECT
|
||||
*
|
||||
FROM (
|
||||
SELECT
|
||||
j.id as id
|
||||
, j.file_id
|
||||
, coalesce(j.color_id, fa.color_id) as color_id
|
||||
, fa.id as analysis_id
|
||||
, fa.max_x
|
||||
, fa.max_y
|
||||
, fa.max_z
|
||||
, fa.max_bed
|
||||
, fa.max_end
|
||||
, fa.nozzle_diameter
|
||||
, fa.filament_id
|
||||
, (SELECT name FROM filament WHERE id = fa.filament_id) AS filament_name
|
||||
, (SELECT name AS name FROM filament_color WHERE id = coalesce(j.color_id, fa.color_id)) AS color_name
|
||||
, j.status_id
|
||||
, (SELECT name FROM job_statuses WHERE id = j.status_id) AS status
|
||||
, j.started_at
|
||||
, j.time_left
|
||||
, j.cancelled_at
|
||||
, j.finished_at
|
||||
, j.user_id
|
||||
, j.printer_id
|
||||
, j.continuous
|
||||
FROM jobs j
|
||||
INNER JOIN files f
|
||||
ON j.file_id = f.id
|
||||
LEFT JOIN file_analysis fa
|
||||
ON fa.file_id = f.id
|
||||
WHERE
|
||||
finished_at IS NULL
|
||||
AND cancelled_at IS NULL
|
||||
AND (:uid IS NULL OR j.user_id = :uid)
|
||||
AND f.id IS NOT NULL
|
||||
)
|
||||
ORDER BY
|
||||
status_id DESC
|
||||
, continuous DESC
|
||||
, id
|
||||
;
|
||||
|
||||
-- name: poll-job-queue^
|
||||
SELECT
|
||||
*
|
||||
FROM jobs
|
||||
WHERE
|
||||
started_at IS NULL
|
||||
mapped_at IS NULL
|
||||
AND started_at IS NULL
|
||||
AND finished_at IS NULL
|
||||
AND printer_id IS NULL
|
||||
LIMIT 1
|
||||
;
|
||||
|
||||
-- name: list-job-history
|
||||
|
@ -208,6 +260,7 @@ WHERE
|
|||
UPDATE jobs
|
||||
SET
|
||||
printer_id = :pid
|
||||
, mapped_at = datetime('now')
|
||||
WHERE
|
||||
id = :jid
|
||||
;
|
||||
|
|
|
@ -149,14 +149,6 @@ input[type="image"] {
|
|||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.start-menu {
|
||||
border-color: $black;
|
||||
border-style: solid;
|
||||
border-width: 4px;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.border-black {
|
||||
border-color: $black;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% import "macros.html.j2" as macros %}
|
||||
<h2>Job queue</h2>
|
||||
{% with jobs = ctx.db.list_job_queue(uid=None if ctx.is_admin else ctx.uid) %}
|
||||
{% with jobs = ctx.db.list_live_jobs(uid=None if ctx.is_admin else ctx.uid) %}
|
||||
{% if jobs %}
|
||||
{% for job in jobs %}
|
||||
<div class="job row u-flex">
|
||||
|
|
|
@ -4,16 +4,28 @@
|
|||
<form class="start-menu inline u-flex" method="post" action="/jobs">
|
||||
<input type="hidden" name="action" value="enqueue" />
|
||||
<input type="hidden" name="file_id" value="{{ file.id }}" />
|
||||
<select name="color_id">
|
||||
{%- for c in ctx.db.list_colors() %}
|
||||
<option value="{{c.id}}" {% if file.color_id == c.id %}selected{%endif%}>{{c.name}}</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
<div class="u-flex u-ml-auto u-mv-auto u-flex u-ml-1">
|
||||
<label class="u-flex u-mv-auto" for="printer_id">Printer</label>
|
||||
<select class="u-flex u-mv-auto" name="printer_id">
|
||||
<option value="None">Any</option>
|
||||
{%- for p in ctx.db.list_printers() %}
|
||||
<option value="{{p.id}}">{{p.name}}</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="u-flex u-ml-auto u-mv-auto u-flex u-ml-1">
|
||||
<label class="u-flex u-mv-auto" for="color_id">Color</label>
|
||||
<select class="u-flex u-mv-auto" name="color_id">
|
||||
{%- for c in ctx.db.list_colors() %}
|
||||
<option value="{{c.id}}" {% if file.color_id == c.id %}selected{%endif%}>{{c.name}}</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="u-flex u-ml-auto u-mv-auto u-flex u-ml-1">
|
||||
<label class="u-flex u-mv-auto" for="continuous">Cont.</label>
|
||||
<input class="u-flex u-mv-auto" type="checkbox" name="continuous" false>
|
||||
</div>
|
||||
<input id="submit" type="image" src="/static/print.svg" height="24" width="24" />
|
||||
<input class="u-flex u-mv-auto" id="submit" type="image" src="/static/print.svg" height="24" width="24" />
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
|
|
|
@ -7,26 +7,28 @@ Supporting the core app with asynchronous maintenance tasks.
|
|||
Mostly related to monitoring and managing Printer state.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from contextlib import closing
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cache
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Callable
|
||||
|
||||
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
|
||||
from flask import Flask as App
|
||||
from flask import render_template
|
||||
from requests import Response
|
||||
from requests.exceptions import (
|
||||
ConnectionError,
|
||||
HTTPError,
|
||||
Timeout,
|
||||
)
|
||||
|
||||
from fastmail import FastMailSMTP
|
||||
from gcode import analyze_gcode_file
|
||||
from octorest import OctoRest
|
||||
from tentacles.db import Db
|
||||
|
||||
|
||||
|
@ -135,14 +137,9 @@ def poll_printers(app: App, db: Db) -> None:
|
|||
def assign_jobs(app: App, db: Db) -> None:
|
||||
"""Assign jobs to printers. Uploading files and job state management is handled separately."""
|
||||
|
||||
for job in db.list_job_queue(uid=None):
|
||||
# FIXME: Jobs which have been mapped are still in the "queue" until they finish
|
||||
# Ignore such as they have already been mapped
|
||||
if job.printer_id:
|
||||
continue
|
||||
|
||||
idle = list(db.list_idle_printers())
|
||||
for printer in idle:
|
||||
# FIXME: Push the scheduler into SQL
|
||||
if job := db.poll_job_queue():
|
||||
for printer in db.list_idle_printers():
|
||||
if (
|
||||
job.analysis_id is not None
|
||||
and printer.limit_x >= job.max_x
|
||||
|
@ -153,9 +150,15 @@ def assign_jobs(app: App, db: Db) -> None:
|
|||
and printer.nozzle_diameter == job.nozzle_diameter
|
||||
and printer.filament_id == job.filament_id
|
||||
and (
|
||||
printer.color_id == job.color_id
|
||||
# Note that the default/undefined color is #1
|
||||
or job.color_id == 1
|
||||
job.color_id == 1
|
||||
or printer.color_id == job.color_id
|
||||
|
||||
)
|
||||
and (
|
||||
# Note that the null printer ID serves as 'any'
|
||||
job.printer_id is None
|
||||
or job.printer_id == printer.id
|
||||
)
|
||||
):
|
||||
db.assign_job(jid=job.id, pid=printer.id)
|
||||
|
@ -276,28 +279,25 @@ def pull_jobs(app: App, db: Db) -> None:
|
|||
"""\
|
||||
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 R30 T600 ; Cool the bed
|
||||
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
|
||||
G1 X155 Y0 Z0.5 F9000 ; Traverse the head almost to the front
|
||||
G1 X200 Y0 F9000 ; Traverse the head across to knock anything off
|
||||
G1 X150 Y200 Z250 F9000 ; Return the head to the presentation state
|
||||
"""
|
||||
)
|
||||
|
||||
client.gcode(
|
||||
"""\
|
||||
M118 A1 action:notification Present bed start
|
||||
else:
|
||||
client.gcode(
|
||||
"""\
|
||||
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....
|
||||
|
@ -311,10 +311,20 @@ M118 A1 action:notification Present bed end
|
|||
|
||||
elif printer_state.get("error"):
|
||||
log.warn(f"Job {job.id} has failed")
|
||||
client.gcode(
|
||||
"""\
|
||||
G1 X150 Y200 Z250 F9000 ; Return the head to the presentation state
|
||||
"""
|
||||
)
|
||||
db.finish_job(jid=job.id, state="failed")
|
||||
|
||||
elif printer_state.get("cancelling"):
|
||||
log.info(f"Job {job.id} has been acknowledged as cancelled")
|
||||
client.gcode(
|
||||
"""\
|
||||
G1 X150 Y200 Z250 F9000 ; Return the head to the presentation state
|
||||
"""
|
||||
)
|
||||
db.finish_job(jid=job.id, state="cancelled")
|
||||
|
||||
elif printer_state.get("printing"):
|
||||
|
@ -402,7 +412,7 @@ def analyze_files(app: App, db: Db):
|
|||
def debug_queue(app: App, db: Db):
|
||||
output = ["---"]
|
||||
|
||||
for job in db.list_job_queue(uid=None):
|
||||
for job in db.list_live_jobs(uid=None):
|
||||
output.append("Job " + repr(job))
|
||||
|
||||
for printer in db.list_idle_printers():
|
||||
|
|
Loading…
Reference in a new issue