Stared work on the manager side

This commit is contained in:
2025-10-23 13:05:38 +11:00
parent 443e6177ca
commit cf5edd1329
29 changed files with 327 additions and 4 deletions

View File

@@ -0,0 +1,11 @@
__pycache__/
*.pyc
*.pyo
*.pyd
*.env
.env*
.vscode/
.idea/
.git/
dist/
build/

View File

Binary file not shown.

Binary file not shown.

115
Web UI Project/app/main.py Normal file
View 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)

View File

View 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>

View 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>

View 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>

View 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>

View 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 %}

View 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
View 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"]

View 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