暂存-说明文档(部分)
@@ -56,8 +56,10 @@ def create_app():
|
||||
app.config['REMEMBER_COOKIE_NAME'] = 'aio_remember'
|
||||
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'))
|
||||
app.config['UPLOAD_FOLDER'] = os.environ.get('UPLOAD_FOLDER', os.path.abspath(os.path.join(app.root_path, '..', 'uploads')))
|
||||
app.config['PRINT_CONFIG_FOLDER'] = os.environ.get('PRINT_CONFIG_FOLDER', os.path.abspath(os.path.join(app.root_path, '..', 'print_config')))
|
||||
app.config['PRUSA_SLICE_BIN'] = os.environ.get('PRUSA_SLICE_BIN', os.path.abspath(os.path.join(app.root_path, '..', 'prusaslicer', 'PrusaSlicer-2.9.4-aarch64-full.AppImage')))
|
||||
|
||||
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
|
||||
|
||||
58
app/assets/doc/printer_helper_de.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Drucker-Helfer — Kurzanleitung
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
- Druckerstatus
|
||||
- Druck vorbereiten
|
||||
- Steuerung
|
||||
- Drucker-Helfer (diese Seite)
|
||||
- Systemkonfiguration (Admin)
|
||||
- OctoPrint-Panel (Admin)
|
||||
|
||||
---
|
||||
|
||||
## Druckerstatus
|
||||
|
||||
Zeigt aktuellen Druckerzustand, Temperaturen und aktive Aufgaben an.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Druck vorbereiten
|
||||
|
||||
GCode an den Drucker senden, Temperaturen setzen und mit `Druck vorbereiten` bzw. `Jetzt drucken` starten.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Steuerung
|
||||
|
||||
Manuelle Grundsteuerungen: Achsen homing, Düsen/Betten bewegen, Pause/Fortsetzen, Druck abbrechen.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Drucker-Helfer (diese Seite)
|
||||
|
||||
Tipps zur Fehlerbehebung (Netzwerk, Filament, Bettleveling) und Checkliste vor dem Drucken.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Systemkonfiguration (Admin)
|
||||
|
||||
Admin-Einstellungen für Druckerabmessungen, Limits, Basisprofile und Verbindungsdaten.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## OctoPrint-Panel (Admin)
|
||||
|
||||
Eingebettetes OctoPrint-Panel: `OctoPrint Basis-URL` und API-Key konfigurieren, Live-Panel verwenden.
|
||||
|
||||

|
||||
|
||||
64
app/assets/doc/printer_helper_en.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Printer Helper — Quick Guide
|
||||
|
||||
## Table of Contents
|
||||
- Printer Status
|
||||
- Prepare Print
|
||||
- Control
|
||||
- Printer Helper (this page)
|
||||
- System Configuration (Admin)
|
||||
- OctoPrint Panel (Admin)
|
||||
|
||||
---
|
||||
|
||||
## Printer Status
|
||||
|
||||
Shows current printer state, temperatures and active job information.
|
||||
|
||||

|
||||
|
||||
Use this page to monitor `Printer Status` and `Active Print Job`.
|
||||
|
||||
---
|
||||
|
||||
## Prepare Print
|
||||
|
||||
Send prepared GCode to the printer, set temperatures and start a print using `Prepare Print` and `Print Now`.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Control
|
||||
|
||||
Basic manual controls: home axes, move nozzle/bed, pause/resume and cancel print.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Printer Helper (this page)
|
||||
|
||||
Guides common troubleshooting steps (connectivity, filament, bed leveling) and quick checks before printing.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## System Configuration (Admin)
|
||||
|
||||
Admin-only settings for printer dimensions, limits, shared profiles and connection settings.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## OctoPrint Panel (Admin)
|
||||
|
||||
Embedded OctoPrint panel: configure `OctoPrint Base URL`, API key and use the live panel when available.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
If you want I can add annotated screenshots for specific printer models.
|
||||
|
||||
60
app/assets/doc/printer_helper_zh-cn.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 打印助手 — 快速指南
|
||||
|
||||
## 目录
|
||||
- 打印机状态
|
||||
- 准备打印
|
||||
- 控制
|
||||
- 打印助手(本页)
|
||||
- 系统配置(管理员)
|
||||
- OctoPrint 面板(管理员)
|
||||
|
||||
---
|
||||
|
||||
## 打印机状态
|
||||
|
||||
显示当前打印机状态、温度和任务信息。
|
||||
|
||||

|
||||
|
||||
可在此查看 `打印机状态` 与 `当前打印任务`。
|
||||
|
||||
---
|
||||
|
||||
## 准备打印
|
||||
|
||||
将准备好的 GCode 发送到打印机,设置温度并使用 `准备打印` 或 `立即打印` 开始。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 控制
|
||||
|
||||
手动控制:回原点、移动喷嘴/平台、暂停/恢复与取消打印。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 打印助手(本页)
|
||||
|
||||
提供常见故障排查步骤(网络、挤出机、床平整)和打印前检查清单。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 系统配置(管理员)
|
||||
|
||||
管理员设置打印机尺寸、限制、基础配置和连接信息。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## OctoPrint 面板(管理员)
|
||||
|
||||
内嵌 OctoPrint 面板:配置 `OctoPrint 基础 URL`、API 密钥并使用可用的实时面板。
|
||||
|
||||

