添加api调用接口

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-08 22:24:33 +08:00
parent a26f7214f9
commit e542c482d7
19 changed files with 410 additions and 25 deletions

View File

@@ -81,9 +81,11 @@ def create_app():
from .routes.auth_routes import auth_bp
from .routes.admin_routes import admin_bp
from .routes.printer_routes import printer_bp
from .utils.api_handle import api_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(printer_bp)
app.register_blueprint(api_bp)
return app

View File

@@ -237,5 +237,28 @@
"Bridge infill": "Brückefüllung",
"Top solid infill": "Oberste solide Füllung",
"Others": "Andere",
"Are you sure you want to clear the board?": "Sind Sie sicher, dass Sie das Brett leeren möchten?"
"Are you sure you want to clear the board?": "Sind Sie sicher, dass Sie das Brett leeren möchten?",
"d": "t",
"h": "std",
"m": "m",
"s": "s",
"Auto Leveling": "Auto-Nivellierung",
"Account Management": "Kontoverwaltung",
"Change Password": "Passwort ändern",
"Current Password": "Aktuelles Passwort",
"Confirm New Password": "Neues Passwort bestätigen",
"Update Password": "Passwort aktualisieren",
"Active Sessions": "Aktive Sitzungen",
"Device": "Gerät",
"IP Address": "IP-Adresse",
"Last Active": "Zuletzt aktiv",
"Action": "Aktion",
"This Device": "Dieses Gerät",
"Unknown Device": "Unbekanntes Gerät",
"Are you sure you want to terminate this session?": "Sind Sie sicher, dass Sie diese Sitzung beenden möchten?",
"Logout from this device?": "Von diesem Gerät abmelden?",
"No active sessions found.": "Keine aktiven Sitzungen gefunden.",
"Please login to view the webcam stream.": "Bitte melden Sie sich an, um die Live-Kamera zu sehen.",
"Remember Me": "Erinnere dich an mich",
"Merge Guest Data": "Gästendaten zusammenführen"
}

View File

@@ -237,5 +237,28 @@
"Bridge infill": "Bridge infill",
"Top solid infill": "Top solid infill",
"Others": "Others",
"Are you sure you want to clear the board?": "Are you sure you want to clear the board?"
"Are you sure you want to clear the board?": "Are you sure you want to clear the board?",
"d": "d",
"h": "h",
"m": "m",
"s": "s",
"Auto Leveling": "Auto Leveling",
"Account Management": "Account Management",
"Change Password": "Change Password",
"Current Password": "Current Password",
"Confirm New Password": "Confirm New Password",
"Update Password": "Update Password",
"Active Sessions": "Active Sessions",
"Device": "Device",
"IP Address": "IP Address",
"Last Active": "Last Active",
"Action": "Action",
"This Device": "This Device",
"Unknown Device": "Unknown Device",
"Are you sure you want to terminate this session?": "Are you sure you want to terminate this session?",
"Logout from this device?": "Logout from this device?",
"No active sessions found.": "No active sessions found.",
"Please login to view the webcam stream.": "Please login to view the webcam stream.",
"Remember Me": "Remember Me",
"Merge Guest Data": "Merge Guest Data"
}

View File

@@ -237,5 +237,28 @@
"Bridge infill": "桥接填充",
"Top solid infill": "顶部实体填充",
"Others": "其他",
"Are you sure you want to clear the board?": "您确定要清空构建板吗?"
"Are you sure you want to clear the board?": "您确定要清空构建板吗?",
"d": "天",
"h": "时",
"m": "分",
"s": "秒",
"Auto Leveling": "自动调平",
"Account Management": "账号管理",
"Change Password": "修改密码",
"Current Password": "当前密码",
"Confirm New Password": "确认新密码",
"Update Password": "更新密码",
"Active Sessions": "活跃会话",
"Device": "设备",
"IP Address": "IP 地址",
"Last Active": "最后活跃",
"Action": "操作",
"This Device": "当前设备",
"Unknown Device": "未知设备",
"Are you sure you want to terminate this session?": "您确定要终止此会话吗?",
"Logout from this device?": "从此设备注销?",
"No active sessions found.": "未找到活跃的会话。",
"Please login to view the webcam stream.": "请登录以查看实时摄像头。",
"Remember Me": "记住我",
"Merge Guest Data": "合并访客数据"
}

