From 4fa627919bf2a1d3f983d87917283e57d639d20c Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie <me@arrdem.com> Date: Sat, 8 Jul 2023 16:47:12 -0600 Subject: [PATCH 1/2] Fix tests, create gcode analysis machinery --- WORKSPACE | 20 ++-- projects/proquint/BUILD | 1 + projects/tentacles/src/python/gcode.py | 96 +++++++++++++++++++ .../src/python/tentacles/__main__.py | 3 +- projects/tentacles/src/python/tentacles/db.py | 3 + .../src/python/tentacles/sql/files.sql | 46 +++++++++ .../src/python/tentacles/sql/jobs.sql | 10 +- .../src/python/tentacles/sql/printers.sql | 13 ++- .../src/python/tentacles/sql/user_keys.sql | 3 +- .../python/tentacles/static/css/style.scss | 4 + .../tentacles/templates/files_list.html.j2 | 6 +- .../tentacles/templates/jobs_history.html.j2 | 21 ++-- .../tentacles/templates/jobs_list.html.j2 | 17 ++-- .../tentacles/src/python/tentacles/workers.py | 22 +++++ projects/tentacles/test/python/conftest.py | 35 +++---- projects/tentacles/test/python/test_gcode.py | 54 +++++++++++ projects/tentacles/test/python/test_store.py | 8 +- .../tentacles/test/python/test_store_users.py | 43 ++++----- 18 files changed, 327 insertions(+), 78 deletions(-) create mode 100644 projects/tentacles/src/python/gcode.py create mode 100644 projects/tentacles/test/python/test_gcode.py diff --git a/WORKSPACE b/WORKSPACE index 6731735..0528d59 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -62,18 +62,18 @@ load("@arrdem_source_pypi//:requirements.bzl", "install_deps") # Call it to define repos for your requirements. install_deps() -# git_repository( -# name = "rules_zapp", -# remote = "https://git.arrdem.com/arrdem/rules_zapp.git", -# commit = "72f82e0ace184fe862f1b19c4f71c3bc36cf335b", -# # tag = "0.1.2", -# ) - -local_repository( - name = "rules_zapp", - path = "/home/arrdem/Documents/hobby/programming/lang/python/rules_zapp", +git_repository( + name = "rules_zapp", + remote = "https://git.arrdem.com/arrdem/rules_zapp.git", + commit = "961be891e5cff539e14f2050d5cd9e82845ce0f2", + # tag = "0.1.2", ) +# local_repository( +# name = "rules_zapp", +# path = "/home/arrdem/Documents/hobby/programming/lang/python/rules_zapp", +# ) + #################################################################################################### # Docker support #################################################################################################### diff --git a/projects/proquint/BUILD b/projects/proquint/BUILD index 05257fc..c55fdfd 100644 --- a/projects/proquint/BUILD +++ b/projects/proquint/BUILD @@ -8,6 +8,7 @@ py_project( zapp_binary( name = "qint", main = "src/python/proquint/__main__.py", + shebang = "#!/usr/bin/env python3", imports = [ "src/python", ], diff --git a/projects/tentacles/src/python/gcode.py b/projects/tentacles/src/python/gcode.py new file mode 100644 index 0000000..c2761a4 --- /dev/null +++ b/projects/tentacles/src/python/gcode.py @@ -0,0 +1,96 @@ +#!/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 + + +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 + + return GcodeAnalysis(**kwargs) + + +def analyze_gcode_file(p: Path) -> Optional[GcodeAnalysis]: + with open(p) as fp: + return analyze_gcode_str(fp.read()) diff --git a/projects/tentacles/src/python/tentacles/__main__.py b/projects/tentacles/src/python/tentacles/__main__.py index fca1a97..ddd310f 100644 --- a/projects/tentacles/src/python/tentacles/__main__.py +++ b/projects/tentacles/src/python/tentacles/__main__.py @@ -143,12 +143,13 @@ def serve(hostname: str, port: int, config: Path, trace: bool): # 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() + # Worker(cherrypy.engine, app, db_factory, debug_queue, frequency=5).start() # Run the server cherrypy.engine.start() diff --git a/projects/tentacles/src/python/tentacles/db.py b/projects/tentacles/src/python/tentacles/db.py index 237c2f4..4fdf900 100644 --- a/projects/tentacles/src/python/tentacles/db.py +++ b/projects/tentacles/src/python/tentacles/db.py @@ -182,6 +182,9 @@ class Db(Queries): digest.update(password.encode("utf-8")) res = super().try_login(username=username, hash=digest.hexdigest()) 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 self.create_key(uid=res.id, name="web session", ttl=ttl) diff --git a/projects/tentacles/src/python/tentacles/sql/files.sql b/projects/tentacles/src/python/tentacles/sql/files.sql index 9b2760e..389d096 100644 --- a/projects/tentacles/src/python/tentacles/sql/files.sql +++ b/projects/tentacles/src/python/tentacles/sql/files.sql @@ -8,6 +8,18 @@ CREATE TABLE IF NOT EXISTS files ( , 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 + , filament_id INTEGER REFERENCES filament(id) + , file_id INTEGER REFERENCES file(id) +); + -- name: create-file^ INSERT INTO files ( user_id @@ -45,3 +57,37 @@ WHERE user_id = :uid AND id = :fid ; + +-- name: create-analysis^ +INSERT INTO file_analysis ( + , max_x + , max_y + , max_z + , max_end + , max_bed + , filament_id + , file_id +) +VALUES ( + :max_x + , :max_y + , :max_z + , :max_end + , :max_bed + , :filament_id + , :file_id +) +RETURNING + id +; + +-- name: list-unanalyzed-files +SELECT + f.id + , f.filename +FROM files f +LEFT JOIN file_analysis fa + ON f.id = fa.file_id +WHERE + fa.file_id IS NULL +; diff --git a/projects/tentacles/src/python/tentacles/sql/jobs.sql b/projects/tentacles/src/python/tentacles/sql/jobs.sql index f1eaf7e..3d8ded8 100644 --- a/projects/tentacles/src/python/tentacles/sql/jobs.sql +++ b/projects/tentacles/src/python/tentacles/sql/jobs.sql @@ -69,10 +69,16 @@ WHERE -- name: list-job-queue SELECT * -FROM jobs +FROM jobs j +INNER JOIN files f + ON j.file_id = f.id +INNER JOIN file_analysis fa + ON fa.file_id = f.id WHERE finished_at IS NULL - AND (:uid IS NULL OR user_id = :uid) + AND (:uid IS NULL OR j.user_id = :uid) + AND f.id IS NOT NULL + AND fa.id IS NOT NULL ; -- name: poll-job-queue^ diff --git a/projects/tentacles/src/python/tentacles/sql/printers.sql b/projects/tentacles/src/python/tentacles/sql/printers.sql index f3e6980..16eb881 100644 --- a/projects/tentacles/src/python/tentacles/sql/printers.sql +++ b/projects/tentacles/src/python/tentacles/sql/printers.sql @@ -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 ( - 'Creality CR-10v3', 300, 300, 400, 100, 260, 1 + 'Creality CR-10v3', 310, 310, 400, 100, 260, 1 ); INSERT INTO printer_chassis (name, limit_x, limit_y, limit_z, limit_bed, limit_hotend, limit_tools) VALUES ( @@ -181,3 +181,14 @@ SELECT , name FROM filament ; + +-- name: create-filament^ +INSERT OR IGNORE INTO filament ( + name +) +VALUES ( + :name +) +RETURNING + id +; diff --git a/projects/tentacles/src/python/tentacles/sql/user_keys.sql b/projects/tentacles/src/python/tentacles/sql/user_keys.sql index 54d2c69..9d841ef 100644 --- a/projects/tentacles/src/python/tentacles/sql/user_keys.sql +++ b/projects/tentacles/src/python/tentacles/sql/user_keys.sql @@ -77,7 +77,8 @@ INNER JOIN users u WHERE (expiration IS NULL OR unixepoch(expiration) > unixepoch('now')) 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 diff --git a/projects/tentacles/src/python/tentacles/static/css/style.scss b/projects/tentacles/src/python/tentacles/static/css/style.scss index 69be564..934724c 100644 --- a/projects/tentacles/src/python/tentacles/static/css/style.scss +++ b/projects/tentacles/src/python/tentacles/static/css/style.scss @@ -58,6 +58,10 @@ label { flex: 1; } +.u-flex-wrap { + flex-wrap: wrap; +} + @media (max-width: 760px) { .file, .printer, .key, .job { flex-direction: column; diff --git a/projects/tentacles/src/python/tentacles/templates/files_list.html.j2 b/projects/tentacles/src/python/tentacles/templates/files_list.html.j2 index 6861143..632bb7c 100644 --- a/projects/tentacles/src/python/tentacles/templates/files_list.html.j2 +++ b/projects/tentacles/src/python/tentacles/templates/files_list.html.j2 @@ -4,10 +4,10 @@ {% if files %} {% for file in files %} <div class="file row u-flex"> - <div class="details six columns"> + <div class="details six columns u-flex u-flex-wrap"> <span class="file-name">{{ file.filename }}</span> - <span class="file-sucesses">{{ file.print_successes }}</span> successes - <span class="file-failures">{{ file.print_failures }}</span> errors + <span class="file-sucesses">{{ file.print_successes }} successes</span> + <span class="file-failures">{{ file.print_failures }} errors</span> </div> <div class="controls u-flex u-ml-auto"> {{ macros.start_job(file.id) }} diff --git a/projects/tentacles/src/python/tentacles/templates/jobs_history.html.j2 b/projects/tentacles/src/python/tentacles/templates/jobs_history.html.j2 index cadc3c4..68441ed 100644 --- a/projects/tentacles/src/python/tentacles/templates/jobs_history.html.j2 +++ b/projects/tentacles/src/python/tentacles/templates/jobs_history.html.j2 @@ -4,11 +4,10 @@ {% if jobs %} {% for job in jobs %} <div class="job row u-flex"> - <div class="details six columns u-flex"> - <div class="job-status u-flex"> - <label for="state">Job</label> - <div class="dot {{ macros.job_state(job) }}" style="--dot-size: 1em;"> </div> - <span name="state">{{ macros.job_state(job) }}</span> + <div class="details six columns u-flex u-flex-wrap"> + <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.printer_id %} <div class="job-printer u-flex"> @@ -16,18 +15,20 @@ <span name="printer">{{ ctx.db.fetch_printer(job.printer_id).name }}</span> </div> {% 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 %} <div class="job-runtime u-flex"> <label for="runtime">Runtime</label> <span name="Runtime">{{ (datetime.fromisoformat(job.finished_at) - datetime.fromisoformat(job.started_at)) }}</span> </div> {% 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 class="controls u-flex u-ml-auto"> + </div> + <div class="controls four columns u-flex u-ml-auto"> {% if ctx.uid %} {{ macros.duplicate_job(job.id) }} {{ macros.delete_job(job.id) }} diff --git a/projects/tentacles/src/python/tentacles/templates/jobs_list.html.j2 b/projects/tentacles/src/python/tentacles/templates/jobs_list.html.j2 index 1846eb6..ef7599c 100644 --- a/projects/tentacles/src/python/tentacles/templates/jobs_list.html.j2 +++ b/projects/tentacles/src/python/tentacles/templates/jobs_list.html.j2 @@ -5,10 +5,9 @@ {% for job in jobs %} <div class="job row u-flex"> <div class="details six columns u-flex"> - <div class="job-status u-flex"> - <label for="state">Job</label> - <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 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.printer_id %} <div class="job-printer u-flex"> @@ -16,10 +15,6 @@ <span name="printer">{{ ctx.db.fetch_printer(job.printer_id).name }}</span> </div> {% 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 %} <div class="job-runtime u-flex"> <label for="runtime">Runtime</label> @@ -27,6 +22,12 @@ </div> {% 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 class="controls u-flex u-ml-auto"> {% if ctx.uid %} {{ macros.duplicate_job(job.id) }} diff --git a/projects/tentacles/src/python/tentacles/workers.py b/projects/tentacles/src/python/tentacles/workers.py index 956cc5d..c366159 100644 --- a/projects/tentacles/src/python/tentacles/workers.py +++ b/projects/tentacles/src/python/tentacles/workers.py @@ -17,6 +17,7 @@ from urllib import parse as urlparse from cherrypy.process.plugins import Monitor from fastmail import FastMailSMTP +from gcode import analyze_gcode_file from flask import Flask as App from octorest import OctoRest as _OR from requests import Response @@ -290,14 +291,35 @@ def send_emails(app, db: Db): 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.filename)) + 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): output = ["---"] + for job in db.list_running_jobs(): output.append(repr(job)) + for printer in db.list_idle_printers(): output.append(repr(printer)) + + for unanalyzed in db.list_unanalyzed_files(): + output.append(repr(unanalyzed)) + print("\n".join(output)) + def toil(*fs): def _helper(*args, **kwargs): for f in fs: diff --git a/projects/tentacles/test/python/conftest.py b/projects/tentacles/test/python/conftest.py index a44ce2a..3121717 100644 --- a/projects/tentacles/test/python/conftest.py +++ b/projects/tentacles/test/python/conftest.py @@ -1,15 +1,21 @@ #!/usr/bin/env python3 from datetime import timedelta +import logging import pytest 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(): conn = Db(":memory:") conn.connect() + conn.migrate() yield conn conn.close() @@ -25,26 +31,23 @@ def password_testy(): @pytest.fixture -def uid_testy(db: Db, username_testy, password_testy): - with db.savepoint(): - return db.try_create_user( - username=username_testy, - email=username_testy, - password=password_testy, - sid=1, - ).id +def uid_testy(db: Db, username_testy, password_testy) -> int: + return db.try_create_user( + username=username_testy, + email=username_testy, + password=password_testy, + sid=1, + gid=0, # Note: to bypass the approve/enable machinery + ).id @pytest.fixture -def login_ttl(): +def login_ttl() -> timedelta: return timedelta(hours=12) @pytest.fixture def sid_testy(db: Db, uid_testy, username_testy, password_testy, login_ttl): - with db.savepoint(): - res = db.try_login( - username=username_testy, password=password_testy, ttl=login_ttl - ) - assert res.user_id == uid_testy - return res.id + res = db.try_login(username=username_testy, password=password_testy, ttl=login_ttl) + assert res.user_id == uid_testy + return res.id diff --git a/projects/tentacles/test/python/test_gcode.py b/projects/tentacles/test/python/test_gcode.py new file mode 100644 index 0000000..12ceb7c --- /dev/null +++ b/projects/tentacles/test/python/test_gcode.py @@ -0,0 +1,54 @@ +#!/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 +; prusaslicer_config = end + +""" + ) + == GcodeAnalysis(100, 100, 100, 100, 195, "PETG") + ) diff --git a/projects/tentacles/test/python/test_store.py b/projects/tentacles/test/python/test_store.py index 3493188..8e3928b 100644 --- a/projects/tentacles/test/python/test_store.py +++ b/projects/tentacles/test/python/test_store.py @@ -3,12 +3,12 @@ from tentacles.db import Db -def test_db_initializes(store: Db): - assert isinstance(store, Db) +def test_db_initializes(db: Db): + assert isinstance(db, Db) -def test_db_savepoint(store: Db): - obj = store.savepoint() +def test_db_savepoint(db: Db): + obj = db.savepoint() assert hasattr(obj, "__enter__") assert hasattr(obj, "__exit__") diff --git a/projects/tentacles/test/python/test_store_users.py b/projects/tentacles/test/python/test_store_users.py index c091fe0..c54513b 100644 --- a/projects/tentacles/test/python/test_store_users.py +++ b/projects/tentacles/test/python/test_store_users.py @@ -1,40 +1,39 @@ #!/usr/bin/env python3 -from tentacles.store import Store +from tentacles.db import Db -def test_mkuser(store: Store, username_testy, password_testy): - res = store.try_create_user( +def test_mkuser(db: Db, username_testy, password_testy): + res = db.try_create_user( username=username_testy, email=username_testy, password=password_testy ) assert res - assert store.list_users() == [(res.id, username_testy)] + assert [(it.id, it.name) for it in db.list_users()] == [(res.id, username_testy)] -def test_mksession(store: Store, uid_testy, username_testy, password_testy, login_ttl): - res = store.try_login( - username=username_testy, password=password_testy, ttl=login_ttl - ) +def test_mksession(db: Db, uid_testy, username_testy, password_testy, login_ttl): + assert uid_testy + res = db.try_login(username=username_testy, password=password_testy, ttl=login_ttl) assert res is not None - assert [it.id for it in store.list_keys(uid=uid_testy)] == [res.id] - assert store.try_key(kid=res.id).user_id == uid_testy + assert [it.id for it in db.list_keys(uid=uid_testy)] == [res.id] + assert db.try_key(kid=res.id).user_id == uid_testy -def test_refresh_key(store: Store, sid_testy, login_ttl): - before = store.fetch_key(kid=sid_testy) - store.refresh_key(kid=sid_testy, ttl=login_ttl * 2) - after = store.fetch_key(kid=sid_testy) +def test_refresh_key(db: Db, sid_testy, login_ttl): + before = db.fetch_key(kid=sid_testy) + db.refresh_key(kid=sid_testy, ttl=login_ttl * 2) + after = db.fetch_key(kid=sid_testy) assert before != after -def tets_mkkey(store: Store, sid_testy, uid_testy): - assert store.try_key(kid=sid_testy) == uid_testy - new_key = store.create_key(kid=sid_testy, ttl=None) +def tets_mkkey(db: Db, sid_testy, uid_testy): + assert db.try_key(kid=sid_testy) == uid_testy + new_key = db.create_key(kid=sid_testy, ttl=None) assert new_key is not None - assert store.try_key(kid=new_key) == uid_testy + assert db.try_key(kid=new_key) == uid_testy -def test_logout(store: Store, uid_testy, sid_testy): - assert store.try_key(kid=sid_testy) - store.delete_key(uid=uid_testy, kid=sid_testy) - assert not store.try_key(kid=sid_testy) +def test_logout(db: Db, uid_testy, sid_testy): + assert db.try_key(kid=sid_testy) + db.delete_key(uid=uid_testy, kid=sid_testy) + assert not db.try_key(kid=sid_testy) From a281f24689b324e037225a7381257df200382343 Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie <me@arrdem.com> Date: Sat, 8 Jul 2023 17:35:17 -0600 Subject: [PATCH 2/2] More limits refinements --- projects/tentacles/src/python/gcode.py | 6 ++++ .../src/python/tentacles/sql/files.sql | 7 +++- .../src/python/tentacles/sql/jobs.sql | 14 ++++++-- .../src/python/tentacles/sql/printers.sql | 16 +++++++-- .../tentacles/templates/streams.html.j2 | 4 +++ .../tentacles/src/python/tentacles/workers.py | 34 ++++++++++++++----- projects/tentacles/test/python/test_gcode.py | 3 +- 7 files changed, 67 insertions(+), 17 deletions(-) diff --git a/projects/tentacles/src/python/gcode.py b/projects/tentacles/src/python/gcode.py index c2761a4..4131aef 100644 --- a/projects/tentacles/src/python/gcode.py +++ b/projects/tentacles/src/python/gcode.py @@ -37,6 +37,7 @@ class GcodeAnalysis: max_bed: int max_end: int filament: str + nozzle: int def parse_point(point: str) -> Tuple[int, int]: @@ -88,6 +89,11 @@ def analyze_gcode_str(text: str) -> Optional[GcodeAnalysis]: else: return None + if "nozzle_diameter" in opts: + kwargs["nozzle"] = float(opts["nozzle_diameter"]) + else: + return None + return GcodeAnalysis(**kwargs) diff --git a/projects/tentacles/src/python/tentacles/sql/files.sql b/projects/tentacles/src/python/tentacles/sql/files.sql index 389d096..9a53828 100644 --- a/projects/tentacles/src/python/tentacles/sql/files.sql +++ b/projects/tentacles/src/python/tentacles/sql/files.sql @@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS file_analysis ( , max_z INTEGER , max_end INTEGER , max_bed INTEGER + , nozzle_diameter FLOAT , filament_id INTEGER REFERENCES filament(id) , file_id INTEGER REFERENCES file(id) ); @@ -60,11 +61,12 @@ WHERE -- name: create-analysis^ INSERT INTO file_analysis ( - , max_x + max_x , max_y , max_z , max_end , max_bed + , nozzle_diameter , filament_id , file_id ) @@ -74,6 +76,7 @@ VALUES ( , :max_z , :max_end , :max_bed + , :nozzle , :filament_id , :file_id ) @@ -84,7 +87,9 @@ RETURNING -- 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 diff --git a/projects/tentacles/src/python/tentacles/sql/jobs.sql b/projects/tentacles/src/python/tentacles/sql/jobs.sql index 3d8ded8..7c9ef2b 100644 --- a/projects/tentacles/src/python/tentacles/sql/jobs.sql +++ b/projects/tentacles/src/python/tentacles/sql/jobs.sql @@ -68,17 +68,25 @@ WHERE -- name: list-job-queue SELECT - * + j.id as id + , j.file_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 FROM jobs j INNER JOIN files f ON j.file_id = f.id -INNER JOIN file_analysis fa +LEFT JOIN file_analysis fa ON fa.file_id = f.id WHERE finished_at IS NULL AND (:uid IS NULL OR j.user_id = :uid) AND f.id IS NOT NULL - AND fa.id IS NOT NULL ; -- name: poll-job-queue^ diff --git a/projects/tentacles/src/python/tentacles/sql/printers.sql b/projects/tentacles/src/python/tentacles/sql/printers.sql index 16eb881..3901f46 100644 --- a/projects/tentacles/src/python/tentacles/sql/printers.sql +++ b/projects/tentacles/src/python/tentacles/sql/printers.sql @@ -76,7 +76,7 @@ ALTER TABLE printers ADD filament_id INTEGER REFERENCES filament(id) DEFAULT 1; ALTER TABLE printers ADD enabled BOOLEAN DEFAULT TRUE; -- name: migration-0005-create-printer-nozzle# -ALTER TABLE printers ADD nozzle_diameter INTEGER default 4; +ALTER TABLE printers ADD nozzle_diameter FLOAT DEFAULT 0.4; -- name: try-create-printer^ INSERT INTO printers ( @@ -116,8 +116,19 @@ SELECT , p.api_key , p.last_poll_date , 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 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 @@ -126,9 +137,8 @@ SELECT , c.limit_x , c.limit_y , c.limit_z - , c.limit_hotend , c.limit_bed - , c.limit_tools + , c.limit_hotend , p.nozzle_diameter FROM printers p LEFT JOIN (SELECT id, printer_id FROM jobs WHERE finished_at IS NULL) j diff --git a/projects/tentacles/src/python/tentacles/templates/streams.html.j2 b/projects/tentacles/src/python/tentacles/templates/streams.html.j2 index 16d8713..450ca89 100644 --- a/projects/tentacles/src/python/tentacles/templates/streams.html.j2 +++ b/projects/tentacles/src/python/tentacles/templates/streams.html.j2 @@ -6,6 +6,10 @@ <div class="u-flex1 webcam" style="max-width: calc(100% / {{printers|length}})"> <label>{{ printer.name }}</label> <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> {% endfor %} </div> diff --git a/projects/tentacles/src/python/tentacles/workers.py b/projects/tentacles/src/python/tentacles/workers.py index c366159..dff20cb 100644 --- a/projects/tentacles/src/python/tentacles/workers.py +++ b/projects/tentacles/src/python/tentacles/workers.py @@ -147,10 +147,20 @@ 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 printer in db.list_idle_printers(): - if job := db.poll_job_queue(): - db.assign_job(jid=job.id, pid=printer.id) - log.info(f"Mapped job {job.id} to printer {printer.id}") + for job in db.list_job_queue(uid=None): + for printer in db.list_idle_printers(): + if ( + printer.limit_x >= job.max_x + 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: @@ -293,7 +303,13 @@ def send_emails(app, db: Db): def analyze_files(app: App, db: Db): for unanalyzed in db.list_unanalyzed_files(): - record = analyze_gcode_file(Path(unanalyzed.filename)) + 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, @@ -308,14 +324,14 @@ def analyze_files(app: App, db: Db): def debug_queue(app: App, db: Db): output = ["---"] - for job in db.list_running_jobs(): - output.append(repr(job)) + for job in db.list_job_queue(uid=None): + output.append("Job " + repr(job)) for printer in db.list_idle_printers(): - output.append(repr(printer)) + output.append("Printer " + repr(printer)) for unanalyzed in db.list_unanalyzed_files(): - output.append(repr(unanalyzed)) + output.append("Unanalyzed file " + repr(unanalyzed)) print("\n".join(output)) diff --git a/projects/tentacles/test/python/test_gcode.py b/projects/tentacles/test/python/test_gcode.py index 12ceb7c..5f1164a 100644 --- a/projects/tentacles/test/python/test_gcode.py +++ b/projects/tentacles/test/python/test_gcode.py @@ -46,9 +46,10 @@ more garbage ; 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") + == GcodeAnalysis(100, 100, 100, 100, 195, "PETG", 1.0) )