|
||||
|
||||
82
app/assets/doc/slice_helper_de.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Slice-Helfer — Kurzanleitung
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
- Startseite
|
||||
- Meine Dateien
|
||||
- Plater (Bauteilplatte)
|
||||
- Konto-Verwaltung
|
||||
- Slice-Helfer (diese Seite)
|
||||
- Systemeinstellungen (Admin)
|
||||
- Benutzerverwaltung (Admin)
|
||||
- API-Schlüssel (Admin)
|
||||
|
||||
---
|
||||
|
||||
## Startseite
|
||||
|
||||
Übersicht des Slicer-Dashboards und schnelle Aktionen.
|
||||
|
||||

|
||||
|
||||
Benutzen Sie die Navigation, um `Startseite` zu öffnen und über `STL Hochladen & Slicen` einen neuen Slice zu starten.
|
||||
|
||||
---
|
||||
|
||||
## Meine Dateien
|
||||
|
||||
Verwalten Sie hochgeladene STL- und GCode-Dateien: hochladen, herunterladen, löschen.
|
||||
|
||||

|
||||
|
||||
Wichtige Aktionen: `STL hochladen`, `GCode Herunterladen`, `Löschen`.
|
||||
|
||||
---
|
||||
|
||||
## Plater (Bauteilplatte)
|
||||
|
||||
Modelle auf der Bauteilplatte anordnen, verschieben, drehen und skalieren. Vor dem Slicen `Zusammenführen & Slicen`.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Konto-Verwaltung
|
||||
|
||||
Für angemeldete Benutzer: Profil, Passwort ändern und aktive Sitzungen verwalten.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Slice-Helfer (diese Seite)
|
||||
|
||||
Erklärung der empfohlenen Slicing-Schritte: `Qualitätsprofil` wählen, `Support` und `Fülldichte` konfigurieren, dann `Hochladen & Slicen`.
|
||||
|
||||

|
||||
|
||||
Statusmeldungen: `Wartend`, `Slicen`, `Gesliced`, `Fehlgeschlagen`.
|
||||
|
||||
---
|
||||
|
||||
## Systemeinstellungen (Admin)
|
||||
|
||||
Admins konfigurieren globale Slicer-Engines und Standardprofile.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Benutzerverwaltung (Admin)
|
||||
|
||||
Admins können Benutzer hinzufügen/ändern und Quoten sowie Rollen setzen (`Benutzer`, `Admin`).
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## API-Schlüssel (Admin)
|
||||
|
||||
Verwalten Sie API-Schlüssel für externe Integrationen: `Neuen API-Schlüssel erstellen` und `Schlüssel generieren`.
|
||||
|
||||

|
||||
|
||||
88
app/assets/doc/slice_helper_en.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Slice Helper — Quick Guide
|
||||
|
||||
## Table of Contents
|
||||
- Home
|
||||
- My Files
|
||||
- Plater (Build Plate)
|
||||
- Account Management
|
||||
- Slice Helper (this page)
|
||||
- System Settings (Admin)
|
||||
- User Management (Admin)
|
||||
- API Keys (Admin)
|
||||
|
||||
---
|
||||
|
||||
## Home
|
||||
|
||||
Overview of the slicer dashboard and quick actions.
|
||||
|
||||

|
||||
|
||||
Use the top navigation to open `Home` and start a new slice via `Upload & Slice STL`.
|
||||
|
||||
---
|
||||
|
||||
## My Files
|
||||
|
||||
Manage uploaded STL and GCode files. You can upload, delete and download sliced GCode.
|
||||
|
||||

|
||||
|
||||
Common actions: `Upload STL`, `Download GCode`, `Delete`.
|
||||
|
||||
---
|
||||
|
||||
## Plater (Build Plate)
|
||||
|
||||
Arrange models on the build plate before slicing. Use translate/rotate/scale tools and `Merge & Slice`.
|
||||
|
||||

|
||||
|
||||
Tip: ensure all models fit the printable area before slicing.
|
||||
|
||||
---
|
||||
|
||||
## Account Management
|
||||
|
||||
Available when logged in. Update profile, change password, and manage active sessions.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Slice Helper (this page)
|
||||
|
||||
This page explains slice workflows and recommended settings. Choose a `Quality Profile`, set `Support` and `Infill Density` then `Upload & Slice`.
|
||||
|
||||

