修改部分参数

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-08 01:16:40 +08:00
parent 40b8cc8023
commit a26f7214f9
30 changed files with 341 additions and 3206 deletions

View File

@@ -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)

View File

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

View File

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

View File

@@ -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()

View File

@@ -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 => {

View File

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

View File

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

View File

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

View File

@@ -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)

View File

@@ -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 []

View File

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

View File

@@ -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,