Stared work on the manager side
This commit is contained in:
@@ -64,7 +64,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libxfixes3 libdrm2 libxdamage1 libxcomposite1 libwayland-client0 libxcb1 \
|
libxfixes3 libdrm2 libxdamage1 libxcomposite1 libwayland-client0 libxcb1 \
|
||||||
python3 make g++ libsqlite3-dev \
|
python3 make g++ libsqlite3-dev \
|
||||||
curl tini \
|
curl tini \
|
||||||
# 👇 add these for libGL.so.1 (Mesa)
|
|
||||||
libgl1 libgl1-mesa-dri libglu1-mesa \
|
libgl1 libgl1-mesa-dri libglu1-mesa \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ app.post("/api/jobs", upload.single("model"), async (req, res) => {
|
|||||||
|
|
||||||
const job: JobRecord = {
|
const job: JobRecord = {
|
||||||
id,
|
id,
|
||||||
filename: req.file.originalname,
|
filename: path.join(outputDir, "transformed_input.stl"),
|
||||||
ext,
|
ext,
|
||||||
profile: profilePath,
|
profile: profilePath,
|
||||||
supports,
|
supports,
|
||||||
@@ -103,6 +103,10 @@ app.post("/api/jobs", upload.single("model"), async (req, res) => {
|
|||||||
finishedAt: null
|
finishedAt: null
|
||||||
};
|
};
|
||||||
insertJob.run(job);
|
insertJob.run(job);
|
||||||
|
try {
|
||||||
|
db.prepare("UPDATE jobs SET rot_x=?, rot_y=?, rot_z=?, scale=? WHERE id=?")
|
||||||
|
.run(rotX, rotY, rotZ, scale, id);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
// Kick slicing async
|
// Kick slicing async
|
||||||
runSlicing(job, outputPath, supports, profilePath).catch(() => { /* already captured in DB */ });
|
runSlicing(job, outputPath, supports, profilePath).catch(() => { /* already captured in DB */ });
|
||||||
@@ -153,7 +157,7 @@ function mapRow(r: any) {
|
|||||||
profile: path.basename(r.profile),
|
profile: path.basename(r.profile),
|
||||||
supports: r.supports,
|
supports: r.supports,
|
||||||
inputPath: r.input_path,
|
inputPath: r.input_path,
|
||||||
outputPath: r.output_path ? `/outputs/${r.id}/result.gcode` : null,
|
outputPath: r.output_path ? `/outputs/${r.id}/result.3mf` : null,
|
||||||
status: r.status,
|
status: r.status,
|
||||||
errorMsg: r.error_msg,
|
errorMsg: r.error_msg,
|
||||||
createdAt: r.created_at,
|
createdAt: r.created_at,
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: . # repo root
|
context: .
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
env_file: .env
|
env_file: .env
|
||||||
ports:
|
ports:
|
||||||
11
Web UI Project/.dockerignore
Normal file
11
Web UI Project/.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.env
|
||||||
|
.env*
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.git/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
0
Web UI Project/app/__init__.py
Normal file
0
Web UI Project/app/__init__.py
Normal file
BIN
Web UI Project/app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
Web UI Project/app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Web UI Project/app/__pycache__/main.cpython-312.pyc
Normal file
BIN
Web UI Project/app/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Web UI Project/app/__pycache__/printer.cpython-312.pyc
Normal file
BIN
Web UI Project/app/__pycache__/printer.cpython-312.pyc
Normal file
Binary file not shown.
115
Web UI Project/app/main.py
Normal file
115
Web UI Project/app/main.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
import uvicorn
|
||||||
|
import bambulabs_api as bl
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
app = FastAPI(title="X1C Connection")
|
||||||
|
|
||||||
|
IP = "10.0.2.10"
|
||||||
|
ACCESS_CODE = "e0f815a7"
|
||||||
|
SERIAL = "00M09A360300272"
|
||||||
|
|
||||||
|
printer = bl.Printer(IP, ACCESS_CODE, SERIAL)
|
||||||
|
|
||||||
|
# Serve /static/*
|
||||||
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
|
||||||
|
# track connection state in-memory for UI (simple flag)
|
||||||
|
connectionState = False
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
def home(request: Request):
|
||||||
|
# Render page and pass current connection state
|
||||||
|
try:
|
||||||
|
connected = bool(connectionState)
|
||||||
|
except NameError:
|
||||||
|
connected = False
|
||||||
|
return templates.TemplateResponse("index.html", {"request": request, "connected": connected})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/connect", response_class=HTMLResponse)
|
||||||
|
def connect(request: Request):
|
||||||
|
global connectionState
|
||||||
|
try:
|
||||||
|
printer.mqtt_start()
|
||||||
|
if(printer.mqtt_start()):
|
||||||
|
connectionState = True
|
||||||
|
except Exception:
|
||||||
|
connectionState = False
|
||||||
|
|
||||||
|
# Return the connect area so HTMX can swap the button immediately
|
||||||
|
return templates.TemplateResponse("_connect_area.html", {"request": request, "connected": connectionState})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/disconnect", response_class=HTMLResponse)
|
||||||
|
def disconnect(request: Request):
|
||||||
|
global connectionState
|
||||||
|
try:
|
||||||
|
printer.mqtt_stop()
|
||||||
|
connectionState = False
|
||||||
|
except Exception:
|
||||||
|
connectionState = False
|
||||||
|
|
||||||
|
# Return the connect area so HTMX can swap the button immediately
|
||||||
|
return templates.TemplateResponse("_connect_area.html", {"request": request, "connected": connectionState})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/status", response_class=HTMLResponse)
|
||||||
|
def status(request: Request):
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Preferred direct calls (if the API exposes them)
|
||||||
|
status = {}
|
||||||
|
status['bed_temp'] = printer.get_bed_temperature()
|
||||||
|
status['nozzle_temp'] = printer.get_nozzle_temperature()
|
||||||
|
status['chamber_temp'] = printer.get_chamber_temperature()
|
||||||
|
status['percentage'] = printer.get_percentage()
|
||||||
|
remaining_time = printer.get_time()
|
||||||
|
current = printer.get_file_name()
|
||||||
|
except Exception as e:
|
||||||
|
return templates.TemplateResponse("_status.html", {"request": request, "status": {"error": str(e)}})
|
||||||
|
|
||||||
|
# Normalize current file: remove /data/Metadata/ prefix or use basename
|
||||||
|
current_file = str(current or '')
|
||||||
|
if current_file.startswith('/data/Metadata/'):
|
||||||
|
current_file = current_file[len('/data/Metadata/'):]
|
||||||
|
else:
|
||||||
|
current_file = os.path.basename(current_file)
|
||||||
|
status['current_file'] = current_file or 'N/A'
|
||||||
|
|
||||||
|
if remaining_time is not None:
|
||||||
|
finish_time = datetime.now() + timedelta(seconds=int(remaining_time))
|
||||||
|
status['finish_time'] = finish_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
status['remaining_time'] = timedelta(seconds=int(remaining_time))
|
||||||
|
|
||||||
|
else:
|
||||||
|
status['finish_time'] = "NA"
|
||||||
|
status['remaining_time'] = "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# stringify values
|
||||||
|
for k in status:
|
||||||
|
try:
|
||||||
|
status[k] = str(status[k])
|
||||||
|
except Exception:
|
||||||
|
status[k] = 'N/A'
|
||||||
|
|
||||||
|
return templates.TemplateResponse("_status.html", {"request": request, "status": status})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Dev convenience; in Docker we launch via CMD
|
||||||
|
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
|
|
||||||
0
Web UI Project/app/static/ass.css
Normal file
0
Web UI Project/app/static/ass.css
Normal file
12
Web UI Project/app/templates/_action_results.html
Normal file
12
Web UI Project/app/templates/_action_results.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="rounded bg-slate-700 border p-4 text-slate-100" data-timeout="5000">
|
||||||
|
<div class="mt-2 text-sm">{{ result }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
const el = document.currentScript && document.currentScript.previousElementSibling;
|
||||||
|
if (!el) return;
|
||||||
|
const t = parseInt(el.dataset.timeout || '3000', 10);
|
||||||
|
setTimeout(()=> { el.parentElement && (el.parentElement.innerHTML = ''); }, t);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
26
Web UI Project/app/templates/_connect_area.html
Normal file
26
Web UI Project/app/templates/_connect_area.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<div id="connect-Button">
|
||||||
|
{% if connected %}
|
||||||
|
<form
|
||||||
|
hx-post="/disconnect"
|
||||||
|
hx-target="#connect-Button"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<button class="px-4 py-2 rounded bg-green-600 text-white hover:bg-red-600">
|
||||||
|
Printer is connected. (Press to disconnect)
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Live status panel, will load once when revealed and then the returned fragment polls itself -->
|
||||||
|
<div id="live-status" hx-post="/status" hx-trigger="revealed" hx-swap="outerHTML" class="mt-4"></div>
|
||||||
|
{% else %}
|
||||||
|
<form
|
||||||
|
hx-post="/connect"
|
||||||
|
hx-target="#connect-Button"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<button class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-500">
|
||||||
|
Connect to Printer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
37
Web UI Project/app/templates/_status.html
Normal file
37
Web UI Project/app/templates/_status.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<div id="status-panel" hx-post="/status" hx-trigger="every 500ms" hx-swap="outerHTML" class="rounded bg-slate-800 border border-slate-700 p-4 text-slate-100">
|
||||||
|
{% if status.error %}
|
||||||
|
<div class="text-red-400">Error: {{ status.error }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<div class="text-sm text-slate-400">Current File</div>
|
||||||
|
<div class="font-medium">{{ status.current_file }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-slate-400">Nozzle Temperature</div>
|
||||||
|
<div class="font-medium">{{ status.nozzle_temp }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-slate-400">Bed Temperature</div>
|
||||||
|
<div class="font-medium">{{ status.bed_temp }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-slate-400">Chamber Temperature</div>
|
||||||
|
<div class="font-medium">{{ status.chamber_temp }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-slate-400">Progress</div>
|
||||||
|
<div class="font-medium">{{ status.percentage }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-slate-400">Remaining Time</div>
|
||||||
|
<div class="font-medium">{{ status.remaining_time }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-slate-400">Time Done</div>
|
||||||
|
<div class="font-medium">{{ status.finish_time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
22
Web UI Project/app/templates/base.html
Normal file
22
Web UI Project/app/templates/base.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{{ request.app.title }}{% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Tailwind via CDN (fine for dev/small apps; switch to compiled later if you want) -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- HTMX: lets you call endpoints and swap HTML fragments, no custom JS needed -->
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-900 text-slate-100">
|
||||||
|
<div class="max-w-3xl mx-auto p-6 bg-slate-800 rounded-lg shadow-md">
|
||||||
|
<h1 class="text-2xl font-bold mb-4 text-white">X1 Carbon Connection</h1>
|
||||||
|
{% block connect %}{% endblock %}
|
||||||
|
<br>
|
||||||
|
{% block status %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
52
Web UI Project/app/templates/index.html
Normal file
52
Web UI Project/app/templates/index.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block connect %}
|
||||||
|
|
||||||
|
<!-- Run action button -->
|
||||||
|
<div id="connect-Button">
|
||||||
|
{% if connected %}
|
||||||
|
<form
|
||||||
|
hx-post="/disconnect"
|
||||||
|
hx-target="#connect-Button"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<button class="px-4 py-2 rounded bg-green-600 text-white hover:bg-red-600">
|
||||||
|
Printer is connected. (Press to disconnect)
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Live status panel, will load once when revealed and then the returned fragment polls itself -->
|
||||||
|
<div id="live-status" hx-post="/status" hx-trigger="load" hx-swap="outerHTML" class="mt-4"></div>
|
||||||
|
{% else %}
|
||||||
|
<form
|
||||||
|
hx-post="/connect"
|
||||||
|
hx-target="#connect-Button"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<button class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-500">
|
||||||
|
Connect to Printer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block status %}
|
||||||
|
<!--
|
||||||
|
<form
|
||||||
|
hx-post="/status"
|
||||||
|
hx-target="#status-result"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
|
<button class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-500">
|
||||||
|
Get Printer Status
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div id="status-result" class="mt-4"></div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
13
Web UI Project/docker-compose.yaml
Normal file
13
Web UI Project/docker-compose.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
image: fastapi-htmx:dev
|
||||||
|
container_name: fastapi-htmx
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
- "8883:8883" # MQTT over TLS
|
||||||
|
- "6000:6000" # MQTT unencrypted
|
||||||
|
- "990:990" # MQTT WebSockets over TLS
|
||||||
|
volumes:
|
||||||
|
- ./app:/app/app # live code edits in dev
|
||||||
|
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
27
Web UI Project/dockerfile
Normal file
27
Web UI Project/dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# --- Base image
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# System deps (add build tools if you need to compile packages)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set workdir
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy deps first for better Docker layer caching
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy app code
|
||||||
|
COPY app /app/app
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Non-root (optional good practice)
|
||||||
|
RUN useradd -m appuser
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Launch server
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
5
Web UI Project/requirements.txt
Normal file
5
Web UI Project/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi==0.115.4
|
||||||
|
uvicorn[standard]==0.32.0
|
||||||
|
jinja2==3.1.4
|
||||||
|
python-multipart==0.0.6
|
||||||
|
bambulabs_api==2.6.5
|
||||||
Reference in New Issue
Block a user