View File

@@ -39,3 +39,9 @@ class SystemConfig(db.Model):
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(50), unique=True, nullable=False)
value = db.Column(db.String(255), nullable=False)
class ApiKey(db.Model):
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False)
name = db.Column(db.String(100), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)

View File

@@ -191,3 +191,34 @@ def delete_user(user_id):
@admin_bp.route('/api_keys')
def api_keys():
from app.models import ApiKey
keys = ApiKey.query.order_by(ApiKey.created_at.desc()).all()
return render_template('admin/api_keys.html', keys=keys)
@admin_bp.route('/api_key/add', methods=['POST'])
def add_api_key():
from app.models import ApiKey
import secrets
name = request.form.get('name')
if not name:
flash("Name is required", "danger")
return redirect(url_for('admin.api_keys'))
key_value = secrets.token_urlsafe(32)
new_key = ApiKey(name=name, key=key_value)
db.session.add(new_key)
db.session.commit()
flash(f"API Key '{name}' created.", "success")
return redirect(url_for('admin.api_keys'))
@admin_bp.route('/api_key/<int:key_id>/delete', methods=['POST'])
def delete_api_key(key_id):
from app.models import ApiKey
key = ApiKey.query.get_or_404(key_id)
db.session.delete(key)
db.session.commit()
flash(f'API Key {key.name} deleted.', 'success')
return redirect(url_for('admin.api_keys'))

View File

@@ -298,7 +298,18 @@ def preview_gcode(file_id):
content = "File not found or not ready."
line_count = 0
time_info = "-"
layer1_time = "-"
filament_used = "-"
if os.path.exists(filepath):
from app.utils.gcode_parser import get_gcode_metadata
metadata = get_gcode_metadata(filepath)
time_info = metadata.get('print_time', '-')
layer1_time = metadata.get('first_layer_time', '-')
filament_used = metadata.get('filament_used', '-')
with open(filepath, 'r') as f:
lines = f.readlines()
line_count = len(lines)
@@ -313,7 +324,8 @@ def preview_gcode(file_id):
configs = {c.key: c.value for c in SystemConfig.query.all()}
offset_x = float(configs.get('offset_x', '0.0'))
offset_y = float(configs.get('offset_y', '0.0'))
return render_template('slice/gcode_preview.html', file=print_file, content=content, line_count=line_count, machine_width=w, machine_depth=h, machine_height=hd, offset_x=offset_x, offset_y=offset_y)
return render_template('slice/gcode_preview.html', file=print_file, content=content, line_count=line_count, machine_width=w, machine_depth=h, machine_height=hd, offset_x=offset_x, offset_y=offset_y,
time_info=time_info, layer1_time=layer1_time, filament_used=filament_used)
@main_bp.route('/delete_file/<int:file_id>', methods=['POST'])
@login_required

View File

