Compare commits
No commits in common. "a281f24689b324e037225a7381257df200382343" and "19c941dc9596d2f467beec7ad99e421e512c0c97" have entirely different histories.
a281f24689
...
19c941dc95
19 changed files with 89 additions and 388 deletions
20
WORKSPACE
20
WORKSPACE
|
@ -62,18 +62,18 @@ load("@arrdem_source_pypi//:requirements.bzl", "install_deps")
|
||||||
# Call it to define repos for your requirements.
|
# Call it to define repos for your requirements.
|
||||||
install_deps()
|
install_deps()
|
||||||
|
|
||||||
git_repository(
|
# git_repository(
|
||||||
name = "rules_zapp",
|
# name = "rules_zapp",
|
||||||
remote = "https://git.arrdem.com/arrdem/rules_zapp.git",
|
# remote = "https://git.arrdem.com/arrdem/rules_zapp.git",
|
||||||
commit = "961be891e5cff539e14f2050d5cd9e82845ce0f2",
|
# commit = "72f82e0ace184fe862f1b19c4f71c3bc36cf335b",
|
||||||
# tag = "0.1.2",
|
# # tag = "0.1.2",
|
||||||
)
|
|
||||||
|
|
||||||
# local_repository(
|
|
||||||
# name = "rules_zapp",
|
|
||||||
# path = "/home/arrdem/Documents/hobby/programming/lang/python/rules_zapp",
|
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
local_repository(
|
||||||
|
name = "rules_zapp",
|
||||||
|
path = "/home/arrdem/Documents/hobby/programming/lang/python/rules_zapp",
|
||||||
|
)
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
# Docker support
|
# Docker support
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
|
|
@ -8,7 +8,6 @@ py_project(
|
||||||
zapp_binary(
|
zapp_binary(
|
||||||
name = "qint",
|
name = "qint",
|
||||||
main = "src/python/proquint/__main__.py",
|
main = "src/python/proquint/__main__.py",
|
||||||
shebang = "#!/usr/bin/env python3",
|
|
||||||
imports = [
|
imports = [
|
||||||
"src/python",
|
"src/python",
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from attrs import define
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
OPTION_PATTERN = re.compile("; (?P<key>[a-z0-9_]+) = (?P<value>.*?)\n")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_prusa_config_str(content: str) -> dict:
|
|
||||||
kvs = {}
|
|
||||||
iter = re.finditer(OPTION_PATTERN, content)
|
|
||||||
while m := next(iter, None):
|
|
||||||
if m.group("key") == "prusaslicer_config" and m.group("value") == "begin":
|
|
||||||
break
|
|
||||||
while m := next(iter, None):
|
|
||||||
if m.group("key") == "prusaslicer_config" and m.group("value") == "end":
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
kvs[m.group("key")] = m.group("value")
|
|
||||||
|
|
||||||
return kvs
|
|
||||||
|
|
||||||
|
|
||||||
def parse_prusa_config(p: Path):
|
|
||||||
with open(p) as fp:
|
|
||||||
return parse_prusa_config_str(fp.read())
|
|
||||||
|
|
||||||
|
|
||||||
@define
|
|
||||||
class GcodeAnalysis:
|
|
||||||
max_x: int
|
|
||||||
max_y: int
|
|
||||||
max_z: int
|
|
||||||
max_bed: int
|
|
||||||
max_end: int
|
|
||||||
filament: str
|
|
||||||
nozzle: int
|
|
||||||
|
|
||||||
|
|
||||||
def parse_point(point: str) -> Tuple[int, int]:
|
|
||||||
a, b = point.split("x")
|
|
||||||
return int(a), int(b)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_bed_shape(coords: str) -> Tuple[int, int]:
|
|
||||||
# Note, these are clockwise from 0, 0
|
|
||||||
a, b, c, d = coords.split(",")
|
|
||||||
dx, _ = parse_point(a)
|
|
||||||
_, dy = parse_point(b)
|
|
||||||
x, y = parse_point(c)
|
|
||||||
return x + dx, y + dy
|
|
||||||
|
|
||||||
|
|
||||||
def analyze_gcode_str(text: str) -> Optional[GcodeAnalysis]:
|
|
||||||
opts = parse_prusa_config_str(text)
|
|
||||||
|
|
||||||
kwargs = {}
|
|
||||||
if "bed_shape" in opts:
|
|
||||||
max_x, max_y = parse_bed_shape(opts["bed_shape"])
|
|
||||||
kwargs["max_x"] = max_x
|
|
||||||
kwargs["max_y"] = max_y
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "max_print_height" in opts:
|
|
||||||
kwargs["max_z"] = int(opts["max_print_height"])
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "first_layer_bed_temperature" in opts:
|
|
||||||
kwargs["max_bed"] = int(opts["first_layer_bed_temperature"])
|
|
||||||
elif "bed_temperature" in opts:
|
|
||||||
kwargs["max_bed"] = int(opts["bed_temperature"])
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "first_layer_temperature" in opts:
|
|
||||||
kwargs["max_end"] = int(opts["first_layer_temperature"])
|
|
||||||
elif "temperature" in opts:
|
|
||||||
kwargs["max_end"] = int(opts["temperature"])
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "filament_type" in opts:
|
|
||||||
kwargs["filament"] = opts["filament_type"]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "nozzle_diameter" in opts:
|
|
||||||
kwargs["nozzle"] = float(opts["nozzle_diameter"])
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return GcodeAnalysis(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def analyze_gcode_file(p: Path) -> Optional[GcodeAnalysis]:
|
|
||||||
with open(p) as fp:
|
|
||||||
return analyze_gcode_str(fp.read())
|
|
|
@ -143,13 +143,12 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
|
||||||
|
|
||||||
# Spawn the worker thread(s)
|
# Spawn the worker thread(s)
|
||||||
Worker(cherrypy.engine, app, db_factory, poll_printers, frequency=5).start()
|
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, assign_jobs, frequency=5).start()
|
||||||
Worker(cherrypy.engine, app, db_factory, push_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, revoke_jobs, frequency=5).start()
|
||||||
Worker(cherrypy.engine, app, db_factory, pull_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, send_emails, frequency=5).start()
|
||||||
# Worker(cherrypy.engine, app, db_factory, debug_queue, frequency=5).start()
|
Worker(cherrypy.engine, app, db_factory, debug_queue, frequency=5).start()
|
||||||
|
|
||||||
# Run the server
|
# Run the server
|
||||||
cherrypy.engine.start()
|
cherrypy.engine.start()
|
||||||
|
|
|
@ -182,9 +182,6 @@ class Db(Queries):
|
||||||
digest.update(password.encode("utf-8"))
|
digest.update(password.encode("utf-8"))
|
||||||
res = super().try_login(username=username, hash=digest.hexdigest())
|
res = super().try_login(username=username, hash=digest.hexdigest())
|
||||||
if not res:
|
if not res:
|
||||||
print("WARNING: Failed to log in!")
|
|
||||||
for it in self._cursor.execute("SELECT * FROM users").fetchall():
|
|
||||||
print("DEBUG", it)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self.create_key(uid=res.id, name="web session", ttl=ttl)
|
return self.create_key(uid=res.id, name="web session", ttl=ttl)
|
||||||
|
|
|
@ -8,19 +8,6 @@ CREATE TABLE IF NOT EXISTS files (
|
||||||
, FOREIGN KEY(user_id) REFERENCES user(id)
|
, FOREIGN KEY(user_id) REFERENCES user(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- name: migration-0004-create-file-analysis#
|
|
||||||
CREATE TABLE IF NOT EXISTS file_analysis (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
||||||
, max_x INTEGER
|
|
||||||
, max_y INTEGER
|
|
||||||
, max_z INTEGER
|
|
||||||
, max_end INTEGER
|
|
||||||
, max_bed INTEGER
|
|
||||||
, nozzle_diameter FLOAT
|
|
||||||
, filament_id INTEGER REFERENCES filament(id)
|
|
||||||
, file_id INTEGER REFERENCES file(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- name: create-file^
|
-- name: create-file^
|
||||||
INSERT INTO files (
|
INSERT INTO files (
|
||||||
user_id
|
user_id
|
||||||
|
@ -58,41 +45,3 @@ WHERE
|
||||||
user_id = :uid
|
user_id = :uid
|
||||||
AND id = :fid
|
AND id = :fid
|
||||||
;
|
;
|
||||||
|
|
||||||
-- name: create-analysis^
|
|
||||||
INSERT INTO file_analysis (
|
|
||||||
max_x
|
|
||||||
, max_y
|
|
||||||
, max_z
|
|
||||||
, max_end
|
|
||||||
, max_bed
|
|
||||||
, nozzle_diameter
|
|
||||||
, filament_id
|
|
||||||
, file_id
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
:max_x
|
|
||||||
, :max_y
|
|
||||||
, :max_z
|
|
||||||
, :max_end
|
|
||||||
, :max_bed
|
|
||||||
, :nozzle
|
|
||||||
, :filament_id
|
|
||||||
, :file_id
|
|
||||||
)
|
|
||||||
RETURNING
|
|
||||||
id
|
|
||||||
;
|
|
||||||
|
|
||||||
-- name: list-unanalyzed-files
|
|
||||||
SELECT
|
|
||||||
f.id
|
|
||||||
, f.path
|
|
||||||
, f.filename
|
|
||||||
, f.user_id
|
|
||||||
FROM files f
|
|
||||||
LEFT JOIN file_analysis fa
|
|
||||||
ON f.id = fa.file_id
|
|
||||||
WHERE
|
|
||||||
fa.file_id IS NULL
|
|
||||||
;
|
|
||||||
|
|
|
@ -68,25 +68,11 @@ WHERE
|
||||||
|
|
||||||
-- name: list-job-queue
|
-- name: list-job-queue
|
||||||
SELECT
|
SELECT
|
||||||
j.id as id
|
*
|
||||||
, j.file_id
|
FROM jobs
|
||||||
, 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
|
|
||||||
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
|
WHERE
|
||||||
finished_at IS NULL
|
finished_at IS NULL
|
||||||
AND (:uid IS NULL OR j.user_id = :uid)
|
AND (:uid IS NULL OR user_id = :uid)
|
||||||
AND f.id IS NOT NULL
|
|
||||||
;
|
;
|
||||||
|
|
||||||
-- name: poll-job-queue^
|
-- name: poll-job-queue^
|
||||||
|
|
|
@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS printer_chassis (
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO printer_chassis (name, limit_x, limit_y, limit_z, limit_bed, limit_hotend, limit_tools) VALUES (
|
INSERT INTO printer_chassis (name, limit_x, limit_y, limit_z, limit_bed, limit_hotend, limit_tools) VALUES (
|
||||||
'Creality CR-10v3', 310, 310, 400, 100, 260, 1
|
'Creality CR-10v3', 300, 300, 400, 100, 260, 1
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO printer_chassis (name, limit_x, limit_y, limit_z, limit_bed, limit_hotend, limit_tools) VALUES (
|
INSERT INTO printer_chassis (name, limit_x, limit_y, limit_z, limit_bed, limit_hotend, limit_tools) VALUES (
|
||||||
|
@ -76,7 +76,7 @@ ALTER TABLE printers ADD filament_id INTEGER REFERENCES filament(id) DEFAULT 1;
|
||||||
ALTER TABLE printers ADD enabled BOOLEAN DEFAULT TRUE;
|
ALTER TABLE printers ADD enabled BOOLEAN DEFAULT TRUE;
|
||||||
|
|
||||||
-- name: migration-0005-create-printer-nozzle#
|
-- name: migration-0005-create-printer-nozzle#
|
||||||
ALTER TABLE printers ADD nozzle_diameter FLOAT DEFAULT 0.4;
|
ALTER TABLE printers ADD nozzle_diameter INTEGER default 4;
|
||||||
|
|
||||||
-- name: try-create-printer^
|
-- name: try-create-printer^
|
||||||
INSERT INTO printers (
|
INSERT INTO printers (
|
||||||
|
@ -116,19 +116,8 @@ SELECT
|
||||||
, p.api_key
|
, p.api_key
|
||||||
, p.last_poll_date
|
, p.last_poll_date
|
||||||
, s.name as status
|
, s.name as status
|
||||||
, p.enabled
|
|
||||||
, f.name as filament_name
|
|
||||||
, c.name as machine_name
|
|
||||||
, c.limit_x
|
|
||||||
, c.limit_y
|
|
||||||
, c.limit_z
|
|
||||||
, c.limit_bed
|
|
||||||
, c.limit_hotend
|
|
||||||
, p.nozzle_diameter
|
|
||||||
FROM printers p
|
FROM printers p
|
||||||
INNER JOIN printer_statuses s ON p.status_id = s.id
|
INNER JOIN printer_statuses s ON p.status_id = s.id
|
||||||
INNER JOIN filament f on p.filament_id = f.id
|
|
||||||
INNER JOIN printer_chassis c on p.chassis_id = c.id
|
|
||||||
;
|
;
|
||||||
|
|
||||||
-- name: list-idle-printers
|
-- name: list-idle-printers
|
||||||
|
@ -137,8 +126,9 @@ SELECT
|
||||||
, c.limit_x
|
, c.limit_x
|
||||||
, c.limit_y
|
, c.limit_y
|
||||||
, c.limit_z
|
, c.limit_z
|
||||||
, c.limit_bed
|
|
||||||
, c.limit_hotend
|
, c.limit_hotend
|
||||||
|
, c.limit_bed
|
||||||
|
, c.limit_tools
|
||||||
, p.nozzle_diameter
|
, p.nozzle_diameter
|
||||||
FROM printers p
|
FROM printers p
|
||||||
LEFT JOIN (SELECT id, printer_id FROM jobs WHERE finished_at IS NULL) j
|
LEFT JOIN (SELECT id, printer_id FROM jobs WHERE finished_at IS NULL) j
|
||||||
|
@ -191,14 +181,3 @@ SELECT
|
||||||
, name
|
, name
|
||||||
FROM filament
|
FROM filament
|
||||||
;
|
;
|
||||||
|
|
||||||
-- name: create-filament^
|
|
||||||
INSERT OR IGNORE INTO filament (
|
|
||||||
name
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
:name
|
|
||||||
)
|
|
||||||
RETURNING
|
|
||||||
id
|
|
||||||
;
|
|
||||||
|
|
|
@ -77,8 +77,7 @@ INNER JOIN users u
|
||||||
WHERE
|
WHERE
|
||||||
(expiration IS NULL OR unixepoch(expiration) > unixepoch('now'))
|
(expiration IS NULL OR unixepoch(expiration) > unixepoch('now'))
|
||||||
AND k.id = :kid
|
AND k.id = :kid
|
||||||
AND (u.enabled_at IS NOT NULL -- and the user is not disabled!
|
AND u.enabled_at IS NOT NULL -- and the user is not disabled!
|
||||||
OR u.group_id = 0) -- or the user is a root
|
|
||||||
;
|
;
|
||||||
|
|
||||||
-- name: refresh-key
|
-- name: refresh-key
|
||||||
|
|
|
@ -58,10 +58,6 @@ label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.u-flex-wrap {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.file, .printer, .key, .job {
|
.file, .printer, .key, .job {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
{% if files %}
|
{% if files %}
|
||||||
{% for file in files %}
|
{% for file in files %}
|
||||||
<div class="file row u-flex">
|
<div class="file row u-flex">
|
||||||
<div class="details six columns u-flex u-flex-wrap">
|
<div class="details six columns">
|
||||||
<span class="file-name">{{ file.filename }}</span>
|
<span class="file-name">{{ file.filename }}</span>
|
||||||
<span class="file-sucesses">{{ file.print_successes }} successes</span>
|
<span class="file-sucesses">{{ file.print_successes }}</span> successes
|
||||||
<span class="file-failures">{{ file.print_failures }} errors</span>
|
<span class="file-failures">{{ file.print_failures }}</span> errors
|
||||||
</div>
|
</div>
|
||||||
<div class="controls u-flex u-ml-auto">
|
<div class="controls u-flex u-ml-auto">
|
||||||
{{ macros.start_job(file.id) }}
|
{{ macros.start_job(file.id) }}
|
||||||
|
|
|
@ -4,10 +4,11 @@
|
||||||
{% if jobs %}
|
{% if jobs %}
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
<div class="job row u-flex">
|
<div class="job row u-flex">
|
||||||
<div class="details six columns u-flex u-flex-wrap">
|
<div class="details six columns u-flex">
|
||||||
<div class="job-filename u-flex">
|
<div class="job-status u-flex">
|
||||||
<label for="filename">File</label>
|
<label for="state">Job</label>
|
||||||
<span name="filename">{{ctx.db.fetch_file(ctx.uid, job.file_id).filename or "it's a secret"}}</span>
|
<div class="dot {{ macros.job_state(job) }}" style="--dot-size: 1em;"> </div>
|
||||||
|
<span name="state">{{ macros.job_state(job) }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% if job.printer_id %}
|
{% if job.printer_id %}
|
||||||
<div class="job-printer u-flex">
|
<div class="job-printer u-flex">
|
||||||
|
@ -15,20 +16,18 @@
|
||||||
<span name="printer">{{ ctx.db.fetch_printer(job.printer_id).name }}</span>
|
<span name="printer">{{ ctx.db.fetch_printer(job.printer_id).name }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="job-filename u-flex">
|
||||||
|
<label for="filename">File</label>
|
||||||
|
<span name="filename">{{ctx.db.fetch_file(ctx.uid, job.file_id).filename or "it's a secret"}}</span>
|
||||||
|
</div>
|
||||||
{% if job.finished_at and job.started_at %}
|
{% if job.finished_at and job.started_at %}
|
||||||
<div class="job-runtime u-flex">
|
<div class="job-runtime u-flex">
|
||||||
<label for="runtime">Runtime</label>
|
<label for="runtime">Runtime</label>
|
||||||
<span name="Runtime">{{ (datetime.fromisoformat(job.finished_at) - datetime.fromisoformat(job.started_at)) }}</span>
|
<span name="Runtime">{{ (datetime.fromisoformat(job.finished_at) - datetime.fromisoformat(job.started_at)) }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
<div class="two columns">
|
|
||||||
<div class="job-status u-flex">
|
|
||||||
<label for="state">Status</label>
|
|
||||||
<div class="dot {{ macros.job_state(job) }} tooltip bottom" data-text="{{ macros.job_state(job) }}" style="--dot-size: 1em;"> </div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="controls u-flex u-ml-auto">
|
||||||
<div class="controls four columns u-flex u-ml-auto">
|
|
||||||
{% if ctx.uid %}
|
{% if ctx.uid %}
|
||||||
{{ macros.duplicate_job(job.id) }}
|
{{ macros.duplicate_job(job.id) }}
|
||||||
{{ macros.delete_job(job.id) }}
|
{{ macros.delete_job(job.id) }}
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
<div class="job row u-flex">
|
<div class="job row u-flex">
|
||||||
<div class="details six columns u-flex">
|
<div class="details six columns u-flex">
|
||||||
<div class="job-filename u-flex">
|
<div class="job-status u-flex">
|
||||||
<label for="filename">File</label>
|
<label for="state">Job</label>
|
||||||
<span name="filename">{{ctx.db.fetch_file(ctx.uid, job.file_id).filename or "it's a secret"}}</span>
|
<div class="dot {{ macros.job_state(job) }} {{ 'dot--basic' if not job.state else '' }}" style="--dot-size: 1em;"> </div>
|
||||||
|
<span name="state">{{ macros.job_state(job) }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% if job.printer_id %}
|
{% if job.printer_id %}
|
||||||
<div class="job-printer u-flex">
|
<div class="job-printer u-flex">
|
||||||
|
@ -15,6 +16,10 @@
|
||||||
<span name="printer">{{ ctx.db.fetch_printer(job.printer_id).name }}</span>
|
<span name="printer">{{ ctx.db.fetch_printer(job.printer_id).name }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="job-filename u-flex">
|
||||||
|
<label for="filename">File</label>
|
||||||
|
<span name="filename">{{ctx.db.fetch_file(ctx.uid, job.file_id).filename or "it's a secret"}}</span>
|
||||||
|
</div>
|
||||||
{% if job.started_at %}
|
{% if job.started_at %}
|
||||||
<div class="job-runtime u-flex">
|
<div class="job-runtime u-flex">
|
||||||
<label for="runtime">Runtime</label>
|
<label for="runtime">Runtime</label>
|
||||||
|
@ -22,12 +27,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="two columns">
|
|
||||||
<div class="job-status u-flex">
|
|
||||||
<label for="state">Status</label>
|
|
||||||
<div class="dot {{ macros.job_state(job) }} tooltip bottom" data-text="{{ macros.job_state(job) }}" style="--dot-size: 1em;"> </div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="controls u-flex u-ml-auto">
|
<div class="controls u-flex u-ml-auto">
|
||||||
{% if ctx.uid %}
|
{% if ctx.uid %}
|
||||||
{{ macros.duplicate_job(job.id) }}
|
{{ macros.duplicate_job(job.id) }}
|
||||||
|
|
|
@ -6,10 +6,6 @@
|
||||||
<div class="u-flex1 webcam" style="max-width: calc(100% / {{printers|length}})">
|
<div class="u-flex1 webcam" style="max-width: calc(100% / {{printers|length}})">
|
||||||
<label>{{ printer.name }}</label>
|
<label>{{ printer.name }}</label>
|
||||||
<img id="printer_{{printer.id}}_stream" src="{{ printer.stream_url }}" style="max-width: 100%;" />
|
<img id="printer_{{printer.id}}_stream" src="{{ printer.stream_url }}" style="max-width: 100%;" />
|
||||||
<span><label>Status</label>{{printer.status}}, {% if printer.enabled %}accepting jobs{%else%}not scheduling{%endif%}</span>
|
|
||||||
<span><label>Loaded material</label>{{printer.filament_name}}</span>
|
|
||||||
<span><label>Machine</label>{{printer.machine_name}}</span>
|
|
||||||
<span><label>Limits</label>{{printer.limit_x}}mm x{{printer.limit_y}}mm x{{printer.limit_z}}mm, bed {{printer.limit_bed}}c, end {{printer.limit_hotend}}c</span>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,7 +17,6 @@ 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 gcode import analyze_gcode_file
|
|
||||||
from flask import Flask as App
|
from flask import Flask as App
|
||||||
from octorest import OctoRest as _OR
|
from octorest import OctoRest as _OR
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
@ -147,20 +146,10 @@ def poll_printers(app: App, db: Db) -> None:
|
||||||
def assign_jobs(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."""
|
"""Assign jobs to printers. Uploading files and job state management is handled separately."""
|
||||||
|
|
||||||
for job in db.list_job_queue(uid=None):
|
for printer in db.list_idle_printers():
|
||||||
for printer in db.list_idle_printers():
|
if job := db.poll_job_queue():
|
||||||
if (
|
db.assign_job(jid=job.id, pid=printer.id)
|
||||||
printer.limit_x >= job.max_x
|
log.info(f"Mapped job {job.id} to printer {printer.id}")
|
||||||
and printer.limit_y >= job.max_y
|
|
||||||
and printer.limit_z >= job.max_z
|
|
||||||
and printer.limit_hotend >= job.max_end
|
|
||||||
and printer.limit_bed >= job.max_bed
|
|
||||||
and printer.nozzle_diameter == job.nozzle_diameter
|
|
||||||
and printer.filament_id == job.filament_id
|
|
||||||
):
|
|
||||||
db.assign_job(jid=job.id, pid=printer.id)
|
|
||||||
log.info(f"Mapped job {job.id} to printer {printer.id}")
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def push_jobs(app: App, db: Db) -> None:
|
def push_jobs(app: App, db: Db) -> None:
|
||||||
|
@ -301,41 +290,14 @@ def send_emails(app, db: Db):
|
||||||
db.send_email(eid=message.id)
|
db.send_email(eid=message.id)
|
||||||
|
|
||||||
|
|
||||||
def analyze_files(app: App, db: Db):
|
|
||||||
for unanalyzed in db.list_unanalyzed_files():
|
|
||||||
record = analyze_gcode_file(Path(unanalyzed.path))
|
|
||||||
if not record:
|
|
||||||
log.error(
|
|
||||||
f"Unable to analyze {unanalyzed.path} ({unanalyzed.filename} owned by {unanalyzed.user_id})!"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
db.create_analysis(
|
|
||||||
file_id=unanalyzed.id,
|
|
||||||
max_x=record.max_x,
|
|
||||||
max_y=record.max_y,
|
|
||||||
max_z=record.max_z,
|
|
||||||
max_end=record.max_end,
|
|
||||||
max_bed=record.max_bed,
|
|
||||||
filament_id=db.create_filament(record.filament),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def debug_queue(app: App, db: Db):
|
def debug_queue(app: App, db: Db):
|
||||||
output = ["---"]
|
output = ["---"]
|
||||||
|
for job in db.list_running_jobs():
|
||||||
for job in db.list_job_queue(uid=None):
|
output.append(repr(job))
|
||||||
output.append("Job " + repr(job))
|
|
||||||
|
|
||||||
for printer in db.list_idle_printers():
|
for printer in db.list_idle_printers():
|
||||||
output.append("Printer " + repr(printer))
|
output.append(repr(printer))
|
||||||
|
|
||||||
for unanalyzed in db.list_unanalyzed_files():
|
|
||||||
output.append("Unanalyzed file " + repr(unanalyzed))
|
|
||||||
|
|
||||||
print("\n".join(output))
|
print("\n".join(output))
|
||||||
|
|
||||||
|
|
||||||
def toil(*fs):
|
def toil(*fs):
|
||||||
def _helper(*args, **kwargs):
|
def _helper(*args, **kwargs):
|
||||||
for f in fs:
|
for f in fs:
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from tentacles.db import Db
|
from tentacles.db import Db
|
||||||
|
|
||||||
# FIXME: Should this be an autouse fixture? Maybe. Doesn't buy much tho.
|
|
||||||
logging.addLevelName(logging.DEBUG - 5, "TRACE")
|
|
||||||
logging.TRACE = logging.DEBUG - 5
|
|
||||||
|
|
||||||
|
@pytest.yield_fixture
|
||||||
@pytest.fixture
|
|
||||||
def db():
|
def db():
|
||||||
conn = Db(":memory:")
|
conn = Db(":memory:")
|
||||||
conn.connect()
|
conn.connect()
|
||||||
conn.migrate()
|
|
||||||
yield conn
|
yield conn
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
@ -31,23 +25,26 @@ def password_testy():
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def uid_testy(db: Db, username_testy, password_testy) -> int:
|
def uid_testy(db: Db, username_testy, password_testy):
|
||||||
return db.try_create_user(
|
with db.savepoint():
|
||||||
username=username_testy,
|
return db.try_create_user(
|
||||||
email=username_testy,
|
username=username_testy,
|
||||||
password=password_testy,
|
email=username_testy,
|
||||||
sid=1,
|
password=password_testy,
|
||||||
gid=0, # Note: to bypass the approve/enable machinery
|
sid=1,
|
||||||
).id
|
).id
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def login_ttl() -> timedelta:
|
def login_ttl():
|
||||||
return timedelta(hours=12)
|
return timedelta(hours=12)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sid_testy(db: Db, uid_testy, username_testy, password_testy, login_ttl):
|
def sid_testy(db: Db, uid_testy, username_testy, password_testy, login_ttl):
|
||||||
res = db.try_login(username=username_testy, password=password_testy, ttl=login_ttl)
|
with db.savepoint():
|
||||||
assert res.user_id == uid_testy
|
res = db.try_login(
|
||||||
return res.id
|
username=username_testy, password=password_testy, ttl=login_ttl
|
||||||
|
)
|
||||||
|
assert res.user_id == uid_testy
|
||||||
|
return res.id
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from gcode import (
|
|
||||||
parse_prusa_config_str,
|
|
||||||
OPTION_PATTERN,
|
|
||||||
analyze_gcode_str,
|
|
||||||
GcodeAnalysis,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_option_pattern():
|
|
||||||
assert re.match(OPTION_PATTERN, "\n") is None
|
|
||||||
assert re.findall(OPTION_PATTERN, "; foo = bar\n")
|
|
||||||
assert re.findall(OPTION_PATTERN, "; foo = bar\n; baz = qux")
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_config_str():
|
|
||||||
assert parse_prusa_config_str("") == {}
|
|
||||||
assert (
|
|
||||||
parse_prusa_config_str(
|
|
||||||
"""
|
|
||||||
; prusaslicer_config = begin
|
|
||||||
; foo = bar
|
|
||||||
; prusaslicer_config = end
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
== {"foo": "bar"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_analyze_gcode():
|
|
||||||
assert (
|
|
||||||
analyze_gcode_str(
|
|
||||||
"""
|
|
||||||
|
|
||||||
gcode garbage
|
|
||||||
|
|
||||||
; some comment
|
|
||||||
more garbage
|
|
||||||
|
|
||||||
; prusaslicer_config = begin
|
|
||||||
; bed_shape = 5x5,95x5,95x95,5x95
|
|
||||||
; max_print_height = 100
|
|
||||||
; first_layer_bed_temperature = 100
|
|
||||||
; first_layer_temperature = 195
|
|
||||||
; filament_type = PETG
|
|
||||||
; nozzle_diameter = 1.0
|
|
||||||
; prusaslicer_config = end
|
|
||||||
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
== GcodeAnalysis(100, 100, 100, 100, 195, "PETG", 1.0)
|
|
||||||
)
|
|
|
@ -3,12 +3,12 @@
|
||||||
from tentacles.db import Db
|
from tentacles.db import Db
|
||||||
|
|
||||||
|
|
||||||
def test_db_initializes(db: Db):
|
def test_db_initializes(store: Db):
|
||||||
assert isinstance(db, Db)
|
assert isinstance(store, Db)
|
||||||
|
|
||||||
|
|
||||||
def test_db_savepoint(db: Db):
|
def test_db_savepoint(store: Db):
|
||||||
obj = db.savepoint()
|
obj = store.savepoint()
|
||||||
|
|
||||||
assert hasattr(obj, "__enter__")
|
assert hasattr(obj, "__enter__")
|
||||||
assert hasattr(obj, "__exit__")
|
assert hasattr(obj, "__exit__")
|
||||||
|
|
|
@ -1,39 +1,40 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from tentacles.db import Db
|
from tentacles.store import Store
|
||||||
|
|
||||||
|
|
||||||
def test_mkuser(db: Db, username_testy, password_testy):
|
def test_mkuser(store: Store, username_testy, password_testy):
|
||||||
res = db.try_create_user(
|
res = store.try_create_user(
|
||||||
username=username_testy, email=username_testy, password=password_testy
|
username=username_testy, email=username_testy, password=password_testy
|
||||||
)
|
)
|
||||||
assert res
|
assert res
|
||||||
assert [(it.id, it.name) for it in db.list_users()] == [(res.id, username_testy)]
|
assert store.list_users() == [(res.id, username_testy)]
|
||||||
|
|
||||||
|
|
||||||
def test_mksession(db: Db, uid_testy, username_testy, password_testy, login_ttl):
|
def test_mksession(store: Store, uid_testy, username_testy, password_testy, login_ttl):
|
||||||
assert uid_testy
|
res = store.try_login(
|
||||||
res = db.try_login(username=username_testy, password=password_testy, ttl=login_ttl)
|
username=username_testy, password=password_testy, ttl=login_ttl
|
||||||
|
)
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert [it.id for it in db.list_keys(uid=uid_testy)] == [res.id]
|
assert [it.id for it in store.list_keys(uid=uid_testy)] == [res.id]
|
||||||
assert db.try_key(kid=res.id).user_id == uid_testy
|
assert store.try_key(kid=res.id).user_id == uid_testy
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_key(db: Db, sid_testy, login_ttl):
|
def test_refresh_key(store: Store, sid_testy, login_ttl):
|
||||||
before = db.fetch_key(kid=sid_testy)
|
before = store.fetch_key(kid=sid_testy)
|
||||||
db.refresh_key(kid=sid_testy, ttl=login_ttl * 2)
|
store.refresh_key(kid=sid_testy, ttl=login_ttl * 2)
|
||||||
after = db.fetch_key(kid=sid_testy)
|
after = store.fetch_key(kid=sid_testy)
|
||||||
assert before != after
|
assert before != after
|
||||||
|
|
||||||
|
|
||||||
def tets_mkkey(db: Db, sid_testy, uid_testy):
|
def tets_mkkey(store: Store, sid_testy, uid_testy):
|
||||||
assert db.try_key(kid=sid_testy) == uid_testy
|
assert store.try_key(kid=sid_testy) == uid_testy
|
||||||
new_key = db.create_key(kid=sid_testy, ttl=None)
|
new_key = store.create_key(kid=sid_testy, ttl=None)
|
||||||
assert new_key is not None
|
assert new_key is not None
|
||||||
assert db.try_key(kid=new_key) == uid_testy
|
assert store.try_key(kid=new_key) == uid_testy
|
||||||
|
|
||||||
|
|
||||||
def test_logout(db: Db, uid_testy, sid_testy):
|
def test_logout(store: Store, uid_testy, sid_testy):
|
||||||
assert db.try_key(kid=sid_testy)
|
assert store.try_key(kid=sid_testy)
|
||||||
db.delete_key(uid=uid_testy, kid=sid_testy)
|
store.delete_key(uid=uid_testy, kid=sid_testy)
|
||||||
assert not db.try_key(kid=sid_testy)
|
assert not store.try_key(kid=sid_testy)
|
||||||
|
|
Loading…
Reference in a new issue