forked from viewit/KX-Bridge-Release
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 326453e2fd | |||
| ffd8ed09d5 | |||
| dc7e92688b | |||
| 3e1ba9df4b | |||
| 41f4700b24 | |||
| 216b2de2c0 | |||
| 394b0e69ab | |||
| 877cddb1ba | |||
| c9043e9630 | |||
| 6165a7f62a | |||
| fa8e0c1491 | |||
| 282c02ae0a | |||
| 72f77d92af | |||
| 3595cf839c | |||
| 2b39cc1a78 | |||
| d20308cf2c | |||
| 710c4831c2 | |||
| 5bff7adad0 |
31
.claude/agents/changelog.md
Normal file
31
.claude/agents/changelog.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: KX-Bridge Changelog
|
||||
description: Generiert einen CHANGELOG.md Eintrag aus Git-Commits seit dem letzten Tag.
|
||||
tools:
|
||||
- run_command
|
||||
- read_file
|
||||
- write_file
|
||||
---
|
||||
|
||||
Du generierst CHANGELOG.md Einträge für KX-Bridge.
|
||||
|
||||
Vorgehen:
|
||||
1. Führe aus: `git log $(git describe --tags --abbrev=0)..HEAD --oneline`
|
||||
2. Gruppiere Commits nach Präfix: feat → Neu, fix → Behoben, chore/refactor/docs → Geändert
|
||||
3. Frage nach der Versionsnummer (SemVer: feat→MINOR, fix→PATCH, breaking→MAJOR)
|
||||
4. Schreibe den Abschnitt im Format:
|
||||
|
||||
```
|
||||
## [VERSION] - DATUM
|
||||
|
||||
### Neu
|
||||
- ...
|
||||
|
||||
### Behoben
|
||||
- ...
|
||||
|
||||
### Geändert
|
||||
- ...
|
||||
```
|
||||
|
||||
5. Füge den Abschnitt am Anfang der bestehenden CHANGELOG.md ein, ohne vorhandene Einträge zu ändern.
|
||||
32
.claude/agents/docker-check.md
Normal file
32
.claude/agents/docker-check.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: KX-Bridge Docker Check
|
||||
description: Prüft Dockerfile, docker-compose und das gebaute Image auf häufige Probleme.
|
||||
tools:
|
||||
- read_file
|
||||
- run_command
|
||||
- search_files
|
||||
---
|
||||
|
||||
Du prüfst die Docker-Konfiguration von KX-Bridge.
|
||||
|
||||
**Dockerfile:**
|
||||
- Base-Image aktuell? (`python:3.11-slim` oder neuer)
|
||||
- `.dockerignore` vorhanden und vollständig?
|
||||
- Keine Secrets oder Zertifikate im Image (`anycubic_slicer.crt/.key` darf NICHT eingebettet sein)
|
||||
- Healthcheck vorhanden?
|
||||
- Kein `COPY . .` ohne `.dockerignore`
|
||||
|
||||
**docker-compose.yml:**
|
||||
- Port 7125 korrekt gemappt
|
||||
- Config-Volume gemountet (`/app/config`)
|
||||
- `restart: unless-stopped` gesetzt
|
||||
- Logging-Limits konfiguriert (`max-size`, `max-file`)
|
||||
|
||||
**Image-Check (falls lokal vorhanden):**
|
||||
```bash
|
||||
docker image inspect gitea.it-drui.de/viewit/kx-bridge:nightly
|
||||
```
|
||||
- Image-Größe sinnvoll (< 500MB)?
|
||||
- Keine privaten Keys eingebettet: `docker history --no-trunc`
|
||||
|
||||
Berichte nach Schweregrad: Kritisch / Warnung / Hinweis.
|
||||
23
.claude/agents/moonraker-debug.md
Normal file
23
.claude/agents/moonraker-debug.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: KX-Bridge Moonraker Debug
|
||||
description: Analysiert Moonraker/Klipper Logs und KX-Bridge Ausgaben auf Fehlerursachen.
|
||||
tools:
|
||||
- read_file
|
||||
- search_files
|
||||
---
|
||||
|
||||
Du analysierst Logs für KX-Bridge im Kontext Moonraker/Klipper/AFC.
|
||||
|
||||
**Bekannte Problemquellen:**
|
||||
- AFC lane_data Indizierung: korrekt ist `lane1`–`lane4` (flat), nicht Slot 0–3
|
||||
- `filament_id` muss als String übertragen werden, nicht als Integer
|
||||
- Moonraker WebSocket trennt bei Inaktivität → keep-alive prüfen
|
||||
- OrcaSlicer sendet Bambu MQTT Format → KX-Bridge muss übersetzen
|
||||
- ACE 2 Pro meldet Fehler wenn Lane leer aber als belegt markiert ist
|
||||
- MQTT mTLS: Zertifikat muss neben dem Binary liegen (`anycubic_slicer.crt/.key`)
|
||||
|
||||
**Bei einem Log:**
|
||||
1. Identifiziere den **ersten** Fehler (nicht den letzten Symptom)
|
||||
2. Zeige den relevanten Log-Kontext (±10 Zeilen um den Fehler)
|
||||
3. Nenne die wahrscheinliche Ursache
|
||||
4. Schlage einen konkreten Fix vor (Datei + Funktion wenn möglich)
|
||||
25
.claude/agents/nightly-prep.md
Normal file
25
.claude/agents/nightly-prep.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: KX-Bridge Nightly Prep
|
||||
description: Bereitet den PR von nightly nach main vor. Prüft ob alle Voraussetzungen für ein Stable Release erfüllt sind.
|
||||
tools:
|
||||
- run_command
|
||||
- read_file
|
||||
---
|
||||
|
||||
Du bereitest einen nightly → main Merge für KX-Bridge vor.
|
||||
|
||||
Führe folgende Checks aus und berichte:
|
||||
|
||||
1. `git log main..nightly --oneline` → alle Commits die noch nicht in main sind
|
||||
2. `git diff main..nightly -- CHANGELOG.md` → ist CHANGELOG.md für alle Änderungen aktualisiert?
|
||||
3. Prüfe ob `tests/` alle geänderten Module abdeckt
|
||||
4. Prüfe ob Dockerfile ein aktuelles Base-Image verwendet
|
||||
5. Schlage eine SemVer-Versionsnummer vor:
|
||||
- `feat:` Commits → MINOR erhöhen
|
||||
- `fix:` Commits → PATCH erhöhen
|
||||
- Breaking Change im Commit-Body → MAJOR erhöhen
|
||||
|
||||
Abschlussbericht:
|
||||
- ✅ Bereit für Release
|
||||
- ⚠️ Offen: [Liste]
|
||||
- ❌ Blockiert durch: [Grund]
|
||||
24
.claude/agents/reviewer.md
Normal file
24
.claude/agents/reviewer.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: KX-Bridge Reviewer
|
||||
description: Reviewt geänderte Dateien vor einem PR auf nightly. Prüft Logik, Fehlerbehandlung, Moonraker-Kompatibilität und Stil.
|
||||
tools:
|
||||
- read_file
|
||||
- list_directory
|
||||
- search_files
|
||||
---
|
||||
|
||||
Du bist Code-Reviewer für KX-Bridge — eine Python-Bridge zwischen OrcaSlicer und Moonraker/Klipper für den Anycubic Kobra X.
|
||||
|
||||
Beim Review prüfst du:
|
||||
- Korrekte Fehlerbehandlung bei Moonraker HTTP/MQTT Calls (keine unbehandelten Exceptions)
|
||||
- Keine hardcodierten IPs oder Ports (müssen aus config.ini kommen)
|
||||
- Thread-Sicherheit bei parallelen Moonraker-Abfragen (asyncio korrekt verwendet)
|
||||
- AFC lane_data Struktur: flache Indizierung lane1–lane4, kein Slot-Mapping 0–3
|
||||
- Kein `print()` statt `logging` (außer in CLI-Hilfsfunktionen)
|
||||
- Typ-Annotationen vorhanden, Python 3.8+ kompatibel (kein `X | Y` Syntax)
|
||||
- Tests für neue öffentliche Funktionen vorhanden
|
||||
|
||||
Ausgabeformat:
|
||||
1. **Kritische Fehler** — blockieren den Merge
|
||||
2. **Warnungen** — sollten vor Merge behoben werden
|
||||
3. **Hinweise** — optional, für zukünftige Verbesserungen
|
||||
26
.claude/agents/test-writer.md
Normal file
26
.claude/agents/test-writer.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: KX-Bridge Test Writer
|
||||
description: Leitet pytest-Tests aus geänderten oder neuen Python-Dateien ab.
|
||||
tools:
|
||||
- read_file
|
||||
- write_file
|
||||
- list_directory
|
||||
- search_files
|
||||
---
|
||||
|
||||
Du schreibst pytest-Tests für KX-Bridge.
|
||||
|
||||
Kontext:
|
||||
- Moonraker API läuft auf Port 7125 (HTTP + WebSocket)
|
||||
- AFC lane_data: flache Indizierung lane1–lane4
|
||||
- Externe HTTP-Calls zu Moonraker werden mit `unittest.mock` gemockt
|
||||
- Python 3.8+ Kompatibilität (kein `X | Y` Union-Syntax)
|
||||
|
||||
Für jede zu testende Funktion schreibst du:
|
||||
1. Happy Path (Normalfall mit validen Eingaben)
|
||||
2. Fehlerfall (Moonraker nicht erreichbar, Timeout, falsche Antwort)
|
||||
3. Grenzwerte (leere lane_data, ungültige filament_id, None-Werte)
|
||||
|
||||
Dateiname: `tests/test_<modulname>.py`
|
||||
Verwende pytest-Fixtures für Moonraker-Mock-Responses.
|
||||
Keine echten Netzwerkaufrufe in Tests.
|
||||
12
.claude/settings.json
Normal file
12
.claude/settings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"project": "KX-Bridge",
|
||||
"language": "de",
|
||||
"defaultAgent": "reviewer",
|
||||
"context": {
|
||||
"repoBase": "gitea.it-drui.de/viewit/KX-Bridge-Release",
|
||||
"defaultBranch": "nightly",
|
||||
"stableBranch": "main",
|
||||
"registry": "gitea.it-drui.de/viewit/kx-bridge",
|
||||
"moonrakerPort": 7125
|
||||
}
|
||||
}
|
||||
29
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
29
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug in KX-Bridge
|
||||
labels: bug
|
||||
---
|
||||
|
||||
## Description
|
||||
<!-- What is happening? -->
|
||||
|
||||
## Steps to Reproduce
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
## Environment
|
||||
- KX-Bridge Version:
|
||||
- OrcaSlicer Version:
|
||||
- Moonraker/Klipper Version:
|
||||
- Operating System:
|
||||
- Installation: Docker / Binary
|
||||
|
||||
## Logs
|
||||
```
|
||||
<!-- docker logs kx-bridge --tail 50 -->
|
||||
```
|
||||
14
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
14
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature or improvement
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
## Description
|
||||
<!-- What should be added or improved? -->
|
||||
|
||||
## Motivation
|
||||
<!-- Why is this useful? What problem does it solve? -->
|
||||
|
||||
## Proposed Implementation
|
||||
<!-- Optional: How could this be implemented technically? -->
|
||||
21
.gitea/pull_request_template.md
Normal file
21
.gitea/pull_request_template.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## Description
|
||||
<!-- What does this PR change? -->
|
||||
|
||||
## Related Issue
|
||||
Closes #
|
||||
|
||||
## Type
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature
|
||||
- [ ] Documentation
|
||||
- [ ] Refactoring
|
||||
|
||||
## Tested with
|
||||
- OrcaSlicer Version:
|
||||
- Printer:
|
||||
- Moonraker/Klipper Version:
|
||||
|
||||
## Checklist
|
||||
- [ ] Tests added/updated
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] No debug code included
|
||||
55
.gitea/workflows/nightly.yml
Normal file
55
.gitea/workflows/nightly.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Nightly Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- nightly
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.it-drui.de
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build & push (amd64 + arm64)
|
||||
run: |
|
||||
DATE=$(date +%Y%m%d)
|
||||
TAGS="gitea.it-drui.de/viewit/kx-bridge:nightly"
|
||||
TAGS="$TAGS,gitea.it-drui.de/viewit/kx-bridge:nightly-$DATE"
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--provenance=false \
|
||||
--no-cache \
|
||||
$(echo "$TAGS" | tr ',' '\n' | sed 's/^/-t /') \
|
||||
.
|
||||
|
||||
- name: Set nightly tag
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
DATE=$(date +%Y%m%d)
|
||||
TAG="nightly-$DATE"
|
||||
git config user.name "gitea-actions"
|
||||
git config user.email "actions@it-drui.de"
|
||||
git tag -f "$TAG"
|
||||
git push origin "$TAG" --force
|
||||
31
.gitea/workflows/pr-check.yml
Normal file
31
.gitea/workflows/pr-check.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
name: PR Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- nightly
|
||||
|
||||
jobs:
|
||||
lint-and-test:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Dependencies installieren
|
||||
run: pip install -r requirements.txt
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
pip install flake8
|
||||
flake8 *.py --max-line-length=120 --extend-ignore=E501
|
||||
|
||||
- name: Tests
|
||||
run: |
|
||||
pip install pytest
|
||||
pytest tests/ -v
|
||||
if: ${{ hashFiles('tests/') != '' }}
|
||||
58
.gitea/workflows/release.yml
Normal file
58
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Stable Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.it-drui.de
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build & push (amd64 + arm64)
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--provenance=false \
|
||||
--no-cache \
|
||||
-t "gitea.it-drui.de/viewit/kx-bridge:latest" \
|
||||
-t "gitea.it-drui.de/viewit/kx-bridge:${VERSION}" \
|
||||
.
|
||||
|
||||
- name: Create Gitea Release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
CHANGELOG=$(awk "/^## \[${VERSION}\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md || echo "")
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases" \
|
||||
-d "{
|
||||
\"tag_name\": \"${VERSION}\",
|
||||
\"name\": \"KX-Bridge ${VERSION}\",
|
||||
\"body\": $(echo "$CHANGELOG" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))'),
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}"
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -17,3 +17,8 @@ config/*.ini
|
||||
data/
|
||||
|
||||
!data/orca_filaments.json
|
||||
|
||||
# Sensitive files — never commit
|
||||
.runner-token
|
||||
secrets/
|
||||
*.token
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.26] – 2026-06-21
|
||||
|
||||
### Neu
|
||||
- **Italienische Sprachunterstützung** (PR #66, @Alex_M). Die Bridge-UI ist jetzt vollständig auf Italienisch verfügbar.
|
||||
|
||||
### Behoben
|
||||
- **Kamera startete immer beim Druckbeginn** (Issue #50). `camera_on_print` fehlte in der `/api/state`-Antwort — JavaScript las `undefined` und startete die Kamera unabhängig vom Setting. Jetzt korrekt im State enthalten.
|
||||
- **Auto-Leveling-Setting wurde im Moonraker-Druckpfad ignoriert** (Issue #57). `handle_print_start` las den Wert nur aus den Bridge-Args, nicht aus dem Request-Body — Dialog-Checkbox und Per-Print-Override hatten keine Wirkung. Verhält sich jetzt identisch zum direkten Druckpfad.
|
||||
- **Filament-Mapping: Freitext-Felder durch Dropdowns ersetzt** (Issue #57). Falsch getippte Vendor/Name-Kombination brach das Profil-Matching ohne Fehlermeldung; Felder sind jetzt Dropdowns (Vendor → Profil, vendor-gefiltert), sodass nur gültige Kombinationen gespeichert werden können.
|
||||
- **Dashboard zeigte generischen Materialtyp statt Profilname** (Issue #57). AMS-Slot-Karten zeigen jetzt den gemappten Profilnamen (z.B. „eSUN PLA-Basic") statt nur „PLA". Fallback auf generischen Typ wenn kein Profil gemappt ist.
|
||||
- **Ghost-Profil auf leerem Slot** (Issue #57). Verwaiste Mappings für leere Slots wurden weiterhin angezeigt; leere Slots zeigen jetzt korrekt „–".
|
||||
- **Skip-Objects-Panel fehlte im Orca-Upload-Flow** (Issue #57). Panel erscheint jetzt in allen Druckflows; bei frischem Upload fragt die Bridge `fileDetails` beim Drucker nach und pollt die Objektliste bis zu 6 Sekunden nach.
|
||||
- **Banner und Dialog erschienen gleichzeitig** (Issue #57). Settings-Save setzt jetzt den Dialog-Cancel-State zurück, sodass der Slot-Mapper nach Wechsel des Start-Print-Verhaltens zuverlässig öffnet.
|
||||
- **„Leeren" lud idle-Datei beim nächsten Poll nach** (Issue #57). Leeren setzt jetzt den lokalen State sofort zurück (`file_ready`, `filename`, `thumbnail`) und löscht alle Dialog-Sperren — Vorschaubild und Aktions-Buttons verschwinden sofort und kommen nicht zurück.
|
||||
- **Material-Matching für „PLA Silk", „Matte PLA" etc.** (PR #64, @p2l). Modifier+Basis-Muster in beliebiger Wortreihenfolge werden jetzt auf den Basis-Typ normalisiert; Dash-Varianten (PLA-CF) bleiben weiterhin korrekt inkompatibel mit ihrem Basis-Typ.
|
||||
|
||||
## [0.9.25] – 2026-06-17
|
||||
|
||||
### Behoben
|
||||
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,5 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.26] – 2026-06-21
|
||||
|
||||
### New
|
||||
- **Italian language support** (PR #66, @Alex_M). The bridge UI is now fully
|
||||
available in Italian.
|
||||
|
||||
### Fixed
|
||||
- **Camera always started at print begin** (issue #50). `camera_on_print` was
|
||||
missing from the `/api/state` response — JavaScript read `undefined` and started
|
||||
the camera regardless of the setting. Now correctly exposed in state.
|
||||
- **Auto-leveling setting ignored in Moonraker print path** (issue #57).
|
||||
`handle_print_start` read the value only from bridge args, not from the request
|
||||
body, so the dialog checkbox and the per-print override had no effect. Now
|
||||
behaves identically to the direct print path.
|
||||
- **Filament mapping free-text fields replaced by dropdowns** (issue #57). A
|
||||
mistyped vendor/name broke profile matching silently; fields are now dropdowns
|
||||
(vendor → profile, vendor-filtered) so only valid combinations can be saved.
|
||||
- **Dashboard showed generic material type instead of profile name** (issue #57).
|
||||
AMS slot cards now display the mapped profile name (e.g. "eSUN PLA-Basic")
|
||||
instead of just "PLA". Falls back to the generic type when no profile is mapped.
|
||||
- **Ghost profile shown on empty slot** (issue #57). Stale mappings for empty
|
||||
slots were still rendered; empty slots now correctly show "–".
|
||||
- **Skip-Objects panel missing in Orca upload flow** (issue #57). Panel now
|
||||
appears in all print flows; on fresh upload the bridge requests `fileDetails`
|
||||
from the printer and retries the object list for up to 6 s.
|
||||
- **Banner and dialog appeared simultaneously** (issue #57). Settings save now
|
||||
resets the dialog cancel state so the slot mapper reliably opens after toggling
|
||||
Start Print Behavior.
|
||||
- **"Clear" reloaded idle file on next poll** (issue #57). Clear now immediately
|
||||
resets local state (`file_ready`, `filename`, `thumbnail`) and clears all dialog
|
||||
locks — the preview and action buttons disappear instantly and do not return.
|
||||
- **Material matching for "PLA Silk", "Matte PLA" etc.** (PR #64, @p2l).
|
||||
Modifier+base patterns in any word order are now normalised to the base type;
|
||||
dash-suffix variants (PLA-CF) remain correctly incompatible with their base.
|
||||
|
||||
## [0.9.25] – 2026-06-17
|
||||
|
||||
### Fixed
|
||||
|
||||
102
CONTRIBUTING.md
Normal file
102
CONTRIBUTING.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Contributing to KX-Bridge
|
||||
|
||||
Thanks for taking the time to contribute! Here's everything you need to know.
|
||||
|
||||
---
|
||||
|
||||
## How to report a bug or request a feature
|
||||
|
||||
Use the issue tracker:
|
||||
|
||||
- **Bug:** [New Bug Report](https://gitea.it-drui.de/viewit/KX-Bridge-Release/issues/new?template=bug_report.md)
|
||||
- **Feature:** [New Feature Request](https://gitea.it-drui.de/viewit/KX-Bridge-Release/issues/new?template=feature_request.md)
|
||||
|
||||
Please fill in the template — especially the **KX-Bridge version** and **logs**.
|
||||
Issues without version info are hard to debug.
|
||||
|
||||
---
|
||||
|
||||
## How to submit a Pull Request
|
||||
|
||||
### 1. Fork the repository
|
||||
|
||||
Click **Fork** at the top of this page.
|
||||
You now have your own copy at `gitea.it-drui.de/your-username/KX-Bridge-Release`.
|
||||
|
||||
### 2. Clone your fork
|
||||
|
||||
```bash
|
||||
git clone https://gitea.it-drui.de/your-username/KX-Bridge-Release.git
|
||||
cd KX-Bridge-Release
|
||||
```
|
||||
|
||||
### 3. Create a branch
|
||||
|
||||
Always branch off `nightly`:
|
||||
|
||||
```bash
|
||||
git checkout nightly
|
||||
git checkout -b feature/my-feature # or fix/my-fix
|
||||
```
|
||||
|
||||
### 4. Make your changes
|
||||
|
||||
- Test your changes locally with Docker:
|
||||
```bash
|
||||
docker build -t kx-bridge:dev .
|
||||
docker run -p 7125:7125 -v ./config:/app/config kx-bridge:dev
|
||||
```
|
||||
- No debug `print()` statements — use `logging`
|
||||
- Keep commits focused; one thing per commit
|
||||
|
||||
### 5. Push and open a PR
|
||||
|
||||
```bash
|
||||
git push origin feature/my-feature
|
||||
```
|
||||
|
||||
Gitea will show a banner — click **"Create Pull Request"**.
|
||||
The PR template will be pre-filled. Set the target branch to **`nightly`**.
|
||||
|
||||
---
|
||||
|
||||
## Branch model
|
||||
|
||||
```
|
||||
main ← stable releases only (merged by maintainer)
|
||||
nightly ← integration branch — PRs go here
|
||||
feature/* ← your feature branch (in your fork)
|
||||
fix/* ← your bugfix branch (in your fork)
|
||||
```
|
||||
|
||||
Your PR always targets `nightly`. The maintainer periodically merges `nightly → main` for a new stable release.
|
||||
|
||||
---
|
||||
|
||||
## Commit style
|
||||
|
||||
Use conventional commit prefixes:
|
||||
|
||||
| Prefix | When |
|
||||
|---|---|
|
||||
| `feat:` | new feature |
|
||||
| `fix:` | bug fix |
|
||||
| `docs:` | documentation only |
|
||||
| `chore:` | maintenance, dependencies |
|
||||
| `refactor:` | code change without new feature or fix |
|
||||
|
||||
Example: `fix: prevent crash when printer is offline during startup`
|
||||
|
||||
---
|
||||
|
||||
## Language
|
||||
|
||||
- **Code and comments:** English
|
||||
- **Issue comments:** match the language of the issue (if someone writes in German, reply in German)
|
||||
- **Commit messages:** English
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
Open a [Discussion](https://gitea.it-drui.de/viewit/KX-Bridge-Release/issues) or leave a comment on the relevant issue.
|
||||
16
README.de.md
16
README.de.md
@@ -15,6 +15,15 @@ Feedback willkommen.</sub>
|
||||
|
||||
<sub>🇬🇧 <a href="README.md">English version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
|
||||
|
||||
</div>
|
||||
|
||||
> [!CAUTION]
|
||||
> **Laufende Wartungsarbeiten diese Woche** — Wir strukturieren das Repository um (Branch-Modell, CI-Workflows, Contribution-Prozess). Es kann zu Änderungen bei Branch-Namen, PR-Templates und der Art wie Releases veröffentlicht werden kommen. Wir entschuldigen uns für etwaige Unannehmlichkeiten. Handling, Workflow und die langfristige Wartbarkeit werden sich dadurch deutlich verbessern.
|
||||
>
|
||||
> 👉 Möchtest du beitragen? Bitte lies zuerst [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br>
|
||||
|
||||
[](https://ko-fi.com/viewitde)
|
||||
@@ -103,13 +112,6 @@ Drucker → Verbindungstyp **Moonraker** → Host: `http://BRIDGE-IP:7125`
|
||||
> ⚠️ Verbindungstyp muss **Moonraker** sein (nicht „Bambu" oder „Klipper").
|
||||
> Vollständige URL inkl. `http://` und Port `:7125` im Host-Feld eintragen.
|
||||
|
||||
---
|
||||
|
||||
## 📺 Video-Tutorial
|
||||
|
||||
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Empfohlener Slicer
|
||||
|
||||
|
||||
14
README.es.md
14
README.es.md
@@ -14,6 +14,15 @@ ninguna está oficialmente probada ni soportada. Se agradece el feedback.</sub>
|
||||
|
||||
<sub>🇬🇧 <a href="README.md">English version</a> · 🇩🇪 <a href="README.de.md">Deutsche Version</a></sub>
|
||||
|
||||
</div>
|
||||
|
||||
> [!CAUTION]
|
||||
> **Trabajos de mantenimiento en curso esta semana** — Estamos reestructurando el repositorio (modelo de ramas, flujos CI, proceso de contribución). Es posible que notes cambios en los nombres de ramas, plantillas de PR y la forma en que se publican las versiones. Pedimos disculpas por los inconvenientes. El manejo, el flujo de trabajo y la mantenibilidad a largo plazo mejorarán significativamente como resultado.
|
||||
>
|
||||
> 👉 ¿Quieres contribuir? Por favor lee primero [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br>
|
||||
|
||||
[](https://ko-fi.com/viewitde)
|
||||
@@ -130,11 +139,6 @@ Impresora → Tipo de conexión **Moonraker** → Host: `http://IP-DEL-PUENTE:71
|
||||
|
||||
---
|
||||
|
||||
## 📺 Vídeo tutorial
|
||||
|
||||
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Slicer recomendado
|
||||
|
||||
|
||||
15
README.md
15
README.md
@@ -14,6 +14,15 @@ officially tested or supported. Feedback welcome.</sub>
|
||||
|
||||
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
|
||||
|
||||
</div>
|
||||
|
||||
> [!CAUTION]
|
||||
> **Ongoing maintenance work this week** — We are restructuring the repository (branch model, CI workflows, contribution process). You may notice changes to branch names, PR templates, and how releases are published. We apologise for any inconvenience. Handling, workflow, and long-term maintainability will be significantly improved as a result.
|
||||
>
|
||||
> 👉 Want to contribute? Please read [CONTRIBUTING.md](CONTRIBUTING.md) first.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br>
|
||||
|
||||
[](https://ko-fi.com/viewitde)
|
||||
@@ -102,13 +111,7 @@ Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125`
|
||||
> ⚠️ Connection type must be **Moonraker** (not "Bambu" or "Klipper").
|
||||
> Enter the full URL including `http://` and port `:7125` in the host field.
|
||||
|
||||
---
|
||||
|
||||
## 📺 Video Tutorial
|
||||
|
||||
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Recommended Slicer
|
||||
|
||||
|
||||
31
agents.md
Normal file
31
agents.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# KX-Bridge Claude Agents
|
||||
|
||||
## Available Agents
|
||||
|
||||
| Agent | File | When to use |
|
||||
|---|---|---|
|
||||
| Reviewer | `.claude/agents/reviewer.md` | Before every PR — checks logic, error handling, Moonraker compatibility |
|
||||
| Changelog | `.claude/agents/changelog.md` | After merge to nightly — generates CHANGELOG.md entry from commits |
|
||||
| Test Writer | `.claude/agents/test-writer.md` | When adding new functions — derives pytest tests |
|
||||
| Nightly Prep | `.claude/agents/nightly-prep.md` | Before a release — checks readiness of nightly → main merge |
|
||||
| Docker Check | `.claude/agents/docker-check.md` | Before image push — validates Dockerfile and compose config |
|
||||
| Moonraker Debug | `.claude/agents/moonraker-debug.md` | On runtime errors — analyzes Moonraker/Klipper logs |
|
||||
|
||||
## Usage
|
||||
|
||||
In VS Code with Claude Code extension:
|
||||
```
|
||||
@reviewer → code review of current changes
|
||||
@changelog → generate CHANGELOG entry
|
||||
@test-writer → write tests for changed files
|
||||
@nightly-prep → check release readiness
|
||||
@docker-check → validate Docker config
|
||||
@moonraker-debug → analyze logs
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
- Moonraker API: Port 7125
|
||||
- AFC lane_data: flat indexing lane1–lane4
|
||||
- Registry: `gitea.it-drui.de/viewit/kx-bridge`
|
||||
- Default PR target: `nightly`
|
||||
@@ -13,6 +13,7 @@ _BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) els
|
||||
CONFIG_SECTION_CONNECTION = "connection"
|
||||
CONFIG_SECTION_PRINT = "print"
|
||||
CONFIG_SECTION_BRIDGE = "bridge"
|
||||
CONFIG_SECTION_SPOOLMAN = "spoolman"
|
||||
|
||||
|
||||
def _find_config_file() -> pathlib.Path | None:
|
||||
@@ -63,6 +64,8 @@ def _load_config_file(path: pathlib.Path):
|
||||
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
|
||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||
"SPOOLMAN_SERVER": (CONFIG_SECTION_SPOOLMAN, "server"),
|
||||
"SPOOLMAN_SYNC_RATE": (CONFIG_SECTION_SPOOLMAN, "sync_rate"),
|
||||
}
|
||||
for env_key, (section, option) in mapping.items():
|
||||
if env_key not in os.environ:
|
||||
@@ -317,3 +320,5 @@ AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
SPOOLMAN_SERVER = get("SPOOLMAN_SERVER", "")
|
||||
SPOOLMAN_SYNC_RATE = int(get("SPOOLMAN_SYNC_RATE", "0"))
|
||||
|
||||
210
docker-compose-KX.yml
Normal file
210
docker-compose-KX.yml
Normal file
@@ -0,0 +1,210 @@
|
||||
# KobraX Full Stack — KX-Bridge + Obico Self-Hosted + Spoolman
|
||||
#
|
||||
# Für Portainer: Stack → Add Stack → Upload → diese Datei wählen
|
||||
#
|
||||
# Voraussetzung: Obico-Images einmalig in Gitea-Registry pushen:
|
||||
# docker tag obico-server-web:latest gitea.it-drui.de/viewit/obico-web:latest
|
||||
# docker tag obico-server-ml_api:latest gitea.it-drui.de/viewit/obico-ml:latest
|
||||
# docker tag obico-server-tasks:latest gitea.it-drui.de/viewit/obico-tasks:latest
|
||||
# docker push gitea.it-drui.de/viewit/obico-web:latest
|
||||
# docker push gitea.it-drui.de/viewit/obico-ml:latest
|
||||
# docker push gitea.it-drui.de/viewit/obico-tasks:latest
|
||||
#
|
||||
# Persistente Daten: /mnt/dockerdata/KobraXStack/<service>/
|
||||
#
|
||||
# Ports:
|
||||
# 7125 — KX-Bridge (Moonraker-API)
|
||||
# 3334 — Obico (Web-UI)
|
||||
# 7912 — Spoolman (Web-UI)
|
||||
#
|
||||
# Obico Admin-Account nach dem ersten Start:
|
||||
# docker exec obico-web python manage.py createsuperuser
|
||||
|
||||
x-obico-base: &obico-base
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /mnt/dockerdata/KobraXStack/obico/data:/data
|
||||
- /mnt/dockerdata/KobraXStack/obico/frontend:/frontend
|
||||
depends_on:
|
||||
- obico-redis
|
||||
environment:
|
||||
DEBUG: "False"
|
||||
REDIS_URL: "redis://obico-redis:6379"
|
||||
DATABASE_URL: "sqlite:////data/db.sqlite3"
|
||||
INTERNAL_MEDIA_HOST: "http://obico-web:3334"
|
||||
ML_API_HOST: "http://obico-ml:3333"
|
||||
ACCOUNT_ALLOW_SIGN_UP: "False"
|
||||
SITE_USES_HTTPS: "False"
|
||||
SITE_IS_PUBLIC: "False"
|
||||
DJANGO_SECRET_KEY: "change-me-to-a-random-secret-key-before-use"
|
||||
WEBPACK_LOADER_ENABLED: "False"
|
||||
networks:
|
||||
- kobrax-stack
|
||||
|
||||
services:
|
||||
|
||||
# ── KX-Bridge ───────────────────────────────────────────────
|
||||
kx-bridge:
|
||||
image: gitea.it-drui.de/viewit/kx-bridge:latest
|
||||
container_name: kx-bridge
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "7125:7125"
|
||||
volumes:
|
||||
- /mnt/dockerdata/KobraXStack/kx-bridge/config:/app/config
|
||||
- /mnt/dockerdata/KobraXStack/kx-bridge/data:/app/data
|
||||
networks:
|
||||
- kobrax-stack
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ── Spoolman ────────────────────────────────────────────────
|
||||
spoolman:
|
||||
image: ghcr.io/donkie/spoolman:latest
|
||||
container_name: spoolman
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "7912:8000"
|
||||
volumes:
|
||||
- /mnt/dockerdata/KobraXStack/spoolman:/home/app/.local/share/spoolman
|
||||
networks:
|
||||
- kobrax-stack
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ── Obico Redis ─────────────────────────────────────────────
|
||||
obico-redis:
|
||||
image: redis:7.2-alpine
|
||||
container_name: obico-redis
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /mnt/dockerdata/KobraXStack/obico/redis:/data
|
||||
networks:
|
||||
- kobrax-stack
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
start_period: 10s
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "5m"
|
||||
max-file: "2"
|
||||
|
||||
# ── Obico ML API ────────────────────────────────────────────
|
||||
obico-ml:
|
||||
image: gitea.it-drui.de/viewit/obico-ml:latest
|
||||
container_name: obico-ml
|
||||
restart: unless-stopped
|
||||
command: bash -c "gunicorn --bind 0.0.0.0:3333 --workers 1 wsgi"
|
||||
working_dir: /app
|
||||
environment:
|
||||
DEBUG: "False"
|
||||
FLASK_APP: "server.py"
|
||||
networks:
|
||||
- kobrax-stack
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3333/hc/ || exit 1"]
|
||||
start_period: 30s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ── Obico Web ───────────────────────────────────────────────
|
||||
obico-web:
|
||||
<<: *obico-base
|
||||
image: gitea.it-drui.de/viewit/obico-web:latest
|
||||
container_name: obico-web
|
||||
ports:
|
||||
- "3334:3334"
|
||||
depends_on:
|
||||
- obico-ml
|
||||
- obico-redis
|
||||
command: >
|
||||
sh -c 'python manage.py migrate &&
|
||||
python manage.py shell -c "from django.contrib.sites.models import Site; s=Site.objects.first(); s.domain=\"192.168.178.204:3334\"; s.name=\"Obico\"; s.save()" &&
|
||||
python manage.py collectstatic --noinput &&
|
||||
daphne -b 0.0.0.0 -p 3334 config.routing:application'
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3334/hc/ || exit 1"]
|
||||
start_period: 60s
|
||||
interval: 90s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ── Obico Tasks (Celery) ────────────────────────────────────
|
||||
obico-tasks:
|
||||
<<: *obico-base
|
||||
image: gitea.it-drui.de/viewit/obico-tasks:latest
|
||||
container_name: obico-tasks
|
||||
command: sh -c "celery -A config worker --beat -l info -c 2 -Q realtime,celery"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ── moonraker-obico Plugin ──────────────────────────────────
|
||||
# Verbindet KX-Bridge mit dem Obico-Server (Spaghetti-Detektion, Remote-UI)
|
||||
# Voraussetzung: /mnt/dockerdata/KobraXStack/moonraker-obico/moonraker-obico.cfg
|
||||
# muss existieren und einen gültigen auth_token enthalten.
|
||||
#
|
||||
# Token holen (nach erstem obico-web Start):
|
||||
# docker exec obico-web python manage.py shell -c "
|
||||
# from app.models import OneTimeVerificationCode, User
|
||||
# from django.utils import timezone; from datetime import timedelta; import random
|
||||
# u = User.objects.first()
|
||||
# c = OneTimeVerificationCode.objects.create(user=u, code='%06d' % random.randint(100000,999999), expired_at=timezone.now()+timedelta(hours=2))
|
||||
# print('CODE:', c.code)"
|
||||
# curl -X POST 'http://localhost:3334/api/v1/octo/verify/?code=<CODE>'
|
||||
# → printer.auth_token aus der Antwort in die cfg eintragen
|
||||
moonraker-obico:
|
||||
image: gitea.it-drui.de/viewit/moonraker-obico:latest
|
||||
container_name: moonraker-obico
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /mnt/dockerdata/KobraXStack/moonraker-obico:/opt/printer_data/config
|
||||
- /mnt/dockerdata/KobraXStack/moonraker-obico/logs:/opt/printer_data/logs
|
||||
command: ["-c", "/opt/printer_data/config/moonraker-obico.cfg"]
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
kobrax-stack:
|
||||
driver: bridge
|
||||
|
||||
# Verzeichnisse müssen auf dem Host existieren:
|
||||
# mkdir -p /mnt/dockerdata/KobraXStack/kx-bridge/config \
|
||||
# /mnt/dockerdata/KobraXStack/kx-bridge/data \
|
||||
# /mnt/dockerdata/KobraXStack/spoolman \
|
||||
# /mnt/dockerdata/KobraXStack/obico/data \
|
||||
# /mnt/dockerdata/KobraXStack/obico/frontend \
|
||||
# /mnt/dockerdata/KobraXStack/obico/redis \
|
||||
# /mnt/dockerdata/KobraXStack/moonraker-obico/logs
|
||||
# Spoolman benötigt UID/GID 1000:
|
||||
# sudo chown -R 1000:1000 /mnt/dockerdata/KobraXStack/spoolman
|
||||
#
|
||||
# moonraker-obico Config anlegen (auth_token nach Obico-Setup eintragen):
|
||||
# cp /path/to/moonraker-obico.cfg.example /mnt/dockerdata/KobraXStack/moonraker-obico/moonraker-obico.cfg
|
||||
3
docker-compose.nightly.yml
Normal file
3
docker-compose.nightly.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
services:
|
||||
kx-bridge:
|
||||
image: gitea.it-drui.de/viewit/kx-bridge:nightly
|
||||
25
docker-compose.portainer-nightly.yml
Normal file
25
docker-compose.portainer-nightly.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
# KX-Bridge Nightly — Portainer Stack
|
||||
#
|
||||
# Paste this into Portainer → Stacks → Add stack → Web editor
|
||||
#
|
||||
# Uses the nightly build — may be unstable, for testing new features early.
|
||||
# For production use, see docker-compose.portainer.yml (:latest).
|
||||
|
||||
services:
|
||||
kx-bridge:
|
||||
image: gitea.it-drui.de/viewit/kx-bridge:nightly
|
||||
volumes:
|
||||
- /mnt/dockerdata/kx-nightly/config:/app/config
|
||||
- /mnt/dockerdata/kx-nightly/data:/app/data
|
||||
ports:
|
||||
# Port 7125 = first printer. Add 7126, 7127, … for each additional printer.
|
||||
- "7125:7125"
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# Verzeichnisse müssen auf dem Host existieren:
|
||||
# mkdir -p /mnt/dockerdata/kx-nightly/config /mnt/dockerdata/kx-nightly/data
|
||||
@@ -48,4 +48,6 @@ MODE_ID = get("MODE_ID", "")
|
||||
DEVICE_ID = get("DEVICE_ID", "")
|
||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
|
||||
@@ -733,6 +733,40 @@ class CameraCache:
|
||||
await asyncio.sleep(2.0)
|
||||
|
||||
|
||||
class SpoolmanClient:
|
||||
"""Thin synchronous HTTP client for Spoolman filament tracking.
|
||||
|
||||
Designed to be called from daemon threads (poll loop, _on_print callbacks).
|
||||
Uses requests (already in requirements) so no event-loop dependency.
|
||||
"""
|
||||
|
||||
def __init__(self, server_url: str, sync_rate: int = 0):
|
||||
self.server_url = server_url.rstrip("/")
|
||||
self.sync_rate = sync_rate
|
||||
|
||||
def _req(self, method: str, path: str, **kwargs):
|
||||
import requests
|
||||
r = requests.request(method, f"{self.server_url}{path}", timeout=5, **kwargs)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def health_check(self) -> bool:
|
||||
try:
|
||||
self._req("GET", "/api/v1/health")
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def list_spools(self) -> list:
|
||||
return self._req("GET", "/api/v1/spool")
|
||||
|
||||
def use_filament(self, spool_id: int, use_length_mm: float) -> None:
|
||||
"""Report consumed filament length in mm. Spoolman converts to weight
|
||||
using the spool's filament profile density."""
|
||||
self._req("PUT", f"/api/v1/spool/{spool_id}/use",
|
||||
json={"use_length": round(use_length_mm, 2)})
|
||||
|
||||
|
||||
class KobraXBridge:
|
||||
def __init__(self, client: KobraXClient, args=None, store=None, printer_id: str = "1", all_bridges=None):
|
||||
self.client = client
|
||||
@@ -793,6 +827,7 @@ class KobraXBridge:
|
||||
"file_ready": "",
|
||||
"print_start_dialog": getattr(args, "print_start_dialog", 1),
|
||||
"filament_mode": "toolhead",
|
||||
"supplies_usage": 0,
|
||||
"ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None},
|
||||
}
|
||||
self._ams_slots: list[dict] = [] # flat global list; each entry has global_index + box_id
|
||||
@@ -819,6 +854,18 @@ class KobraXBridge:
|
||||
self._pending_preprint_skip: list[str] = []
|
||||
self._pending_preprint_skip_deadline: float = 0.0
|
||||
|
||||
# Spoolman filament tracking
|
||||
_sm_url = (getattr(args, "spoolman_server", "") or "").strip()
|
||||
self._spoolman: SpoolmanClient | None = (
|
||||
SpoolmanClient(_sm_url, getattr(args, "spoolman_sync_rate", 0))
|
||||
if _sm_url else None
|
||||
)
|
||||
self._spoolman_slot_spools: dict[int, int] = {} # {ams_slot_idx: spoolman_spool_id}
|
||||
self._spoolman_slot_usage: dict[int, float] = {} # per-slot accumulated mm this print
|
||||
self._spoolman_slot_reported: dict[int, float] = {} # per-slot mm already sent to Spoolman
|
||||
self._spoolman_last_usage: float = 0.0 # supplies_usage at last attribution tick
|
||||
self._spoolman_last_sync: float = 0.0
|
||||
|
||||
# Theme-Name prüfen (keine Sonderzeichen oder Umlaute)
|
||||
raw_theme = (getattr(args, "ui_theme", None) or "default").strip()
|
||||
if not _UI_THEME_NAME_RE.match(raw_theme):
|
||||
@@ -837,6 +884,139 @@ class KobraXBridge:
|
||||
client.callbacks["light/report"] = self._on_light
|
||||
client.callbacks["skip/report"] = self._on_skip
|
||||
|
||||
if self._spoolman:
|
||||
threading.Thread(
|
||||
target=lambda: log.info(
|
||||
f"Spoolman: {'OK' if self._spoolman.health_check() else 'unreachable'} "
|
||||
f"at {self._spoolman.server_url}"
|
||||
),
|
||||
daemon=True, name="spoolman-health",
|
||||
).start()
|
||||
|
||||
# ── Spoolman helpers ──────────────────────────────────────────────────────
|
||||
|
||||
def _spoolman_filament_mm(self) -> float:
|
||||
"""Total filament_used_mm for the current print file from the GCode DB."""
|
||||
filename = self._state.get("filename", "")
|
||||
if not filename:
|
||||
return 0.0
|
||||
try:
|
||||
gf = self._store.get_file_by_name(filename)
|
||||
return float(gf.get("filament_used_mm") or 0.0) if gf else 0.0
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
def _spoolman_attribute_tick(self, activity_map: dict) -> None:
|
||||
"""Attribute the supplies_usage delta since last tick to the active slot.
|
||||
|
||||
Skips attribution during loading/unloading transitions (tool changes +
|
||||
purges) to avoid charging the wrong spool for purge material."""
|
||||
if not self._spoolman or not self._spoolman_slot_spools:
|
||||
return
|
||||
if self._state.get("print_state") != "printing":
|
||||
return
|
||||
current = self._state.get("supplies_usage", 0)
|
||||
delta = current - self._spoolman_last_usage
|
||||
self._spoolman_last_usage = current
|
||||
if delta <= 0:
|
||||
return
|
||||
loaded = self._ams_loaded_slot
|
||||
if loaded < 0:
|
||||
return
|
||||
if activity_map.get(loaded):
|
||||
return
|
||||
self._spoolman_slot_usage[loaded] = self._spoolman_slot_usage.get(loaded, 0.0) + delta
|
||||
|
||||
def _spoolman_unreported(self) -> dict[int, float]:
|
||||
"""Return {slot_idx: mm} of usage not yet reported to Spoolman.
|
||||
|
||||
Falls back to equal split of total supplies_usage when per-slot
|
||||
attribution data is absent (e.g. single-extruder with no AMS)."""
|
||||
total_used = self._state.get("supplies_usage", 0)
|
||||
if self._spoolman_slot_usage:
|
||||
return {
|
||||
slot: self._spoolman_slot_usage.get(slot, 0.0)
|
||||
- self._spoolman_slot_reported.get(slot, 0.0)
|
||||
for slot in self._spoolman_slot_spools
|
||||
}
|
||||
n = len(self._spoolman_slot_spools)
|
||||
already = sum(self._spoolman_slot_reported.values())
|
||||
per = (total_used - already) / n if n else 0.0
|
||||
return {slot: per for slot in self._spoolman_slot_spools}
|
||||
|
||||
def _spoolman_report(self, unreported: dict[int, float], min_mm: float = 0.1) -> None:
|
||||
"""Fire-and-forget report of unreported mm to each mapped spool."""
|
||||
sm = self._spoolman
|
||||
for slot_idx, mm in unreported.items():
|
||||
if mm < min_mm:
|
||||
continue
|
||||
spool_id = self._spoolman_slot_spools.get(slot_idx)
|
||||
if not spool_id:
|
||||
continue
|
||||
self._spoolman_slot_reported[slot_idx] = (
|
||||
self._spoolman_slot_reported.get(slot_idx, 0.0) + mm
|
||||
)
|
||||
def _send(sid=spool_id, length=mm):
|
||||
try:
|
||||
sm.use_filament(sid, length)
|
||||
log.info(f"Spoolman: {length:.1f} mm → spool {sid}")
|
||||
except Exception as e:
|
||||
log.warning(f"Spoolman: report failed (spool {sid}): {e}")
|
||||
threading.Thread(target=_send, daemon=True, name="spoolman-report").start()
|
||||
|
||||
def _spoolman_notify_end(self):
|
||||
"""Report remaining filament on print end."""
|
||||
if not self._spoolman or not self._spoolman_slot_spools:
|
||||
return
|
||||
self._spoolman_report(self._spoolman_unreported())
|
||||
|
||||
def _spoolman_sync_midprint(self):
|
||||
"""Report incremental filament usage during a print (sync_rate interval)."""
|
||||
if not self._spoolman or not self._spoolman_slot_spools:
|
||||
return
|
||||
self._spoolman_report(self._spoolman_unreported(), min_mm=10.0)
|
||||
|
||||
# ── Spoolman API handlers ─────────────────────────────────────────────────
|
||||
|
||||
async def handle_kx_spoolman_status(self, request):
|
||||
"""GET /kx/spoolman/status"""
|
||||
return self._json_cors({
|
||||
"configured": bool(self._spoolman),
|
||||
"server": self._spoolman.server_url if self._spoolman else "",
|
||||
"sync_rate": self._spoolman.sync_rate if self._spoolman else 0,
|
||||
"slot_spools": {str(k): v for k, v in self._spoolman_slot_spools.items()},
|
||||
})
|
||||
|
||||
async def handle_kx_spoolman_spools(self, request):
|
||||
"""GET /kx/spoolman/spools — proxied from Spoolman."""
|
||||
if not self._spoolman:
|
||||
return self._json_cors({"error": "Spoolman not configured"}, status=503)
|
||||
try:
|
||||
spools = await asyncio.get_event_loop().run_in_executor(
|
||||
None, self._spoolman.list_spools
|
||||
)
|
||||
return self._json_cors({"spools": spools})
|
||||
except Exception as e:
|
||||
log.warning(f"Spoolman: list_spools failed: {e}")
|
||||
return self._json_cors({"error": str(e)}, status=502)
|
||||
|
||||
async def handle_kx_spoolman_set_active(self, request):
|
||||
"""POST /kx/spoolman/active-spool
|
||||
Body: {"slot_map": {"0": 42, "2": 17}} — AMS slot index → Spoolman spool ID."""
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return self._json_cors({"error": "invalid JSON"}, status=400)
|
||||
slot_map = data.get("slot_map") or {}
|
||||
self._spoolman_slot_spools = {
|
||||
int(k): int(v) for k, v in slot_map.items()
|
||||
if str(v).isdigit() and int(v) > 0
|
||||
}
|
||||
self._spoolman_slot_usage = {}
|
||||
self._spoolman_slot_reported = {}
|
||||
self._spoolman_last_usage = 0.0
|
||||
return self._json_cors({"slot_spools": {str(k): v for k, v in self._spoolman_slot_spools.items()}})
|
||||
|
||||
def _default_ace_dry_presets(self) -> dict[str, dict]:
|
||||
return {
|
||||
"pla": {"temp": 45, "duration_sec": 4 * 3600},
|
||||
@@ -951,15 +1131,21 @@ class KobraXBridge:
|
||||
printer_id=self._printer_id,
|
||||
)
|
||||
log.info(f"Job started: {self._current_job_id} for {filename}")
|
||||
self._spoolman_slot_usage = {}
|
||||
self._spoolman_slot_reported = {}
|
||||
self._spoolman_last_usage = 0.0
|
||||
self._spoolman_last_sync = 0.0
|
||||
|
||||
# Job-History: Druckende erkennen
|
||||
if kobra_state in ("finished",) and self._current_job_id:
|
||||
self._store.finish_job(self._current_job_id, status="completed")
|
||||
log.info(f"Job abgeschlossen: {self._current_job_id}")
|
||||
self._spoolman_notify_end()
|
||||
self._current_job_id = ""
|
||||
elif kobra_state in ("stoped", "canceled") and self._current_job_id:
|
||||
self._store.finish_job(self._current_job_id, status="cancelled")
|
||||
log.info(f"Job abgebrochen: {self._current_job_id}")
|
||||
self._spoolman_notify_end()
|
||||
self._current_job_id = ""
|
||||
|
||||
# Nach Druckende das Upload-Banner verschwinden lassen (Issue #29): der
|
||||
@@ -976,6 +1162,7 @@ class KobraXBridge:
|
||||
self._state["slicer_time"] = 0
|
||||
self._state["layer_height"] = 0.0
|
||||
self._state["first_layer_height"] = 0.0
|
||||
self._state["supplies_usage"] = 0
|
||||
self._thumbnail_b64 = ""
|
||||
self._state["filename"] = d.get("filename", self._state["filename"])
|
||||
if "progress" in d:
|
||||
@@ -990,6 +1177,8 @@ class KobraXBridge:
|
||||
self._state["total_layers"] = d["total_layers"]
|
||||
if "taskid" in d:
|
||||
self._state["taskid"] = str(d["taskid"])
|
||||
if "supplies_usage" in d:
|
||||
self._state["supplies_usage"] = int(d["supplies_usage"])
|
||||
settings = d.get("settings") or {}
|
||||
if "print_speed_mode" in settings:
|
||||
self._state["print_speed_mode"] = int(settings["print_speed_mode"])
|
||||
@@ -1077,6 +1266,25 @@ class KobraXBridge:
|
||||
if (not skipped and self._pending_preprint_skip
|
||||
and now <= self._pending_preprint_skip_deadline):
|
||||
return
|
||||
|
||||
# Während eines aktiven Drucks sind Skip-Zustände effektiv monoton.
|
||||
# Manche Firmware-Reports kommen zwischenzeitlich leer/teilweise zurück;
|
||||
# diese dürfen bereits bestätigte Skip-Objekte nicht aus der UI löschen.
|
||||
existing_skipped = [str(n) for n in (self._skip_state.get("skipped") or []) if n]
|
||||
existing_set = set(existing_skipped)
|
||||
incoming_skipped = [str(n) for n in (skipped or []) if n]
|
||||
incoming_set = set(incoming_skipped)
|
||||
active_print = self._state.get("print_state") in ("printing", "paused")
|
||||
if active_print and existing_set:
|
||||
if not incoming_set:
|
||||
skipped = list(existing_skipped)
|
||||
elif not incoming_set.issuperset(existing_set):
|
||||
merged = list(existing_skipped)
|
||||
for n in incoming_skipped:
|
||||
if n not in existing_set:
|
||||
merged.append(n)
|
||||
skipped = merged
|
||||
|
||||
# Pending-Lock aufheben sobald Drucker die gewünschten Objekte bestätigt
|
||||
if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip):
|
||||
self._pending_preprint_skip = []
|
||||
@@ -1123,7 +1331,7 @@ class KobraXBridge:
|
||||
return False
|
||||
for i in range(max(1, int(retries))):
|
||||
try:
|
||||
if self._state.get("kobra_state") != "printing":
|
||||
if self._state.get("print_state") not in ("printing", "paused"):
|
||||
time.sleep(max(0.1, float(delay_s)))
|
||||
continue
|
||||
resp = self.client.skip_objects(wanted)
|
||||
@@ -2453,6 +2661,18 @@ class KobraXBridge:
|
||||
names = json.loads(f.get("objects_skip_parts") or "[]")
|
||||
except Exception:
|
||||
names = []
|
||||
# Noch keine Objekte im Store (frischer Orca-/Web-Upload): einmal aktiv
|
||||
# file/fileDetails beim Drucker anfragen. _on_file() füllt den Store nach,
|
||||
# das Frontend pollt diesen Endpoint und bekommt die Liste beim nächsten
|
||||
# Versuch (Issue #57 — Skip-Parität auch außerhalb des File-Browsers).
|
||||
if not names:
|
||||
fn = f.get("filename") or ""
|
||||
if fn:
|
||||
try:
|
||||
self.client.publish("file", "fileDetails",
|
||||
{"root": "local", "filename": fn}, timeout=0)
|
||||
except Exception as e:
|
||||
log.debug(f"fileDetails-Nachfrage fehlgeschlagen: {e}")
|
||||
return self._json_cors({
|
||||
"result": {
|
||||
"names": names,
|
||||
@@ -2479,29 +2699,8 @@ class KobraXBridge:
|
||||
return self._json_cors({"error": str(e)}, status=502)
|
||||
return self._json_cors({"result": "ok", "names": names})
|
||||
|
||||
async def handle_kx_skip_query(self, request):
|
||||
"""Druck-Objektliste vom Drucker neu abfragen.
|
||||
|
||||
POST /kx/skip/query → triggert skip/query_obj, gibt zuletzt bekannten
|
||||
Stand zurück (skip/report kommt async, Frontend pollt /kx/skip/state).
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, lambda: self.client.query_skip_objects())
|
||||
except Exception as e:
|
||||
return self._json_cors({"error": str(e)}, status=502)
|
||||
return self._json_cors({"result": self._skip_state})
|
||||
|
||||
async def handle_kx_skip_state(self, request):
|
||||
"""Aktueller Skip-State.
|
||||
|
||||
Kombiniert:
|
||||
- Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell
|
||||
laufenden filename (file/report beim Druckstart hat die Liste gefüllt).
|
||||
skip/query_obj liefert nämlich NUR die bereits geskippten zurück,
|
||||
nicht die Gesamtliste.
|
||||
- Geskippt: aus self._skip_state (von skip/report aktualisiert).
|
||||
"""
|
||||
def _build_skip_state_result(self) -> dict:
|
||||
"""Baut den kombinierten Skip-State für UI-Endpunkte."""
|
||||
filename = self._state.get("filename", "")
|
||||
all_objects: list[str] = []
|
||||
svg = ""
|
||||
@@ -2513,14 +2712,46 @@ class KobraXBridge:
|
||||
svg = f.get("svg_image") or ""
|
||||
except Exception as e:
|
||||
log.warning(f"skip_state lookup failed: {e}")
|
||||
result = {
|
||||
return {
|
||||
"objects": all_objects,
|
||||
"skipped": list(self._skip_state.get("skipped", [])),
|
||||
"svg_b64": svg,
|
||||
"ts": self._skip_state.get("ts", 0),
|
||||
"filename": filename,
|
||||
}
|
||||
return self._json_cors({"result": result})
|
||||
|
||||
async def handle_kx_skip_query(self, request):
|
||||
"""Druck-Objektliste vom Drucker neu abfragen.
|
||||
|
||||
POST /kx/skip/query → triggert skip/query_obj, wartet kurz auf den
|
||||
async skip/report und gibt den zusammengeführten Skip-State zurück.
|
||||
"""
|
||||
prev_ts = int(self._skip_state.get("ts", 0) or 0)
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, lambda: self.client.query_skip_objects())
|
||||
except Exception as e:
|
||||
return self._json_cors({"error": str(e)}, status=502)
|
||||
|
||||
deadline = time.time() + 1.5
|
||||
while time.time() < deadline:
|
||||
if int(self._skip_state.get("ts", 0) or 0) > prev_ts:
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
return self._json_cors({"result": self._build_skip_state_result()})
|
||||
|
||||
async def handle_kx_skip_state(self, request):
|
||||
"""Aktueller Skip-State.
|
||||
|
||||
Kombiniert:
|
||||
- Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell
|
||||
laufenden filename (file/report beim Druckstart hat die Liste gefüllt).
|
||||
skip/query_obj liefert nämlich NUR die bereits geskippten zurück,
|
||||
nicht die Gesamtliste.
|
||||
- Geskippt: aus self._skip_state (von skip/report aktualisiert).
|
||||
"""
|
||||
return self._json_cors({"result": self._build_skip_state_result()})
|
||||
|
||||
async def handle_kx_printers(self, request):
|
||||
# Aktive Drucker (mit IP) sammeln
|
||||
@@ -2615,8 +2846,8 @@ class KobraXBridge:
|
||||
},
|
||||
}
|
||||
|
||||
# Pre-Print-Skip sofort im UI-Status spiegeln
|
||||
self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())}
|
||||
# UI erst nach echter Drucker-Bestätigung als "geskippt" markieren.
|
||||
self._skip_state = {"skipped": [], "ts": int(time.time())}
|
||||
if excluded_objects:
|
||||
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
|
||||
self._pending_preprint_skip_deadline = time.time() + 12.0
|
||||
@@ -2925,10 +3156,10 @@ class KobraXBridge:
|
||||
if not file_data:
|
||||
return web.json_response({"error": "no file received"}, status=400)
|
||||
|
||||
# Nur druckbare Dateien zulassen (Issue #59) — verhindert dass z.B. ein
|
||||
# JPG im File-Browser landet. OrcaSlicer-Uploads sind .gcode/.gcode.3mf,
|
||||
# der Kobra X akzeptiert .gcode/.3mf/.bgcode.
|
||||
_allowed_ext = (".gcode", ".gcode.3mf", ".3mf", ".bgcode")
|
||||
# Nur druckbare Dateien zulassen (Issue #59) — der Kobra X akzeptiert
|
||||
# ausschließlich .gcode und .bgcode; .3mf-Uploads werden vom Drucker
|
||||
# nicht verarbeitet und daher abgelehnt (Issue #59, @gangoke).
|
||||
_allowed_ext = (".gcode", ".bgcode")
|
||||
_fn_lower = (remote_filename or "").lower()
|
||||
if not _fn_lower.endswith(_allowed_ext):
|
||||
log.warning(f"Upload abgelehnt (kein GCode): {remote_filename}")
|
||||
@@ -3139,7 +3370,8 @@ class KobraXBridge:
|
||||
ams_box_mapping = self._build_auto_ams_box_mapping()
|
||||
|
||||
use_ams = len(ams_box_mapping) > 0
|
||||
auto_leveling = getattr(self._args, "auto_leveling", 1)
|
||||
# Dialog-Checkbox (body) hat Vorrang, sonst Setting-Default (wie handle_kx_print).
|
||||
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
|
||||
url = self._state.get("last_upload_url", "")
|
||||
filesize = self._state.get("last_upload_size", 0)
|
||||
md5 = self._state.get("last_upload_md5", "")
|
||||
@@ -3169,6 +3401,15 @@ class KobraXBridge:
|
||||
},
|
||||
}
|
||||
|
||||
# UI erst nach echter Drucker-Bestätigung als "geskippt" markieren.
|
||||
self._skip_state = {"skipped": [], "ts": int(time.time())}
|
||||
if excluded_objects:
|
||||
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
|
||||
self._pending_preprint_skip_deadline = time.time() + 12.0
|
||||
else:
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
|
||||
log.info(
|
||||
f"print/start api=1 mode={self._filament_mode} "
|
||||
f"ams={len(ams_box_mapping)} slots assignments={filament_assignments is not None}"
|
||||
@@ -3181,6 +3422,9 @@ class KobraXBridge:
|
||||
if result is None:
|
||||
return web.json_response({"error": "Keine Antwort vom Drucker"}, status=504)
|
||||
|
||||
if excluded_objects:
|
||||
loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects))
|
||||
|
||||
return web.json_response({"result": "ok"})
|
||||
|
||||
async def handle_print_pause(self, request):
|
||||
@@ -3815,6 +4059,8 @@ class KobraXBridge:
|
||||
"camera_url": s["camera_url"],
|
||||
"fan_speed": s["fan_speed"],
|
||||
"print_speed_mode": s["print_speed_mode"],
|
||||
"auto_leveling": getattr(self._args, "auto_leveling", 1),
|
||||
"camera_on_print": getattr(self._args, "camera_on_print", 0),
|
||||
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
|
||||
"light_on": s["light_on"],
|
||||
"light_brightness": s["light_brightness"],
|
||||
@@ -3972,6 +4218,8 @@ class KobraXBridge:
|
||||
"filament_profiles": {str(k): v for k, v in self._filament_profiles.items()},
|
||||
"visible_vendors": self._visible_vendors,
|
||||
"ace_dry_presets": self._ace_dry_presets,
|
||||
"spoolman_server": getattr(self._args, "spoolman_server", "") or "",
|
||||
"spoolman_sync_rate": getattr(self._args, "spoolman_sync_rate", 0),
|
||||
})
|
||||
|
||||
async def handle_api_settings_post(self, request):
|
||||
@@ -3986,7 +4234,7 @@ class KobraXBridge:
|
||||
cfg.read(config_path, encoding="utf-8")
|
||||
|
||||
# Sections sicherstellen
|
||||
for section in ("connection", "print", "bridge", "ace_dry_presets"):
|
||||
for section in ("connection", "print", "bridge", "ace_dry_presets", "spoolman"):
|
||||
if not cfg.has_section(section):
|
||||
cfg.add_section(section)
|
||||
|
||||
@@ -4016,6 +4264,16 @@ class KobraXBridge:
|
||||
elif cfg.has_option("bridge", "printer_name"):
|
||||
cfg.remove_option("bridge", "printer_name")
|
||||
|
||||
# Spoolman
|
||||
if "spoolman_server" in data:
|
||||
cfg.set("spoolman", "server", str(data["spoolman_server"]).strip())
|
||||
if "spoolman_sync_rate" in data:
|
||||
try:
|
||||
sr = max(0, int(data["spoolman_sync_rate"]))
|
||||
except (TypeError, ValueError):
|
||||
sr = 30
|
||||
cfg.set("spoolman", "sync_rate", str(sr))
|
||||
|
||||
incoming_presets = data.get("ace_dry_presets") if isinstance(data, dict) else None
|
||||
presets = self._sanitize_ace_dry_presets(incoming_presets if isinstance(incoming_presets, dict) else self._ace_dry_presets)
|
||||
for key, val in presets.items():
|
||||
@@ -4747,6 +5005,14 @@ class KobraXBridge:
|
||||
print_r = self.client.publish("print", "query", timeout=3.0)
|
||||
if print_r:
|
||||
self._on_print(print_r)
|
||||
# Spoolman mid-print sync
|
||||
if (self._spoolman and self._spoolman.sync_rate > 0
|
||||
and self._spoolman_slot_spools
|
||||
and self._state.get("print_state") == "printing"):
|
||||
now = time.time()
|
||||
if now - self._spoolman_last_sync >= self._spoolman.sync_rate:
|
||||
self._spoolman_sync_midprint()
|
||||
self._spoolman_last_sync = now
|
||||
box = self.client.query_multicolor_box()
|
||||
if box:
|
||||
data = box.get("data") or {}
|
||||
@@ -4763,6 +5029,10 @@ class KobraXBridge:
|
||||
if global_slots:
|
||||
self._ams_slots = global_slots
|
||||
self._ams_loaded_slot = global_loaded
|
||||
self._spoolman_attribute_tick(activity_map)
|
||||
else:
|
||||
# No multiColorBox data — still attribute (no transitions to skip)
|
||||
self._spoolman_attribute_tick({})
|
||||
except Exception as e:
|
||||
log.warning(f"Poll-Fehler: {e}")
|
||||
# Prüfen ob Drucker wirklich weg ist
|
||||
@@ -4897,6 +5167,9 @@ def build_app(bridge: KobraXBridge) -> web.Application:
|
||||
r.add_post("/kx/skip", bridge.handle_kx_skip)
|
||||
r.add_post("/kx/skip/query", bridge.handle_kx_skip_query)
|
||||
r.add_get("/kx/skip/state", bridge.handle_kx_skip_state)
|
||||
r.add_get("/kx/spoolman/status", bridge.handle_kx_spoolman_status)
|
||||
r.add_get("/kx/spoolman/spools", bridge.handle_kx_spoolman_spools)
|
||||
r.add_post("/kx/spoolman/active-spool", bridge.handle_kx_spoolman_set_active)
|
||||
r.add_route("OPTIONS", "/kx/{path:.*}", bridge.handle_kx_options)
|
||||
|
||||
# Root + Printer-Routen (Single-Page, JS liest Pathname)
|
||||
@@ -5062,6 +5335,10 @@ def main():
|
||||
parser.add_argument("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING)
|
||||
parser.add_argument("--print-start-dialog", dest="print_start_dialog", type=int, default=env_loader.PRINT_START_DIALOG)
|
||||
parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int)
|
||||
parser.add_argument("--spoolman-server", default=env_loader.SPOOLMAN_SERVER,
|
||||
help="Spoolman URL (e.g. http://192.168.x.x:7912); leave empty to disable")
|
||||
parser.add_argument("--spoolman-sync-rate", type=int, default=env_loader.SPOOLMAN_SYNC_RATE,
|
||||
help="Mid-print filament sync interval in seconds (0 = only on print end)")
|
||||
|
||||
parser.add_argument("--host", default="0.0.0.0",
|
||||
help="Bind-Adresse für den Bridge-Server")
|
||||
|
||||
18
moonraker-obico.cfg.example
Normal file
18
moonraker-obico.cfg.example
Normal file
@@ -0,0 +1,18 @@
|
||||
[server]
|
||||
url = http://127.0.0.1:3334
|
||||
auth_token = REPLACE_ME
|
||||
sentry_opt = out
|
||||
|
||||
[moonraker]
|
||||
host = 127.0.0.1
|
||||
port = 7125
|
||||
|
||||
[webcam]
|
||||
disable_video_streaming = False
|
||||
snapshot_url = http://127.0.0.1:7125/api/camera/snapshot
|
||||
stream_url = http://127.0.0.1:7125/api/camera/stream
|
||||
target_fps = 5
|
||||
|
||||
[logging]
|
||||
path = /opt/printer_data/logs/moonraker-obico.log
|
||||
level = INFO
|
||||
21
pull_request_template.md
Normal file
21
pull_request_template.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## Description
|
||||
<!-- What does this PR change? -->
|
||||
|
||||
## Related Issue
|
||||
Closes #
|
||||
|
||||
## Type
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature
|
||||
- [ ] Documentation
|
||||
- [ ] Refactoring
|
||||
|
||||
## Tested with
|
||||
- OrcaSlicer Version:
|
||||
- Printer:
|
||||
- Moonraker/Klipper Version:
|
||||
|
||||
## Checklist
|
||||
- [ ] Tests added/updated
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] No debug code included
|
||||
@@ -9,9 +9,10 @@ var camOn=false;
|
||||
var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print
|
||||
var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support)
|
||||
var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55)
|
||||
var _idleCleared=false; // User hat idle-Datei explizit „geleert" → kein Nachladen von s.filename (Issue #57)
|
||||
var _fdDialogOpen=false; // Dialog ist gerade offen
|
||||
var _fdAutoOpenedFile=null; // Dateiname für den der Dialog auto-geöffnet wurde
|
||||
var _fdUserCancelled=false; // User hat den Auto-Open-Dialog abgebrochen
|
||||
var _fdAutoOpenedFile=sessionStorage.getItem('fdAutoOpenedFile')||null;
|
||||
var _fdUserCancelled=sessionStorage.getItem('fdUserCancelled')==='1';
|
||||
var currentStep=1;
|
||||
var currentPanel='dashboard';
|
||||
var aceAutoRefillPrefs=(function(){
|
||||
@@ -43,6 +44,73 @@ var ACE_DRY_PRESETS={
|
||||
custom_3:{name:'Custom 3',temp:45,duration_sec:4*3600}
|
||||
};
|
||||
|
||||
// Spoolman state
|
||||
var _spoolmanStatus={configured:false,server:'',sync_rate:0,slot_spools:{}};
|
||||
var _spoolmanSpools=[];
|
||||
var _slotSpoolMap={}; // {String(global_index): spoolman_spool_id} — last committed assignment
|
||||
|
||||
function _loadSpoolmanStatus(){
|
||||
fetch(_apiUrl('/kx/spoolman/status')).then(function(r){return r.json();}).then(function(d){
|
||||
_spoolmanStatus=d;
|
||||
_slotSpoolMap=d.slot_spools||{};
|
||||
_updateSpoolmanStatusDot();
|
||||
}).catch(function(){});
|
||||
}
|
||||
function _updateSpoolmanStatusDot(){
|
||||
var dot=document.getElementById('spoolman-status-dot');
|
||||
var lbl=document.getElementById('spoolman-status-lbl');
|
||||
if(!dot||!lbl)return;
|
||||
if(_spoolmanStatus.configured){
|
||||
dot.style.color='var(--ok)';lbl.textContent=_spoolmanStatus.server||'verbunden';
|
||||
} else {
|
||||
dot.style.color='var(--txt2)';lbl.textContent='nicht konfiguriert';
|
||||
}
|
||||
}
|
||||
|
||||
function _buildSpoolmanSection(){
|
||||
var sec=document.getElementById('fd-spoolman-section');
|
||||
var rows=document.getElementById('fd-spoolman-rows');
|
||||
var loading=document.getElementById('fd-spoolman-loading');
|
||||
if(!sec||!rows)return;
|
||||
if(!_spoolmanStatus.configured){sec.style.display='none';return;}
|
||||
sec.style.display='';
|
||||
rows.innerHTML='';
|
||||
if(loading)loading.style.display='';
|
||||
|
||||
var usedSlots={};
|
||||
document.querySelectorAll('#fd-slots select').forEach(function(sel){
|
||||
var idx=parseInt(sel.value);
|
||||
if(idx>=0){
|
||||
var slot=(_amsSlots||[]).find(function(s){return s.slot_index===idx;});
|
||||
if(slot&&!usedSlots[idx])usedSlots[idx]=slot;
|
||||
}
|
||||
});
|
||||
|
||||
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(d){
|
||||
if(loading)loading.style.display='none';
|
||||
_spoolmanSpools=d.spools||[];
|
||||
var slotKeys=Object.keys(usedSlots).map(Number).sort(function(a,b){return a-b;});
|
||||
if(!slotKeys.length){rows.innerHTML='<span style="font-size:11px;color:var(--txt2)">–</span>';return;}
|
||||
rows.innerHTML=slotKeys.map(function(idx){
|
||||
var slot=usedSlots[idx];
|
||||
var col=(slot.color_hex||'#888');
|
||||
var currentSpool=_slotSpoolMap[String(idx)]||'';
|
||||
var opts='<option value="">–</option>'+_spoolmanSpools.map(function(sp){
|
||||
var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':'';
|
||||
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor+' ':'';
|
||||
var name=sp.filament&&sp.filament.name?sp.filament.name:'Spool';
|
||||
return '<option value="'+sp.id+'"'+(sp.id==currentSpool?' selected':'')+'>'+
|
||||
escHtml('#'+sp.id+' '+vendor+name+rem)+'</option>';
|
||||
}).join('');
|
||||
return '<div style="display:flex;align-items:center;gap:8px;font-size:12px">'+
|
||||
'<span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:'+col+';border:1px solid var(--border);flex-shrink:0"></span>'+
|
||||
'<span style="color:var(--txt2);min-width:46px">Slot '+(idx+1)+'</span>'+
|
||||
'<select data-spool-slot="'+idx+'" style="flex:1;padding:3px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+
|
||||
opts+'</select></div>';
|
||||
}).join('');
|
||||
}).catch(function(){if(loading)loading.style.display='none';});
|
||||
}
|
||||
|
||||
function _aceAutoRefillGet(aceId){return !!aceAutoRefillPrefs[String(aceId)];}
|
||||
function _aceAutoRefillSet(aceId,on){
|
||||
aceAutoRefillPrefs[String(aceId)]=!!on;
|
||||
@@ -108,6 +176,7 @@ function _langToggleLabel(lang){
|
||||
if(lang==='de')return 'Deutsch';
|
||||
if(lang==='en')return 'English';
|
||||
if(lang==='fr')return 'Français';
|
||||
if(lang==='it')return 'Italiano';
|
||||
if(lang==='zh-cn')return '简体中文';
|
||||
return 'Espanol';
|
||||
}
|
||||
@@ -115,10 +184,10 @@ function _langToggleLabel(lang){
|
||||
function _mapSupportedLang(lang){
|
||||
if(!lang)return '';
|
||||
var l=String(lang).toLowerCase().replace(/_/g,'-').trim();
|
||||
if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='zh-cn')return l;
|
||||
if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='it'||l==='zh-cn')return l;
|
||||
|
||||
var base=l.split('-')[0];
|
||||
if(base==='de'||base==='en'||base==='es'||base==='fr')return base;
|
||||
if(base==='de'||base==='en'||base==='es'||base==='fr'||base==='it')return base;
|
||||
|
||||
if(base==='zh'){
|
||||
if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn';
|
||||
@@ -327,6 +396,11 @@ function applyLang(){
|
||||
setText('setcat-lbl-display',T.settings_cat_display||'Darstellung');
|
||||
setText('setcat-lbl-display2',T.settings_cat_display||'Darstellung');
|
||||
setText('setcat-lbl-filament',T.settings_cat_filament||'Filament');
|
||||
setText('setcat-lbl-integrations',T.settings_integrations||'Integrationen');
|
||||
setText('modal-sec-spoolman',T.modal_sec_spoolman||'Spoolman');
|
||||
setText('lbl-spoolman-url',T.lbl_spoolman_url||'Server-URL');
|
||||
setText('lbl-spoolman-sync-rate',T.lbl_spoolman_sync_rate||'Sync-Rate (s, 0=aus)');
|
||||
setText('modal-sec-obico',T.modal_sec_obico||'Obico');
|
||||
setText('setcat-lbl-system',T.settings_version||'System');
|
||||
setText('lbl-set-lang',T.settings_cat_language||'Sprache');
|
||||
setText('lbl-set-theme',T.settings_cat_theme||'Hell / Dunkel umschalten');
|
||||
@@ -664,10 +738,18 @@ function applyState(){
|
||||
if(s.file_ready&&s.print_state==='standby'){
|
||||
document.getElementById('file-ready-name').textContent=s.file_ready;
|
||||
// Neue Datei → Abbruch-Sperre aufheben
|
||||
if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready) _fdUserCancelled=false;
|
||||
if(shouldAutoOpen&&!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
|
||||
_fdAutoOpenedFile=s.file_ready;
|
||||
startReadyFileWithSlots(s.file_ready,true);
|
||||
if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready){
|
||||
_fdUserCancelled=false;
|
||||
sessionStorage.removeItem('fdUserCancelled');
|
||||
sessionStorage.removeItem('fdAutoOpenedFile');
|
||||
}
|
||||
if(shouldAutoOpen){
|
||||
// Dialog-Modus: Banner niemals anzeigen.
|
||||
frb.style.display='none';
|
||||
if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
|
||||
_fdAutoOpenedFile=s.file_ready;
|
||||
startReadyFileWithSlots(s.file_ready,true);
|
||||
}
|
||||
} else {
|
||||
frb.style.display='flex';
|
||||
bannerVisible=true;
|
||||
@@ -686,8 +768,11 @@ function applyState(){
|
||||
// Zuletzt geladene Datei merken (Issue #55): solange sie über den State
|
||||
// sichtbar ist. Beim Druckende/Abbruch leert die Bridge file_ready+filename
|
||||
// (Issue #29) — die gemerkte Referenz bleibt für die Karten-Aktionen.
|
||||
// Echte ready-Datei oder laufender Druck hebt einen vorherigen „Clear" auf.
|
||||
if(s.file_ready||printing) _idleCleared=false;
|
||||
if(s.file_ready) _lastLoadedFile=s.file_ready;
|
||||
else if(s.filename) _lastLoadedFile=s.filename;
|
||||
else if(s.filename && !_idleCleared) _lastLoadedFile=s.filename;
|
||||
else if(_idleCleared) _lastLoadedFile=null;
|
||||
// Idle-Aktionen (Drucken/Slots/Leeren) nur wenn nicht gedruckt wird, eine
|
||||
// Datei bekannt ist und der grüne Banner nicht ohnehin schon dieselbe Aktion
|
||||
// anbietet.
|
||||
@@ -864,7 +949,13 @@ function applyState(){
|
||||
var activity=(slot.activity||'');
|
||||
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–');
|
||||
var slotLabel=T.label_slot+' '+(globalIdx+1);
|
||||
var profile=(window._slotProfileMap||{})[globalIdx];
|
||||
// Gemapptes Profil nur für belegte Slots verwenden — sonst zeigt ein
|
||||
// verwaistes Mapping (Slot wurde geleert) ein „Geister"-Profil (Issue #57).
|
||||
var profile=empty?null:(window._slotProfileMap||{})[globalIdx];
|
||||
var genericType=(slot.type||slot.material_type||'–');
|
||||
// Material-Label: bei belegtem Slot mit Mapping den konkreten Profilnamen
|
||||
// (z.B. „eSUN PLA+") statt nur des generischen Typs zeigen (Issue #57 Punkt 4).
|
||||
var materialLabel=empty?'–':((profile&&profile.name)?profile.name:genericType);
|
||||
var vendorBadge='';
|
||||
if(!empty && profile && profile.vendor){
|
||||
var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':'');
|
||||
@@ -873,7 +964,7 @@ function applyState(){
|
||||
html+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(empty?' empty':'')
|
||||
+'" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
|
||||
+'<div class="slot-circle" style="background:'+col+'"></div>'
|
||||
+'<div class="slot-material">'+(empty?'–':(slot.type||slot.material_type||'–'))+'</div>'
|
||||
+'<div class="slot-material" title="'+(empty?'':genericType)+'">'+materialLabel+'</div>'
|
||||
+vendorBadge
|
||||
+'<div class="slot-label">'+slotLabel+'</div>'
|
||||
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
|
||||
@@ -895,7 +986,7 @@ function applyState(){
|
||||
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
|
||||
|
||||
// auto-start camera during print (unless user explicitly stopped it)
|
||||
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){
|
||||
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped&&s.camera_on_print){
|
||||
camStart();
|
||||
}
|
||||
// reset user-stopped flag when print ends so next print auto-starts again
|
||||
@@ -983,6 +1074,10 @@ function openSettings(){
|
||||
pi.value=sec;
|
||||
}
|
||||
renderFilamentMapping(d.filament_profiles||{});
|
||||
// Spoolman
|
||||
var su=document.getElementById('s-spoolman-url');if(su)su.value=d.spoolman_server||'';
|
||||
var sr=document.getElementById('s-spoolman-sync-rate');if(sr)sr.value=(d.spoolman_sync_rate!==undefined?d.spoolman_sync_rate:30);
|
||||
_updateSpoolmanStatusDot();
|
||||
});
|
||||
// Sprach-Select im Settings-Panel mit aktueller Sprache spiegeln
|
||||
var ls=document.getElementById('s-lang-select');
|
||||
@@ -1011,6 +1106,11 @@ function onPollIntervalInput(){
|
||||
}
|
||||
|
||||
// ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ──
|
||||
// Pro Slot ein einzelnes Profil-Dropdown (vendor+name gemeinsam, gekeyt per
|
||||
// _profileKey). Kein Freitext mehr → das (vendor,name)→id-Matching kann nicht
|
||||
// mehr durch manuelle Eingabe brechen (Issue #57 Punkt 1). Optionen werden aus
|
||||
// /kx/filament/profiles geladen, nach Vendor gruppiert, User-Profile zuerst,
|
||||
// mit demselben Vendor-Sichtbarkeitsfilter wie das Slot-Edit-Dropdown.
|
||||
function renderFilamentMapping(map){
|
||||
var el=document.getElementById('filament-mapping-list');
|
||||
if(!el)return;
|
||||
@@ -1020,21 +1120,61 @@ function renderFilamentMapping(map){
|
||||
var idHint=m.id?' <span style="color:var(--txt2);font-size:11px">('+m.id+')</span>':'';
|
||||
rows+='<div class="modal-field" style="margin-bottom:8px">'
|
||||
+'<label>Slot '+(i+1)+idHint+'</label>'
|
||||
+'<div style="display:flex;gap:6px;flex-wrap:wrap">'
|
||||
+'<input type="text" id="fmap-'+i+'-vendor" placeholder="Vendor (z.B. Polymaker)" value="'+(m.vendor||'')+'" style="flex:1;min-width:120px">'
|
||||
+'<input type="text" id="fmap-'+i+'-name" placeholder="Name (z.B. PolyTerra PLA)" value="'+(m.name||'')+'" style="flex:1;min-width:140px">'
|
||||
+'</div></div>';
|
||||
+'<select id="fmap-'+i+'" data-vendor="'+(m.vendor||'')+'" data-name="'+(m.name||'')+'" style="width:100%"></select>'
|
||||
+'</div>';
|
||||
}
|
||||
el.innerHTML=rows;
|
||||
// Dropdowns befüllen (async, geteilter Profil-Cache + Vendor-Filter)
|
||||
for(var j=0;j<4;j++){ _fillMappingDropdown(j); }
|
||||
}
|
||||
function _fillMappingDropdown(slot){
|
||||
var sel=document.getElementById('fmap-'+slot);
|
||||
if(!sel) return;
|
||||
var wantKey=_profileKey(sel.dataset.vendor, sel.dataset.name);
|
||||
_loadOrcaFilaments(function(profiles){
|
||||
sel.innerHTML='<option value="">'+(tr('slot_edit_profile_default')||'Generic (Standard)')+'</option>';
|
||||
var userProfs=profiles.filter(function(p){return p.is_user;});
|
||||
var systemProfs=profiles.filter(function(p){return !p.is_user;});
|
||||
function _opt(g,p){
|
||||
var o=document.createElement('option');
|
||||
o.value=_profileKey(p.vendor,p.name);
|
||||
o.dataset.vendor=p.vendor; o.dataset.name=p.name; o.dataset.id=p.id||'';
|
||||
o.textContent=(p.is_user?'★ ':'')+p.name+(p.vendor?' — '+p.vendor:'');
|
||||
if(o.value===wantKey)o.selected=true;
|
||||
g.appendChild(o);
|
||||
}
|
||||
if(userProfs.length){
|
||||
var gUser=document.createElement('optgroup');
|
||||
gUser.label='★ '+(tr('orca_profile_user_label')||'Eigene Profile');
|
||||
userProfs.forEach(function(p){_opt(gUser,p);});
|
||||
sel.appendChild(gUser);
|
||||
}
|
||||
_loadVisibleVendors(function(vis){
|
||||
var filtered=systemProfs;
|
||||
if(vis&&vis.length){
|
||||
var allow={};vis.forEach(function(v){allow[v]=1;});allow['Generic']=1;
|
||||
filtered=systemProfs.filter(function(p){return allow[p.vendor];});
|
||||
}
|
||||
var byVendor={};
|
||||
filtered.forEach(function(p){(byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p);});
|
||||
Object.keys(byVendor).sort().forEach(function(v){
|
||||
var g=document.createElement('optgroup');g.label=v;
|
||||
byVendor[v].forEach(function(p){_opt(g,p);});
|
||||
sel.appendChild(g);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
function saveFilamentMapping(){
|
||||
// Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend).
|
||||
// Leere Felder = Mapping entfernen.
|
||||
// Leere Auswahl ("") = Mapping entfernen.
|
||||
var chain=Promise.resolve();
|
||||
for(var i=0;i<4;i++){
|
||||
(function(slot){
|
||||
var vendor=((document.getElementById('fmap-'+slot+'-vendor')||{}).value||'').trim();
|
||||
var name=((document.getElementById('fmap-'+slot+'-name')||{}).value||'').trim();
|
||||
var sel=document.getElementById('fmap-'+slot);
|
||||
var opt=sel?sel.options[sel.selectedIndex]:null;
|
||||
var vendor=(opt&&opt.dataset.vendor)||'';
|
||||
var name=(opt&&opt.dataset.name)||'';
|
||||
chain=chain.then(function(){
|
||||
return fetch(_apiUrl('/kx/filament/slots/'+slot+'/profile'),
|
||||
{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
@@ -1193,6 +1333,7 @@ function doProfileImportUpload(files){
|
||||
var _slotEditIndex=-1;
|
||||
var _slotEditLoaded=false;
|
||||
var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS'];
|
||||
var _BASE_MATERIAL_TYPES=['PLA','PETG','ABS','ASA','TPU','TPE','PA','PC','HIPS','PEI','PEEK'];
|
||||
function updateSlotEditFeedButton(){
|
||||
var btn=document.getElementById('btn-slot-edit-feed');
|
||||
if(!btn)return;
|
||||
@@ -1329,23 +1470,39 @@ function slotEditFeed(){
|
||||
}
|
||||
function startReadyFile(filename){
|
||||
var fn=filename||S.file_ready;
|
||||
function _doStartReadyFile(){
|
||||
var btn=document.getElementById('file-ready-btn');
|
||||
if(btn){btn.disabled=true;btn.textContent='…';}
|
||||
post('/printer/print/start',{filename:fn})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(){
|
||||
document.getElementById('file-ready-banner').style.display='none';
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
})
|
||||
.catch(function(e){
|
||||
clog(tr('log_error')+' '+e,'msg-err');
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
});
|
||||
}
|
||||
function _gateAndStart(fileObj){
|
||||
if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){
|
||||
maybeGateWebUpload(fileObj, function(){ startReadyFile(fn); });
|
||||
return;
|
||||
}
|
||||
_doStartReadyFile();
|
||||
}
|
||||
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
|
||||
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFile(fn); });
|
||||
if(currentFile){
|
||||
_gateAndStart(currentFile);
|
||||
return;
|
||||
}
|
||||
var btn=document.getElementById('file-ready-btn');
|
||||
if(btn){btn.disabled=true;btn.textContent='…';}
|
||||
post('/printer/print/start',{filename:fn})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(r){
|
||||
document.getElementById('file-ready-banner').style.display='none';
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
})
|
||||
.catch(function(e){
|
||||
clog(tr('log_error')+' '+e,'msg-err');
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
});
|
||||
fetch(_apiUrl('/kx/files')).then(function(r){return r.json();}).then(function(d){
|
||||
storeFiles=d.result||[];
|
||||
var refreshed=(storeFiles||[]).find(function(f){return f.filename===fn;})||null;
|
||||
_gateAndStart(refreshed);
|
||||
}).catch(function(){
|
||||
_doStartReadyFile();
|
||||
});
|
||||
}
|
||||
function cancelReadyFile(){
|
||||
post('/api/file_ready/clear',{})
|
||||
@@ -1361,8 +1518,17 @@ function startIdleFileWithSlots(){
|
||||
}
|
||||
function clearIdleFile(){
|
||||
_lastLoadedFile=null;
|
||||
_idleCleared=true; // verhindert Nachladen von s.filename im nächsten poll() (Issue #57)
|
||||
_fdAutoOpenedFile=null; // nächster Upload derselben Datei soll Dialog wieder öffnen
|
||||
_fdUserCancelled=false;
|
||||
_fdDialogOpen=false;
|
||||
sessionStorage.removeItem('fdAutoOpenedFile');
|
||||
sessionStorage.removeItem('fdUserCancelled');
|
||||
sessionStorage.removeItem('webVerifyCancelledFileId');
|
||||
S.file_ready=''; S.filename=''; S.thumbnail=''; // sofort lokal leeren, kein Warten auf nächsten Poll
|
||||
var ib=document.getElementById('d-idle-btns');if(ib)ib.style.display='none';
|
||||
var fn=document.getElementById('d-fname');if(fn){fn.textContent='–';fn.title='';}
|
||||
var thumb=document.getElementById('d-thumbnail');if(thumb){thumb.style.display='none';thumb.src='';}
|
||||
post('/api/file_ready/clear',{}).catch(function(){});
|
||||
}
|
||||
function selectMatPreset(m){
|
||||
@@ -1461,6 +1627,11 @@ function saveSettings(){
|
||||
btn.disabled=true;btn.textContent='…';
|
||||
var webUploadWarning=(document.getElementById('s-web-upload-warning')||{}).checked?1:0;
|
||||
S.web_upload_warning=webUploadWarning;
|
||||
// Start-Print-Behavior-Wechsel könnte den Auto-Open sonst dauerhaft blockieren
|
||||
// (alter _fdUserCancelled bei gleicher file_ready) → Dialog-State zurücksetzen (Issue #57).
|
||||
_fdUserCancelled=false;_fdAutoOpenedFile=null;
|
||||
sessionStorage.removeItem('fdUserCancelled');sessionStorage.removeItem('fdAutoOpenedFile');
|
||||
sessionStorage.removeItem('webVerifyCancelledFileId');
|
||||
post('/api/settings',{
|
||||
printer_name: document.getElementById('s-printer-name').value,
|
||||
printer_ip: document.getElementById('s-printer-ip').value,
|
||||
@@ -1475,6 +1646,8 @@ function saveSettings(){
|
||||
print_start_dialog: parseInt((document.getElementById('s-file-ready-mode')||{}).value||'1',10),
|
||||
web_upload_warning:webUploadWarning,
|
||||
poll_interval: Math.min(60,Math.max(1,parseInt((document.getElementById('s-poll-interval')||{}).value,10)||3)),
|
||||
spoolman_server: (document.getElementById('s-spoolman-url')||{}).value||'',
|
||||
spoolman_sync_rate: Math.max(0,parseInt((document.getElementById('s-spoolman-sync-rate')||{}).value||'30',10)),
|
||||
}).then(function(){
|
||||
btn.textContent=T.update_restarting;
|
||||
setTimeout(function(){
|
||||
@@ -2132,6 +2305,7 @@ var _filamentDialogMode='store'; // 'store' oder 'banner'
|
||||
var _pendingWebVerifyFileId=null;
|
||||
var _pendingWebVerifyFilename='';
|
||||
var _pendingWebVerifyAction=null;
|
||||
var _pendingWebVerifyAutoOpen=false;
|
||||
// GCode-Store-Dateiliste. MUSS deklariert sein – sonst ReferenceError, wenn
|
||||
// "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen
|
||||
// wurde (Issue #29 / Theme-Auslagerung PR #27).
|
||||
@@ -2199,7 +2373,8 @@ function clearWebUploadWarningFlag(fileId, onDone){
|
||||
});
|
||||
}
|
||||
|
||||
function maybeGateWebUpload(fileObj, onContinue){
|
||||
function maybeGateWebUpload(fileObj, onContinue, opts){
|
||||
opts=opts||{};
|
||||
if(!fileObj || !fileObj.web_unverified){
|
||||
if(onContinue) onContinue();
|
||||
return;
|
||||
@@ -2208,15 +2383,20 @@ function maybeGateWebUpload(fileObj, onContinue){
|
||||
if(onContinue) onContinue();
|
||||
return;
|
||||
}
|
||||
var cancelledId=sessionStorage.getItem('webVerifyCancelledFileId')||'';
|
||||
if(opts.autoOpen && cancelledId && cancelledId===String(fileObj.id||'')){
|
||||
return;
|
||||
}
|
||||
openWebVerifyDialog(fileObj.id, fileObj.filename, function(){
|
||||
clearWebUploadWarningFlag(fileObj.id, onContinue);
|
||||
});
|
||||
}, !!opts.autoOpen);
|
||||
}
|
||||
|
||||
function openWebVerifyDialog(fileId, filename, onConfirm){
|
||||
function openWebVerifyDialog(fileId, filename, onConfirm, autoOpen){
|
||||
_pendingWebVerifyFileId=fileId;
|
||||
_pendingWebVerifyFilename=filename;
|
||||
_pendingWebVerifyAction=onConfirm||null;
|
||||
_pendingWebVerifyAutoOpen=!!autoOpen;
|
||||
var status=document.getElementById('store-web-verify-status');
|
||||
if(status){status.textContent='';}
|
||||
openStoreWebVerifyDialog();
|
||||
@@ -2230,9 +2410,13 @@ function openStoreWebVerifyDialog(){
|
||||
function closeStoreWebVerifyDialog(){
|
||||
var modal=document.getElementById('store-web-verify-dialog');
|
||||
if(modal){modal.classList.remove('open');}
|
||||
if(_pendingWebVerifyAutoOpen && _pendingWebVerifyFileId){
|
||||
sessionStorage.setItem('webVerifyCancelledFileId', String(_pendingWebVerifyFileId));
|
||||
}
|
||||
_pendingWebVerifyFileId=null;
|
||||
_pendingWebVerifyFilename='';
|
||||
_pendingWebVerifyAction=null;
|
||||
_pendingWebVerifyAutoOpen=false;
|
||||
}
|
||||
|
||||
function confirmStoreWebVerify(){
|
||||
@@ -2252,9 +2436,11 @@ function confirmStoreWebVerify(){
|
||||
.then(function(){
|
||||
var fileObj=(storeFiles||[]).find(function(f){return f.id===fileId;});
|
||||
if(fileObj){fileObj.web_unverified=false;}
|
||||
sessionStorage.removeItem('webVerifyCancelledFileId');
|
||||
_pendingWebVerifyFileId=null;
|
||||
_pendingWebVerifyFilename='';
|
||||
_pendingWebVerifyAction=null;
|
||||
_pendingWebVerifyAutoOpen=false;
|
||||
closeStoreWebVerifyDialog();
|
||||
loadStore();
|
||||
if(typeof action==='function') action();
|
||||
@@ -2270,7 +2456,7 @@ function startReadyFileWithSlots(filename,_autoOpen){
|
||||
var fn=filename||S.file_ready;
|
||||
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
|
||||
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn); });
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen});
|
||||
return;
|
||||
}
|
||||
_filamentDialogMode='banner';
|
||||
@@ -2291,23 +2477,31 @@ function startReadyFileWithSlots(filename,_autoOpen){
|
||||
});
|
||||
}
|
||||
|
||||
function _proceedWithFileObj(fileObj){
|
||||
if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){
|
||||
// Verify-Gate war beim ersten Lookup noch nicht aktiv (storeFiles leer) — jetzt prüfen.
|
||||
if(_autoOpen){_fdDialogOpen=false;}
|
||||
maybeGateWebUpload(fileObj, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen});
|
||||
return;
|
||||
}
|
||||
if(fileObj){
|
||||
_storeFileId=fileObj.id;
|
||||
_setGcodeFilamentsFromFileObj(fileObj);
|
||||
}
|
||||
openWithSlots();
|
||||
}
|
||||
|
||||
var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
|
||||
if(fileObj){
|
||||
_storeFileId=fileObj.id;
|
||||
_setGcodeFilamentsFromFileObj(fileObj);
|
||||
openWithSlots();
|
||||
_proceedWithFileObj(fileObj);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: refresh file list, then resolve current file by filename.
|
||||
fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){
|
||||
storeFiles=d.result||[];
|
||||
var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
|
||||
if(refreshed){
|
||||
_storeFileId=refreshed.id;
|
||||
_setGcodeFilamentsFromFileObj(refreshed);
|
||||
}
|
||||
openWithSlots();
|
||||
var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;})||null;
|
||||
_proceedWithFileObj(refreshed);
|
||||
}).catch(function(){
|
||||
openWithSlots();
|
||||
});
|
||||
@@ -2332,6 +2526,17 @@ function _normalizeMaterialKey(material){
|
||||
var key=(material||'').toUpperCase().replace(/[^A-Z0-9+]/g,'');
|
||||
// Orca often uses PLA for PLA+, while AMS may report PLA+.
|
||||
if(key==='PLA+'||key==='PLAPLUS') return 'PLA';
|
||||
// Handle modifier+base patterns in either order: "Matte PLA", "Silk PETG",
|
||||
// "PLA Silk", "PLA Matte". OrcaSlicer always writes the base type in GCode
|
||||
// (filament_type = PLA), but users label slots with the full product-style name.
|
||||
var trimmed=(material||'').trim();
|
||||
if(trimmed.indexOf(' ')>=0){
|
||||
var words=trimmed.toUpperCase().split(/\s+/);
|
||||
for(var i=0;i<words.length;i++){
|
||||
var w=words[i].replace(/[^A-Z0-9+]/g,'');
|
||||
if(_BASE_MATERIAL_TYPES.indexOf(w)>=0) return w;
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
function _materialsCompatible(a,b){
|
||||
@@ -2367,6 +2572,7 @@ function openFilamentDialog(slots){
|
||||
_amsSlots=slots
|
||||
.filter(function(s){return s.status==='loaded';})
|
||||
.sort(function(a,b){return (a.slot_index||0)-(b.slot_index||0);});
|
||||
_loadSpoolmanStatus();
|
||||
var dlg=document.getElementById('filament-dialog');
|
||||
var title=document.getElementById('fd-title');
|
||||
var body=document.getElementById('fd-slots');
|
||||
@@ -2387,19 +2593,30 @@ function openFilamentDialog(slots){
|
||||
if(objBody)objBody.style.display='none'; // immer eingeklappt starten
|
||||
if(objArrow)objArrow.style.transform='';
|
||||
if(_storeFileId){
|
||||
fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects'))
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
var names=(d.result&&d.result.names)||[];
|
||||
_printObjectsSvg=(d.result&&d.result.svg_b64)||'';
|
||||
if(names.length>=2){
|
||||
_printObjects=names.map(function(n){return {name:n,skip:false};});
|
||||
renderObjectChecklist(); renderObjectSvg();
|
||||
var cnt=document.getElementById('fd-objects-count');
|
||||
if(cnt)cnt.textContent='('+names.length+')';
|
||||
if(objSection)objSection.style.display='block';
|
||||
}
|
||||
}).catch(function(){});
|
||||
// Bei frischem Orca-/Web-Upload liefert der Drucker die Objektliste
|
||||
// (objects_skip_parts) erst per fileDetails nach → ist im Store kurz leer.
|
||||
// Daher mehrfach nachfragen, bis Objekte da sind (Issue #57 Skip-Parität).
|
||||
var _objFid=_storeFileId;
|
||||
var _objTries=0;
|
||||
(function _loadObjects(){
|
||||
if(_objFid!==_storeFileId) return; // Dialog wechselte Datei → abbrechen
|
||||
fetch(_apiUrl('/kx/files/'+encodeURIComponent(_objFid)+'/objects'))
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
var names=(d.result&&d.result.names)||[];
|
||||
var svg=(d.result&&d.result.svg_b64)||'';
|
||||
if(names.length>=2){
|
||||
_printObjectsSvg=svg;
|
||||
_printObjects=names.map(function(n){return {name:n,skip:false};});
|
||||
renderObjectChecklist(); renderObjectSvg();
|
||||
var cnt=document.getElementById('fd-objects-count');
|
||||
if(cnt)cnt.textContent='('+names.length+')';
|
||||
if(objSection)objSection.style.display='block';
|
||||
} else if(_objTries++ < 6){
|
||||
setTimeout(_loadObjects, 1000); // bis ~6s auf fileDetails warten
|
||||
}
|
||||
}).catch(function(){});
|
||||
})();
|
||||
}
|
||||
|
||||
// GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten
|
||||
@@ -2485,13 +2702,19 @@ function openFilamentDialog(slots){
|
||||
}).join('');
|
||||
}
|
||||
if(dlg)dlg.classList.add('open');
|
||||
// Spoolman-Section nach Slot-Render aufbauen (braucht die selects im DOM)
|
||||
setTimeout(_buildSpoolmanSection, 0);
|
||||
}
|
||||
|
||||
function closeFilamentDialog(){
|
||||
var dlg=document.getElementById('filament-dialog');
|
||||
if(dlg)dlg.classList.remove('open');
|
||||
_fdDialogOpen=false;
|
||||
if(_fdAutoOpenedFile) _fdUserCancelled=true;
|
||||
if(_fdAutoOpenedFile){
|
||||
_fdUserCancelled=true;
|
||||
sessionStorage.setItem('fdUserCancelled','1');
|
||||
sessionStorage.setItem('fdAutoOpenedFile',_fdAutoOpenedFile);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmFilamentPrint(){
|
||||
@@ -2533,21 +2756,54 @@ function confirmFilamentPrint(){
|
||||
var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;});
|
||||
var fdAlEl=document.getElementById('fd-auto-leveling');
|
||||
var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0);
|
||||
// Spoolman: Slot→Spool-Mapping aus Dialog sammeln und senden
|
||||
if(_spoolmanStatus.configured){
|
||||
var slotMap={};
|
||||
document.querySelectorAll('[data-spool-slot]').forEach(function(sel){
|
||||
var idx=sel.dataset.spoolSlot;
|
||||
var val=parseInt(sel.value);
|
||||
if(val>0)slotMap[idx]=val;
|
||||
});
|
||||
_slotSpoolMap=slotMap;
|
||||
post('/kx/spoolman/active-spool',{slot_map:slotMap}).catch(function(){});
|
||||
}
|
||||
closeFilamentDialog();
|
||||
if(_filamentDialogMode==='banner'){
|
||||
// Banner-Modus: normaler print/start mit Slot-Override
|
||||
// Banner-Modus: /kx/print bevorzugen wenn _storeFileId bekannt (gleicher Pfad wie File-Browser).
|
||||
var btn=document.getElementById('file-ready-btn');
|
||||
if(btn){btn.disabled=true;btn.textContent='…';}
|
||||
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(){
|
||||
document.getElementById('file-ready-banner').style.display='none';
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
})
|
||||
.catch(function(e){
|
||||
clog(tr('log_error')+' '+e,'msg-err');
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
var startPromise;
|
||||
if(_storeFileId){
|
||||
startPromise=fetch(_apiUrl('/kx/print'),{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({
|
||||
file_id:_storeFileId,
|
||||
filament_assignments:assignments,
|
||||
excluded_objects:excludedObjects,
|
||||
auto_leveling:fdAutoLeveling
|
||||
})
|
||||
});
|
||||
}else{
|
||||
startPromise=post('/printer/print/start',{
|
||||
filename:S.file_ready||_storeFilename,
|
||||
filament_assignments:assignments,
|
||||
excluded_objects:excludedObjects,
|
||||
auto_leveling:fdAutoLeveling
|
||||
});
|
||||
}
|
||||
startPromise.then(function(r){
|
||||
if(!r.ok){return r.text().then(function(t){throw new Error(t||('HTTP '+r.status));});}
|
||||
return r.json();
|
||||
}).then(function(d){
|
||||
if(d&&d.error) throw new Error(d.error);
|
||||
if(d&&d.result&&d.result!=='ok') throw new Error(String(d.result));
|
||||
document.getElementById('file-ready-banner').style.display='none';
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
}).catch(function(e){
|
||||
clog(tr('log_error')+' '+e,'msg-err');
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
});
|
||||
} else {
|
||||
// Store-Modus: POST /kx/print
|
||||
fetch(_apiUrl('/kx/print'),{
|
||||
@@ -2618,35 +2874,45 @@ function renderObjectSvg(){
|
||||
// ── Mid-Print Skip ──
|
||||
var _skipObjects=[]; // [{name, skipped, willSkip}]
|
||||
var _skipSvg='';
|
||||
var _skipPollTimer=null;
|
||||
function _applySkipDialogState(s){
|
||||
s=s||{};
|
||||
_skipSvg=s.svg_b64||'';
|
||||
var skipped=s.skipped||[];
|
||||
// Pending-Auswahl (willSkip) beim Refresh erhalten.
|
||||
var prevWillSkip={};
|
||||
(_skipObjects||[]).forEach(function(o){if(o&&o.name)prevWillSkip[o.name]=!!o.willSkip;});
|
||||
_skipObjects=(s.objects||[]).map(function(n){
|
||||
var isSkipped=(skipped.indexOf(n)>=0);
|
||||
return {name:n, skipped:isSkipped, willSkip:isSkipped?false:!!prevWillSkip[n]};
|
||||
});
|
||||
renderSkipList(); renderSkipSvg();
|
||||
}
|
||||
function openSkipDialog(){
|
||||
document.getElementById('skip-status').textContent='';
|
||||
document.getElementById('skip-confirm').disabled=false;
|
||||
_refreshSkipDialog();
|
||||
if(_skipPollTimer)clearInterval(_skipPollTimer);
|
||||
_skipPollTimer=setInterval(function(){
|
||||
var dlg=document.getElementById('skip-dialog');
|
||||
if(!(dlg&&dlg.classList.contains('open')))return;
|
||||
_refreshSkipDialog();
|
||||
},2000);
|
||||
document.getElementById('skip-dialog').classList.add('open');
|
||||
}
|
||||
function _refreshSkipDialog(){
|
||||
// Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped
|
||||
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){
|
||||
var s=d.result||{};
|
||||
_skipSvg=s.svg_b64||'';
|
||||
_skipObjects=(s.objects||[]).map(function(n){
|
||||
return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false};
|
||||
// query-Endpoint wartet intern auf frischen skip/report (bis 1.5s).
|
||||
fetch(_apiUrl('/kx/skip/query'),{method:'POST'})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(d){_applySkipDialogState(d.result||{});})
|
||||
.catch(function(){
|
||||
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json();}).then(function(d){
|
||||
_applySkipDialogState(d.result||{});
|
||||
}).catch(function(){});
|
||||
});
|
||||
renderSkipList(); renderSkipSvg();
|
||||
});
|
||||
// Frisch nachfragen (skipped-Liste aktualisieren)
|
||||
fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){
|
||||
setTimeout(function(){
|
||||
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){
|
||||
var s=d.result||{};
|
||||
var skipped=s.skipped||[];
|
||||
_skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; });
|
||||
renderSkipList(); renderSkipSvg();
|
||||
});
|
||||
}, 500);
|
||||
}).catch(function(){});
|
||||
}
|
||||
function closeSkipDialog(){
|
||||
if(_skipPollTimer){clearInterval(_skipPollTimer);_skipPollTimer=null;}
|
||||
document.getElementById('skip-dialog').classList.remove('open');
|
||||
}
|
||||
function _shortLabel(name){
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<option value="en">English</option>
|
||||
<option value="es">Espanol</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -434,6 +435,7 @@
|
||||
<button class="set-cat" id="setcat-printer" onclick="showSettingsCat('printer')"><span>🖨</span> <span id="setcat-lbl-printer">Drucker</span></button>
|
||||
<button class="set-cat" id="setcat-display" onclick="showSettingsCat('display')"><span>🎨</span> <span id="setcat-lbl-display">Darstellung</span></button>
|
||||
<button class="set-cat" id="setcat-filament" onclick="showSettingsCat('filament')"><span>🧵</span> <span id="setcat-lbl-filament">Filament</span></button>
|
||||
<button class="set-cat" id="setcat-integrations" onclick="showSettingsCat('integrations')"><span>⚡</span> <span id="setcat-lbl-integrations">Integrationen</span></button>
|
||||
<button class="set-cat" id="setcat-system" onclick="showSettingsCat('system')"><span>⚙</span> <span id="setcat-lbl-system">System</span></button>
|
||||
</div>
|
||||
|
||||
@@ -521,6 +523,7 @@
|
||||
<option value="en">English</option>
|
||||
<option value="es">Espanol</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -568,6 +571,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integrationen -->
|
||||
<div class="set-group" id="setgrp-integrations">
|
||||
<!-- Spoolman -->
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🧵</span> <span id="modal-sec-spoolman">Spoolman</span></div>
|
||||
<div class="set-row">
|
||||
<label id="lbl-spoolman-url">Server-URL</label>
|
||||
<input type="text" id="s-spoolman-url" placeholder="http://spoolman:7912" style="width:200px">
|
||||
</div>
|
||||
<div class="set-row">
|
||||
<label id="lbl-spoolman-sync-rate">Sync-Rate (s, 0=aus)</label>
|
||||
<input type="number" id="s-spoolman-sync-rate" min="0" max="3600" value="30" style="width:80px">
|
||||
</div>
|
||||
<div id="spoolman-status-row" style="margin-top:6px;font-size:12px;color:var(--txt2)">
|
||||
<span id="spoolman-status-dot">●</span> <span id="spoolman-status-lbl"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Obico -->
|
||||
<div class="card" style="margin-top:10px">
|
||||
<div class="card-title"><span>🕵</span> <span id="modal-sec-obico">Obico</span></div>
|
||||
<div style="font-size:12px;color:var(--txt2);line-height:1.6" id="obico-info-box">
|
||||
Obico wird über den <code>moonraker-obico</code>-Container konfiguriert.<br>
|
||||
Config-Datei: <code id="obico-cfg-path">/mnt/dockerdata/KobraXStack/moonraker-obico/moonraker-obico.cfg</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System -->
|
||||
<div class="set-group" id="setgrp-system">
|
||||
<div class="card">
|
||||
@@ -644,6 +674,13 @@
|
||||
<label for="fd-auto-leveling" style="margin:0;cursor:pointer;font-size:13px" id="fd-lbl-auto-leveling">Auto-Leveling</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fd-spoolman-section" style="display:none;margin-bottom:16px;border-top:1px solid var(--border);padding-top:12px">
|
||||
<p style="font-size:12px;color:var(--txt2);margin-bottom:8px;display:flex;align-items:center;gap:6px">
|
||||
<span id="fd-spoolman-lbl">🧵 Spoolman</span>
|
||||
<span id="fd-spoolman-loading" style="display:none;font-size:10px">…</span>
|
||||
</p>
|
||||
<div id="fd-spoolman-rows" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button id="fd-cancel" onclick="closeFilamentDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="fd-print" onclick="confirmFilamentPrint()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">▶ Drucken</button>
|
||||
|
||||
@@ -287,5 +287,10 @@
|
||||
"log_clear": "✕ Leeren",
|
||||
"log_filter_placeholder": "Filtern…",
|
||||
"skip_cancel": "Abbrechen",
|
||||
"skip_confirm": "Überspringen"
|
||||
}
|
||||
"skip_confirm": "Überspringen",
|
||||
"settings_integrations": "Integrationen",
|
||||
"modal_sec_spoolman": "Spoolman",
|
||||
"lbl_spoolman_url": "Server-URL",
|
||||
"lbl_spoolman_sync_rate": "Sync-Rate (s, 0=aus)",
|
||||
"modal_sec_obico": "Obico"
|
||||
}
|
||||
@@ -287,5 +287,10 @@
|
||||
"sf_new": "New",
|
||||
"ss_date": "↓ Date",
|
||||
"ss_name": "A–Z Name",
|
||||
"ss_dur": "⏱ Print time"
|
||||
}
|
||||
"ss_dur": "⏱ Print time",
|
||||
"settings_integrations": "Integrations",
|
||||
"modal_sec_spoolman": "Spoolman",
|
||||
"lbl_spoolman_url": "Server URL",
|
||||
"lbl_spoolman_sync_rate": "Sync rate (s, 0=off)",
|
||||
"modal_sec_obico": "Obico"
|
||||
}
|
||||
@@ -287,5 +287,10 @@
|
||||
"log_clear": "✕ Limpiar",
|
||||
"log_filter_placeholder": "Filtrar…",
|
||||
"skip_cancel": "Cancelar",
|
||||
"skip_confirm": "Omitir"
|
||||
}
|
||||
"skip_confirm": "Omitir",
|
||||
"settings_integrations": "Integraciones",
|
||||
"modal_sec_spoolman": "Spoolman",
|
||||
"lbl_spoolman_url": "URL del servidor",
|
||||
"lbl_spoolman_sync_rate": "Tasa de sincronización (s, 0=desact.)",
|
||||
"modal_sec_obico": "Obico"
|
||||
}
|
||||
@@ -287,5 +287,10 @@
|
||||
"log_clear": "✕ Effacer",
|
||||
"log_filter_placeholder": "Filtrer…",
|
||||
"skip_cancel": "Annuler",
|
||||
"skip_confirm": "Ignorer"
|
||||
}
|
||||
"skip_confirm": "Ignorer",
|
||||
"settings_integrations": "Intégrations",
|
||||
"modal_sec_spoolman": "Spoolman",
|
||||
"lbl_spoolman_url": "URL du serveur",
|
||||
"lbl_spoolman_sync_rate": "Taux de sync. (s, 0=désact.)",
|
||||
"modal_sec_obico": "Obico"
|
||||
}
|
||||
296
web/translations/it.json
Normal file
296
web/translations/it.json
Normal file
@@ -0,0 +1,296 @@
|
||||
{
|
||||
"header_status_standby": "Pronto",
|
||||
"header_status_printing": "In stampa",
|
||||
"header_status_complete": "Completato",
|
||||
"header_status_error": "Errore",
|
||||
"kobra_free": "Pronto",
|
||||
"kobra_busy": "Occupato",
|
||||
"kobra_printing": "In stampa",
|
||||
"kobra_preheating": "Preriscaldamento",
|
||||
"kobra_auto_leveling": "Livellamento automatico",
|
||||
"kobra_checking": "Verifica",
|
||||
"kobra_updated": "Aggiornamento",
|
||||
"kobra_init": "Inizializzazione",
|
||||
"kobra_pausing": "Pausa in corso...",
|
||||
"kobra_paused": "In pausa",
|
||||
"kobra_resuming": "Ripresa...",
|
||||
"kobra_resumed": "Ripreso",
|
||||
"kobra_stopping": "Arresto...",
|
||||
"kobra_stoped": "Arrestato",
|
||||
"kobra_finished": "Finito",
|
||||
"kobra_failed": "Errore",
|
||||
"kobra_canceled": "Annullato",
|
||||
"kobra_offline": "Offline",
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_print": "Stampa",
|
||||
"nav_temps": "Temperature",
|
||||
"nav_motion": "Movimento",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "Luce / Ventola",
|
||||
"nav_console": "Console",
|
||||
"card_progress": "Avanzamento",
|
||||
"card_temps": "Temperature",
|
||||
"card_light_fan": "Ventola",
|
||||
"card_speed": "Velocità di stampa",
|
||||
"card_cam": "Camera",
|
||||
"lbl_elapsed": "Trascorso:",
|
||||
"lbl_remaining": "Rimanente:",
|
||||
"lbl_slicer_time": "Stima slicer:",
|
||||
"lbl_layers": "Layer",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silenzioso",
|
||||
"speed_normal": "⚡ Normale",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"lbl_light": "💡 Luce",
|
||||
"lbl_feed": "Carica",
|
||||
"lbl_unload": "Rimuovi",
|
||||
"card_ace_dry": "Essiccazione ACE",
|
||||
"ace_dry_dryer": "Essiccatore",
|
||||
"ace_dry_status_off": "Stato: Spento",
|
||||
"ace_dry_status_on": "Stato: Attivo",
|
||||
"ace_dry_status_remaining": "Rimanente",
|
||||
"ace_dry_humidity": "Umidità",
|
||||
"ace_dry_current_temp": "Temperatura",
|
||||
"ace_dry_chart": "Cronologia (Temp/Umidità)",
|
||||
"ace_dry_temp": "Temperatura (°C)",
|
||||
"ace_dry_duration": "Durata (min)",
|
||||
"ace_dry_start": "▶ Avvia",
|
||||
"ace_dry_stop": "■ Ferma",
|
||||
"ace_dry_auto_refill": "Ricarica automatica",
|
||||
"ace_dry_enable": "Abilita essiccazione",
|
||||
"ace_dry_temp_line": "Temperatura di essiccazione",
|
||||
"ace_dry_time_line": "Tempo di essiccazione",
|
||||
"ace_dry_ui_pending": "(Solo interfaccia, backend a seguire)",
|
||||
"ace_dry_dialog_title": "Impostazioni Temp/Tempo essiccatore",
|
||||
"ace_dry_dialog_temp": "Temperatura (30-80°C)",
|
||||
"ace_dry_dialog_time": "Tempo rim. (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "Conferma",
|
||||
"ace_dry_dialog_cancel": "Annulla",
|
||||
"ace_dry_dialog_save_restart": "Salva e riavvia",
|
||||
"ace_dry_dialog_custom_name": "Nome personalizzato",
|
||||
"ace_dry_dialog_reset_default": "Ripristina predefiniti",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Personalizzato",
|
||||
"cam_placeholder": "📷 Camera non avviata",
|
||||
"cam_stream_unavailable": "Flusso video non disponibile",
|
||||
"btn_cam_start": "▶ Camera",
|
||||
"btn_cam_stop": "◼ Camera",
|
||||
"btn_pause": "⏸ Pausa",
|
||||
"btn_resume": "▶ Riprendi",
|
||||
"btn_cancel": "✕ Stop",
|
||||
"label_nozzle": "Ugello",
|
||||
"label_bed": "Piatto",
|
||||
"label_fan": "🌀 Ventola",
|
||||
"label_light": "💡 Luce",
|
||||
"label_on_off": "On / Off",
|
||||
"label_speed": "Velocità",
|
||||
"panel_print_title": "Controllo stampa",
|
||||
"panel_print_btn_pause": "⏸ Pausa",
|
||||
"panel_print_btn_resume": "▶ Riprendi",
|
||||
"panel_print_btn_cancel": "✕ Annulla",
|
||||
"panel_print_temps_live": "Temperature (In tempo reale)",
|
||||
"label_set": "Imposta",
|
||||
"label_off": "Off",
|
||||
"panel_temps_nozzle": "Ugello",
|
||||
"panel_temps_bed": "Piatto riscaldato",
|
||||
"panel_temps_chart": "Cronologia (ultime 60 letture)",
|
||||
"label_target_c": "Target:",
|
||||
"panel_motion_xy": "Assi XY",
|
||||
"panel_motion_z": "Asse Z",
|
||||
"label_step": "Ampiezza passo:",
|
||||
"btn_home_z": "Home Z",
|
||||
"btn_home_xy": "Home XY",
|
||||
"btn_home_all": "Home generale",
|
||||
"btn_disable_motors": "Spegni motori",
|
||||
"panel_ams_title": "Filamento",
|
||||
"card_ams": "Filamento",
|
||||
"ams_no_data": "Nessun dato ricevuto dall' AMS",
|
||||
"label_slot": "Slot",
|
||||
"ams_empty": "Vuoto",
|
||||
"panel_extras_light": "Luce",
|
||||
"panel_extras_fan": "Ventola",
|
||||
"panel_extras_camera": "Camera",
|
||||
"btn_cam_start2": "▶ Avvia",
|
||||
"btn_cam_stop2": "◼ Ferma",
|
||||
"panel_console_title": "Registro eventi",
|
||||
"log_light_on": "Luce accesa",
|
||||
"log_light_off": "Luce spenta",
|
||||
"log_fan": "Ventola →",
|
||||
"log_nozzle": "Ugello →",
|
||||
"log_bed": "Piatto →",
|
||||
"log_axis": "Asse",
|
||||
"log_home": "Home",
|
||||
"log_home_all": "Home generale",
|
||||
"log_cam_start": "Camera avviata:",
|
||||
"log_cam_stop": "Camera arrestata",
|
||||
"log_poll_error": "Errore di sincronizzazione:",
|
||||
"log_error": "Errore:",
|
||||
"confirm_cancel": "Annullare davvero la stampa?",
|
||||
"settings_title": "Impostazioni",
|
||||
"settings_connection": "Connessione",
|
||||
"settings_print": "Impostazioni di stampa",
|
||||
"settings_poll": "Intervallo di sincronizzazione (secondi)",
|
||||
"nav_settings": "Impostazioni",
|
||||
"settings_cat_display": "Aspetto",
|
||||
"settings_cat_filament": "Filamento",
|
||||
"settings_cat_language": "Lingua",
|
||||
"settings_cat_theme": "Alterna chiaro / scuro",
|
||||
"settings_filament_mapping": "Mappatura profilo filamento (per slot)",
|
||||
"settings_filament_mapping_save": "Salva mappatura",
|
||||
"settings_visible_vendors": "Produttori visibili (menu del profilo)",
|
||||
"settings_visible_vendors_hint": "Solo questi produttori appariranno nel menu del profilo dello slot. Se non selezioni nulla = mostra tutti. I profili \"Generici\" e i tuoi personali sono sempre visibili.",
|
||||
"settings_visible_vendors_save": "Salva selezione",
|
||||
"progress_action_print": "Stampa",
|
||||
"progress_action_slots": "Mappa slot",
|
||||
"progress_action_clear": "Cancella",
|
||||
"settings_version": "Versione",
|
||||
"settings_save": "Salva e riavvia",
|
||||
"settings_printer_name": "Nome stampante",
|
||||
"settings_printer_ip": "IP stampante",
|
||||
"settings_mqtt_port": "Porta MQTT",
|
||||
"settings_username": "Nome utente MQTT",
|
||||
"settings_password": "Password MQTT",
|
||||
"settings_device_id": "ID dispositivo",
|
||||
"settings_mode_id": "ID modalità",
|
||||
"hint_ip_no_port": "Solo indirizzo IP, senza porta (es. 192.168.1.102)",
|
||||
"settings_default_slot": "Slot predefinito (colore singolo)",
|
||||
"settings_slot_auto": "Auto (tutti gli slot caricati)",
|
||||
"settings_auto_leveling": "Livellamento automatico predefinito",
|
||||
"fd_options_title": "Opzioni di stampa",
|
||||
"print_auto_leveling": "Livellamento automatico",
|
||||
"settings_file_ready_mode": "Comportamento all'avvio stampa",
|
||||
"settings_file_ready_banner": "Barra di stampa",
|
||||
"settings_file_ready_dialog": "Finestra di dialogo di stampa",
|
||||
"settings_camera_on_print": "Attiva la camera all'avvio della stampa",
|
||||
"settings_web_upload_warning": "Mostra un avviso quando si stampano caricamenti web",
|
||||
"update_check": "Controlla aggiornamenti",
|
||||
"update_checking": "Verifica in corso...",
|
||||
"update_available": "disponibile",
|
||||
"update_none": "Già aggiornato",
|
||||
"update_apply": "Installa ora",
|
||||
"update_applying": "Download in corso...",
|
||||
"update_restarting": "Riavvio in corso...",
|
||||
"update_error": "Errore",
|
||||
"btn_connect": "⚡ Connetti",
|
||||
"btn_disconnect": "✕ Disconnetti",
|
||||
"lbl_conn_error": "Errore di connessione:",
|
||||
"slot_edit_title": "Modifica slot",
|
||||
"slot_edit_color": "Colore",
|
||||
"slot_edit_material": "Materiale",
|
||||
"slot_edit_load": "⬇ Carica",
|
||||
"slot_edit_unload": "⬆ Rimuovi",
|
||||
"slot_edit_save": "💾 Salva",
|
||||
"slot_edit_custom": "es. PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "Slot AMS",
|
||||
"slot_edit_profile": "Profilo OrcaSlicer",
|
||||
"slot_edit_profile_hint": "Inviato durante la sincronizzazione con OrcaSlicer come marchio specifico invece di un semplice \"Generico\"",
|
||||
"slot_edit_profile_default": "— Generico (predefinito) —",
|
||||
"orca_profile_section": "Profili OrcaSlicer",
|
||||
"orca_profile_hint": "Importa i tuoi profili di filamento OrcaSlicer (apri la cartella utente tramite Aiuto → Mostra cartella di configurazione)",
|
||||
"orca_profile_import_btn": "Importa profili",
|
||||
"orca_profile_import_link": "★ Importa i tuoi profili…",
|
||||
"orca_profile_import_title": "Importa i tuoi profili OrcaSlicer",
|
||||
"orca_profile_help_html": "Carica un file <b>ZIP</b> della tua cartella filamenti di OrcaSlicer o file singoli <b>.json</b>.<br>In OrcaSlicer: <i>Aiuto → Mostra cartella di configurazione → user/<id>/filament/</i>",
|
||||
"orca_profile_dropmsg": "Trascina qui o fai clic",
|
||||
"orca_profile_list_label": "Attualmente importati",
|
||||
"orca_profile_user_label": "Profili personali",
|
||||
"orca_profile_user_empty": "– nessuno –",
|
||||
"orca_profile_uploading": "Caricamento in corso…",
|
||||
"orca_profile_done": "Importato",
|
||||
"orca_profile_skipped": "saltato",
|
||||
"log_dir_all": "Tutti",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Dir:",
|
||||
"log_lvl_label": "Livello:",
|
||||
"log_lvl_err": "⛔ Errori",
|
||||
"log_lvl_warn": "⚠ Avvisi",
|
||||
"log_topic_label": "Argomento:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Stampa",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Stato",
|
||||
"log_download": "⬇ Scarica",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Cancella",
|
||||
"log_filter_placeholder": "Filtra…",
|
||||
"file_ready_btn": "▶ Avvia stampa",
|
||||
"file_slots_btn": "🎨 Seleziona slot",
|
||||
"file_cancel_btn": "✕ Annulla",
|
||||
"nav_printers": "Stampanti",
|
||||
"skip_title": "✂ Salta oggetti",
|
||||
"skip_hint": "Deseleziona gli oggetti che non vuoi più stampare:",
|
||||
"skip_btn_label": "Oggetti",
|
||||
"skip_no_objects": "Nessun oggetto in questa stampa.",
|
||||
"skip_already": "saltato",
|
||||
"skip_cancel": "Annulla",
|
||||
"skip_confirm": "Salta",
|
||||
"skip_select_at_least_one": "Seleziona almeno un oggetto.",
|
||||
"skip_sending": "Invio in corso …",
|
||||
"skip_success": "Gli oggetti verranno saltati.",
|
||||
"fd_objects_hint": "Salta oggetti (opzionale):",
|
||||
"fd_objects_toggle": "Salta oggetti",
|
||||
"fd_slots_hint": "Assegna il canale GCode allo slot AMS:",
|
||||
"fd_cancel": "Annulla",
|
||||
"fd_print": "▶ Stampa",
|
||||
"fd_no_slots_msg": "Nessuno slot AMS caricato.{br}Avviare comunque la stampa?",
|
||||
"fd_slot": "Slot",
|
||||
"fd_no_matching_material": "Nessun materiale corrispondente",
|
||||
"fd_used": "USATO",
|
||||
"add_printer": "Aggiungi stampante",
|
||||
"apd_lbl_ip": "IP stampante",
|
||||
"apd_lbl_name": "Nome (opzionale)",
|
||||
"apd_placeholder_name": "es. Kobra X Soggiorno",
|
||||
"apd_cancel": "Annulla",
|
||||
"apd_confirm": "Aggiungi",
|
||||
"apd_fetching": "Recupero dati dalla stampante…",
|
||||
"apd_success": "Stampante aggiunta, riavvio del bridge in corso…",
|
||||
"apd_err_ip": "Inserisci un indirizzo IP",
|
||||
"printers_remove": "Rimuovi stampante",
|
||||
"printers_remove_confirm": "Rimuovere la stampante \"{name}\"? Il bridge si riavvierà.",
|
||||
"printers_active": "● attiva",
|
||||
"printers_switch": "Cambia →",
|
||||
"printers_current": "Stampante corrente",
|
||||
"printers_loading": "Caricamento in corso…",
|
||||
"printers_none": "Nessuna stampante configurata.",
|
||||
"printers_empty_hint": "Nessuna stampante ancora configurata.",
|
||||
"nav_browser": "Browser",
|
||||
"panel_browser_title": "Browser dei file",
|
||||
"store_search_placeholder": "🔍 Cerca…",
|
||||
"store_empty": "Nessun file caricato.",
|
||||
"store_refresh": "↻ Aggiorna",
|
||||
"store_print": "▶ Stampa",
|
||||
"store_download": "⬇ Scarica",
|
||||
"store_delete_confirm": "Eliminare il file?",
|
||||
"store_print_confirm": "Stampare il file?",
|
||||
"store_web_verify_title": "Verifica file",
|
||||
"store_web_verify_msg": "Verifica che questo file sia stato creato per Anycubic Kobra X.",
|
||||
"store_web_verify_confirm": "Conferma",
|
||||
"store_web_verify_abort": "Interrompi",
|
||||
"store_no_results": "Nessun file trovato.",
|
||||
"store_never": "mai stampato",
|
||||
"store_estimate": "Stima",
|
||||
"store_upload_label_prefix": "Trascina il GCode qui o ",
|
||||
"store_upload_label_browse": "sfoglia",
|
||||
"store_upload_busy": "⏳ Caricamento in corso…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ Sono consentiti solo file GCode (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "Tutti",
|
||||
"sf_ok": "✓ Completato",
|
||||
"sf_err": "✗ Fallito",
|
||||
"sf_new": "Nuovo",
|
||||
"ss_date": "↓ Data",
|
||||
"ss_name": "Nome A–Z",
|
||||
"ss_dur": "⏱ Tempo di stampa",
|
||||
"settings_integrations": "Integrazioni",
|
||||
"modal_sec_spoolman": "Spoolman",
|
||||
"lbl_spoolman_url": "URL server",
|
||||
"lbl_spoolman_sync_rate": "Frequenza sync (s, 0=disatt.)",
|
||||
"modal_sec_obico": "Obico"
|
||||
}
|
||||
@@ -287,5 +287,10 @@
|
||||
"log_clear": "✕ 清空",
|
||||
"log_filter_placeholder": "筛选…",
|
||||
"skip_cancel": "取消",
|
||||
"skip_confirm": "跳过"
|
||||
}
|
||||
"skip_confirm": "跳过",
|
||||
"settings_integrations": "集成",
|
||||
"modal_sec_spoolman": "Spoolman",
|
||||
"lbl_spoolman_url": "服务器地址",
|
||||
"lbl_spoolman_sync_rate": "同步频率(秒,0=关闭)",
|
||||
"modal_sec_obico": "Obico"
|
||||
}
|
||||
Reference in New Issue
Block a user