@@ -23,9 +23,15 @@ def _enrich_job_data(job_data):
if job_data and job_data.get('job', {}).get('file', {}).get('name'):
from app.models import PrintFile
internal_name = job_data['job']['file']['name']
print_file = PrintFile.query.filter_by(filename=internal_name).first()
if print_file and print_file.original_filename:
job_data['job']['file']['display_name'] = print_file.original_filename
internal_stl_name = str(internal_name)[:-5]+"stl"
user_files = {}
print_file = None
for f in PrintFile.query.filter_by(user_id=current_user.id, status='sliced').all():
user_files[f.filename] = f.original_filename
if internal_stl_name in user_files.keys():
print_file = user_files[str(internal_name)[:-5]+"stl"]
if print_file:
job_data['job']['file']['display_name'] = print_file
else:
job_data['job']['file']['display_name'] = internal_name
return job_data
@@ -40,9 +46,13 @@ def status():
if client:
try:
status_data = client.get_printer_status()
print(status_data)
print(client.get_job_info())
job_data = _enrich_job_data(client.get_job_info())
except Exception as e:
error = str(e)
print(error)
else:
error = "OctoPrint is not configured."
@@ -73,6 +83,7 @@ def get_gcode_dir():
def prepare():
from app.models import PrintFile
import os
from app.utils.gcode_parser import get_gcode_metadata
# Query only the sliced GCode files belonging to the current user
user_files = PrintFile.query.filter_by(user_id=current_user.id, status='sliced').order_by(PrintFile.created_at.desc()).all()
@@ -95,8 +106,16 @@ def prepare():
gcode_path = os.path.join(gcode_dir, gcode_filename)
size = 0
f.meta_print_time = '-'
f.meta_first_layer_time = '-'
f.meta_filament_used = '-'
if os.path.exists(gcode_path):
size = os.path.getsize(gcode_path)
metadata = get_gcode_metadata(gcode_path)
f.meta_print_time = metadata.get('print_time', '-')
f.meta_first_layer_time = metadata.get('first_layer_time', '-')
f.meta_filament_used = metadata.get('filament_used', '-')
# Upload to OctoPrint if not found but exists locally
if client and gcode_filename not in octo_files_dict and size > 0:
@@ -118,7 +137,10 @@ def prepare():
'size': size,
'origin': 'local',
'path': gcode_filename,
'gcodeAnalysis': analysis
'gcodeAnalysis': analysis,
'meta_print_time': f.meta_print_time,
'meta_first_layer_time': f.meta_first_layer_time,
'meta_filament_used': f.meta_filament_used
})
error = None
@@ -220,6 +242,8 @@ def api_command():
try:
if cmd == 'home':
client.home_axes()
elif cmd == 'auto_level':
client.auto_leveling()
elif cmd == 'pause':
client.pause_print()
elif cmd == 'cancel':