|
||||
|
||||
Status messages: `Waiting`, `Slicing`, `Sliced`, `Failed`.
|
||||
|
||||
---
|
||||
|
||||
## System Settings (Admin)
|
||||
|
||||
Admins can configure global slicer engines and default profiles under `System Settings`.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## User Management (Admin)
|
||||
|
||||
Admins can add/edit users, set quotas and roles (`User`, `Admin`).
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## API Keys (Admin)
|
||||
|
||||
Manage API keys used by external tools. `Create New API Key`, name it and `Generate Key`.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
If you need example workflows or screenshots, tell me which page to expand.
|
||||
|
||||
84
app/assets/doc/slice_helper_zh-cn.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 切片助手 — 快速指南
|
||||
|
||||
## 目录
|
||||
- 主页
|
||||
- 我的文件
|
||||
- 构建板 (Plater)
|
||||
- 账号管理
|
||||
- 切片助手(本页)
|
||||
- 系统设置(管理员)
|
||||
- 用户管理(管理员)
|
||||
- API 密钥(管理员)
|
||||
|
||||
---
|
||||
|
||||
## 主页
|
||||
|
||||
切片仪表盘概览与快速操作入口。
|
||||
|
||||

|
||||
|
||||
使用导航栏进入“主页”,通过 `上传并切片 STL` 开始新切片。
|
||||
|
||||
---
|
||||
|
||||
## 我的文件
|
||||
|
||||
管理已上传的 STL 与 GCode,可上传、下载或删除文件。
|
||||
|
||||

|
||||
|
||||
常用操作:`上传STL`、`下载 GCode`、`删除`。
|
||||
|
||||
---
|
||||
|
||||
## 构建板 (Plater)
|
||||
|
||||
在构建板上放置与调整模型(平移/旋转/缩放),确认位置后使用 `合并并切片`。
|
||||
|
||||

|
||||
|
||||
提示:切片前确保模型均在可打印范围内。
|
||||
|
||||
---
|
||||
|
||||
## 账号管理
|
||||
|
||||
登录用户可在此更新资料、修改密码并管理活跃会话。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 切片助手(本页)
|
||||
|
||||
本页说明推荐的切片流程与设置:选择 `质量配置`、设置 `支撑` 与 `填充密度`,然后 `上传 & 切片`。
|
||||
|
||||

|
||||
|
||||
状态提示:`等待中`、`切片中`、`已切片`、`失败`。
|
||||
|
||||
---
|
||||
|
||||
## 系统设置(管理员)
|
||||
|
||||
管理员可在此配置全局切片引擎与默认配置文件。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 用户管理(管理员)
|
||||
|
||||
管理员可添加/编辑用户并设置配额与角色(`普通用户`、`管理员`)。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## API 密钥(管理员)
|
||||
|
||||
管理外部工具使用的 API 密钥;点击 `创建新的 API 密钥`,输入名称并 `生成密钥`。
|
||||
|
||||

