@@ -57,6 +57,7 @@ def create_app():
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../instance/aio_3d.db'
|
||||
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {'connect_args': {'timeout': 15}}
|
||||
app.config['UPLOAD_FOLDER'] = os.path.abspath(os.path.join(app.root_path, '..', 'uploads'))
|
||||
app.config['PRINT_CONFIG_FOLDER'] = os.path.abspath(os.path.join(app.root_path, '..', 'print_config'))
|
||||
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ def settings():
|
||||
default_support_pattern = request.form.get('default_support_pattern', 'tree')
|
||||
default_quality = request.form.get('default_quality', 'base_global_standard.inst.cfg')
|
||||
default_material = request.form.get('default_material', '')
|
||||
default_printer = request.form.get('default_printer', '')
|
||||
gcode_upload_folder = request.form.get('gcode_upload_folder', '').strip()
|
||||
slicer_engine = request.form.get('slicer_engine', 'cura')
|
||||
build_plate_model_path = request.form.get('build_plate_model_path', '').strip()
|
||||
@@ -61,6 +62,7 @@ def settings():
|
||||
('default_support_pattern', default_support_pattern),
|
||||
('default_quality', default_quality),
|
||||
('default_material', default_material),
|
||||
('default_printer', default_printer),
|
||||
('gcode_upload_folder', gcode_upload_folder),
|
||||
('slicer_engine', slicer_engine),
|
||||
('build_plate_model_path', build_plate_model_path),
|
||||
@@ -187,15 +189,5 @@ def delete_user(user_id):
|
||||
flash(f'User {user.username} and all their files have been deleted.', 'success')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
def get_bed_dimensions():
|
||||
try:
|
||||
path = os.path.join(current_app.root_path, '..', 'print_config', 'printers', 'creality_ender3v3se.def.json')
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
w = data['overrides']['machine_width']['default_value']
|
||||
h = data['overrides']['machine_depth']['default_value']
|
||||
hd = data['overrides']['machine_height']['default_value']
|
||||
return w, h, hd
|
||||
except:
|
||||
return 200, 200, 200
|
||||
|
||||
|
||||
|
||||
@@ -14,19 +14,19 @@ from app import i18n_dict
|
||||
# import trimesh.repair
|
||||
from app.utils.stl_simplifier import simplify_stl
|
||||
from app.routes.admin_routes import get_gcode_dir
|
||||
|
||||
from app.utils.slice_engines import get_slicer_engine
|
||||
from app.models import UserSession
|
||||
from flask_login import logout_user
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
@main_bp.before_app_request
|
||||
def check_user_session():
|
||||
if current_user.is_authenticated and not current_user.is_guest:
|
||||
from app.models import UserSession
|
||||
session_token = session.get('user_session_token')
|
||||
if session_token:
|
||||
user_session = UserSession.query.filter_by(session_token=session_token).first()
|
||||
if not user_session or not user_session.is_active:
|
||||
from flask_login import logout_user
|
||||
logout_user()
|
||||
session.pop('user_session_token', None)
|
||||
flash('Your session has been terminated.', 'warning')
|
||||
@@ -305,8 +305,11 @@ def preview_gcode(file_id):
|
||||
content = "".join(lines[:500]) # Preview first 500 lines
|
||||
if line_count > 500:
|
||||
content += f"\n... \n[Preview truncated. Total lines: {line_count}. Please download to view full file.]"
|
||||
|
||||
w, h, hd = get_bed_dimensions()
|
||||
|
||||
engine_name = SystemConfig.query.filter_by(key='slicer_engine').first()
|
||||
if engine_name:
|
||||
engine = get_slicer_engine(str(engine_name.value), current_app.config['PRINT_CONFIG_FOLDER'])
|
||||
w, h, hd = engine.get_bed_dimensions()
|
||||
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'))
|
||||
@@ -344,25 +347,17 @@ def delete_file(file_id):
|
||||
|
||||
# --- Auth Routes ---
|
||||
|
||||
def get_bed_dimensions():
|
||||
try:
|
||||
path = os.path.join(current_app.root_path, '..', 'print_config', 'printers', 'creality_ender3v3se.def.json')
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
w = data['overrides']['machine_width']['default_value']
|
||||
h = data['overrides']['machine_depth']['default_value']
|
||||
hd = data['overrides']['machine_height']['default_value']
|
||||
return w, h, hd
|
||||
except:
|
||||
return 200, 200, 200
|
||||
|
||||
@main_bp.route('/plater')
|
||||
@login_required
|
||||
def plater():
|
||||
quota_mb, current_size = get_quota_info(current_user, 'gcode')
|
||||
quota_exceeded = (quota_mb > 0 and current_size >= quota_mb * 1024 * 1024)
|
||||
|
||||
w, h, hd = get_bed_dimensions()
|
||||
engine_name = SystemConfig.query.filter_by(key='slicer_engine').first()
|
||||
if engine_name:
|
||||
engine = get_slicer_engine(str(engine_name.value), current_app.config['PRINT_CONFIG_FOLDER'])
|
||||
w, h, hd = engine.get_bed_dimensions()
|
||||
print(f"Bed dimensions: {w}x{h}x{hd}")
|
||||
|
||||
|
||||
configs = {c.key: c.value for c in SystemConfig.query.all()}
|
||||
@@ -574,12 +569,12 @@ def build_plate_model():
|
||||
@main_bp.route('/api/engine_options/<engine_name>')
|
||||
@login_required
|
||||
def engine_options(engine_name):
|
||||
from app.utils.slice_engines import get_slicer_engine
|
||||
engine = get_slicer_engine(engine_name)
|
||||
presets = engine.get_quality_presets(current_app)
|
||||
patterns = engine.get_support_patterns(current_app)
|
||||
materials = engine.get_materials(current_app) if hasattr(engine, 'get_materials') else []
|
||||
return jsonify({'presets': presets, 'support_patterns': patterns, 'materials': materials})
|
||||
engine = get_slicer_engine(engine_name, current_app.config['PRINT_CONFIG_FOLDER'])
|
||||
presets = engine.get_quality_presets()
|
||||
patterns = engine.get_support_patterns()
|
||||
materials = engine.get_materials() if hasattr(engine, 'get_materials') else []
|
||||
printers = engine.get_all_printers() if hasattr(engine, 'get_all_printers') else []
|
||||
return jsonify({'presets': presets, 'support_patterns': patterns, 'materials': materials, 'printers': printers})
|
||||
|
||||
@main_bp.route('/account', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
|
||||
@@ -19,6 +19,17 @@ def get_octo_client():
|
||||
return OctoPrintClient(url.value, apikey.value)
|
||||
return None
|
||||
|
||||
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
|
||||
else:
|
||||
job_data['job']['file']['display_name'] = internal_name
|
||||
return job_data
|
||||
|
||||
@printer_bp.route('/status')
|
||||
@login_required
|
||||
def status():
|
||||
@@ -29,7 +40,7 @@ def status():
|
||||
if client:
|
||||
try:
|
||||
status_data = client.get_printer_status()
|
||||
job_data = client.get_job_info()
|
||||
job_data = _enrich_job_data(client.get_job_info())
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
else:
|
||||
@@ -37,6 +48,20 @@ def status():
|
||||
|
||||
return render_template('printer/status.html', status=status_data, job=job_data, error=error)
|
||||
|
||||
@printer_bp.route('/api/status_data')
|
||||
@login_required
|
||||
def api_status_data():
|
||||
client = get_octo_client()
|
||||
if client:
|
||||
try:
|
||||
status_data = client.get_printer_status()
|
||||
job_data = _enrich_job_data(client.get_job_info())
|
||||
return jsonify({'success': True, 'status': status_data, 'job': job_data})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
return jsonify({'success': False, 'error': 'OctoPrint is not configured.'})
|
||||
|
||||
|
||||
def get_gcode_dir():
|
||||
conf = SystemConfig.query.filter_by(key='gcode_upload_folder').first()
|
||||
if conf and conf.value and os.path.exists(conf.value):
|
||||
@@ -102,13 +127,45 @@ def prepare():
|
||||
|
||||
return render_template('printer/prepare.html', files=files, error=error)
|
||||
|
||||
|
||||
def check_printer_control_permission(client):
|
||||
from flask_login import current_user
|
||||
if current_user.is_admin:
|
||||
return True, None
|
||||
|
||||
try:
|
||||
status_data = client.get_printer_status()
|
||||
state = status_data.get('state', {}).get('text', '')
|
||||
active_states = ['Printing', 'Paused', 'Pausing', 'Resuming', 'Cancelling']
|
||||
if state not in active_states:
|
||||
return True, None
|
||||
|
||||
job_info = client.get_job_info()
|
||||
internal_name = job_info.get('job', {}).get('file', {}).get('name')
|
||||
if not internal_name:
|
||||
return False, "现在有任务正在运行,非管理员无法进行控制。"
|
||||
|
||||
from app.models import PrintFile
|
||||
pf = PrintFile.query.filter_by(filename=internal_name).first()
|
||||
if pf and pf.user_id == current_user.id:
|
||||
return True, None
|
||||
else:
|
||||
return False, "现在有任务正在运行,您无权进行此操作。只有管理员或任务发起者可以进行控制。"
|
||||
except Exception:
|
||||
pass
|
||||
return True, None
|
||||
|
||||
@printer_bp.route('/api/print_file', methods=['POST'])
|
||||
|
||||
@login_required
|
||||
def api_print_file():
|
||||
path = request.json.get('path')
|
||||
location = request.json.get('origin', 'local')
|
||||
client = get_octo_client()
|
||||
if client and path:
|
||||
allowed, err_msg = check_printer_control_permission(client)
|
||||
if not allowed:
|
||||
return jsonify({"success": False, "error": err_msg})
|
||||
try:
|
||||
client.select_file(location, path, print_after_select=True)
|
||||
return jsonify({"success": True})
|
||||
@@ -117,14 +174,34 @@ def api_print_file():
|
||||
return jsonify({"success": False, "error": "Not configured or missing path"})
|
||||
|
||||
@printer_bp.route('/control')
|
||||
@login_required
|
||||
def control():
|
||||
client = get_octo_client()
|
||||
webcam_url = None
|
||||
error = None
|
||||
if client:
|
||||
try:
|
||||
webcam_url = client.get_webcam_stream_url()
|
||||
raw_url = client.get_webcam_stream_url()
|
||||
# If it's an absolute url pointing to the base url, strip it or proxy it via octo_proxy
|
||||
from urllib.parse import urlparse, urlencode
|
||||
parsed_raw = urlparse(raw_url)
|
||||
base_config = SystemConfig.query.filter_by(key='octoprint_url').first()
|
||||
if base_config and base_config.value:
|
||||
base_url = base_config.value.rstrip('/')
|
||||
parsed_base = urlparse(base_url)
|
||||
# If they share the same host, replace with proxy
|
||||
# Usually OctoPrint webcam streams are on the same host or relative
|
||||
path = parsed_raw.path
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
query = parsed_raw.query
|
||||
|
||||
# build proxy url
|
||||
if query:
|
||||
webcam_url = url_for('printer.octo_proxy', path=path) + '?' + query
|
||||
else:
|
||||
webcam_url = url_for('printer.octo_proxy', path=path)
|
||||
else:
|
||||
webcam_url = raw_url
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
else:
|
||||
@@ -137,6 +214,9 @@ def api_command():
|
||||
cmd = request.json.get('command')
|
||||
client = get_octo_client()
|
||||
if client and cmd:
|
||||
allowed, err_msg = check_printer_control_permission(client)
|
||||
if not allowed:
|
||||
return jsonify({"success": False, "error": err_msg})
|
||||
try:
|
||||
if cmd == 'home':
|
||||
client.home_axes()
|
||||
@@ -246,7 +326,7 @@ def octo_embed():
|
||||
@printer_bp.route('/proxy/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
|
||||
@login_required
|
||||
def octo_proxy(path):
|
||||
if not current_user.is_admin:
|
||||
if current_user.is_guest:
|
||||
return "Unauthorized", 403
|
||||
|
||||
url_config = SystemConfig.query.filter_by(key='octoprint_url').first()
|
||||
|
||||
@@ -41,6 +41,14 @@
|
||||
<div class="form-text"><i class="bi bi-info-circle me-1"></i>{{ _('Absolute path to a custom build plate STL model to show in the plater. Leave empty to use none.') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="default_printer" class="form-label">{{ _('Default Printer Profile') }}</label>
|
||||
<select class="form-select" name="default_printer" id="default_printer" data-selected="{{ configs.get('default_printer', '') }}">
|
||||
<!-- Loaded via JS -->
|
||||
</select>
|
||||
<div class="form-text"><i class="bi bi-info-circle me-1"></i>{{ _('Main configuration for the printer dimensions, limits and base profiles.') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="default_infill" class="form-label">{{ _('Default Infill Density (%)') }}</label>
|
||||
<input type="number" class="form-control" name="default_infill" id="default_infill" value="{{ configs.get('default_infill', '20') }}" min="0" max="100">
|
||||
@@ -160,11 +168,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const qualitySelect = document.getElementById('default_quality');
|
||||
const materialSelect = document.getElementById('default_material');
|
||||
const patternSelect = document.getElementById('default_support_pattern');
|
||||
const printerSelect = document.getElementById('default_printer');
|
||||
|
||||
function updateOptions(engine) {
|
||||
fetch(`/api/engine_options/${engine}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const selPtr = printerSelect.getAttribute('data-selected');
|
||||
printerSelect.innerHTML = '';
|
||||
if(data.printers) {
|
||||
data.printers.forEach(p => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id; opt.textContent = p.name;
|
||||
printerSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
if(selPtr) printerSelect.value = selPtr;
|
||||
const selQ = qualitySelect.getAttribute('data-selected');
|
||||
qualitySelect.innerHTML = '';
|
||||
data.presets.forEach(p => {
|
||||
|
||||
@@ -107,13 +107,13 @@
|
||||
<i class="bi bi-arrows-move me-2"></i>{{ _('Control') }}
|
||||
</a>
|
||||
</li>
|
||||
{% if current_user.is_authenticated and not current_user.is_guest %}
|
||||
<!-- {% if current_user.is_authenticated and not current_user.is_guest %}
|
||||
<li class="nav-item mb-1">
|
||||
<a class="nav-link text-dark {% if request.endpoint == 'main.account' %}active text-white shadow-sm{% endif %}" href="{{ url_for('main.account') }}">
|
||||
<i class="bi bi-person-badge me-2"></i>{{ _('Account Management') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %} -->
|
||||
</ul>
|
||||
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
|
||||
@@ -17,8 +17,15 @@
|
||||
<div class="card-header bg-dark text-light fw-bold rounded-top">
|
||||
<i class="bi bi-camera-video me-1"></i>{{ _('Live Webcam') }}
|
||||
</div>
|
||||
<div class="card-body p-0 ratio ratio-16x9">
|
||||
<div class="card-body p-0 ratio ratio-16x9 bg-secondary bg-opacity-25 d-flex align-items-center justify-content-center">
|
||||
{% if current_user.is_guest %}
|
||||
<div class="text-center text-dark">
|
||||
<i class="bi bi-lock-fill display-4 d-block mb-3"></i>
|
||||
<h5 class="mb-0">{{ _('Please login to view the webcam stream.') }}</h5>
|
||||
</div>
|
||||
{% else %}
|
||||
<img src="{{ webcam_url }}" alt="{{ _('Loading webcam stream...') }}" class="w-100 h-100 object-fit-cover">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<i class="bi bi-info-circle me-1"></i>{{ _('Current State') }}
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<h3 class="display-6 mt-3 text-primary">{{ status.get('state', {}).get('text', 'Unknown') }}</h3>
|
||||
<h3 class="display-6 mt-3 text-primary" id="printer-state-text">{{ status.get('state', {}).get('text', 'Unknown') if status else 'Unknown' }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,43 +33,42 @@
|
||||
<i class="bi bi-thermometer-half me-1"></i>{{ _('Temperatures') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% set temps = status.get('temperature', {}) %}
|
||||
{% set temps = status.get('temperature', {}) if status else {} %}
|
||||
|
||||
<h5 class="mb-1"><i class="bi bi-fire text-danger me-2"></i>{{ _('Tool/Nozzle') }}</h5>
|
||||
<h4 class="ms-4 mb-4">
|
||||
{{ temps.get('tool0', {}).get('actual', 0) }} °C
|
||||
<small class="text-muted fs-6">/ {{ temps.get('tool0', {}).get('target', 0) }} °C</small>
|
||||
<span id="tool-actual">{{ temps.get('tool0', {}).get('actual', 0) }}</span> °C
|
||||
<small class="text-muted fs-6">/ <span id="tool-target">{{ temps.get('tool0', {}).get('target', 0) }}</span> °C</small>
|
||||
</h4>
|
||||
|
||||
<h5 class="mb-1"><i class="bi bi-square-fill text-warning me-2"></i>{{ _('Bed') }}</h5>
|
||||
<h4 class="ms-4">
|
||||
{{ temps.get('bed', {}).get('actual', 0) }} °C
|
||||
<small class="text-muted fs-6">/ {{ temps.get('bed', {}).get('target', 0) }} °C</small>
|
||||
<span id="bed-actual">{{ temps.get('bed', {}).get('actual', 0) }}</span> °C
|
||||
<small class="text-muted fs-6">/ <span id="bed-target">{{ temps.get('bed', {}).get('target', 0) }}</span> °C</small>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if job and job.get('job', {}).get('file', {}).get('name') %}
|
||||
<div class="card shadow-sm mt-4 border-success">
|
||||
<div class="card shadow-sm mt-4 border-success" id="active-job-card" style="display: {% if job and job.get('job', {}).get('file', {}).get('name') %}block{% else %}none{% endif %};">
|
||||
<div class="card-header bg-success text-white fw-bold">
|
||||
<i class="bi bi-play-circle me-1"></i>{{ _('Active Print Job') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>{{ job.get('job', {}).get('file', {}).get('name') }}</h5>
|
||||
<h5 id="job-file-name">{{ job.get('job', {}).get('file', {}).get('display_name') or job.get('job', {}).get('file', {}).get('name') if job else '' }}</h5>
|
||||
|
||||
{% set progress = job.get('progress', {}).get('completion', 0) %}
|
||||
{% set progress = job.get('progress', {}).get('completion', 0) if job else 0 %}
|
||||
{% if progress == None %}{% set progress = 0 %}{% endif %}
|
||||
<div class="progress mt-3 mb-2" style="height: 25px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: {{ progress }}%;" aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100">
|
||||
{{ "%.1f"|format(progress) }}%
|
||||
<div id="job-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: {{ progress }}%;" aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100">
|
||||
<span id="job-progress-text">{{ "%.1f"|format(progress) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between text-muted small mt-2">
|
||||
<span><strong>{{ _('Print Time:') }}</strong> {{ job.get('progress', {}).get('printTime', 0) }}s</span>
|
||||
<span><strong>{{ _('Time Left:') }}</strong> {{ job.get('progress', {}).get('printTimeLeft', 0) }}s</span>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 gap-2 d-flex">
|
||||
@@ -78,11 +77,47 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function updateStatus() {
|
||||
fetch('{{ url_for("printer.api_status_data") }}')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
if(data.status && data.status.state) {
|
||||
document.getElementById('printer-state-text').innerText = data.status.state.text || 'Unknown';
|
||||
}
|
||||
if(data.status && data.status.temperature) {
|
||||
const tool = data.status.temperature.tool0 || {};
|
||||
const bed = data.status.temperature.bed || {};
|
||||
document.getElementById('tool-actual').innerText = tool.actual !== undefined ? tool.actual : 0;
|
||||
document.getElementById('tool-target').innerText = tool.target !== undefined ? tool.target : 0;
|
||||
document.getElementById('bed-actual').innerText = bed.actual !== undefined ? bed.actual : 0;
|
||||
document.getElementById('bed-target').innerText = bed.target !== undefined ? bed.target : 0;
|
||||
}
|
||||
const jobCard = document.getElementById('active-job-card');
|
||||
if(data.job && data.job.job && data.job.job.file && data.job.job.file.name) {
|
||||
jobCard.style.display = 'block';
|
||||
document.getElementById('job-file-name').innerText = data.job.job.file.display_name || data.job.job.file.name;
|
||||
let progress = data.job.progress && data.job.progress.completion ? data.job.progress.completion : 0;
|
||||
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;
|
||||
} else {
|
||||
jobCard.style.display = 'none';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => console.error("Error fetching status:", err));
|
||||
}
|
||||
{% if not error %}
|
||||
setInterval(updateStatus, 1000);
|
||||
{% endif %}
|
||||
|
||||
function sendCmd(cmd) {
|
||||
if(cmd === 'cancel') {
|
||||
window.customConfirm("{{ _('Are you sure you want to cancel the print?') }}", () => doSendCmd(cmd));
|
||||
@@ -99,12 +134,11 @@ function doSendCmd(cmd) {
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
window.location.reload();
|
||||
updateStatus();
|
||||
} else {
|
||||
window.customAlert("Error: " + data.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
setTimeout(() => { if (!window.pauseRefresh) window.location.reload(); }, 15000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -8,7 +8,7 @@ def get_all_engines():
|
||||
PrusaSlicerEngine()
|
||||
]
|
||||
|
||||
def get_slicer_engine(engine_name="cura"):
|
||||
def get_slicer_engine(engine_name="prusa", print_config_folder=None):
|
||||
"""
|
||||
Factory function to retrieve the requested slicing engine instance.
|
||||
Valid names: 'cura', 'prusa_slicer'
|
||||
@@ -16,9 +16,9 @@ def get_slicer_engine(engine_name="cura"):
|
||||
engine_name = engine_name.lower().strip()
|
||||
|
||||
if engine_name in ['cura', 'cura_engine', 'curaengine']:
|
||||
return CuraEngine()
|
||||
return CuraEngine(print_config_folder)
|
||||
elif engine_name in ['prusa', 'prusa_slicer', 'prusaslicer']:
|
||||
return PrusaSlicerEngine()
|
||||
return PrusaSlicerEngine(print_config_folder)
|
||||
else:
|
||||
# Default fallback
|
||||
return CuraEngine()
|
||||
return PrusaSlicerEngine(print_config_folder)
|
||||
|
||||
@@ -6,11 +6,12 @@ import configparser
|
||||
from app.utils.conf_parse import ConfParse
|
||||
|
||||
class CuraEngine:
|
||||
def __init__(self):
|
||||
def __init__(self, print_config_folder=None):
|
||||
self.name = "cura"
|
||||
self.display_name = "UltiMaker Cura"
|
||||
self.is_available = self._check_available()
|
||||
|
||||
self.print_config_folder = os.path.join(print_config_folder, "cura_engine") if print_config_folder else None
|
||||
|
||||
def _check_available(self):
|
||||
try:
|
||||
# check if CuraEngine is available in PATH
|
||||
@@ -21,13 +22,6 @@ class CuraEngine:
|
||||
self.display_name = "UltiMaker Cura"
|
||||
self.is_available = self._check_available()
|
||||
|
||||
def _check_available(self):
|
||||
try:
|
||||
# check if CuraEngine is available in PATH
|
||||
result = subprocess.run(["CuraEngine", "help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return result.returncode == 0 or b"Usage:" in result.stdout or b"Usage:" in result.stderr
|
||||
except (FileNotFoundError, OSError):
|
||||
return False
|
||||
|
||||
def slice(self, app, stl_filepath, gcode_filepath, **kwargs):
|
||||
"""
|
||||
@@ -41,23 +35,28 @@ class CuraEngine:
|
||||
|
||||
tmp_def_path = None
|
||||
try:
|
||||
base_config_path = os.path.abspath(os.path.join(app.root_path, '..', 'print_config'))
|
||||
print_config_path = os.path.join(base_config_path, 'cura_engine')
|
||||
printers_path = os.path.join(print_config_path, 'printers')
|
||||
extruders_path = os.path.join(print_config_path, 'extruders')
|
||||
materials_path = os.path.join(print_config_path, 'materials')
|
||||
presets_path = os.path.join(print_config_path, 'quality')
|
||||
variants_path = os.path.join(print_config_path, 'variants')
|
||||
printers_path = os.path.join(self.print_config_folder, 'printers') if self.print_config_folder else None
|
||||
extruders_path = os.path.join(self.print_config_folder, 'extruders') if self.print_config_folder else None
|
||||
materials_path = os.path.join(self.print_config_folder, 'materials') if self.print_config_folder else None
|
||||
presets_path = os.path.join(self.print_config_folder, 'quality') if self.print_config_folder else None
|
||||
variants_path = os.path.join(self.print_config_folder, 'variants') if self.print_config_folder else None
|
||||
|
||||
env = os.environ.copy()
|
||||
env["CURA_ENGINE_SEARCH_PATH"] = f"{printers_path}:{extruders_path}:{materials_path}:{presets_path}:{variants_path}"
|
||||
|
||||
|
||||
from app.models import SystemConfig
|
||||
db_printer = SystemConfig.query.filter_by(key='default_printer').first()
|
||||
p_val = db_printer.value if db_printer and db_printer.value else 'creality_ender3v3se.def.json'
|
||||
if not p_val.endswith('.def.json'): p_val += '.def.json'
|
||||
|
||||
def_files = [
|
||||
os.path.join(printers_path, "fdmprinter.def.json"),
|
||||
os.path.join(printers_path, "fdmextruder.def.json"),
|
||||
os.path.join(printers_path, "creality_base.def.json"),
|
||||
os.path.join(printers_path, "creality_ender3v3se.def.json")
|
||||
os.path.join(printers_path, p_val)
|
||||
]
|
||||
|
||||
|
||||
inst_files_list = []
|
||||
quality_type = None
|
||||
@@ -82,19 +81,19 @@ class CuraEngine:
|
||||
if os.path.exists(m_path): inst_files_list.append(m_path)
|
||||
if variant_type:
|
||||
variant_d = variant_type.split("mm")[0]
|
||||
v_path = os.path.join(variants_path, "creality", f"creality_ender3v3se_{variant_d}.inst.cfg")
|
||||
v_path = os.path.join(variants_path, "creality", f"{p_val.replace('.def.json', '')}_{variant_d}.inst.cfg")
|
||||
if os.path.exists(v_path): inst_files_list.append(v_path)
|
||||
|
||||
if support_pattern == 'tree':
|
||||
t_path = os.path.join(print_config_path, 'supports', 'tree.inst.cfg')
|
||||
if os.path.exists(t_path): inst_files_list.append(t_path)
|
||||
t_path = os.path.join(self.print_config_folder, 'supports', 'tree.inst.cfg') if self.print_config_folder else None
|
||||
if t_path and os.path.exists(t_path): inst_files_list.append(t_path)
|
||||
elif support_pattern and support_pattern != 'false':
|
||||
n_path = os.path.join(print_config_path, 'supports', 'normal.inst.cfg')
|
||||
if os.path.exists(n_path): inst_files_list.append(n_path)
|
||||
n_path = os.path.join(self.print_config_folder, 'supports', 'normal.inst.cfg') if self.print_config_folder else None
|
||||
if n_path and os.path.exists(n_path): inst_files_list.append(n_path)
|
||||
|
||||
if quality_preset and quality_type:
|
||||
g_path = os.path.join(presets_path, 'creality', 'globals', f"{quality_type}.inst.cfg")
|
||||
if os.path.exists(g_path): inst_files_list.append(g_path)
|
||||
g_path = os.path.join(self.print_config_folder, 'creality', 'globals', f"{quality_type}.inst.cfg") if self.print_config_folder else None
|
||||
if g_path and os.path.exists(g_path): inst_files_list.append(g_path)
|
||||
|
||||
if quality_preset and preset_path and os.path.exists(preset_path):
|
||||
inst_files_list.append(preset_path)
|
||||
@@ -192,10 +191,10 @@ class CuraEngine:
|
||||
except Exception as e:
|
||||
app.logger.error(f"Failed to delete temp JSON config {tmp_def_path}: {e}")
|
||||
|
||||
def get_quality_presets(self, app):
|
||||
def get_quality_presets(self):
|
||||
try:
|
||||
path = os.path.join(app.root_path, '..', 'print_config', 'cura_engine', 'quality', 'creality', 'presets')
|
||||
if not os.path.exists(path): return []
|
||||
path = os.path.join(self.print_config_folder, 'quality', 'creality', 'presets') if self.print_config_folder else None
|
||||
if not path or not os.path.exists(path): return []
|
||||
files = [f for f in os.listdir(path) if f.endswith('.inst.cfg')]
|
||||
presets = []
|
||||
for f in files:
|
||||
@@ -205,7 +204,7 @@ class CuraEngine:
|
||||
except:
|
||||
return []
|
||||
|
||||
def get_support_patterns(self, app):
|
||||
def get_support_patterns(self):
|
||||
return [
|
||||
{'id': 'tree', 'name': 'Tree'},
|
||||
{'id': 'lines', 'name': 'Lines'},
|
||||
@@ -219,10 +218,10 @@ class CuraEngine:
|
||||
{'id': 'octagon', 'name': 'Octagon'}
|
||||
]
|
||||
|
||||
def get_materials(self, app):
|
||||
def get_materials(self):
|
||||
try:
|
||||
path = os.path.join(app.root_path, '..', 'print_config', 'cura_engine', 'materials')
|
||||
if not os.path.exists(path): return []
|
||||
path = os.path.join(self.print_config_folder, 'materials') if self.print_config_folder else None
|
||||
if not path or not os.path.exists(path): return []
|
||||
files = [f for f in os.listdir(path) if f.endswith('.inst.cfg')]
|
||||
materials = []
|
||||
for f in files:
|
||||
@@ -231,3 +230,33 @@ class CuraEngine:
|
||||
return materials
|
||||
except:
|
||||
return []
|
||||
|
||||
def get_bed_dimensions(self):
|
||||
from app.models import SystemConfig
|
||||
import json
|
||||
try:
|
||||
db_printer = SystemConfig.query.filter_by(key='default_printer').first()
|
||||
p_val = db_printer.value if db_printer and db_printer.value else 'creality_ender3v3se.def.json'
|
||||
if not p_val.endswith('.def.json'): p_val += '.def.json'
|
||||
path = os.path.join(self.print_config_folder, 'printers', p_val)
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
w = data['overrides']['machine_width']['default_value']
|
||||
h = data['overrides']['machine_depth']['default_value']
|
||||
hd = data['overrides']['machine_height']['default_value']
|
||||
return w, h, hd
|
||||
except:
|
||||
pass
|
||||
return 220, 220, 250
|
||||
def get_all_printers(self):
|
||||
try:
|
||||
path = os.path.join(self.print_config_folder, 'printers') if self.print_config_folder else None
|
||||
if not path or not os.path.exists(path): return []
|
||||
files = [f for f in os.listdir(path) if f.endswith('.inst.cfg')]
|
||||
printers = []
|
||||
for f in files:
|
||||
printers.append({'id': f, 'name': f.replace('..def.json', '').replace('generic_', 'Generic ').replace('_', ' ').title()})
|
||||
printers.sort(key=lambda x: x['name'])
|
||||
return printers
|
||||
except:
|
||||
return []
|
||||
@@ -4,11 +4,12 @@ import configparser
|
||||
import uuid
|
||||
|
||||
class PrusaSlicerEngine:
|
||||
def __init__(self):
|
||||
def __init__(self, print_config_folder=None):
|
||||
self.name = "prusa_slicer"
|
||||
self.display_name = "PrusaSlicer"
|
||||
self.is_available = self._check_available()
|
||||
|
||||
self.print_config_folder = os.path.join(print_config_folder, 'prusa_slicer') if print_config_folder else None
|
||||
|
||||
def _check_available(self):
|
||||
try:
|
||||
result = subprocess.run(["prusa-slicer", "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@@ -47,18 +48,22 @@ class PrusaSlicerEngine:
|
||||
# print(support_pattern)
|
||||
all_configs = {}
|
||||
|
||||
printer_ini = os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', 'printers', 'Ender3_V3_SE.ini')
|
||||
if os.path.exists(printer_ini):
|
||||
from app.models import SystemConfig
|
||||
db_printer = SystemConfig.query.filter_by(key='default_printer').first()
|
||||
p_val = db_printer.value if db_printer and db_printer.value else 'Ender3_V3_SE'
|
||||
if not p_val.endswith('.ini'): p_val += '.ini'
|
||||
printer_ini = os.path.join(self.print_config_folder, 'printers', p_val) if self.print_config_folder else None
|
||||
if printer_ini and os.path.exists(printer_ini):
|
||||
self.add_ini_keys(printer_ini, 'settings', all_configs)
|
||||
|
||||
if quality_preset:
|
||||
q_ini = os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', 'quality', f"{quality_preset}.ini")
|
||||
if os.path.exists(q_ini):
|
||||
q_ini = os.path.join(self.print_config_folder, 'quality', f"{quality_preset}.ini") if self.print_config_folder else None
|
||||
if q_ini and os.path.exists(q_ini):
|
||||
self.add_ini_keys(q_ini, 'settings', all_configs)
|
||||
|
||||
if material_preset:
|
||||
m_ini = os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', 'materials', f"{material_preset}.ini")
|
||||
if os.path.exists(m_ini):
|
||||
m_ini = os.path.join(self.print_config_folder, 'materials', f"{material_preset}.ini") if self.print_config_folder else None
|
||||
if m_ini and os.path.exists(m_ini):
|
||||
self.add_ini_keys(m_ini, 'settings', all_configs)
|
||||
|
||||
if infill_density is not None:
|
||||
@@ -69,20 +74,19 @@ class PrusaSlicerEngine:
|
||||
if support_enable == 'buildplate':
|
||||
command.append("--support-material-buildplate-only")
|
||||
# PrusaSlicer equivalent for tree supports => organic
|
||||
support_pattern_ini = os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', 'supports', f'{support_pattern}.ini')
|
||||
if os.path.exists(support_pattern_ini):
|
||||
support_pattern_ini = os.path.join(self.print_config_folder, 'supports', f'{support_pattern}.ini') if self.print_config_folder else None
|
||||
if support_pattern_ini and os.path.exists(support_pattern_ini):
|
||||
self.add_ini_keys(support_pattern_ini, 'settings', all_configs)
|
||||
else:
|
||||
# Load the default no_support.ini if no support is enabled
|
||||
no_support_ini = os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', 'supports', 'no_support.ini')
|
||||
if os.path.exists(no_support_ini):
|
||||
no_support_ini = os.path.join(self.print_config_folder, 'supports', 'no_support.ini') if self.print_config_folder else None
|
||||
if no_support_ini and os.path.exists(no_support_ini):
|
||||
self.add_ini_keys(no_support_ini, 'settings', all_configs)
|
||||
else:
|
||||
all_configs['support_material'] = '0'
|
||||
|
||||
tmp_ini_filename = f"tmp_{uuid.uuid4().hex}.ini"
|
||||
tmp_ini_path = os.path.join(app.config['UPLOAD_FOLDER'], tmp_ini_filename)
|
||||
print(f'****tmp_ini_path: {tmp_ini_path}')
|
||||
with open(tmp_ini_path, 'w') as f:
|
||||
for key, value in all_configs.items():
|
||||
f.write(f"{key} = {value}\n")
|
||||
@@ -108,11 +112,11 @@ class PrusaSlicerEngine:
|
||||
app.logger.error(f"PrusaSlicer Exception: {e}")
|
||||
return False, str(e)
|
||||
|
||||
def get_quality_presets(self, app):
|
||||
all_files = [f for f in os.listdir(os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer',"quality")) if f.endswith('.ini')]
|
||||
def get_quality_presets(self):
|
||||
all_files = [f for f in os.listdir(os.path.join(self.print_config_folder, "quality")) if f.endswith('.ini')] if self.print_config_folder else []
|
||||
quality_presets = []
|
||||
for file in all_files:
|
||||
with open(os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', "quality", file), 'r') as f:
|
||||
with open(os.path.join(self.print_config_folder, "quality", file), 'r') as f:
|
||||
config = configparser.ConfigParser()
|
||||
config.read_file(f)
|
||||
if 'metadata' in config:
|
||||
@@ -122,11 +126,11 @@ class PrusaSlicerEngine:
|
||||
})
|
||||
return quality_presets
|
||||
|
||||
def get_support_patterns(self, app):
|
||||
all_files = [f for f in os.listdir(os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer',"supports")) if f.endswith('.ini')]
|
||||
def get_support_patterns(self):
|
||||
all_files = [f for f in os.listdir(os.path.join(self.print_config_folder,"supports")) if f.endswith('.ini')] if self.print_config_folder else []
|
||||
support_presets = []
|
||||
for file in all_files:
|
||||
with open(os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', "supports", file), 'r') as f:
|
||||
with open(os.path.join(self.print_config_folder, "supports", file), 'r') as f:
|
||||
config = configparser.ConfigParser()
|
||||
config.read_file(f)
|
||||
if 'metadata' in config:
|
||||
@@ -136,15 +140,54 @@ class PrusaSlicerEngine:
|
||||
})
|
||||
return support_presets
|
||||
|
||||
def get_materials(self, app):
|
||||
def get_materials(self):
|
||||
all_files = [f for f in os.listdir(os.path.join(self.print_config_folder, "materials")) if f.endswith('.ini')] if self.print_config_folder else []
|
||||
materials = []
|
||||
for file in all_files:
|
||||
with open(os.path.join(self.print_config_folder, "materials", file), 'r') as f:
|
||||
config = configparser.ConfigParser()
|
||||
config.read_file(f)
|
||||
if 'metadata' in config:
|
||||
materials.append({
|
||||
'id': file.replace('.ini', ''),
|
||||
'name': config['metadata'].get('show_name', file.replace('.ini', '').replace('_', ' '))
|
||||
})
|
||||
return materials
|
||||
|
||||
|
||||
def get_bed_dimensions(self):
|
||||
from app.models import SystemConfig
|
||||
import configparser
|
||||
try:
|
||||
path = os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', 'materials')
|
||||
if not os.path.exists(path): return []
|
||||
files = [f for f in os.listdir(path) if f.endswith('.ini')]
|
||||
materials = []
|
||||
for f in files:
|
||||
materials.append({'id': f.replace('.ini', ''), 'name': f.replace('.ini', '').replace('_', ' ')})
|
||||
materials.sort(key=lambda x: x['name'])
|
||||
return materials
|
||||
db_printer = SystemConfig.query.filter_by(key='default_printer').first()
|
||||
p_val = db_printer.value if db_printer and db_printer.value else 'Ender3_V3_SE.ini'
|
||||
if not p_val.endswith('.ini'): p_val += '.ini'
|
||||
path = os.path.join(self.print_config_folder, 'printers', p_val)
|
||||
config = configparser.ConfigParser()
|
||||
config.read(path)
|
||||
if 'settings' in config and 'bed_shape' in config['settings']:
|
||||
# format is usually like 0x0,220x0,220x220,0x220
|
||||
coords = config['settings']['bed_shape'].split(',')
|
||||
max_x = max([float(c.split('x')[0]) for c in coords])
|
||||
max_y = max([float(c.split('x')[1]) for c in coords])
|
||||
# height
|
||||
h = 250
|
||||
if 'max_print_height' in config['settings']:
|
||||
h = float(config['settings']['max_print_height'])
|
||||
return max_x, max_y, h
|
||||
except:
|
||||
return []
|
||||
pass
|
||||
return 220, 220, 250
|
||||
def get_all_printers(self):
|
||||
all_files = [f for f in os.listdir(os.path.join(self.print_config_folder, "printers")) if f.endswith('.ini')] if self.print_config_folder else []
|
||||
printers = []
|
||||
for file in all_files:
|
||||
with open(os.path.join(self.print_config_folder, "printers", file), 'r') as f:
|
||||
config = configparser.ConfigParser()
|
||||
config.read_file(f)
|
||||
if 'metadata' in config:
|
||||
printers.append({
|
||||
'id': file.replace('.ini', ''),
|
||||
'name': config['metadata'].get('show_name', file.replace('.ini', '').replace('_', ' '))
|
||||
})
|
||||
return printers
|
||||
|
||||
@@ -6,7 +6,7 @@ from app.utils.conf_parse import ConfParse
|
||||
import json
|
||||
import uuid
|
||||
import configparser
|
||||
|
||||
from app.utils.slice_engines import get_slicer_engine
|
||||
|
||||
import os
|
||||
|
||||
@@ -44,17 +44,15 @@ def slice_stl_task(file_id, stl_filepath, quality_preset=None, material_preset=N
|
||||
|
||||
# Remove DB session to avoid locking the sqlite db during long slicing operations
|
||||
db.session.remove()
|
||||
|
||||
from app.utils.slice_engines import get_slicer_engine
|
||||
|
||||
|
||||
try:
|
||||
# Optionally fetch the preferred engine from db conf or just default to cura
|
||||
# For now default to cura or whichever is passed via kwargs if implemented later
|
||||
# Optionally fetch the preferred engine from db conf or just default to prusa
|
||||
# For now default to prusa or whichever is passed via kwargs if implemented later
|
||||
conf_engine = SystemConfig.query.filter_by(key='slicer_engine').first()
|
||||
engine_name = conf_engine.value if conf_engine and conf_engine.value else "cura"
|
||||
engine_name = conf_engine.value if conf_engine and conf_engine.value else "prusa"
|
||||
db.session.remove()
|
||||
|
||||
slicer = get_slicer_engine(engine_name)
|
||||
slicer = get_slicer_engine(engine_name,app.config['PRINT_CONFIG_FOLDER'])
|
||||
|
||||
success, err_msg = slicer.slice(
|
||||
app=app,
|
||||
|
||||
Reference in New Issue
Block a user