View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>API Keys Management</h2>
<div class="card mb-4">
<div class="card-header">Create New API Key</div>
<div class="card-body">
<form action="{{ url_for('admin.add_api_key') }}" method="POST" class="form-inline">
<input type="text" name="name" class="form-control mb-2 mr-sm-2" placeholder="Key Name" required>
<button type="submit" class="btn btn-primary mb-2">Generate Key</button>
</form>
</div>
</div>
<table class="table table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>API Key</th>
<th>Created At</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for key in keys %}
<tr>
<td>{{ key.id }}</td>
<td>{{ key.name }}</td>
<td><code>{{ key.key }}</code></td>
<td>{{ key.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>
<form action="{{ url_for('admin.delete_api_key', key_id=key.id) }}" method="POST" style="display:inline;">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure you want to delete this API Key?');">Delete</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center">No API keys found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -43,11 +43,23 @@
/* 提升 Accordion 折叠栏动画更平滑 */
.collapsing { transition: height 0.35s cubic-bezier(0.25, 0.8, 0.25, 1) !important; }
/* 移动端适配 */
@media (max-width: 767.98px) {
body { padding-top: 126px !important; }
.sidebar { top: 126px; width: 100%; border-bottom: 1px solid #ddd; box-shadow: 0 4px 6px rgba(0,0,0,.1); }
.sidebar-sticky { height: calc(100vh - 126px); }
.mobile-subnav { display: flex !important; }
}
@media (max-width: 454.98px) {
.navbar-brand { display: none !important; }
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top shadow-sm">
<div class="container-fluid position-relative d-flex justify-content-between align-items-center">
<div class="fixed-top">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm">
<div class="container-fluid position-relative d-flex justify-content-between align-items-center">
<a class="navbar-brand fw-bold" href="{{ url_for('main.index') }}"><i class="bi bi-printer me-2"></i>AIO 3D Slicer</a>
<div class="d-none d-md-flex mx-auto" style="position: absolute; left: 50%; transform: translateX(-50%);">
@@ -82,6 +94,18 @@
</div>
</nav>
<!-- 移动端专属二级导航栏 -->
<div class="d-none mobile-subnav align-items-center bg-white border-bottom px-3 py-2 shadow-sm d-md-none" style="justify-content: space-between;">
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-expanded="false" aria-controls="sidebarMenu">
<i class="bi bi-list fs-4"></i>
</button>
<div class="btn-group border border-secondary shadow-sm rounded-pill p-1 bg-dark" role="group" style="background-color: #1a1e21 !important;">
<a href="{{ url_for('main.files') }}" class="btn btn-sm rounded-pill {% if not request.blueprint == 'printer' %}btn-primary active fw-bold text-white px-3{% else %}btn-transparent text-secondary border-0 px-2{% endif %}" style="transition: all 0.2s;"><i class="bi bi-layers me-1"></i>{{ _('Slicer') }}</a>
<a href="{{ url_for('printer.status') }}" class="btn btn-sm rounded-pill {% if request.blueprint == 'printer' %}btn-primary active fw-bold text-white px-3{% else %}btn-transparent text-secondary border-0 px-2{% endif %}" style="transition: all 0.2s;"><i class="bi bi-printer-fill me-1"></i>{{ _('Printer') }}</a>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-white sidebar collapse border-end">
@@ -178,6 +202,11 @@
<i class="bi bi-people me-2"></i>{{ _('User Management') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark {% if request.endpoint == 'admin.api_keys' %}active text-white shadow-sm{% endif %}" href="{{ url_for('admin.api_keys') }}">
<i class="bi bi-key me-2"></i>API Keys
</a>
</li>
</ul>
{% endif %}
{% endif %}

View File

@@ -37,11 +37,23 @@
<i class="bi bi-dpad me-1"></i>{{ _('Basic Control') }}
</div>
<div class="card-body text-center d-flex flex-column justify-content-center align-items-center">
<!-- Home button -->
<button class="btn btn-lg btn-primary rounded-circle mb-4 shadow" style="width: 80px; height: 80px;" onclick="sendCommand('home')" title="{{ _('Home All Axes') }}">
<i class="bi bi-house-door fs-2"></i>
</button>
<div class="text-muted mb-4">{{ _('Home All Axes') }} (G28)</div>
<!-- Motion Controls -->
<div class="d-flex gap-4 justify-content-center mb-4 w-100">
<!-- Home button -->
<div>
<button class="btn btn-lg btn-primary rounded-circle shadow mb-2" style="width: 80px; height: 80px;" onclick="sendCommand('home')" title="{{ _('Home All Axes') }}">
<i class="bi bi-house-door fs-2"></i>
</button>
<div class="text-muted small">{{ _('Home All Axes') }}<br>(G28)</div>
</div>
<!-- Auto Level button -->
<div>
<button class="btn btn-lg btn-info rounded-circle shadow mb-2 text-white" style="width: 80px; height: 80px;" onclick="sendCommand('auto_level')" title="{{ _('Auto Leveling') }}">
<i class="bi bi-grid-3x3 fs-2"></i>
</button>
<div class="text-muted small">{{ _('Auto Leveling') }}<br>(G29)</div>
</div>
</div>
<!-- Quick macros -->
<div class="d-flex gap-3 justify-content-center flex-wrap w-100">
@@ -59,7 +71,7 @@
<script>
function sendCommand(cmdName) {
if (cmdName === 'cancel' || cmdName === 'home') {
if (cmdName === 'cancel' || cmdName === 'home' || cmdName === 'auto_level') {
window.customConfirm("{{ _('Are you sure you want to perform this action?') }}", () => doSendCommand(cmdName));
} else {
doSendCommand(cmdName);

View File

@@ -41,7 +41,7 @@
<div id="file-{{ f.id }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3 transition-style">
<div class="me-auto text-truncate" style="max-width: 80%;">
<h6 class="mb-1"><i class="bi bi-file-earmark-code text-primary me-2"></i>{{ f.name }}</h6>
<small class="text-muted d-block">{{ _('Size:') }} {{ f.size }} bytes, {{ _('Time:') }} {{ f.gcodeAnalysis.estimatedPrintTime if f.gcodeAnalysis else 'Unknown' }}s</small>
<small class="text-muted d-block pb-1">{{ _('Size:') }} {{ f.size | filesizeformat }}, {% if f.meta_print_time and f.meta_print_time != '-' %}{{ _('Estimated Time:') }} {{ f.meta_print_time }}{% else %}{{ _('Time:') }} {{ f.gcodeAnalysis.estimatedPrintTime if f.gcodeAnalysis else 'Unknown' }}s{% endif %}</small>
</div>
<div>
<a href="{{ url_for('main.preview_gcode', file_id=f.id) }}" class="btn btn-sm btn-outline-info rounded-pill px-3 shadow-sm me-2"><i class="bi bi-eye me-1"></i>{{ _('Preview') }}</a>

View File

@@ -67,8 +67,8 @@
</div>
<div class="d-flex justify-content-between text-muted small mt-2">
<span><strong>{{ _('Print Time:') }}</strong> <span id="job-print-time">{{ job.get('progress', {}).get('printTime', 0) if job else 0 }}</span>s</span>
<span><strong>{{ _('Time Left:') }}</strong> <span id="job-time-left">{{ job.get('progress', {}).get('printTimeLeft', 0) if job else 0 }}</span>s</span>
<span><strong>{{ _('Print Time:') }}</strong> <span id="job-print-time"></span></span>
<span><strong>{{ _('Time Left:') }}</strong> <span id="job-time-left"></span></span>
</div>
<div class="mt-4 gap-2 d-flex">
@@ -81,6 +81,22 @@
{% endif %}
<script>
function formatTime(seconds) {
if (!seconds && seconds !== 0) return '0{{ _("s") }}';
seconds = Math.round(seconds);
let d = Math.floor(seconds / 86400);
let h = Math.floor((seconds % 86400) / 3600);
let m = Math.floor((seconds % 3600) / 60);
let s = Math.floor(seconds % 60);
let res = [];
if (d > 0) res.push(d + '{{ _("d") }}');
if (h > 0 || d > 0) res.push(h + '{{ _("h") }}');
if (m > 0 || h > 0 || d > 0) res.push(m + '{{ _("m") }}');
if (s > 0 || res.length === 0) res.push(s + '{{ _("s") }}');
return res.join(' ');
}
function updateStatus() {
fetch('{{ url_for("printer.api_status_data") }}')
.then(res => res.json())
@@ -105,8 +121,8 @@ function updateStatus() {
document.getElementById('job-progress-bar').style.width = progress + '%';
document.getElementById('job-progress-bar').setAttribute('aria-valuenow', progress);
document.getElementById('job-progress-text').innerText = progress.toFixed(1) + '%';
document.getElementById('job-print-time').innerText = data.job.progress ? (data.job.progress.printTime || 0) : 0;
document.getElementById('job-time-left').innerText = data.job.progress ? (data.job.progress.printTimeLeft || 0) : 0;
document.getElementById('job-print-time').innerText = formatTime(data.job.progress ? data.job.progress.printTime : 0);
document.getElementById('job-time-left').innerText = formatTime(data.job.progress ? data.job.progress.printTimeLeft : 0);
} else {
jobCard.style.display = 'none';
}
@@ -115,6 +131,8 @@ function updateStatus() {
.catch(err => console.error("Error fetching status:", err));
}
{% if not error %}
// Run once immediately to populate initial data consistently
updateStatus();
setInterval(updateStatus, 1000);
{% endif %}

View File

@@ -58,7 +58,22 @@
<div class="d-flex align-items-center">
<i class="bi bi-display me-2 text-secondary fs-5"></i>
<div>
<div class="text-truncate" style="max-width: 150px" title="{{ s.user_agent }}">{{ s.user_agent.split(' ')[0] if s.user_agent else _('Unknown Device') }}</div>
{% set device_name = _('Unknown Device') %}
{% if s.user_agent %}
{% set ua = s.user_agent|lower %}
{% if 'windows' in ua %}
{% set device_name = 'Windows' %}
{% elif 'android' in ua %}
{% set device_name = 'Android' %}
{% elif 'iphone' in ua or 'ipad' in ua %}
{% set device_name = 'iOS' %}
{% elif 'mac' in ua or 'darwin' in ua %}
{% set device_name = 'macOS' %}
{% elif 'linux' in ua %}
{% set device_name = 'Linux' %}
{% endif %}
{% endif %}
<div class="text-truncate" style="max-width: 150px; cursor: help;" title="{{ s.user_agent or _('Unknown Device') }}">{{ device_name }}</div>
{% if s.session_token == current_token %}
<span class="badge bg-primary mt-1">{{ _('This Device') }}</span>
{% endif %}

View File

@@ -1,9 +1,16 @@
{% extends 'base.html' %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="bi bi-eye text-primary me-2"></i>{{ _('GCode Preview') }}: {{ file.original_filename }}</h1>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-2 border-bottom">
<div>
<h1 class="h2 mb-1"><i class="bi bi-eye text-primary me-2"></i>{{ _('GCode Preview') }}: {{ file.original_filename }}</h1>
<div class="text-muted small">
<span class="me-3"><i class="bi bi-clock-history me-1"></i>{{ _('Estimated Time:') }} <span class="fw-bold">{{ time_info }}</span></span>
<span class="me-3"><i class="bi bi-layers me-1"></i>{{ _('First Layer Time:') }} <span class="fw-bold">{{ layer1_time }}</span></span>
<span><i class="bi bi-rulers me-1"></i>{{ _('Filament Used [mm]:') }} <span class="fw-bold">{{ filament_used }}</span></span>
</div>
</div>
<div class="mt-2 mt-md-0">
<a href="{{ url_for('printer.prepare') }}#file-{{ file.id }}" class="btn btn-warning btn-sm rounded shadow-sm fw-bold"><i class="bi bi-printer"></i> {{ _('Go to Print') }}</a>
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary btn-sm rounded shadow-sm ms-2"><i class="bi bi-download"></i> {{ _('Download GCode') }}</a>
<a href="{{ url_for('main.files') }}" class="btn btn-outline-secondary btn-sm rounded ms-2 shadow-sm"><i class="bi bi-arrow-left"></i> {{ _('Back') }}</a>

77
app/utils/api_handle.py Normal file
View File

@@ -0,0 +1,77 @@
import functools
from flask import Blueprint, request, jsonify
from app.models import ApiKey
from app.utils.octoprint_client import OctoPrintClient
from app.models import SystemConfig
api_bp = Blueprint('api_handle', __name__, url_prefix='/api/v1')
def get_octo_client():
url = SystemConfig.query.filter_by(key='octoprint_url').first()
apikey = SystemConfig.query.filter_by(key='octoprint_apikey').first()
if url and url.value and apikey and apikey.value:
return OctoPrintClient(url.value, apikey.value)
return None
def require_api_key(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
api_key_header = request.headers.get('X-Api-Key')
if not api_key_header:
return jsonify({'error': 'Missing API Key in headers (X-Api-Key)'}), 401
key_record = ApiKey.query.filter_by(key=api_key_header).first()
if not key_record:
return jsonify({'error': 'Invalid API Key'}), 401
return f(*args, **kwargs)
return decorated
@api_bp.route('/status', methods=['GET'])
@require_api_key
def get_status():
client = get_octo_client()
if not client:
return jsonify({'error': 'Printer not configured'}), 503
try:
status_data = client.get_printer_status()
job_data = client.get_job_info()
return jsonify({'status': status_data, 'job': job_data})
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/octoprint_client', methods=['POST'])
@require_api_key
def invoke_octoprint_client():
"""
Expects JSON payload like:
{
"method": "pause_print",
"kwargs": {"action": "pause"}
}
"""
client = get_octo_client()
if not client:
return jsonify({'error': 'Printer not configured'}), 503
data = request.get_json()
if not data or 'method' not in data:
return jsonify({'error': 'Missing method in JSON payload'}), 400
method_name = data['method']
kwargs = data.get('kwargs', {})
args = data.get('args', [])
if not hasattr(client, method_name):
return jsonify({'error': f'Method {method_name} not found on OctoPrintClient'}), 400
func = getattr(client, method_name)
if not callable(func) or method_name.startswith('_'):
return jsonify({'error': f'Method {method_name} is not allowed'}), 403
try:
result = func(*args, **kwargs)
return jsonify({'success': True, 'result': result})
except Exception as e:
return jsonify({'error': str(e)}), 500

32
app/utils/gcode_parser.py Normal file
View File

@@ -0,0 +1,32 @@
import os
def get_gcode_metadata(filepath):
metadata = {
'print_time': '-',
'first_layer_time': '-',
'filament_used': '-'
}
if not os.path.exists(filepath):
return metadata
try:
# Read the last few KB to find estimated time and filament used
with open(filepath, 'rb') as f:
f.seek(0, 2)
file_size = f.tell()
chunk_size = min(65536, file_size) # read last 64KB
f.seek(file_size - chunk_size)
chunk = f.read().decode('utf-8', errors='ignore')
lines = chunk.splitlines()
for line in reversed(lines):
if line.startswith('; estimated printing time (normal mode) ='):
metadata['print_time'] = line.split('=')[1].strip()
elif line.startswith('; estimated first layer printing time (normal mode) ='):
metadata['first_layer_time'] = line.split('=')[1].strip()
elif line.startswith('; filament used [mm] ='):
metadata['filament_used'] = line.split('=')[1].strip()
except Exception:
pass
return metadata

View File

@@ -141,6 +141,9 @@ class OctoPrintClient:
"""Convenience method to home the printer axes."""
return self._request("POST", "/api/printer/printhead", json={"command": "home", "axes": axes})
def auto_leveling(self):
return self.send_gcode("G29")
# -------------------------------------------------------------------------
# Webcam / Video
# -------------------------------------------------------------------------

View File

@@ -48,7 +48,7 @@ single_extruder_multi_material = 1
start_filament_gcode = ;Do nothing
wipe = 1
before_layer_gcode = ;BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n
start_gcode = M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nM140 S{first_layer_bed_temperature[0]} ;Set final bed temp \nG28 ;Home \n \nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nM104 S{first_layer_temperature[0]} ;Set final nozzle temp \nG1 X-13 Y20 Z3 F5000.0 ;Move to out position \nM190 S{first_layer_bed_temperature[0]} ;Wait for bed temp to stabilize \nM109 S{temperature[0]} ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \n G1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800
start_gcode = M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nM140 S{first_layer_bed_temperature[0]} ;Set final bed temp \nG28 ;Home \n \nG92 E0 ;Reset Extruder \nG1 Z20.0 F3000 ;Move Z Axis up \nM104 S{first_layer_temperature[0]} ;Set final nozzle temp \nG1 X-13 Y20 Z3 F5000.0 ;Move to out position \nM190 S{first_layer_bed_temperature[0]} ;Wait for bed temp to stabilize \nM109 S{first_layer_temperature[0]} ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \nG1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800
end_gcode = G91 ;Relative positionning \nG1 E-2 F2700 ;Retract a bit \nG1 E-2 Z0.2 F2400 ;Retract and raise Z \nG1 X5 Y5 F3000 ;Wipe out \nG1 Z10 ;Raise Z more \nG90 ;Absolute positionning \n \nG1 X0 Y220 ;Present print \nM106 S0 ;Turn-off fan \nM104 S0 ;Turn-off hotend \nM140 S0 ;Turn-off bed \n \nM84 X Y E ;Disable all steppers but Z
machine_max_acceleration_travel = 2000,2000
pause_print_gcode = M25