|
||||
|
||||
@@ -269,5 +269,13 @@
|
||||
"Are you sure you want to delete this API Key?": "Sind Sie sicher, dass Sie diesen API-Schlüssel löschen möchten?",
|
||||
"API Key Name": "API-Schlüsselname",
|
||||
"No API keys found.": "Keine API-Schlüssel gefunden.",
|
||||
"API Keys": "API-Schlüssel"
|
||||
"API Keys": "API-Schlüssel",
|
||||
"Slice Helper": "Slice-Helfer",
|
||||
"Printer Helper": "Drucker-Helfer",
|
||||
"For security reasons, please change your default admin password.": "Aus Sicherheitsgründen ändern Sie bitte Ihr Standard-Administratorpasswort.",
|
||||
"Your new password cannot be the default \"admin123\".": "Ihr neues Passwort darf nicht das Standardpasswort \"admin123\" sein.",
|
||||
"Current password is incorrect.": "Das aktuelle Passwort ist falsch.",
|
||||
"New passwords do not match.": "Die neuen Passwörter stimmen nicht überein.",
|
||||
"New password must be at least 6 characters.": "Das neue Passwort muss mindestens 6 Zeichen lang sein.",
|
||||
"Password updated successfully.": "Passwort erfolgreich aktualisiert."
|
||||
}
|
||||
@@ -269,5 +269,13 @@
|
||||
"Are you sure you want to delete this API Key?": "Are you sure you want to delete this API Key?",
|
||||
"API Key Name": "API Key Name",
|
||||
"No API keys found.": "No API keys found.",
|
||||
"API Keys": "API Keys"
|
||||
"API Keys": "API Keys",
|
||||
"Slice Helper": "Slice Helper",
|
||||
"Printer Helper": "Printer Helper",
|
||||
"For security reasons, please change your default admin password.": "For security reasons, please change your default admin password.",
|
||||
"Your new password cannot be the default \"admin123\".": "Your new password cannot be the default \"admin123\".",
|
||||
"Current password is incorrect.": "Current password is incorrect.",
|
||||
"New passwords do not match.": "New passwords do not match.",
|
||||
"New password must be at least 6 characters.": "New password must be at least 6 characters.",
|
||||
"Password updated successfully.": "Password updated successfully."
|
||||
}
|
||||
@@ -269,5 +269,13 @@
|
||||
"Are you sure you want to delete this API Key?": "您确定要删除此 API 密钥吗?",
|
||||
"API Key Name": "API 密钥名称",
|
||||
"No API keys found.": "未找到 API 密钥。",
|
||||
"API Keys": "API 密钥"
|
||||
"API Keys": "API 密钥",
|
||||
"Slice Helper": "切片助手",
|
||||
"Printer Helper": "打印助手",
|
||||
"For security reasons, please change your default admin password.": "出于安全原因,请修改您的默认管理员密码。",
|
||||
"Your new password cannot be the default \"admin123\".": "新密码不能设置为系统默认的 \"admin123\"。",
|
||||
"Current password is incorrect.": "当前密码不正确。",
|
||||
"New passwords do not match.": "新密码不匹配。",
|
||||
"New password must be at least 6 characters.": "新密码必须至少6个字符。",
|
||||
"Password updated successfully.": "密码更新成功。"
|
||||
}
|
||||
BIN
app/assets/img/favicon.ico
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
app/assets/img/favicon.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
app/assets/img/logo.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
app/assets/img/slice_helper/account-management_de.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
app/assets/img/slice_helper/account-management_en.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
app/assets/img/slice_helper/account-management_zh-cn.png
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
app/assets/img/slice_helper/api-keys_de.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
app/assets/img/slice_helper/api-keys_en.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
app/assets/img/slice_helper/api-keys_zh-cn.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
app/assets/img/slice_helper/home_de.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
app/assets/img/slice_helper/home_en.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
app/assets/img/slice_helper/home_zh-cn.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
app/assets/img/slice_helper/my-files_de.png
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
app/assets/img/slice_helper/my-files_en.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
app/assets/img/slice_helper/my-files_zh-cn.png
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
app/assets/img/slice_helper/plater_de.png
Normal file
|
After Width: | Height: | Size: 410 KiB |
BIN
app/assets/img/slice_helper/plater_en.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
app/assets/img/slice_helper/plater_zh-cn.png
Normal file
|
After Width: | Height: | Size: 407 KiB |
BIN
app/assets/img/slice_helper/slice-helper_de.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
app/assets/img/slice_helper/slice-helper_en.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
app/assets/img/slice_helper/slice-helper_zh-cn.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
app/assets/img/slice_helper/system-settings_de.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
app/assets/img/slice_helper/system-settings_en.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
app/assets/img/slice_helper/system-settings_zh-cn.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
app/assets/img/slice_helper/user-management_de.png
Normal file
|
After Width: | Height: | Size: 345 KiB |
BIN
app/assets/img/slice_helper/user-management_en.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
app/assets/img/slice_helper/user-management_zh-cn.png
Normal file
|
After Width: | Height: | Size: 344 KiB |
@@ -90,12 +90,40 @@ def settings():
|
||||
def users():
|
||||
all_users = User.query.order_by(User.created_at.desc()).all()
|
||||
user_quotas = {}
|
||||
|
||||
# Load defaults
|
||||
def_guest_stl = SystemConfig.query.filter_by(key="default_guest_stl_quota_mb").first()
|
||||
def_guest_stl_val = def_guest_stl.value if def_guest_stl else '0'
|
||||
def_guest_gcode = SystemConfig.query.filter_by(key="default_guest_gcode_quota_mb").first()
|
||||
def_guest_gcode_val = def_guest_gcode.value if def_guest_gcode else '0'
|
||||
|
||||
def_user_stl = SystemConfig.query.filter_by(key="default_user_stl_quota_mb").first()
|
||||
def_user_stl_val = def_user_stl.value if def_user_stl else '0'
|
||||
def_user_gcode = SystemConfig.query.filter_by(key="default_user_gcode_quota_mb").first()
|
||||
def_user_gcode_val = def_user_gcode.value if def_user_gcode else '0'
|
||||
|
||||
for u in all_users:
|
||||
if u.is_admin:
|
||||
eff_stl = '0'
|
||||
eff_gcode = '0'
|
||||
elif u.is_guest:
|
||||
eff_stl = def_guest_stl_val
|
||||
eff_gcode = def_guest_gcode_val
|
||||
else:
|
||||
eff_stl = def_user_stl_val
|
||||
eff_gcode = def_user_gcode_val
|
||||
|
||||
sq = SystemConfig.query.filter_by(key=f"user_{u.id}_stl_quota_mb").first()
|
||||
gq = SystemConfig.query.filter_by(key=f"user_{u.id}_gcode_quota_mb").first()
|
||||
|
||||
user_stl = sq.value if sq else '0'
|
||||
user_gcode = gq.value if gq else '0'
|
||||
|
||||
user_quotas[u.id] = {
|
||||
'stl': sq.value if sq else '0',
|
||||
'gcode': gq.value if gq else '0'
|
||||
'stl': user_stl,
|
||||
'gcode': user_gcode,
|
||||
'eff_stl': eff_stl if user_stl == '0' else user_stl,
|
||||
'eff_gcode': eff_gcode if user_gcode == '0' else user_gcode,
|
||||
}
|
||||
return render_template('admin/users.html', users=all_users, user_quotas=user_quotas)
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ def login():
|
||||
|
||||
|
||||
if user and check_password_hash(user.password_hash, password):
|
||||
# Clear old password check flag
|
||||
session.pop('pwd_check_done', None)
|
||||
session.pop('must_change_password', None)
|
||||
login_user(user, remember=remember)
|
||||
session_token = str(uuid.uuid4())
|
||||
# 尝试获取反向代理传递的真实 IP
|
||||
|
||||
@@ -21,6 +21,8 @@ main_bp = Blueprint('main', __name__)
|
||||
def check_user_session():
|
||||
if current_user.is_authenticated and not current_user.is_guest:
|
||||
session_token = session.get('user_session_token')
|
||||
client_ip = request.headers.get('X-Real-IP') or request.remote_addr
|
||||
|
||||
if session_token:
|
||||
user_session = UserSession.query.filter_by(session_token=session_token).first()
|
||||
if not user_session or not user_session.is_active:
|
||||
@@ -30,7 +32,36 @@ def check_user_session():
|
||||
return redirect(url_for('auth.login'))
|
||||
else:
|
||||
user_session.last_active = datetime.utcnow()
|
||||
user_session.ip_address = client_ip
|
||||
db.session.commit()
|
||||
else:
|
||||
# Re-authenticated via remember me, but no session token
|
||||
new_session_token = str(uuid.uuid4())
|
||||
user_session = UserSession(
|
||||
user_id=current_user.id,
|
||||
session_token=new_session_token,
|
||||
ip_address=client_ip,
|
||||
user_agent=request.user_agent.string,
|
||||
last_active=datetime.utcnow()
|
||||
)
|
||||
db.session.add(user_session)
|
||||
db.session.commit()
|
||||
session['user_session_token'] = new_session_token
|
||||
|
||||
# Check default admin password securely without checking hash on every request
|
||||
if current_user.is_admin:
|
||||
if session.get('pwd_check_done') is None:
|
||||
session['pwd_check_done'] = True
|
||||
if check_password_hash(current_user.password_hash, 'admin123'):
|
||||
session['must_change_password'] = True
|
||||
else:
|
||||
session.pop('must_change_password', None)
|
||||
|
||||
if session.get('must_change_password'):
|
||||
if request.endpoint and request.endpoint not in ['main.account', 'auth.logout', 'static']:
|
||||
flash('For security reasons, please change your default admin password.', 'warning')
|
||||
return redirect(url_for('main.account'))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -317,7 +348,7 @@ def preview_gcode(file_id):
|
||||
|
||||
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'])
|
||||
engine = get_slicer_engine(str(engine_name.value), current_app.config['PRINT_CONFIG_FOLDER'], current_app.config['PRUSA_SLICE_BIN'])
|
||||
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'))
|
||||
@@ -365,7 +396,7 @@ def plater():
|
||||
|
||||
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'])
|
||||
engine = get_slicer_engine(str(engine_name.value), current_app.config['PRINT_CONFIG_FOLDER'], current_app.config['PRUSA_SLICE_BIN'])
|
||||
w, h, hd = engine.get_bed_dimensions()
|
||||
print(f"Bed dimensions: {w}x{h}x{hd}")
|
||||
|
||||
@@ -384,6 +415,27 @@ def plater():
|
||||
models = [{'id': f.id, 'name': f.original_filename, 'status': f.status, 'url': url_for('main.serve_proxy_file', file_id=f.id), 'transform_matrix': f.transform_matrix} for f in user_files]
|
||||
return render_template('slice/plater.html', w=w, h=h, hd=hd, last_quality=default_quality, last_material=default_material, models=models, offset_x=offset_x, offset_y=offset_y, default_infill=default_infill, default_support=default_support, default_support_pattern=default_support_pattern, quota_exceeded=quota_exceeded, configs=configs)
|
||||
|
||||
|
||||
import re
|
||||
import markdown
|
||||
|
||||
@main_bp.route('/helper_slice')
|
||||
def helper_slice():
|
||||
lang = request.cookies.get('lang', 'en')
|
||||
filepath = os.path.join(current_app.root_path, 'assets', 'doc', f'slice_helper_{lang}.md')
|
||||
if not os.path.exists(filepath):
|
||||
filepath = os.path.join(current_app.root_path, 'assets', 'doc', 'slice_helper_en.md')
|
||||
|
||||
content_html = ""
|
||||
if os.path.exists(filepath):
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
md_text = f.read()
|
||||
content_html = markdown.markdown(md_text, extensions=['fenced_code', 'tables'])
|
||||
# Rewrite relative image links to /assets/doc/
|
||||
content_html = re.sub(r'src="(?!http|/)([^"]+)"', r'src="/assets/doc/\1"', content_html)
|
||||
|
||||
return render_template('slice/helper_slice.html', content_html=content_html)
|
||||
|
||||
@main_bp.route('/file/<int:file_id>')
|
||||
@login_required
|
||||
def serve_file(file_id):
|
||||
@@ -579,7 +631,7 @@ def build_plate_model():
|
||||
@main_bp.route('/api/engine_options/<engine_name>')
|
||||
@login_required
|
||||
def engine_options(engine_name):
|
||||
engine = get_slicer_engine(engine_name, current_app.config['PRINT_CONFIG_FOLDER'])
|
||||
engine = get_slicer_engine(engine_name, current_app.config['PRINT_CONFIG_FOLDER'], current_app.config['PRUSA_SLICE_BIN'])
|
||||
presets = engine.get_quality_presets()
|
||||
patterns = engine.get_support_patterns()
|
||||
materials = engine.get_materials() if hasattr(engine, 'get_materials') else []
|
||||
@@ -607,9 +659,13 @@ def account():
|
||||
flash('New passwords do not match.', 'danger')
|
||||
elif len(new_pass) < 6:
|
||||
flash('New password must be at least 6 characters.', 'danger')
|
||||
elif current_user.is_admin and new_pass == 'admin123':
|
||||
flash('Your new password cannot be the default "admin123".', 'danger')
|
||||
else:
|
||||
current_user.password_hash = generate_password_hash(new_pass)
|
||||
db.session.commit()
|
||||
# If they just changed it, clear the must change flag
|
||||
session.pop('must_change_password', None)
|
||||
flash('Password updated successfully.', 'success')
|
||||
|
||||
elif action == 'terminate_session':
|
||||
|
||||
@@ -228,6 +228,26 @@ def control():
|
||||
error = "OctoPrint is not configured."
|
||||
return render_template('printer/control.html', webcam_url=webcam_url, error=error)
|
||||
|
||||
import re
|
||||
import markdown
|
||||
|
||||
@printer_bp.route('/helper_printer')
|
||||
def helper_printer():
|
||||
lang = request.cookies.get('lang', 'en')
|
||||
filepath = os.path.join(current_app.root_path, 'assets', 'doc', f'printer_helper_{lang}.md')
|
||||
if not os.path.exists(filepath):
|
||||
filepath = os.path.join(current_app.root_path, 'assets', 'doc', 'printer_helper_en.md')
|
||||
|
||||
content_html = ""
|
||||
if os.path.exists(filepath):
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
md_text = f.read()
|
||||
content_html = markdown.markdown(md_text, extensions=['fenced_code', 'tables'])
|
||||
# Rewrite relative image links to /assets/doc/
|
||||
content_html = re.sub(r'src="(?!http|/)([^"]+)"', r'src="/assets/doc/\1"', content_html)
|
||||
|
||||
return render_template('printer/helper_printer.html', content_html=content_html)
|
||||
|
||||
@printer_bp.route('/api/command', methods=['POST'])
|
||||
@login_required
|
||||
def api_command():
|
||||
@@ -456,7 +476,9 @@ def octo_proxy(path):
|
||||
class WebSocketResponse(Response):
|
||||
def __call__(self, *args, **kwargs):
|
||||
print("WS Response __call__")
|
||||
if getattr(ws, 'mode', 'werkzeug') == 'werkzeug':
|
||||
if getattr(ws, 'mode', 'werkzeug') == 'gunicorn':
|
||||
raise StopIteration()
|
||||
elif getattr(ws, 'mode', 'werkzeug') == 'werkzeug':
|
||||
return super().__call__(*args, **kwargs)
|
||||
return []
|
||||
|
||||
|
||||
@@ -35,8 +35,25 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted d-block">STL: {{ user_quotas[user.id]['stl'] if user_quotas[user.id]['stl'] != '0' else _('Unlimited') }} MB</small>
|
||||
<small class="text-muted d-block">GCode: {{ user_quotas[user.id]['gcode'] if user_quotas[user.id]['gcode'] != '0' else _('Unlimited') }} MB</small>
|
||||
{% if user.is_admin %}
|
||||
<small class="text-muted d-block">STL: {{ _('Unlimited') }}</small>
|
||||
<small class="text-muted d-block">GCode: {{ _('Unlimited') }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted d-block">STL:
|
||||
{% if user_quotas[user.id]['stl'] == '0' %}
|
||||
{{ _('Default') }} ({{ user_quotas[user.id]['eff_stl'] if user_quotas[user.id]['eff_stl'] != '0' else _('Unlimited') }} MB)
|
||||
{% else %}
|
||||
{{ user_quotas[user.id]['stl'] }} MB
|
||||
{% endif %}
|
||||
</small>
|
||||
<small class="text-muted d-block">GCode:
|
||||
{% if user_quotas[user.id]['gcode'] == '0' %}
|
||||
{{ _('Default') }} ({{ user_quotas[user.id]['eff_gcode'] if user_quotas[user.id]['eff_gcode'] != '0' else _('Unlimited') }} MB)
|
||||
{% else %}
|
||||
{{ user_quotas[user.id]['gcode'] }} MB
|
||||
{% endif %}
|
||||
</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
@@ -66,11 +83,11 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ _('STL Quota') }} (MB) <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
|
||||
<label class="form-label">{{ _('STL Quota') }} (MB) <small class="text-muted">(0 = {{ _('Default') }})</small></label>
|
||||
<input type="number" class="form-control" name="stl_quota_mb" value="{{ user_quotas[user.id]['stl'] }}" min="0">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ _('GCode Quota') }} (MB) <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
|
||||
<label class="form-label">{{ _('GCode Quota') }} (MB) <small class="text-muted">(0 = {{ _('Default') }})</small></label>
|
||||
<input type="number" class="form-control" name="gcode_quota_mb" value="{{ user_quotas[user.id]['gcode'] }}" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AIO 3D Slicer</title>
|
||||
<link href="{{ url_for('static', filename='img/favicon.ico') }}" rel="icon" type="image/x-icon">
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
@@ -131,6 +132,11 @@
|
||||
<i class="bi bi-arrows-move me-2"></i>{{ _('Control') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item mb-1">
|
||||
<a class="nav-link text-dark {% if request.endpoint == 'printer.helper_printer' %}active text-white shadow-sm{% endif %}" href="{{ url_for('printer.helper_printer') }}">
|
||||
<i class="bi bi-question-square me-2"></i>{{ _('Printer Helper') }}
|
||||
</a>
|
||||
</li>
|
||||
<!-- {% 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') }}">
|
||||
@@ -185,6 +191,11 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item mb-1">
|
||||
<a class="nav-link text-dark {% if request.endpoint == 'main.helper_slice' %}active text-white shadow-sm{% endif %}" href="{{ url_for('main.helper_slice') }}">
|
||||
<i class="bi bi-question-square me-2"></i>{{ _('Slice Helper') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
|
||||
56
app/templates/printer/helper_printer.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% 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">{{ _('Printer Helper') }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Helper Content -->
|
||||
<div class="col-12 markdown-body p-4 bg-white rounded shadow-sm">
|
||||
{% if content_html %}
|
||||
{{ content_html|safe }}
|
||||
{% else %}
|
||||
<p class="text-muted">{{ _('Documentation not available.') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 简单的 Markdown 样式优化 */
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-body h1 {
|
||||
font-size: 2em;
|
||||
padding-bottom: .3em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
}
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.markdown-body pre {
|
||||
background-color: #f6f8fa;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.markdown-body code {
|
||||
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
|
||||
}
|
||||
.markdown-body table {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
color: #212529;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.markdown-body table th, .markdown-body table td {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -84,7 +84,7 @@
|
||||
<span class="badge bg-secondary font-monospace">{{ s.ip_address }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">{{ s.last_active.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||
<small class="text-muted local-time" data-utc="{{ s.last_active.isoformat() }}Z">{{ s.last_active.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
{% if s.session_token != current_token %}
|
||||
@@ -111,4 +111,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
document.querySelectorAll('.local-time').forEach(function(el) {
|
||||
const utcDate = new Date(el.getAttribute('data-utc'));
|
||||
if (!isNaN(utcDate)) {
|
||||
|
||||
const pad = (n) => n.toString().padStart(2, '0');
|
||||
const yyyy = utcDate.getFullYear();
|
||||
const MM = pad(utcDate.getMonth() + 1);
|
||||
const dd = pad(utcDate.getDate());
|
||||
const HH = pad(utcDate.getHours());
|
||||
const mm = pad(utcDate.getMinutes());
|
||||
const ss = pad(utcDate.getSeconds());
|
||||
|
||||
el.textContent = `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
56
app/templates/slice/helper_slice.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% 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">{{ _('Slice Helper') }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Helper Content -->
|
||||
<div class="col-12 markdown-body p-4 bg-white rounded shadow-sm">
|
||||
{% if content_html %}
|
||||
{{ content_html|safe }}
|
||||
{% else %}
|
||||
<p class="text-muted">{{ _('Documentation not available.') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 简单的 Markdown 样式优化 */
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-body h1 {
|
||||
font-size: 2em;
|
||||
padding-bottom: .3em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
}
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.markdown-body pre {
|
||||
background-color: #f6f8fa;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.markdown-body code {
|
||||
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
|
||||
}
|
||||
.markdown-body table {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
color: #212529;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.markdown-body table th, .markdown-body table td {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -110,6 +110,10 @@ class OctoPrintClient:
|
||||
"""Get information about the current print job and progress."""
|
||||
return self._request("GET", "/api/job")
|
||||
|
||||
def get_printer_err_log(self):
|
||||
"""Fetch the printer error log, if available."""
|
||||
return self._request("GET", "/api/printer/error")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Printer Control
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -8,7 +8,7 @@ def get_all_engines():
|
||||
PrusaSlicerEngine()
|
||||
]
|
||||
|
||||
def get_slicer_engine(engine_name="prusa", print_config_folder=None):
|
||||
def get_slicer_engine(engine_name="prusa", print_config_folder=None, config_slice_bin_path=None):
|
||||
"""
|
||||
Factory function to retrieve the requested slicing engine instance.
|
||||
Valid names: 'cura', 'prusa_slicer'
|
||||
@@ -18,7 +18,7 @@ def get_slicer_engine(engine_name="prusa", print_config_folder=None):
|
||||
if engine_name in ['cura', 'cura_engine', 'curaengine']:
|
||||
return CuraEngine(print_config_folder)
|
||||
elif engine_name in ['prusa', 'prusa_slicer', 'prusaslicer']:
|
||||
return PrusaSlicerEngine(print_config_folder)
|
||||
return PrusaSlicerEngine(print_config_folder, config_slice_bin_path)
|
||||
else:
|
||||
# Default fallback
|
||||
return PrusaSlicerEngine(print_config_folder)
|
||||
return PrusaSlicerEngine(print_config_folder, config_slice_bin_path)
|
||||
|
||||
@@ -20,8 +20,6 @@ class CuraEngine:
|
||||
return result.returncode == 0 or b"Usage:" in result.stdout or b"Usage:" in result.stderr
|
||||
except (FileNotFoundError, OSError):
|
||||
return False
|
||||
self.display_name = "UltiMaker Cura"
|
||||
self.is_available = self._check_available()
|
||||
|
||||
|
||||
def slice(self, app, stl_filepath, gcode_filepath, **kwargs):
|
||||
|
||||
@@ -5,25 +5,19 @@ import uuid
|
||||
import shutil
|
||||
from app.models import SystemConfig
|
||||
|
||||
# Default PrusaSlicer AppImage (aarch64) download URL
|
||||
PRUSA_DOWNLOAD_URL = "https://github.com/davidk/PrusaSlicer-ARM.AppImage/releases/download/version_2.9.4/PrusaSlicer-2.9.4-aarch64-full.AppImage"
|
||||
|
||||
class PrusaSlicerEngine:
|
||||
def __init__(self, print_config_folder=None):
|
||||
def __init__(self, print_config_folder=None, config_slice_bin_path=None):
|
||||
self.name = "prusa_slicer"
|
||||
self.display_name = "PrusaSlicer"
|
||||
self.config_slice_bin_path = config_slice_bin_path
|
||||
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:
|
||||
# Prefer explicit environment variable, then PATH, then a bundled AppImage under the repo
|
||||
prusa_bin = os.environ.get('PRUSA_SLICER_BIN') or shutil.which('prusa-slicer') or shutil.which('prusa-slicer.exe')
|
||||
if not prusa_bin:
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||
local_appimage = os.path.join(repo_root, 'prusaslicer', os.path.basename(PRUSA_DOWNLOAD_URL))
|
||||
if os.path.isfile(local_appimage) and os.access(local_appimage, os.X_OK):
|
||||
prusa_bin = local_appimage
|
||||
prusa_bin = self.config_slice_bin_path or shutil.which('prusa-slicer') or shutil.which('prusa-slicer.exe')
|
||||
if not prusa_bin:
|
||||
return False
|
||||
result = subprocess.run([prusa_bin, "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@@ -44,30 +38,7 @@ class PrusaSlicerEngine:
|
||||
Slices via prusa-slicer CLI mapping standard kwargs to PRUSA parameters where possible.
|
||||
"""
|
||||
try:
|
||||
# Determine prusa-slicer binary location (env -> PATH -> bundled appimage in repo)
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||
prusa_env = os.environ.get('PRUSA_SLICER_BIN')
|
||||
candidates = []
|
||||
if prusa_env:
|
||||
candidates.append(prusa_env)
|
||||
which_bin = shutil.which('prusa-slicer') or shutil.which('prusa-slicer.exe')
|
||||
if which_bin:
|
||||
candidates.append(which_bin)
|
||||
candidates.extend([
|
||||
os.path.join(repo_root, 'prusaslicer', 'prusa-slicer'),
|
||||
os.path.join(repo_root, 'prusaslicer', os.path.basename(PRUSA_DOWNLOAD_URL)),
|
||||
os.path.join(repo_root, os.path.basename(PRUSA_DOWNLOAD_URL))
|
||||
])
|
||||
|
||||
prusa_bin = None
|
||||
for c in candidates:
|
||||
if c and os.path.isfile(c) and os.access(c, os.X_OK):
|
||||
prusa_bin = c
|
||||
break
|
||||
|
||||
if not prusa_bin:
|
||||
# fallback to plain name so subprocess will try PATH and give a clear error
|
||||
prusa_bin = 'prusa-slicer'
|
||||
prusa_bin = self.config_slice_bin_path or shutil.which('prusa-slicer') or shutil.which('prusa-slicer.exe')
|
||||
|
||||
# Base command
|
||||
command = [prusa_bin, '-g', stl_filepath, '--output', gcode_filepath]
|
||||
|
||||