Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 326453e2fd | |||
| ffd8ed09d5 | |||
| dc7e92688b | |||
| 3e1ba9df4b | |||
| 41f4700b24 | |||
| 216b2de2c0 | |||
| 394b0e69ab | |||
| 877cddb1ba | |||
| c9043e9630 | |||
| 6165a7f62a | |||
| fa8e0c1491 | |||
| 282c02ae0a | |||
| 72f77d92af | |||
| 3595cf839c | |||
| 2b39cc1a78 | |||
| d20308cf2c | |||
| 710c4831c2 | |||
| 5bff7adad0 | |||
| eea570052f | |||
| 303297bfbf | |||
| 6b9ad9d426 | |||
| ed30568092 | |||
| 1f300589d1 | |||
| 930e3774af | |||
| 636889bdbc | |||
| 3f6ea269e6 | |||
| 3fff6e25f0 | |||
| 0f5a8cbc72 | |||
| a40f14af8e | |||
| 466b8c518d | |||
| 1c5396b37d | |||
| c23deebde5 | |||
| 76738e5961 | |||
| 9c82073540 | |||
| 031e34d8ea | |||
|
|
fc89dfffa5 | ||
| ac695ecf36 | |||
| 23b8a69065 | |||
| 22dc58258c | |||
| e4b4d091f3 | |||
| ba209827ce | |||
| d26b37b332 | |||
| 6f269833d2 | |||
| d808cd3ea8 | |||
|
|
ecd444525a | ||
|
|
d4bb79a68f | ||
|
|
cdaf74985c | ||
|
|
8383c59b39 | ||
| 1645de4cad | |||
|
|
42898c385c | ||
| 6c5dd14dbd | |||
| c2d16270bc | |||
| fd4b9b1254 | |||
| 21cd356757 | |||
| 40a27a47fc | |||
| 7815c66a82 |
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
|
||||
}"
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -9,3 +9,16 @@ releases/*/extract_credentials
|
||||
releases/*/extract_credentials.exe
|
||||
|
||||
!kx-bridge.spec
|
||||
|
||||
# Laufzeit-Daten und Drucker-Credentials — nie committen
|
||||
config/config.ini
|
||||
config/*.ini
|
||||
!config/config.ini.example
|
||||
data/
|
||||
|
||||
!data/orca_filaments.json
|
||||
|
||||
# Sensitive files — never commit
|
||||
.runner-token
|
||||
secrets/
|
||||
*.token
|
||||
|
||||
386
CHANGELOG.de.md
386
CHANGELOG.de.md
@@ -1,5 +1,391 @@
|
||||
# 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
|
||||
- **Zufällige Abstürze / Container-Restarts — Segfault in `libcrypto.so.3`
|
||||
(Issue #53).** Der MQTT-über-TLS-Client teilte einen einzelnen SSL-Socket
|
||||
zwischen dem Reader-Thread (`recv`) und den Sender-Threads (`sendall`), ohne sie
|
||||
zu serialisieren. CPythons `ssl`-Modul erlaubt kein gleichzeitiges Lesen und
|
||||
Schreiben auf demselben Socket — die Überlappung korrumpierte den internen
|
||||
OpenSSL-Zustand und löste eine Heap-Corruption + Segfault aus, die auf manchen
|
||||
Hosts timing-bedingt zuverlässig auftrat. Sämtliche Socket-Zugriffe (recv /
|
||||
sendall / close / reconnect) werden nun unter einem einzigen Lock serialisiert;
|
||||
der Reader prüft die Bereitschaft mit `select()` außerhalb des Locks, damit die
|
||||
Sender nie ausgehungert werden. Reconnect und Disconnect tauschen den Socket
|
||||
jetzt atomar. Dank an @BasK für den detaillierten Fault-Handler-Trace.
|
||||
- **File-Browser akzeptierte Nicht-GCode-Uploads (Issue #59).** Drag & Drop umging
|
||||
den `accept`-Filter des Dateidialogs, sodass z.B. ein JPG hochgeladen werden
|
||||
konnte. Uploads werden jetzt client- und serverseitig validiert; nur `.gcode`,
|
||||
`.gcode.3mf`, `.3mf` und `.bgcode` werden akzeptiert. Dank an @gangoke.
|
||||
|
||||
## [0.9.24] – 2026-06-16
|
||||
|
||||
### Neu
|
||||
- **Objekte überspringen in jedem Druck-Flow (Issue #57).** Der „Objekte
|
||||
überspringen"-Bereich im Slot-Mapper erschien bisher nur beim Druck aus dem
|
||||
Browser-Tab. Er ist jetzt in allen Flows verfügbar (inkl. Upload / Print-Leiste),
|
||||
standardmäßig eingeklappt hinter einem `✂ Objekte überspringen (N)`-Header, damit
|
||||
der Dialog kompakt bleibt — Klick klappt Vorschau + Checkliste auf.
|
||||
- **Slot-Mapper zeigt konkreten Profilnamen (Issue #57).** Jeder Slot zeigt nun das
|
||||
zugeordnete Filament-Profil (z.B. „PolyTerra PLA — Polymaker") in den Dropdown-
|
||||
Optionen und als Hover-Tooltip am Slot-Marker, statt nur des generischen Typs.
|
||||
Fällt auf den generischen Typ zurück, wenn kein Profil gemappt ist.
|
||||
|
||||
## [0.9.23] – 2026-06-16
|
||||
|
||||
### Neu
|
||||
- **Druckdialog nach Upload automatisch öffnen.** Eine neue Einstellung
|
||||
`print_start_dialog` (Einstellungen → Drucker → „Druckstart-Verhalten") steuert,
|
||||
was nach einem Upload bei leerlaufendem Drucker passiert: „Print-Dialog" öffnet
|
||||
den Slot-Zuordnungs-Dialog automatisch, „Print-Leiste" behält das bisherige
|
||||
Banner. Basiert auf PR #56 von @gangoke.
|
||||
- **Auto-Leveling-Schalter pro Druck.** Der Druckdialog hat jetzt eine eigene
|
||||
Auto-Leveling-Checkbox, die den globalen Standard für einen einzelnen Druck
|
||||
überschreibt.
|
||||
|
||||
### Behoben
|
||||
- **Objekt-Skip wurde beim Druckstart still ignoriert (PR #56, @gangoke).** Der
|
||||
Skip-Befehl wurde gesendet, *bevor* der Drucker im `printing`-Status war, und
|
||||
daher verworfen. Der Skip wird nun in einer Retry-Schleife erneut angewendet,
|
||||
sobald der Druck bestätigt läuft — mit einer Pending-Sperre, damit die UI den
|
||||
Skip-Status nicht vorzeitig zurücksetzt.
|
||||
- **Upload während eines laufenden Drucks überschrieb die Vorschau des laufenden
|
||||
Auftrags.** Ein neuer Upload während des Drucks ersetzt nicht mehr Thumbnail /
|
||||
file_ready des Auftrags auf dem Druckbett.
|
||||
|
||||
## [0.9.22] – 2026-06-16
|
||||
|
||||
### Neu
|
||||
- **Neu strukturiertes Einstellungs-Panel.** Das Einstellungs-Modal wurde durch
|
||||
ein dauerhaftes Master-Detail-Panel mit fünf Kategorien ersetzt: Verbindung,
|
||||
Drucker, Darstellung, Filament und System. Das Poll-Intervall ist nun live
|
||||
einstellbar.
|
||||
- **Vendor-Sichtbarkeitsfilter (Issue #41).** Eine neue Checkliste in den
|
||||
Filament-Einstellungen beschränkt das Slot-Profil-Dropdown auf bestimmte
|
||||
Hersteller. „Generic" und eigene importierte Profile sind immer sichtbar.
|
||||
- **Idle-Datei-Aktionen in der Fortschritts-Karte (Issue #55).** Nach einem
|
||||
Upload bei leerlaufendem Drucker erscheinen drei Schnellaktionen direkt in der
|
||||
Fortschritts-Karte: ▶ Drucken, ⚙ Slots zuordnen und ✕ Leeren.
|
||||
|
||||
### Behoben
|
||||
- **Mobileraker-Kompatibilität (Issue #48).** Absturz in `ConfigExtruder.fromJson`
|
||||
(leeres `configfile.config`), Hänger beim Refresh (Metadata-Endlosschleife) und
|
||||
fehlende ETA/Restzeit behoben.
|
||||
|
||||
## [0.9.21] – 2026-06-14
|
||||
|
||||
### Behoben
|
||||
- **Kamera-Stream auf Android (Chrome / Firefox) nicht sichtbar.** Android-Browser
|
||||
unterstützen `multipart/x-mixed-replace` (MJPEG) nicht. Die UI erkennt Android
|
||||
jetzt automatisch und fällt auf Snapshot-Polling mit 5 fps zurück
|
||||
(`/api/camera/snapshot` alle 200 ms) — keine Server-Änderung nötig.
|
||||
|
||||
### Geändert
|
||||
- Docker-Image auf **Debian 12 (Bookworm)** gepinnt (`python:3.11-slim-bookworm`),
|
||||
um Kompatibilitätsprobleme mit glibc 2.41 zu vermeiden, die das aktuell von
|
||||
`python:3.11-slim` gezogene Debian 13 Basis-Image mitbringt.
|
||||
- MQTT- und HTTP-Verbindungen erzwingen jetzt **IPv4** (`AF_INET`), um
|
||||
Verbindungsfehler auf Hosts zu verhindern, bei denen der Drucker nur über IPv4
|
||||
erreichbar ist, das OS aber IPv6 bevorzugt.
|
||||
- Extruder-Stub in der Moonraker-`configfile`-Antwort enthält jetzt `sensor_type`
|
||||
und `filament_diameter` — behebt einen Mobileraker-Absturz
|
||||
(`Null is not a subtype of Object`, Issue #48).
|
||||
|
||||
## [0.9.20] – 2026-06-08
|
||||
|
||||
### Neu
|
||||
- **Französische Sprachunterstützung (PR #45 von @Nathacks)**
|
||||
- **Z-Höhe in der Print-UI (PR #49 von @Nathacks).** Zeigt die aktuelle
|
||||
Z-Position in mm unterhalb des Layer-Zählers.
|
||||
|
||||
### Behoben
|
||||
- **Kamera-Autostart ignorierte das "Kamera bei Druckstart einschalten"-
|
||||
Setting nach einem Bridge-Restart (Issue #50).** Das Setting wurde in
|
||||
der Prozessumgebung gecacht — nach dem Speichern in der UI überlebte
|
||||
der alte Wert den Restart und der neue Wert aus `config.ini` wurde
|
||||
nicht gelesen.
|
||||
- **Kamera startete nach manuellem Stopp während eines Drucks automatisch
|
||||
neu (Issue #50).** Ein neues `_camera_user_stopped`-Flag unterdrückt
|
||||
den Autostart für die aktuelle Drucksitzung. Es wird beim Druckende
|
||||
zurückgesetzt.
|
||||
- **Falscher "Stream nicht verfügbar"-Fehler-Toast beim manuellen
|
||||
Kamera-Stopp.** Der Bild-Fehler-Handler war noch registriert als
|
||||
`img.src` geleert wurde.
|
||||
- **JS-Fehler (`ReferenceError: br is not defined`) beim Licht-Toggle.**
|
||||
Variable wurde aus dem falschen Scope referenziert.
|
||||
- Webcam-URLs sind jetzt absolut, damit Mobileraker/Obico-Clients sie
|
||||
erreichen können.
|
||||
|
||||
## [0.9.19.1] – 2026-06-04
|
||||
|
||||
### Behoben
|
||||
- Standalone-Binaries (Linux/Windows) zeigten `vunknown` als Version.
|
||||
Die `VERSION`-Datei ist jetzt ins PyInstaller-Onefile eingebettet.
|
||||
- Bei fehlenden TLS-Zertifikaten (`anycubic_slicer.crt`/`.key`) gab
|
||||
es nur den rohen Fehler `[Errno 2] No such file or directory`. Die
|
||||
Bridge meldet jetzt klar, wo die Dateien hingelegt werden müssen
|
||||
und dass `anycubic-certs.zip` aus dem Gitea-Release stammt.
|
||||
|
||||
### Geändert
|
||||
- Filament-Profil-Liste neu kuratiert: 209 statt 399 Einträge.
|
||||
Profile die nur für drucker-spezifische Vendor-Bundles existieren
|
||||
(z.B. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
||||
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) sind rausgeflogen
|
||||
— OrcaSlicer hätte sie im Standard-Kobra-X-Setup beim Sync
|
||||
ohnehin nicht gefunden, weil die jeweiligen Vendor-Bundles nur
|
||||
bei aktivem Drucker-Vendor geladen werden. Für solche Filamente
|
||||
bleibt der Custom-Profile-Import (Issue #41) der Weg.
|
||||
|
||||
## [0.9.19] – 2026-06-02
|
||||
|
||||
### Neu
|
||||
- **🎯 Filament-Sync mit OrcaSlicer matched jetzt das richtige Preset**
|
||||
statt immer auf „Generic PLA" zu landen. Voraussetzung: ein
|
||||
OrcaSlicer-Build mit dem
|
||||
[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)
|
||||
Empfangs-Patch (im OrcaSlicer-KX-Build dabei). Die Bridge sendet pro
|
||||
AMS-Slot jetzt `name` + `vendor_name` im Lane-Pfad UND
|
||||
`gate_filament_name` im Happy-Hare-MMU-Pfad (OrcaSlicer wechselt bei
|
||||
AMS-Setups automatisch auf den HH-Pfad).
|
||||
- **Eigene OrcaSlicer-Profile in die Bridge importieren (Issue #41).**
|
||||
Settings-Tab → „OrcaSlicer-Profile" oder direkt im Slot-Edit-Dialog
|
||||
(„★ Eigene Profile importieren…") lädst du deine `.json`-Files aus
|
||||
`~/.config/OrcaSlicer/user/<id>/filament/` hoch — einzeln oder als
|
||||
ZIP. Erscheint dann im Slot-Dropdown unter „★ Eigene Profile" und
|
||||
wird beim Sync an Orca als User-Match weitergegeben. Funktioniert
|
||||
über HTTP, also auch wenn die Bridge im Docker auf Raspi/NAS läuft
|
||||
und OrcaSlicer auf dem Desktop. Auch reine Override-Profile mit nur
|
||||
`inherits: "Generic PLA @System"` + ein paar Tweaks werden korrekt
|
||||
erkannt — die Bridge resolved die vererbten Felder aus dem
|
||||
System-Parent.
|
||||
|
||||
### Fixes
|
||||
- **AMS-Sync landete hartnäckig auf „Generic PLA":** das Orca-
|
||||
Datenmodell hat 68 duplikate `filament_id`-Werte (`OGFL99` allein
|
||||
136 mal), und die Bridge wählte oft eine ID die für den Kobra X
|
||||
nicht `is_compatible` war (z.B. `GFL92` aus dem Kobra-2-Profil →
|
||||
Orca verwarf es). Generator priorisiert jetzt Kobra-X-Varianten und
|
||||
filtert Phantom-Profile (Cross-Vendor-Overrides) raus —
|
||||
`orca_filaments.json` von 1035 → 400 saubere Profile.
|
||||
- **Slot ohne expliziten Override sendet jetzt `Generic <Typ>`** statt
|
||||
einer impliziten Vendor-Annahme. Library-Generic-Profile haben
|
||||
`compatible_printers: []` (= alle Drucker), sind also immer sichtbar
|
||||
und matchen verlässlich.
|
||||
- **Slot-Karte zeigt den Hersteller direkt nach dem Speichern** —
|
||||
ohne Browser-Reload. `poll()` war async, das Re-Render kam erst
|
||||
beim nächsten Tick.
|
||||
- **ACE-Trockner-Toggle warf 502-Fehler obwohl der Trockner ein-/aus-
|
||||
ging (PR #42 von @gangoke):** `setDry` jetzt fire-and-forget wie
|
||||
`setAutoFeed`. Der Drucker antwortet auf diesem Push-Topic mit
|
||||
`code: 0` statt `code: 200`, das hat die Bridge fälschlich als
|
||||
Fehler interpretiert.
|
||||
|
||||
### Datenmodell / API
|
||||
- `orca_filaments.json` regeneriert: nur echte Vendor-Profile und
|
||||
OrcaFilamentLibrary-Profile bleiben drin. Bambu/Polymaker/SUNLU-
|
||||
Library-Profile dabei, Qidi-Cross-Bundles raus.
|
||||
- Neue Endpoints: `POST /kx/filament/profiles/user` (ZIP/JSON-Upload),
|
||||
`GET /kx/filament/profiles/user` (Liste der User-Imports),
|
||||
`DELETE /kx/filament/profiles/user[?vendor=…&name=…]`. Persistenz
|
||||
in `<KX_DATA_DIR>/orca_filaments.user.json` (Volume — überlebt
|
||||
Image-Updates).
|
||||
|
||||
### Build
|
||||
- Neues Modul `bridge/orca_filaments.py` (gemeinsame Parser-Helfer
|
||||
für Generator und Import-Endpoint).
|
||||
- Dockerfile + release.sh um `orca_filaments.py` erweitert.
|
||||
|
||||
## [0.9.18] – 2026-05-31
|
||||
|
||||
### Neu
|
||||
- **🎉 Filament-Material und -Farbe pro AMS-Slot aus der Bridge an den
|
||||
Drucker senden:** Im Slot-Edit-Dialog gewählte Werte gehen jetzt
|
||||
tatsächlich an den Drucker und werden persistent übernommen — am
|
||||
Drucker-Display siehst du sofort dieselbe Belegung wie in der Bridge-UI.
|
||||
In 0.9.17 wurde der Befehl über das falsche MQTT-Topic
|
||||
(`slicer/printer/…`) gesendet, der Drucker hat ihn stillschweigend
|
||||
ignoriert. Jetzt geht er über `web/printer/…` wie der Anycubic
|
||||
Slicer Next es macht (verifiziert via Live-MQTT-Sniff +
|
||||
Workbench-Vue-Source). **Achtung:** der Drucker muss im Idle-Zustand
|
||||
sein, und leere Slots lassen sich nicht beschriften.
|
||||
- **Mehrsprachiges UI – spanische Übersetzung von Muttersprachler
|
||||
überarbeitet (PR #40 von @pezfisk):** fehlende Akzente
|
||||
(impresión, cámara, después, animación, …), Begriffe vereinheitlicht
|
||||
(Pause → Pausa, Start → Iniciar, Layer → Capa). Plus neues
|
||||
`README.es.md` und Cross-Links in den drei READMEs.
|
||||
- **Z-Höhen-Anzeige in Obico** funktioniert jetzt. Der Drucker liefert
|
||||
keine echte Z-Position via MQTT (per Live-Sniff bestätigt), die Bridge
|
||||
schätzt sie aus `curr_layer × layer_height + first_layer_height`.
|
||||
Layer-Heights kommen aus dem GCode-Header beim Upload, persistiert
|
||||
im GCode-Store; Fallback aus dem OrcaSlicer-Default-Filename
|
||||
(`…_0.2_…gcode`). `/server/files/metadata` liefert zusätzlich
|
||||
`object_height` (Gesamt-Z), damit Obicos `mmProgress`-Widget
|
||||
`aktuelles Z / Gesamt-Z` anzeigt.
|
||||
- **Slot-Karte zeigt den OrcaSlicer-Profil-Vendor** unter dem Material
|
||||
(z.B. „PLA / Polymaker"), mit Profil-Namen + interner ID als
|
||||
Tooltip. So ist auf einen Blick erkennbar welcher Slot-Override
|
||||
aktiv ist.
|
||||
|
||||
### Fixes
|
||||
- **Slot-Profil-Auswahl im AMS-Dialog (Issue #39 von @harrygeier):**
|
||||
drei separate Bugs in 0.9.17 sorgten dafür dass die gewählte Marke
|
||||
nach dem Speichern verschwand und beim erneuten Öffnen ein falsches
|
||||
Material angezeigt wurde.
|
||||
- `multiColorBox/setInfo` über das falsche MQTT-Topic — siehe oben.
|
||||
- Speichern lief in zwei parallelen Requests (Profil-Override +
|
||||
Material/Farbe) → Race-Bedingung. Läuft jetzt sequenziell und
|
||||
reloaded den lokalen State bevor der Dialog geschlossen wird.
|
||||
- OrcaSlicer-Filament-IDs sind nicht eindeutig — `orca_filaments.json`
|
||||
hat 68 duplikate IDs, `OGFL99` allein ist 136 Vendor-Profilen
|
||||
zugeordnet (Erkenntnis von @gangoke). Der primäre Selector ist
|
||||
jetzt `(vendor, name)` — über alle 1002 Profile eindeutig.
|
||||
- **MQTT-Reconnect (Issue #33 von @icebear):** wurde der Drucker über
|
||||
Nacht ausgeschaltet, schlug die Bridge nach 5 Reconnect-Versuchen
|
||||
(~60 s gesamt) endgültig fehl — Filament-Sync ging morgens noch
|
||||
(weil das HTTP ist), aber Print-Start scheiterte mit
|
||||
„connection refused", User musste die Bridge selbst neu starten.
|
||||
Reader-Thread reconnectet jetzt **endlos** (Backoff cappt bei 60 s)
|
||||
bis der Drucker wieder antwortet, mit DEBUG-Logging nach den ersten
|
||||
5 Versuchen damit das Log nicht über Nacht zugemüllt wird.
|
||||
- **„Unknown child pid"-Warnungen im Log:** beim Killen der Kamera-
|
||||
`ffmpeg`-Prozesse fehlte das `wait()` — Children blieben als
|
||||
Zombies und asyncio meldete sie alle ~20 s. Gefixt im CameraCache
|
||||
+ `/api/camera/stream`.
|
||||
|
||||
### UI-Aufräumen
|
||||
- **Pause-Button als Toggle:** druckt der Drucker → `⏸ Pause`, ist
|
||||
pausiert → `▶ Weiter`. Der separate „Weiter"-Button entfällt.
|
||||
- **Pause + Stopp komplett ausgeblendet wenn Drucker idle** — bei
|
||||
Standby waren beide Buttons vorher dauerhaft sichtbar, was beim
|
||||
Idle-Drucker verwirrend wirkte.
|
||||
|
||||
### Build
|
||||
- **GCode-Store-Migration:** neue Spalten `layer_height` +
|
||||
`first_layer_height` in `gcode_files` (automatisch beim ersten
|
||||
Start von 0.9.18 angelegt).
|
||||
|
||||
## [0.9.17] – 2026-05-30
|
||||
|
||||
### Neu
|
||||
- **🧪 Obico-Anbindung (experimentell):** Die Bridge spielt jetzt einen
|
||||
Moonraker, der vom [moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||
Plugin akzeptiert wird. Damit funktionieren Time-Lapse, Layer-aligned
|
||||
First-Layer-Scan und WebRTC-Live-Stream gegen einen (selbst gehosteten oder
|
||||
Cloud-) Obico-Server. **Hinweis:** Das KI-Modell zur Spaghetti-Erkennung
|
||||
ist auf seitliche Kamera-Winkel (Ender/Voron) trainiert — wie zuverlässig
|
||||
es beim Kobra X mit Top-Down-Kamera funktioniert, muss empirisch getestet
|
||||
werden (bei uns ging es schon ganz gut). Stream, Time-Lapse und Telemetrie
|
||||
laufen, die Failure-Erkennung ist deshalb noch als experimentell markiert.
|
||||
- **Mehrsprachiges UI (PR #37 von @gangoke):** Inline-Translations sind raus,
|
||||
stattdessen wechselbares Sprach-Dropdown mit Globe-Icon. Auto-Auswahl nach
|
||||
Browser-Locale, manuelle Wahl wird im LocalStorage gemerkt. Sprachen: 🇩🇪 🇬🇧
|
||||
🇪🇸 🇨🇳 (ES + ZH-CN sind KI-übersetzt und noch nicht von Muttersprachlern
|
||||
geprüft).
|
||||
- **OrcaSlicer-Filament-Profil pro AMS-Slot:** Im Slot-Bearbeiten-Dialog kannst
|
||||
du jetzt ein konkretes OrcaSlicer-Profil (z.B. „PolyTerra PLA — Polymaker")
|
||||
pro Slot wählen — die Bridge sendet diese Information beim AMS-Sync mit,
|
||||
statt nur „Generic PLA". Die Profil-Liste wird aus dem OrcaSlicer-Source
|
||||
generiert (~1000 Profile, 43 Hersteller). Damit OrcaSlicer den Hint
|
||||
vollständig respektiert, wird ein passender Patch im OrcaSlicer-KX-Build
|
||||
folgen.
|
||||
- **H.264-Direkt-Stream:** Neuer Endpunkt `/api/camera/h264` liefert den
|
||||
Drucker-Kamera-Stream ohne Re-Encoding als MPEG-TS — Latenz drastisch
|
||||
reduziert, Bridge-CPU bei Obico-Stream von ~13 % auf ~3 %.
|
||||
|
||||
### Fixes
|
||||
- **Temperatur-Setzen über Bridge-UI / Obico löste Drucker-Systemfehler aus:**
|
||||
Per Live-MQTT-Sniff vom Anycubic Slicer Next korrigiert — der Befehl
|
||||
`tempature/set` braucht ein `type`-Feld (0=Nozzle, 1=Bett, 2=beide) und
|
||||
muss über das `web/printer/…`-Topic, nicht `slicer/printer/…`. Nozzle/Bett
|
||||
über die Bridge heizen jetzt sauber.
|
||||
- **Große GCode-Uploads (>50 MB) brachen mit Timeout ab:** Der
|
||||
Connect-Timeout vom Socket lief auch während des `sendall()` — bei ~200 MB
|
||||
über LAN brauchte das Schieben mehr als die 30 s und wurde fälschlich als
|
||||
Connect-Timeout abgebrochen. Jetzt sind Connect-, Send- und Read-Phase
|
||||
separat getimeoutet.
|
||||
- **Kamera-Snapshot war langsam und konnte sich mit dem Live-Stream blockieren:**
|
||||
Die Bridge hält nun einen zentralen Kamera-Cache (ein einziger ffmpeg-Prozess
|
||||
zieht vom Drucker, alle Konsumenten teilen sich den Stream). Snapshots
|
||||
kommen in ~1.3 ms aus dem RAM statt nach 1-2 s per neuer ffmpeg-Instanz.
|
||||
Behebt außerdem das Single-Client-Limit am Drucker (HTTP 429 bei parallelen
|
||||
Zugriffen).
|
||||
- **Sprachwechsel aktualisierte den GCode-Browser nicht:** Die in die
|
||||
File-Karten eingebackenen Texte („Drucken", „Schätzung", „Download") blieben
|
||||
in der alten Sprache. Beim Sprachwechsel werden die Karten jetzt neu
|
||||
gerendert.
|
||||
- **GCode Web-Upload + Download + Verify-Dialog (PR #32 von @gangoke):**
|
||||
Dateien können direkt im Browser hoch/runtergeladen werden, mit
|
||||
Warn-Dialog wenn ein nicht durch OrcaSlicer hochgeladener GCode gestartet
|
||||
wird.
|
||||
|
||||
### CI/Build
|
||||
- Multi-Arch Docker-Image (amd64 + arm64) per Gitea-Actions automatisiert.
|
||||
- Release-Build über lokalen CodeBuilder für alle drei Targets
|
||||
(linux-amd64, linux-arm64, windows.exe).
|
||||
|
||||
## [0.9.16] – 2026-05-22
|
||||
|
||||
### Neu
|
||||
- **Kamera bei Druckstart automatisch einschalten:** neue Einstellung „Kamera bei
|
||||
Druckstart einschalten" — die Bridge startet den Kamera-Stream automatisch, wenn
|
||||
ein Druck beginnt (für OrcaSlicer und die Bridge-UI).
|
||||
|
||||
### Fixes
|
||||
- **Einfarbiger Druck durch leeren AMS-Slot blockiert:** OrcaSlicer schreibt alle
|
||||
konfigurierten Filamente in den GCode-Header, auch wenn das Modell nur eines
|
||||
nutzt — die Bridge meldete dem Drucker dadurch alle Farben als nötig, und ein
|
||||
leerer ungenutzter Slot brach den Druck ab. Die Bridge mappt jetzt nur die im
|
||||
GCode tatsächlich genutzten Filamente.
|
||||
- **Filament-Sync jetzt positionstreu:** Bei einem leeren Slot in der Mitte
|
||||
(z.B. Slot 1 gelb, 2 leer, 3 rot, 4 weiß) zeigte OrcaSlicer die Farben auf den
|
||||
falschen Slots. Behoben — leere Slots behalten ihre Position, und das
|
||||
Sync-Farbformat folgt der Happy-Hare-Konvention (RRGGBB ohne `#`).
|
||||
- **Slicer-Zeit + Thumbnail fehlten nach Browser-Reload** (oder bei Druckstart
|
||||
direkt aus OrcaSlicer): beide werden jetzt aus dem GCode-Store anhand des
|
||||
Dateinamens wiederhergestellt statt aus flüchtigem State.
|
||||
- **Deutsche Übersetzungslücken** im ACE-Trockner-Dialog behoben.
|
||||
|
||||
### Logging
|
||||
- Wiederholte Log-Zeilen werden als Zähler („×N") zusammengefasst statt zu spammen;
|
||||
Status-Poll-Verkehr wird nicht mehr auf INFO geloggt.
|
||||
- Neuer Level-Filter (Alle / Fehler / Warnungen), Toast bei neuen Fehlern, volle
|
||||
Tracebacks im Browser-Log und ein Download-Dateiname mit Zeitstempel.
|
||||
|
||||
## [0.9.15] – 2026-05-21
|
||||
|
||||
### Fixes (Issue #29)
|
||||
- **UI im OrcaSlicer-Device-Tab kaputt:** OrcaSlicers eingebetteter Webview lädt
|
||||
nur das nackte HTML und ignoriert externe `<script>`/`<link>`-Tags — nach der
|
||||
v0.9.14-Theme-Auslagerung funktionierte dort kein Button mehr. Die Bridge
|
||||
bettet CSS + JS jetzt inline in die Seite ein — funktioniert in Browser UND
|
||||
OrcaSlicer-Webview.
|
||||
- **Dropdowns unlesbar (weiß auf weiß) im OrcaSlicer-Webview:** `color-scheme` +
|
||||
explizite `select`/`option`-Farben ergänzt, damit die nativen Dropdowns in
|
||||
Hell- und Dunkel-Theme korrekt dargestellt werden.
|
||||
- **„Select slots"-Button tat direkt nach Upload nichts:** eine fehlende
|
||||
Variablen-Deklaration (`storeFiles`) warf einen `ReferenceError`, wenn vor dem
|
||||
Laden des Browser-Tabs geklickt wurde. Behoben.
|
||||
- **Upload-Banner kam nach abgeschlossenem Druck zurück:** der „file ready"-Status
|
||||
wurde nur bei Stop/Abbruch geleert, nicht bei `finished`. Jetzt auch nach
|
||||
erfolgreichem Druckende geleert.
|
||||
|
||||
## [0.9.14] – 2026-05-21
|
||||
|
||||
### Neu
|
||||
|
||||
421
CHANGELOG.md
421
CHANGELOG.md
@@ -1,5 +1,426 @@
|
||||
# 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
|
||||
- **Random crashes / container restarts — segfault in `libcrypto.so.3` (issue #53).**
|
||||
The MQTT-over-TLS client shared a single SSL socket between the reader thread
|
||||
(`recv`) and the sender threads (`sendall`) without serializing them. CPython's
|
||||
`ssl` module does not allow concurrent read and write on the same socket — the
|
||||
overlap corrupted OpenSSL's internal state, causing a heap corruption and a
|
||||
segfault that manifested reliably on some hosts (timing-dependent). All socket
|
||||
access (recv / sendall / close / reconnect) is now serialized under a single
|
||||
lock; the reader probes readiness with `select()` outside the lock so senders
|
||||
are never starved. Reconnect and disconnect now swap the socket atomically.
|
||||
Thanks to @BasK for the detailed fault-handler trace that pinpointed this.
|
||||
- **File browser accepted non-GCode uploads (issue #59).** Drag & drop bypassed
|
||||
the file picker's `accept` filter, so e.g. a JPG could be uploaded. Uploads are
|
||||
now validated both client- and server-side; only `.gcode`, `.gcode.3mf`, `.3mf`
|
||||
and `.bgcode` are accepted. Thanks @gangoke.
|
||||
|
||||
## [0.9.24] – 2026-06-16
|
||||
|
||||
### New
|
||||
- **Skip Objects available in every print flow (issue #57).** The "Skip objects"
|
||||
panel in the Slot Mapper used to appear only when printing from the Browser tab.
|
||||
It now shows in all flows (upload / print bar included), collapsed by default
|
||||
behind a `✂ Skip objects (N)` header to keep the dialog compact, expanding on
|
||||
click with the object preview and checklist.
|
||||
- **Slot Mapper shows the specific profile name (issue #57).** Each slot now
|
||||
displays its mapped filament profile (e.g. "PolyTerra PLA — Polymaker") in the
|
||||
dropdown options and as a hover tooltip on the slot marker, instead of just the
|
||||
generic type. Falls back to the generic type when no profile is mapped.
|
||||
|
||||
## [0.9.23] – 2026-06-16
|
||||
|
||||
### New
|
||||
- **Auto-open print dialog after upload.** A new `print_start_dialog` setting
|
||||
(Settings → Printer → "Start Print Behavior") controls what happens after a
|
||||
file is uploaded while the printer is idle: `Print Dialog` opens the
|
||||
slot-assignment dialog automatically, `Print Bar` keeps the previous banner
|
||||
behaviour. Based on PR #56 by @gangoke.
|
||||
- **Per-print auto-leveling toggle.** The print dialog now has its own
|
||||
auto-leveling checkbox that overrides the global default for a single print.
|
||||
|
||||
### Fixed
|
||||
- **Object skip was silently ignored at print start (PR #56, @gangoke).** The
|
||||
skip command was sent *before* the printer entered the `printing` state, so it
|
||||
was dropped. The skip is now re-applied in a retry loop once the print is
|
||||
confirmed running, with a pending-lock so the UI doesn't reset the skip state
|
||||
prematurely.
|
||||
- **Upload during an active print overwrote the running job's preview.**
|
||||
Uploading a new file while printing no longer replaces the thumbnail /
|
||||
file_ready of the job currently on the bed.
|
||||
|
||||
## [0.9.22] – 2026-06-16
|
||||
|
||||
### New
|
||||
- **Restructured Settings panel.** The settings modal has been replaced by a
|
||||
persistent Master-Detail panel with five categories: Connection, Printer,
|
||||
Appearance, Filament, and System. Poll interval is now adjustable live.
|
||||
- **Vendor visibility filter (issue #41).** A new checklist in the Filament
|
||||
settings lets you restrict the slot profile dropdown to specific manufacturers.
|
||||
"Generic" and your own imported profiles are always visible. The list updates
|
||||
automatically after a profile import.
|
||||
- **Idle file actions in the progress card (issue #55).** After uploading a file
|
||||
while the printer is idle, three quick-action buttons appear directly in the
|
||||
progress card: ▶ Print, ⚙ Map Slots, and ✕ Clear — matching the file browser
|
||||
workflow without navigating away.
|
||||
|
||||
### Fixed
|
||||
- **Mobileraker: app crashed with `Null is not a subtype of Object` in
|
||||
`ConfigExtruder.fromJson` (issue #48).** `configfile.config` was returned as
|
||||
an empty object `{}`. Mobileraker parses both `configfile.settings` and
|
||||
`configfile.config` through the same strict Dart parser — both are now
|
||||
populated with the same extruder/bed/stepper stub.
|
||||
- **Mobileraker: app hung indefinitely on refresh (issue #48).** The WebSocket
|
||||
`server.files.metadata` handler called a non-existent store method
|
||||
(`get_file_by_filename`), always returning empty metadata. Mobileraker retried
|
||||
this thousands of times per second. Both the HTTP and WS paths now share a
|
||||
single `_build_file_metadata()` method.
|
||||
- **Mobileraker: ETA / remaining time not shown (issue #48).** A side effect of
|
||||
the metadata loop fix — once `currentFile` resolves, Mobileraker can calculate
|
||||
ETA from `estimated_time`.
|
||||
- **Mobileraker: `notify_status_update` triggered repeated `ConfigFile.parse`
|
||||
(issue #48).** Static objects (`configfile`, `webhooks`, `heaters`, `history`)
|
||||
were included in every live status push. They are now filtered out; only live
|
||||
telemetry is broadcast.
|
||||
- `motion_report` (`live_position`, `live_velocity`) added to printer objects
|
||||
for Mobileraker motion display.
|
||||
- Saving filament slot profiles no longer silently drops the `visible_vendors`
|
||||
setting from `config.ini`.
|
||||
|
||||
## [0.9.21] – 2026-06-14
|
||||
|
||||
### Fixed
|
||||
- **Camera stream not visible on Android (Chrome / Firefox).** Android
|
||||
browsers do not support `multipart/x-mixed-replace` (MJPEG). The UI
|
||||
now detects Android and falls back to snapshot-polling at 5 fps
|
||||
(`/api/camera/snapshot` every 200 ms) — no server-side change needed.
|
||||
|
||||
### Changed
|
||||
- Docker image now pinned to **Debian 12 (Bookworm)** (`python:3.11-slim-bookworm`)
|
||||
to avoid glibc 2.41 compatibility issues introduced by the Debian 13
|
||||
base image that `python:3.11-slim` recently started pulling.
|
||||
- MQTT and HTTP connections now **force IPv4** (`AF_INET`) to prevent
|
||||
connection failures on hosts where the printer is only reachable via
|
||||
IPv4 but the OS prefers IPv6.
|
||||
- Extruder stub in the Moonraker `configfile` response now includes
|
||||
`sensor_type` and `filament_diameter` — fixes a Mobileraker crash
|
||||
(`Null is not a subtype of Object`, issue #48).
|
||||
|
||||
## [0.9.20] – 2026-06-08
|
||||
|
||||
### New
|
||||
- **French language support (PR #45 by @Nathacks)**
|
||||
- **Z height display in the print UI (PR #49 by @Nathacks).** Shows
|
||||
current Z position in mm below the layer counter.
|
||||
|
||||
### Fixed
|
||||
- **Camera auto-start ignored "Enable camera on print start" setting
|
||||
after a bridge restart (issue #50).** The setting was cached in the
|
||||
process environment — after saving it in the UI, the old value
|
||||
survived the restart and the new value from `config.ini` was never
|
||||
read.
|
||||
- **Camera restarted automatically after manual stop during a print
|
||||
(issue #50).** A new `_camera_user_stopped` flag suppresses
|
||||
auto-restart for the current print session. It resets when the
|
||||
print ends.
|
||||
- **Spurious "stream unavailable" error toast when stopping the camera
|
||||
manually.** The image error handler was still registered when
|
||||
`img.src` was cleared.
|
||||
- **JS error (`ReferenceError: br is not defined`) when toggling the
|
||||
light.** Variable was referenced from the wrong scope.
|
||||
- Webcam URLs are now absolute so that Mobileraker/Obico clients can
|
||||
reach them.
|
||||
|
||||
## [0.9.19.1] – 2026-06-04
|
||||
|
||||
### Fixed
|
||||
- Standalone binaries (Linux/Windows) reported `vunknown` as their
|
||||
version. The `VERSION` file is now embedded into the PyInstaller
|
||||
onefile bundle.
|
||||
- When the TLS certificates (`anycubic_slicer.crt`/`.key`) were
|
||||
missing, the bridge only logged the raw `[Errno 2] No such file
|
||||
or directory`. It now states clearly where the files need to be
|
||||
placed and that `anycubic-certs.zip` from the Gitea release is the
|
||||
source.
|
||||
|
||||
### Changed
|
||||
- Filament profile list re-curated: 209 entries instead of 399.
|
||||
Profiles that only exist inside printer-specific vendor bundles
|
||||
(e.g. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
||||
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) were dropped —
|
||||
OrcaSlicer wouldn't have found them in a default Kobra X setup
|
||||
anyway, because the matching vendor bundle is only loaded when
|
||||
the corresponding printer vendor is active. For those filaments
|
||||
the custom profile import (issue #41) remains the way.
|
||||
|
||||
## [0.9.19] – 2026-06-02
|
||||
|
||||
### New
|
||||
- **🎯 Filament sync with OrcaSlicer now picks the right preset**
|
||||
instead of always falling back to "Generic PLA". Requires an
|
||||
OrcaSlicer build with the
|
||||
[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)
|
||||
receive-side patch (included in the OrcaSlicer-KX build). The bridge
|
||||
sends `name` + `vendor_name` per AMS slot on the lane path AND
|
||||
`gate_filament_name` per gate on the Happy-Hare MMU path (OrcaSlicer
|
||||
switches to the HH path automatically for AMS setups).
|
||||
- **Import your own OrcaSlicer profiles into the bridge (issue #41).**
|
||||
Settings → "OrcaSlicer Profiles" or directly in the slot-edit dialog
|
||||
("★ Import own profiles…") lets you upload `.json` files from
|
||||
`~/.config/OrcaSlicer/user/<id>/filament/` — single files or as a
|
||||
ZIP. They show up in the slot dropdown under "★ Own profiles" and
|
||||
are passed through to Orca on sync as user matches. Works over HTTP
|
||||
so the bridge can run in Docker on a Raspi/NAS while OrcaSlicer
|
||||
lives on a desktop. Override-only profiles with just
|
||||
`inherits: "Generic PLA @System"` + a few tweaks are detected
|
||||
correctly — the bridge resolves the inherited fields from the
|
||||
system parent.
|
||||
|
||||
### Fixes
|
||||
- **AMS sync stuck on "Generic PLA":** the Orca data model has 68
|
||||
duplicate `filament_id` values (`OGFL99` alone shared by 136
|
||||
profiles), and the bridge often picked an ID that was not
|
||||
`is_compatible` with the Kobra X (e.g. `GFL92` from the Kobra-2
|
||||
profile → Orca rejected it). The generator now prioritises Kobra-X
|
||||
variants and filters out phantom profiles (cross-vendor overrides) —
|
||||
`orca_filaments.json` dropped from 1035 to 400 clean profiles.
|
||||
- **Slot without an explicit override now sends `Generic <type>`**
|
||||
instead of an implicit vendor guess. Library generic profiles have
|
||||
`compatible_printers: []` (= all printers), so they are always
|
||||
visible and match reliably.
|
||||
- **Slot card shows the vendor right after save** without a browser
|
||||
reload. `poll()` was async, the re-render only happened on the next
|
||||
tick.
|
||||
- **ACE dryer toggle threw a 502 even though the dryer worked
|
||||
(PR #42 by @gangoke):** `setDry` is now fire-and-forget like
|
||||
`setAutoFeed`. The printer answers on that push topic with
|
||||
`code: 0` instead of `code: 200`, which the bridge wrongly treated
|
||||
as an error.
|
||||
|
||||
### Data model / API
|
||||
- `orca_filaments.json` regenerated: only real vendor profiles and
|
||||
OrcaFilamentLibrary profiles. Bambu/Polymaker/SUNLU library profiles
|
||||
in, Qidi cross-bundles out.
|
||||
- New endpoints: `POST /kx/filament/profiles/user` (ZIP/JSON upload),
|
||||
`GET /kx/filament/profiles/user` (list user imports),
|
||||
`DELETE /kx/filament/profiles/user[?vendor=…&name=…]`. Persisted in
|
||||
`<KX_DATA_DIR>/orca_filaments.user.json` (volume — survives image
|
||||
updates).
|
||||
|
||||
### Build
|
||||
- New module `bridge/orca_filaments.py` (shared parser helpers used by
|
||||
the generator and the import endpoint).
|
||||
- Dockerfile + release.sh updated to include `orca_filaments.py`.
|
||||
|
||||
## [0.9.18] – 2026-05-31
|
||||
|
||||
### New
|
||||
- **🎉 Push filament material and colour from the bridge to the
|
||||
printer:** The values you pick in the slot-edit dialog now actually
|
||||
reach the printer and stick — the printer display shows the same
|
||||
slot setup as the bridge UI right away. In 0.9.17 the command was
|
||||
sent over the wrong MQTT topic (`slicer/printer/…`) and the printer
|
||||
silently dropped it. It now goes via `web/printer/…` like the
|
||||
Anycubic Slicer Next does (verified by live MQTT sniff +
|
||||
Workbench-Vue source). **Note:** the printer must be idle, and
|
||||
empty slots can not be labelled.
|
||||
- **Spanish translation reviewed by a native speaker (PR #40 by
|
||||
@pezfisk):** missing accents (impresión, cámara, después,
|
||||
animación, …) and term consistency (Pause → Pausa, Start →
|
||||
Iniciar, Layer → Capa). New `README.es.md` and cross-links between
|
||||
the three READMEs.
|
||||
- **Z-height now shows up in Obico.** The printer does not report a
|
||||
real Z position over MQTT (live-sniff confirmed), so the bridge
|
||||
estimates it from `current_layer × layer_height + first_layer_height`.
|
||||
Layer heights are parsed from the gcode header at upload time and
|
||||
persisted in the gcode store; fallback for prints started directly
|
||||
from the slicer is the OrcaSlicer default filename pattern
|
||||
(`…_0.2_…gcode`). `/server/files/metadata` also serves
|
||||
`object_height` (total Z) so Obicos `mmProgress` widget can render
|
||||
`current Z / total Z`.
|
||||
- **Slot card shows the OrcaSlicer profile vendor** under the
|
||||
material (e.g. `PLA / Polymaker`), with the profile name + internal
|
||||
ID as tooltip. Lets you see at a glance which slot override is
|
||||
active.
|
||||
|
||||
### Fixes
|
||||
- **Slot profile picker in the AMS dialog (issue #39 by
|
||||
@harrygeier):** three separate bugs in 0.9.17 caused the chosen
|
||||
brand to disappear after save and a different material to show up
|
||||
on re-open.
|
||||
- `multiColorBox/setInfo` was sent on the wrong MQTT topic — see
|
||||
above.
|
||||
- Save fired two parallel requests (profile override + material/
|
||||
colour) → race. Now sequential, and the local state is reloaded
|
||||
before the dialog closes.
|
||||
- OrcaSlicer filament IDs are not unique — `orca_filaments.json`
|
||||
has 68 duplicate IDs, `OGFL99` alone is shared by 136 vendor
|
||||
profiles (caught by @gangoke). The primary selector is now
|
||||
`(vendor, name)` — unique across all 1002 profiles.
|
||||
- **MQTT reconnect (issue #33 by @icebear):** if the printer was
|
||||
powered off overnight the bridge gave up after 5 reconnect attempts
|
||||
(~60 s total) — filament sync still worked in the morning (its
|
||||
HTTP), but starting a print failed with `connection refused` and
|
||||
the user had to restart the bridge itself. The reader thread now
|
||||
reconnects **forever** (backoff caps at 60 s) until the printer
|
||||
responds again, with logs dropping to DEBUG after the first 5
|
||||
attempts so an overnight outage does not spam the log.
|
||||
- **`Unknown child pid` warnings in the log:** the camera ffmpeg
|
||||
helpers were killed without awaiting their `wait()` — children
|
||||
lingered as zombies and asyncio reported them every ~20 s. Fixed
|
||||
in CameraCache + `/api/camera/stream`.
|
||||
|
||||
### UI polish
|
||||
- **Pause button is now a toggle:** while printing → `⏸ Pause`,
|
||||
while paused → `▶ Resume`. The separate resume button is gone.
|
||||
- **Pause + stop hidden when the printer is idle** — both used to be
|
||||
visible at all times, which was confusing on a standby printer.
|
||||
|
||||
### Build
|
||||
- **gcode store migration:** new columns `layer_height` +
|
||||
`first_layer_height` on `gcode_files` (added automatically on first
|
||||
start of 0.9.18).
|
||||
|
||||
## [0.9.17] – 2026-05-30
|
||||
|
||||
### New
|
||||
- **🧪 Obico integration (experimental):** The bridge now exposes a
|
||||
Moonraker-compatible surface that the
|
||||
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||
plugin accepts. Time-lapses, layer-aligned first-layer scan and WebRTC
|
||||
live streaming work against a (self-hosted or cloud) Obico server.
|
||||
**Note:** the spaghetti-detection ML model is trained on side-view
|
||||
cameras (Ender/Voron); how well it works with the Kobra X's top-down
|
||||
camera is still to be evaluated empirically (it already looked
|
||||
promising in our tests). Stream, time-lapse and telemetry work — the
|
||||
failure-detection side stays flagged as experimental for now.
|
||||
- **Multi-language UI (PR #37 by @gangoke):** Inline translations have
|
||||
moved into JSON files; a globe-icon dropdown lets you switch language.
|
||||
Browser locale is auto-detected; manual choice persists in
|
||||
LocalStorage. Languages: 🇩🇪 🇬🇧 🇪🇸 🇨🇳 (ES + ZH-CN are AI-translated
|
||||
and not verified by native speakers yet).
|
||||
- **OrcaSlicer filament profile per AMS slot:** The slot-edit dialog now
|
||||
lets you pick a concrete OrcaSlicer profile (e.g. "PolyTerra PLA —
|
||||
Polymaker") per slot; the bridge sends it along on AMS sync instead
|
||||
of just "Generic PLA". Profile list is generated from the OrcaSlicer
|
||||
source (~1000 profiles, 43 vendors). A matching patch in
|
||||
OrcaSlicer-KX is on the way so OrcaSlicer fully honours the hint.
|
||||
- **H.264 direct stream:** New `/api/camera/h264` endpoint serves the
|
||||
printer camera stream as MPEG-TS without re-encoding — dramatically
|
||||
reduces latency, bridge CPU during Obico streaming drops from ~13 %
|
||||
to ~3 %.
|
||||
|
||||
### Fixes
|
||||
- **Setting temperature via bridge UI / Obico caused a printer system
|
||||
error:** Fixed via live MQTT capture from Anycubic Slicer Next — the
|
||||
`tempature/set` command needs a `type` field (0=nozzle, 1=bed,
|
||||
2=both) and must go over the `web/printer/…` topic, not
|
||||
`slicer/printer/…`. Nozzle/bed heating from the bridge now works.
|
||||
- **Large GCode uploads (>50 MB) timed out:** The socket connect timeout
|
||||
was active during `sendall()` too — pushing ~200 MB over LAN took
|
||||
more than 30 s and was falsely aborted. Connect / send / read phases
|
||||
are now timed out separately.
|
||||
- **Camera snapshots were slow and could collide with the live stream:**
|
||||
The bridge now keeps a central camera cache (one ffmpeg pulls from
|
||||
the printer, all consumers share it). Snapshots return in ~1.3 ms
|
||||
from RAM instead of 1–2 s per spawned ffmpeg. Also resolves the
|
||||
single-client limit on the printer (HTTP 429 on parallel access).
|
||||
- **Language switch did not refresh the GCode browser:** Strings baked
|
||||
into the file cards ("Print", "Estimate", "Download") stayed in the
|
||||
previous language. Cards are now re-rendered on language switch.
|
||||
- **GCode web upload + download + verify dialog (PR #32 by @gangoke):**
|
||||
Files can be uploaded / downloaded directly in the browser, with a
|
||||
warning dialog when starting a GCode that was not uploaded via
|
||||
OrcaSlicer.
|
||||
|
||||
### CI/Build
|
||||
- Multi-arch Docker image (amd64 + arm64) automated via Gitea Actions.
|
||||
- Release builds for all three targets (linux-amd64, linux-arm64,
|
||||
windows.exe) via the local CodeBuilder.
|
||||
|
||||
## [0.9.16] – 2026-05-22
|
||||
|
||||
### New
|
||||
- **Auto-start camera on print:** new setting "Turn camera on at print start" —
|
||||
when enabled, the bridge starts the camera stream automatically when a print
|
||||
begins (works for both OrcaSlicer and the Bridge UI).
|
||||
|
||||
### Fixes
|
||||
- **Single-color print blocked by an empty AMS slot:** OrcaSlicer writes all
|
||||
configured filaments into the GCode header even when the model uses only one,
|
||||
so the bridge told the printer it needed every color — and an empty unused slot
|
||||
aborted the print. The bridge now maps only the filaments actually used by the
|
||||
GCode.
|
||||
- **Filament sync now position-accurate:** with an empty slot in the middle
|
||||
(e.g. slot 1 yellow, 2 empty, 3 red, 4 white) OrcaSlicer showed the colors
|
||||
shifted onto the wrong slots. Fixed — empty slots keep their position, and the
|
||||
sync color format follows the Happy Hare convention (RRGGBB without `#`).
|
||||
- **Slicer time + thumbnail missing after a browser reload** (or when a print was
|
||||
started directly from OrcaSlicer): both are now restored from the GCode store
|
||||
by filename instead of relying on volatile state.
|
||||
- **German translation gaps** in the ACE dryer dialog fixed.
|
||||
|
||||
### Logging
|
||||
- Repeated log lines are collapsed into a counter ("×N") instead of spamming the
|
||||
console; status-poll traffic is no longer logged at INFO.
|
||||
- New log level filter (All / Errors / Warnings), a toast on new errors, full
|
||||
tracebacks forwarded to the browser log, and a timestamped download filename.
|
||||
|
||||
## [0.9.15] – 2026-05-21
|
||||
|
||||
### Fixes (Issue #29)
|
||||
- **UI in the OrcaSlicer device tab was broken:** OrcaSlicer's embedded webview
|
||||
only loads the bare HTML and ignores external `<script>`/`<link>` tags, so after
|
||||
the v0.9.14 theme split none of the buttons worked in the device tab. The
|
||||
bridge now inlines CSS + JS into the page — works in both the browser and the
|
||||
OrcaSlicer webview.
|
||||
- **Dropdowns unreadable (white-on-white) in the OrcaSlicer webview:** added
|
||||
`color-scheme` + explicit `select`/`option` colors so the native dropdowns
|
||||
render correctly in dark and light theme.
|
||||
- **"Select slots" button did nothing right after an upload:** a missing variable
|
||||
declaration (`storeFiles`) threw a `ReferenceError` when clicked before the
|
||||
Browser tab had loaded. Fixed.
|
||||
- **Upload banner came back after a finished print:** the "file ready" state was
|
||||
only cleared on stop/cancel, not on `finished`. Now cleared on completion too.
|
||||
|
||||
## [0.9.14] – 2026-05-21
|
||||
|
||||
### New
|
||||
|
||||
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.
|
||||
@@ -1,15 +1,21 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY kobrax_moonraker_bridge.py .
|
||||
COPY web/ ./web/
|
||||
# Statische Daten (orca_filaments.json etc.) liegen in /app/static/, NICHT in
|
||||
# /app/data/ — letzteres wird vom User als Volume gemountet (Runtime-State).
|
||||
COPY data/ ./static/
|
||||
COPY config_loader.py .
|
||||
COPY env_loader.py .
|
||||
COPY kobrax_client.py .
|
||||
COPY orca_filaments.py .
|
||||
COPY VERSION .
|
||||
COPY anycubic_slicer.crt .
|
||||
COPY anycubic_slicer.key .
|
||||
|
||||
65
README.de.md
65
README.de.md
@@ -8,7 +8,21 @@
|
||||
|
||||
Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
||||
|
||||
<sub>🇬🇧 <a href="README.md">English version</a></sub>
|
||||
<sub>🧪 Ein Community-Bericht auf Reddit deutet darauf hin, dass die Bridge auch
|
||||
mit dem **Kobra S1** und **Kobra S1 Max** funktioniert — die Protokolle wirken
|
||||
kompatibel, beides ist aber weder offiziell getestet noch unterstützt.
|
||||
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>
|
||||
|
||||
@@ -32,12 +46,17 @@ Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
||||
|---|---|
|
||||
| 🖨️ | **Druckersteuerung** — Start, Pause, Resume, Abbruch, Temperaturen, Druckgeschwindigkeit |
|
||||
| 📊 | **Live-Status** — Temperatur, Fortschritt, Layer, Restzeit, Kamera-Stream |
|
||||
| 🎨 | **AMS / Multicolor** — Filament-Slots, Per-Kanal-Remapping, MMU-Emulation für OrcaSlicer Filament-Sync |
|
||||
| 🎨 | **AMS / Multicolor** — Slots mit **Profil-Picker pro Slot** (eigene Marke aus OrcaSlicer-Profilen pro Slot zuweisen); Bridge schreibt Material und Farbe ans Drucker-Display zurück |
|
||||
| 📦 | **Eigene OrcaSlicer-Profile importieren** — ZIP aus `~/.config/OrcaSlicer/user/<id>/filament/` in die Bridge ziehen; tauchen im Slot-Dropdown unter ★ Eigene Profile auf |
|
||||
| 📷 | **Obico-Integration (experimentell)** — Time-Lapse und WebRTC-Livestream gegen einen selbst gehosteten [Obico-Server](https://github.com/TheSpaghettiDetective/obico-server) via moonraker-obico |
|
||||
| 📐 | **H.264-Direkt-Stream + Z-Höhe** — sparsamer Kamera-Pfad für Obico, aktuelle Z aus der Layer-Höhe abgeleitet (Mm-Progress-Widget) |
|
||||
| 🗂️ | **GCode-Browser** — hochgeladene Dateien mit Thumbnail, Druckhistorie, Suche & Filter |
|
||||
| 🧩 | **Multi-Printer** — mehrere Drucker in **einer** Bridge-Instanz, Umschalten per Dropdown |
|
||||
| ➕ | **Drucker hinzufügen per Klick** — nur die IP eingeben, Zugangsdaten werden automatisch importiert |
|
||||
| 🔁 | **Robuster MQTT-Reconnect** — Bridge überlebt nächtlichen Drucker-Reboot ohne manuellen Neustart |
|
||||
| 🌐 | **Mehrsprachiges UI** — DE / EN / ES / 中文, Browser-Sprache automatisch erkannt |
|
||||
| 🔄 | **Self-Update** — neue Versionen direkt im Browser installieren |
|
||||
| 🌐 | **OrcaSlicer** — volles Moonraker-Protokoll (HTTP + WebSocket), DE/EN UI |
|
||||
| 🧠 | **OrcaSlicer** — volles Moonraker-Protokoll (HTTP + WebSocket); für korrekten Vendor-Match pro Slot den [OrcaSlicer-KX-Build](#-empfohlener-slicer) nutzen |
|
||||
|
||||
---
|
||||
|
||||
@@ -93,11 +112,31 @@ 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
|
||||
## 🎨 Empfohlener Slicer
|
||||
|
||||
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||
Für sauberen AMS-Filament-Sync gibt es einen **gepatchten OrcaSlicer-Build**:
|
||||
|
||||
→ **[OrcaSlicer-KX Releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
|
||||
|
||||
**Upstream-PRs im KX-Build:**
|
||||
|
||||
- **[PR #13372](https://github.com/SoftFever/OrcaSlicer/pull/13372)** — Moonraker / Happy-Hare AMS-Sync-Fix (Slot-Positionen bleiben auch bei leerem Slot in der Mitte korrekt)
|
||||
- **[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)** — Vendor- + Name-Matching für Moonraker (liest `name` + `vendor_name` pro Slot und matched gegen deine Filament-Presets), von [@LordGuenni](https://github.com/LordGuenni)
|
||||
- **[PR #13315](https://github.com/SoftFever/OrcaSlicer/pull/13315)** — Eindeutige `filament_id` für User-Presets (neu erstellte eigene Profile bekommen eine frische ID statt das `OGFL99` vom Generic-Parent zu erben), von [@mrnoisytiger](https://github.com/mrnoisytiger)
|
||||
|
||||
**Plus vier KX-eigene Verbesserungen on top:**
|
||||
|
||||
- Bridge-Filament-Hint (`tray_info_idx` + Vendor) respektieren
|
||||
- Vendor-Match auch wenn das gewählte Base-Preset **nicht is_compatible** mit dem aktiven Drucker ist (so matchen Profile aus anderen Drucker-Setups trotzdem über die Marke)
|
||||
- Vendor-Match wenn `tray_info_idx` gesetzt ist, das Preset aber inkompatibel
|
||||
- Zwei-Pass-Suche: erst kompatible Presets, dann alle sichtbaren
|
||||
|
||||
**Warum das zusammen wichtig ist:** ohne #13719 landen die AMS-Slots in OrcaSlicer alle auf `Generic PLA` / `Generic PETG`, obwohl die Bridge die konkrete Marke schon mitsendet (`name + vendor_name + gate_filament_name`). Mit dem KX-Build matched OrcaSlicer deine echten User-Presets — auch die, die du via [Eigene OrcaSlicer-Profile importieren](#-features) in die Bridge gezogen hast.
|
||||
|
||||
Stock-Upstream-OrcaSlicer funktioniert für Slicing und Drucken weiterhin — nur das Per-Slot-Vendor-Matching beim AMS-Sync fällt dann weg. Material und Farbe pro Slot kannst du auch ohne den KX-Build über die Bridge ans Drucker-Display schreiben (das läuft über MQTT, nicht über den Slicer).
|
||||
|
||||
OrcaSlicer-KX ist ein Build von [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); der Quellcode der Upstream-PRs ist auf GitHub, die KX-spezifischen Patches im OrcaSlicer-KX-Repo.
|
||||
|
||||
---
|
||||
|
||||
@@ -106,6 +145,13 @@ Drucker → Verbindungstyp **Moonraker** → Host: `http://BRIDGE-IP:7125`
|
||||
- **[Home-Assistant-Integration](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||
von [@gangoke](https://github.com/gangoke) — bindet Sensoren, Drucksteuerung,
|
||||
Licht, Kamera und das GCode-Vorschaubild als native Home-Assistant-Entitäten ein.
|
||||
- **[Obico (selbst gehostet)](https://github.com/TheSpaghettiDetective/obico-server)** —
|
||||
die Bridge bietet eine Moonraker-kompatible API, die
|
||||
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||
akzeptiert; damit hast du Time-Lapse und WebRTC-Live-Stream gegen deinen
|
||||
eigenen Obico-Server. Die KI-Spaghetti-Erkennung ist beim Kobra X
|
||||
experimentell — der Top-Down-Kamerawinkel weicht von dem ab, auf den
|
||||
das Modell trainiert wurde.
|
||||
|
||||
> Dies sind **Community-Projekte**, die nicht von KX-Bridge betreut oder
|
||||
> supportet werden. Bei Fragen oder Problemen bitte das verlinkte Repository nutzen.
|
||||
@@ -132,9 +178,10 @@ dann `extract_credentials` ausführen → gibt Username, Passwort, Device-ID und
|
||||
## ⚙️ Nützliche Befehle
|
||||
|
||||
```bash
|
||||
docker compose logs -f # Logs anzeigen
|
||||
docker compose down # Bridge stoppen
|
||||
docker compose up -d --build # Bridge neu bauen & starten (nach Update)
|
||||
docker compose logs -f # Logs anzeigen
|
||||
docker compose down # Bridge stoppen
|
||||
docker compose pull && docker compose up -d # auf neueste veröffentlichte Version updaten
|
||||
docker compose up -d --build # lokal selber bauen (statt zu pullen)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
281
README.es.md
Normal file
281
README.es.md
Normal file
@@ -0,0 +1,281 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="knlogo.png" alt="KX-Bridge" width="160"/>
|
||||
|
||||
# KX-Bridge
|
||||
|
||||
**Controla tu Anycubic Kobra X con OrcaSlicer — sin Klipper, sin Raspberry Pi.**
|
||||
|
||||
Un puente compatible con Moonraker que se comunica directamente con la impresora.
|
||||
|
||||
<sub>🧪 Un usuario en Reddit ha reportado que el puente también funciona con la
|
||||
**Kobra S1** y la **Kobra S1 Max** — los protocolos parecen compatibles, pero
|
||||
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)
|
||||
|
||||
[](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
|
||||
|
||||
[](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
|
||||
|
||||
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||
|
||||
<sub>¿Te gusta KX-Bridge? Un café en <a href="https://ko-fi.com/viewitde">Ko-fi</a> mantiene el proyecto vivo. ☕</sub>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ✨ Características
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 🖨️ | **Control de impresora** — iniciar, pausar, reanudar, cancelar, temperaturas, velocidad de impresión |
|
||||
| 📊 | **Estado en tiempo real** — temperatura, progreso, capas, tiempo restante, transmisión de cámara |
|
||||
| 🎨 | **AMS / multicolor** — ranuras con **selector de perfil por ranura** (asigna tu propia marca de los perfiles de OrcaSlicer a cada ranura); el puente escribe material y color al display de la impresora |
|
||||
| 📦 | **Importa tus propios perfiles de OrcaSlicer** — arrastra un ZIP de `~/.config/OrcaSlicer/user/<id>/filament/` al puente; aparecen en el desplegable de la ranura bajo ★ Perfiles propios |
|
||||
| 📷 | **Integración con Obico (experimental)** — Time-Lapse y stream en vivo WebRTC contra un [servidor Obico](https://github.com/TheSpaghettiDetective/obico-server) autoalojado vía moonraker-obico |
|
||||
| 📐 | **Stream H.264 directo + altura Z** — ruta de cámara de bajo consumo de CPU para Obico, Z actual derivada de la altura de capa (widget de progreso) |
|
||||
| 🗂️ | **Explorador de GCode** — archivos subidos con vistas previas, historial de impresión, búsqueda y filtros |
|
||||
| 🧩 | **Multi-impresora** — múltiples impresoras en **una** instancia del puente, cambia mediante un menú desplegable |
|
||||
| ➕ | **Añade una impresora con un clic** — solo introduce la IP, las credenciales se importan automáticamente |
|
||||
| 🔁 | **Reconexión MQTT robusta** — el puente sobrevive a reinicios nocturnos de la impresora sin reinicio manual |
|
||||
| 🌐 | **Interfaz multilingüe** — DE / EN / ES / 中文, detecta automáticamente el idioma del navegador |
|
||||
| 🔄 | **Actualización automática** — instala nuevas versiones directamente desde el navegador |
|
||||
| 🧠 | **OrcaSlicer** — protocolo Moonraker completo (HTTP + WebSocket); usa el [build OrcaSlicer-KX](#-slicer-recomendado) para emparejamiento correcto de vendor por ranura |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Inicio rápido
|
||||
|
||||
### 1. Prepara la impresora
|
||||
|
||||
Activa el modo LAN en la Kobra X:
|
||||
**Pantalla de la impresora → Ajustes → Activar modo LAN**
|
||||
|
||||
### 2. Inicia el puente
|
||||
|
||||
**Docker (recomendado):**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Binario Linux (sin Docker):**
|
||||
```bash
|
||||
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
|
||||
```
|
||||
|
||||
**EXE Windows (sin Docker):**
|
||||
```
|
||||
kx-bridge.exe
|
||||
```
|
||||
|
||||
> ⚠️ **Certificados TLS necesarios para el binario standalone**
|
||||
>
|
||||
> El bridge habla con el MQTT de la impresora vía mTLS y necesita dos
|
||||
> ficheros de certificado **junto al binario**:
|
||||
>
|
||||
> - `anycubic_slicer.crt`
|
||||
> - `anycubic_slicer.key`
|
||||
>
|
||||
> Ambos vienen en **`anycubic-certs.zip`** en la misma página de release.
|
||||
> Descárgalo y extrae los dos ficheros en el mismo directorio que
|
||||
> `kx-bridge-linux-amd64` o `kx-bridge.exe`. Sin ellos verás
|
||||
> `Verbindung fehlgeschlagen: TLS-Zertifikate fehlen …` (0.9.19.1+) o
|
||||
> `[Errno 2] No such file or directory` (versiones anteriores).
|
||||
>
|
||||
> Estructura correcta:
|
||||
> ```
|
||||
> ~/kx-bridge/
|
||||
> ├── kx-bridge-linux-amd64 (o kx-bridge.exe)
|
||||
> ├── anycubic_slicer.crt ← de anycubic-certs.zip
|
||||
> ├── anycubic_slicer.key ← de anycubic-certs.zip
|
||||
> └── config/ (se crea en el primer arranque)
|
||||
> ```
|
||||
>
|
||||
> Los usuarios de Docker no necesitan hacer esto — los certificados
|
||||
> están incluidos en la imagen.
|
||||
|
||||
> Con los binarios de Linux y Windows, `config/` y `data/` (configuración,
|
||||
> SQLite, almacén de GCode) viven junto al programa. Copia toda la carpeta
|
||||
> para mover la instalación.
|
||||
|
||||
**Python directamente:**
|
||||
```bash
|
||||
pip install -r bridge/requirements.txt
|
||||
python bridge/kobrax_moonraker_bridge.py
|
||||
```
|
||||
|
||||
### 3. Configura la impresora
|
||||
|
||||
Abre la interfaz web: **`http://IP-DEL-PUENTE:7125`**
|
||||
|
||||
En el primer inicio, la pestaña **Impresoras** muestra *"+ Añadir impresora"* — solo introduce la dirección IP
|
||||
de la impresora, el resto (usuario, contraseña, ID de dispositivo) se obtiene de la impresora y se desencripta
|
||||
automáticamente. Listo.
|
||||
|
||||
> ¿Más de una impresora? Simplemente haz clic en *"+ Añadir impresora"* de nuevo — cada una recibe su propio puerto
|
||||
> (7125, 7126, …) y se puede seleccionar desde el menú desplegable del encabezado.
|
||||
|
||||
### 4. Conecta OrcaSlicer
|
||||
|
||||
Impresora → Tipo de conexión **Moonraker** → Host: `http://IP-DEL-PUENTE:7125`
|
||||
|
||||
> ⚠️ El tipo de conexión debe ser **Moonraker** (no "Bambu" ni "Klipper").
|
||||
> Introduce la URL completa incluyendo `http://` y el puerto `:7125` en el campo de host.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 🎨 Slicer recomendado
|
||||
|
||||
Para una sincronización de filamento AMS correcta ofrecemos una **versión modificada de OrcaSlicer**:
|
||||
|
||||
→ **[Lanzamientos de OrcaSlicer-KX](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
|
||||
|
||||
**PRs upstream incluidos en el build KX:**
|
||||
|
||||
- **[PR #13372](https://github.com/SoftFever/OrcaSlicer/pull/13372)** — Corrección de sincronización Moonraker / Happy-Hare AMS (las posiciones de las ranuras se mantienen correctas incluso con ranuras vacías)
|
||||
- **[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)** — Coincidencia de Vendor + Nombre para Moonraker (lee `name` + `vendor_name` por ranura y los empareja con los presets de filamento del usuario), por [@LordGuenni](https://github.com/LordGuenni)
|
||||
- **[PR #13315](https://github.com/SoftFever/OrcaSlicer/pull/13315)** — `filament_id` único para presets de usuario (los perfiles nuevos reciben un ID nuevo en vez de heredar `OGFL99` del padre genérico), por [@mrnoisytiger](https://github.com/mrnoisytiger)
|
||||
|
||||
**Más cuatro mejoras específicas de KX encima:**
|
||||
|
||||
- Respetar el hint de filamento del puente (`tray_info_idx` + vendor)
|
||||
- Coincidencia por vendor incluso cuando el preset base no es **is_compatible** con la impresora activa (así un perfil copiado de otra máquina sigue coincidiendo por vendor)
|
||||
- Coincidencia por vendor cuando `tray_info_idx` está definido pero su preset es incompatible
|
||||
- Búsqueda de dos pasadas: primero presets compatibles, luego todos los visibles
|
||||
|
||||
**Por qué importa:** sin #13719 todas las ranuras AMS caen en `Generic PLA` / `Generic PETG` aunque el puente ya envíe la marca concreta (`name + vendor_name + gate_filament_name`). Con el build KX, OrcaSlicer coincide con tus presets de usuario reales — incluyendo los perfiles que importaste al puente vía [Importa tus propios perfiles de OrcaSlicer](#-características).
|
||||
|
||||
OrcaSlicer upstream también funciona para rebanar e imprimir — solo pierdes la coincidencia de vendor por ranura en la sincronización AMS. El material y color por ranura se pueden empujar puente → impresora con cualquier slicer (eso va por MQTT, no por el slicer).
|
||||
|
||||
OrcaSlicer-KX es un build de [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); el código fuente de cada PR upstream está en GitHub, los parches específicos de KX en el repo OrcaSlicer-KX.
|
||||
|
||||
---
|
||||
|
||||
## 🏠 Comunidad e integraciones
|
||||
|
||||
- **[Integración con Home Assistant](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||
por [@gangoke](https://github.com/gangoke) — expone sensores, controles de impresión,
|
||||
luz, cámara y la vista previa del GCode como entidades nativas de Home Assistant.
|
||||
- **[Obico (autoalojado)](https://github.com/TheSpaghettiDetective/obico-server)** —
|
||||
el puente expone una API compatible con Moonraker que
|
||||
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||
acepta, así obtienes Time-Lapse y streaming en vivo WebRTC contra tu propio
|
||||
servidor Obico. La detección de fallos por IA es experimental en la Kobra X
|
||||
(el ángulo cenital de la cámara difiere del que el modelo fue entrenado).
|
||||
|
||||
> Estos son **proyectos de la comunidad**, no mantenidos ni soportados por KX-Bridge.
|
||||
> Para preguntas o problemas, utiliza el repositorio enlazado.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Obtener credenciales manualmente
|
||||
|
||||
Normalmente no es necesario — *"+ Añadir impresora"* lo hace automáticamente. Si lo necesitas:
|
||||
|
||||
```bash
|
||||
fetch_credentials --ip 192.168.x.x --write-config
|
||||
```
|
||||
Obtiene las credenciales directamente de la impresora vía HTTP y las escribe en `config/config.ini`.
|
||||
Solo se requiere la IP de la impresora, no hace falta un slicer.
|
||||
|
||||
Alternativamente (si se desconoce la IP): abre AnycubicSlicerNext, conecta la impresora, luego ejecuta
|
||||
`extract_credentials` → muestra usuario, contraseña, ID de dispositivo y la IP de la impresora.
|
||||
|
||||
> **Descargas:** [Lanzamientos](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases) → `fetch_credentials` / `extract_credentials` (Linux y Windows)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Comandos útiles
|
||||
|
||||
```bash
|
||||
docker compose logs -f # mostrar registros
|
||||
docker compose down # detener el puente
|
||||
docker compose pull && docker compose up -d # actualizar a la imagen publicada más reciente
|
||||
docker compose up -d --build # recompilar localmente (en lugar de descargar)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🩹 Solución de problemas
|
||||
|
||||
<details>
|
||||
<summary><b>"Credenciales MQTT incorrectas" al iniciar</b></summary>
|
||||
|
||||
- Vuelve a añadir la impresora mediante *"+ Añadir impresora"*, o ejecuta
|
||||
`fetch_credentials --ip <ip> --write-config` y reinicia el puente
|
||||
- Introduce solo la dirección IP, sin puerto (✗ `192.168.1.102:9883` → ✓ `192.168.1.102`)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Impresora no encontrada / modo LAN no activado</b></summary>
|
||||
|
||||
- En la pantalla de la impresora: Ajustes → Activar modo LAN
|
||||
- La impresora y el puente deben estar en la misma red
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Docker: Permiso denegado</b></summary>
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER # luego cierra la sesión y vuelve a iniciarla
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Actualizar desde 0.9.1 o anterior</b></summary>
|
||||
|
||||
A partir de 0.9.2, KX-Bridge almacena la configuración en `config/config.ini` en lugar de `.env`.
|
||||
La migración se ejecuta automáticamente en el primer inicio después de la actualización — no requiere acción.
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Seguridad
|
||||
|
||||
- El puente es accesible en la red local en `http://<IP-del-host>:7125` — **no** lo expongas a internet
|
||||
- `config/config.ini` contiene las credenciales de la impresora — no las compartas públicamente
|
||||
- Las credenciales **no** otorgan acceso a los servicios en la nube de Anycubic
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licencia
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
KX-Bridge se publica bajo la **GNU General Public License v3.0**. Consulta
|
||||
[LICENSE](LICENSE) para el texto completo. Las bifurcaciones y modificaciones deben
|
||||
permanecer bajo GPLv3 si se redistribuyen.
|
||||
|
||||
La implementación del protocolo MQTT es el resultado de una ingeniería inversa
|
||||
independiente con fines de interoperabilidad (§69e UrhG / Directiva de Software de la UE
|
||||
Art. 6). El material de terceros en el repositorio (certificados TLS de Anycubic)
|
||||
**no** está cubierto por GPLv3 y se incluye únicamente para permitir la
|
||||
autenticación contra impresoras que el usuario final ya posee. Consulta
|
||||
[NOTICE.md](NOTICE.md) para más detalles y el aviso legal.
|
||||
|
||||
Este proyecto es independiente y no está afiliado con Anycubic.
|
||||
|
||||
<div align="center">
|
||||
<br>
|
||||
|
||||
**Si KX-Bridge te ayuda, el proyecto agradece tu apoyo:**
|
||||
|
||||
[](https://ko-fi.com/viewitde)
|
||||
|
||||
</div>
|
||||
64
README.md
64
README.md
@@ -8,7 +8,20 @@
|
||||
|
||||
A Moonraker-compatible bridge that talks directly to the printer.
|
||||
|
||||
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a></sub>
|
||||
<sub>🧪 A community report on Reddit suggests the bridge also works with the
|
||||
**Kobra S1** and **Kobra S1 Max** — protocols look compatible, but neither is
|
||||
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>
|
||||
|
||||
@@ -32,12 +45,17 @@ A Moonraker-compatible bridge that talks directly to the printer.
|
||||
|---|---|
|
||||
| 🖨️ | **Printer control** — start, pause, resume, cancel, temperatures, print speed |
|
||||
| 📊 | **Live status** — temperature, progress, layers, remaining time, camera stream |
|
||||
| 🎨 | **AMS / multicolor** — filament slots, per-channel remapping, MMU emulation for OrcaSlicer filament sync |
|
||||
| 🎨 | **AMS / multicolor** — slots with per-slot **profile picker** (assign your own brand from OrcaSlicer profiles per slot); bridge writes material & colour back to the printer display |
|
||||
| 📦 | **Import your own OrcaSlicer profiles** — drag a ZIP from `~/.config/OrcaSlicer/user/<id>/filament/` into the bridge; they show up in the slot dropdown under ★ Own profiles |
|
||||
| 📷 | **Obico integration (experimental)** — Time-Lapse and WebRTC live stream against a self-hosted [Obico server](https://github.com/TheSpaghettiDetective/obico-server) via moonraker-obico |
|
||||
| 📐 | **Direct H.264 stream + Z-height** — low-CPU camera path for Obico, current Z derived from layer-height for the print-progress widget |
|
||||
| 🗂️ | **GCode browser** — uploaded files with thumbnails, print history, search & filter |
|
||||
| 🧩 | **Multi-printer** — multiple printers in **one** bridge instance, switch via dropdown |
|
||||
| ➕ | **Add a printer with one click** — just enter the IP, credentials are imported automatically |
|
||||
| 🔁 | **Robust MQTT reconnect** — bridge survives overnight printer reboots without manual restart |
|
||||
| 🌐 | **Multi-language UI** — DE / EN / ES / 中文, auto-detect browser locale |
|
||||
| 🔄 | **Self-update** — install new versions directly in the browser |
|
||||
| 🌐 | **OrcaSlicer** — full Moonraker protocol (HTTP + WebSocket), EN/DE UI |
|
||||
| 🧠 | **OrcaSlicer** — full Moonraker protocol (HTTP + WebSocket); pair with the [OrcaSlicer-KX build](#-recommended-slicer) for proper per-slot vendor matching |
|
||||
|
||||
---
|
||||
|
||||
@@ -93,11 +111,32 @@ 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
|
||||
|
||||
For proper AMS filament-sync we ship a **patched OrcaSlicer build**:
|
||||
|
||||
→ **[OrcaSlicer-KX releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
|
||||
|
||||
**Upstream PRs bundled in the KX build:**
|
||||
|
||||
- **[PR #13372](https://github.com/SoftFever/OrcaSlicer/pull/13372)** — Moonraker / Happy-Hare AMS sync fix (slot positions stay correct even with empty slots)
|
||||
- **[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)** — Vendor + Name matching for Moonraker (reads `name` + `vendor_name` per slot and matches against the user's filament presets), by [@LordGuenni](https://github.com/LordGuenni)
|
||||
- **[PR #13315](https://github.com/SoftFever/OrcaSlicer/pull/13315)** — Unique `filament_id` for user presets (so newly created custom profiles get a fresh ID instead of inheriting `OGFL99` from the generic parent), by [@mrnoisytiger](https://github.com/mrnoisytiger)
|
||||
|
||||
**Plus four KX-specific matching improvements on top:**
|
||||
|
||||
- Respect the bridge filament hint (`tray_info_idx` + vendor)
|
||||
- Vendor match also when the chosen base preset is **not is_compatible** with the active printer (so a profile copied from a different machine still matches by vendor)
|
||||
- Vendor match when `tray_info_idx` is set but its preset is incompatible
|
||||
- Two-pass lookup: first compatible presets, then all visible ones
|
||||
|
||||
**Why this matters:** without #13719 the AMS slots in OrcaSlicer all fall back to `Generic PLA` / `Generic PETG` even though the bridge already sends the concrete brand (`name + vendor_name + gate_filament_name`). With the KX build OrcaSlicer matches your actual user presets — including profiles you imported into the bridge via the [Import your own OrcaSlicer profiles](#-features) flow.
|
||||
|
||||
Stock upstream OrcaSlicer still works for slicing and printing — you just lose the per-slot brand matching on AMS sync. Slot material + colour can still be pushed bridge → printer either way (that goes over MQTT, not via the slicer).
|
||||
|
||||
OrcaSlicer-KX is a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); source for each upstream PR is on GitHub, KX-specific patches live in the OrcaSlicer-KX repo.
|
||||
|
||||
---
|
||||
|
||||
@@ -106,6 +145,12 @@ Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125`
|
||||
- **[Home Assistant integration](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||
by [@gangoke](https://github.com/gangoke) — exposes sensors, print controls,
|
||||
light, camera and the GCode thumbnail as native Home Assistant entities.
|
||||
- **[Obico (self-hosted)](https://github.com/TheSpaghettiDetective/obico-server)** —
|
||||
the bridge exposes a Moonraker-compatible surface that
|
||||
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||
accepts, so you get Time-Lapse and WebRTC live streaming against your own
|
||||
Obico server. The AI failure-detection side is experimental on the Kobra X
|
||||
(top-down camera angle differs from what the model was trained on).
|
||||
|
||||
> These are **community projects**, not maintained or supported by KX-Bridge.
|
||||
> For questions or issues, please use the linked repository.
|
||||
@@ -132,9 +177,10 @@ Alternatively (if the IP is unknown): open AnycubicSlicerNext, connect the print
|
||||
## ⚙️ Useful commands
|
||||
|
||||
```bash
|
||||
docker compose logs -f # show logs
|
||||
docker compose down # stop the bridge
|
||||
docker compose up -d --build # rebuild & start (after an update)
|
||||
docker compose logs -f # show logs
|
||||
docker compose down # stop the bridge
|
||||
docker compose pull && docker compose up -d # update to the latest published image
|
||||
docker compose up -d --build # rebuild locally (instead of pulling)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
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`
|
||||
@@ -2,8 +2,10 @@
|
||||
# Kopiere diese Datei nach config.ini und trage deine Werte ein:
|
||||
# cp config.ini.example config.ini
|
||||
#
|
||||
# Credentials mit extract_credentials.exe (Windows) oder
|
||||
# extract_credentials (Linux) aus dem laufenden AnycubicSlicerNext auslesen.
|
||||
# Credentials automatisch eintragen:
|
||||
# python3 tools/fetch_credentials.py --ip 192.168.x.x --write-config
|
||||
# Alternativ (Windows, ohne Drucker-IP bekannt):
|
||||
# extract_credentials.exe --write-env (liest aus laufendem AnycubicSlicerNext)
|
||||
|
||||
[connection]
|
||||
# IP-Adresse des Druckers im lokalen Netzwerk
|
||||
@@ -29,6 +31,65 @@ default_ams_slot = auto
|
||||
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
|
||||
auto_leveling = 1
|
||||
|
||||
# Kamera-Stream bei Druckstart automatisch einschalten (1 = an, 0 = aus)
|
||||
camera_on_print = 0
|
||||
|
||||
# Warnung vor Druck von Web-Uploads (1 = an, 0 = aus)
|
||||
web_upload_warning = 1
|
||||
|
||||
[bridge]
|
||||
# Poll-Intervall in Sekunden
|
||||
poll_interval = 3
|
||||
|
||||
# ─── Multi-Printer (optional) ──────────────────────────────────────────────────
|
||||
# Mehrere Drucker können als [printer_1], [printer_2], … definiert werden.
|
||||
# Jede Bridge-Instanz verbindet sich mit einem Drucker (je eigener Port).
|
||||
# bridge_url zeigt auf die jeweilige Bridge-Instanz (für den /kx/printers-Endpunkt).
|
||||
# Die [connection]-Sektion wird weiterhin als Fallback für diese Instanz verwendet.
|
||||
#
|
||||
# Beispiel:
|
||||
# [printer_1]
|
||||
# name = Kobra X Links
|
||||
# bridge_url = http://192.168.178.95:7125
|
||||
# printer_ip = 192.168.178.95
|
||||
# mqtt_port = 9883
|
||||
# username = userXXXXXXXXXX
|
||||
# password = XXXXXXXXXXXXXXX
|
||||
# device_id = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# mode_id = 20030
|
||||
#
|
||||
# [printer_2]
|
||||
# name = Kobra X Rechts
|
||||
# bridge_url = http://192.168.178.96:7125
|
||||
# printer_ip = 192.168.178.96
|
||||
# mqtt_port = 9883
|
||||
# username = userYYYYYYYYYY
|
||||
# password = YYYYYYYYYYYYYYY
|
||||
# device_id = yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
# mode_id = 20030
|
||||
|
||||
[ace_dry_presets]
|
||||
# Vordefinierte Dry-Set Presets (Temp in °C, Dauer in Sekunden)
|
||||
pla_temp = 45
|
||||
pla_duration_sec = 14400
|
||||
pla_plus_temp = 45
|
||||
pla_plus_duration_sec = 14400
|
||||
petg_temp = 50
|
||||
petg_duration_sec = 14400
|
||||
tpu_temp = 55
|
||||
tpu_duration_sec = 14400
|
||||
abs_asa_temp = 45
|
||||
abs_asa_duration_sec = 28800
|
||||
pa_pc_temp = 55
|
||||
pa_pc_duration_sec = 43200
|
||||
|
||||
# Custom Presets (Name + Temp + Dauer)
|
||||
custom_1_name = Custom 1
|
||||
custom_1_temp = 45
|
||||
custom_1_duration_sec = 14400
|
||||
custom_2_name = Custom 2
|
||||
custom_2_temp = 45
|
||||
custom_2_duration_sec = 14400
|
||||
custom_3_name = Custom 3
|
||||
custom_3_temp = 45
|
||||
custom_3_duration_sec = 14400
|
||||
|
||||
147
config_loader.py
147
config_loader.py
@@ -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:
|
||||
@@ -59,7 +60,12 @@ def _load_config_file(path: pathlib.Path):
|
||||
"DEVICE_ID": (CONFIG_SECTION_CONNECTION, "device_id"),
|
||||
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||
"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:
|
||||
@@ -71,6 +77,18 @@ def _load_config_file(path: pathlib.Path):
|
||||
pass
|
||||
|
||||
|
||||
# Backward compatibility: old key FILE_READY_DIALOG → PRINT_START_DIALOG
|
||||
if "PRINT_START_DIALOG" not in os.environ:
|
||||
try:
|
||||
legacy = cfg.get(CONFIG_SECTION_PRINT, "file_ready_dialog")
|
||||
if legacy:
|
||||
os.environ["PRINT_START_DIALOG"] = legacy
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
pass
|
||||
if "PRINT_START_DIALOG" not in os.environ and "FILE_READY_DIALOG" in os.environ:
|
||||
os.environ["PRINT_START_DIALOG"] = os.environ["FILE_READY_DIALOG"]
|
||||
|
||||
|
||||
def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
||||
"""Einmalige Migration: .env → config.ini anlegen."""
|
||||
env_vals: dict[str, str] = {}
|
||||
@@ -95,6 +113,8 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
||||
cfg[CONFIG_SECTION_PRINT] = {
|
||||
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
||||
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
||||
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
||||
"web_upload_warning": env_vals.get("WEB_UPLOAD_WARNING", "1"),
|
||||
}
|
||||
cfg[CONFIG_SECTION_BRIDGE] = {
|
||||
"poll_interval": "3",
|
||||
@@ -162,6 +182,128 @@ def list_printers() -> list[dict]:
|
||||
return printers
|
||||
|
||||
|
||||
def list_filament_profiles() -> dict[int, dict]:
|
||||
"""Liest die [filament_profiles]-Sektion aus config.ini.
|
||||
|
||||
Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird
|
||||
aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt
|
||||
(als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit
|
||||
derselben filament_id wie 'OGFL99', d.h. die ID ist nicht eindeutig):
|
||||
|
||||
[filament_profiles]
|
||||
slot_0_vendor = Polymaker
|
||||
slot_0_name = PolyTerra PLA
|
||||
slot_0_id = OGFL01
|
||||
|
||||
Gibt einen Dict {slot_index: {"id": ..., "vendor": ..., "name": ...}}
|
||||
zurück. Leere/fehlende Slots werden NICHT aufgenommen — das Default-Mapping
|
||||
(per filament_type) in der Bridge bleibt dann aktiv.
|
||||
|
||||
Backwards-Kompat: alte Configs mit nur (vendor, id) bleiben lesbar; `name`
|
||||
fehlt dann und der Aufrufer kann optional aus der orca_filaments.json
|
||||
rekonstruieren.
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return {}
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_section("filament_profiles"):
|
||||
return {}
|
||||
result: dict[int, dict] = {}
|
||||
for key, value in cfg.items("filament_profiles"):
|
||||
# Erwartet: slot_<idx>_id oder slot_<idx>_vendor oder slot_<idx>_name
|
||||
if not key.startswith("slot_"):
|
||||
continue
|
||||
parts = key.split("_", 2)
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
try:
|
||||
slot_idx = int(parts[1])
|
||||
except ValueError:
|
||||
continue
|
||||
field = parts[2]
|
||||
if field not in ("id", "vendor", "name"):
|
||||
continue
|
||||
if not value.strip():
|
||||
continue
|
||||
result.setdefault(slot_idx, {})[field] = value.strip()
|
||||
return result
|
||||
|
||||
|
||||
def save_filament_profiles(profiles: dict[int, dict]) -> bool:
|
||||
"""Schreibt die übergebenen Slot-Profile in die [filament_profiles]-
|
||||
Sektion der config.ini. Existierende Einträge werden komplett ersetzt.
|
||||
|
||||
profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker", "name": "PolyTerra PLA"}}
|
||||
Mindestens vendor+name müssen gesetzt sein; id ist optional (Hint).
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
# visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der
|
||||
# Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren.
|
||||
preserved_vendors = None
|
||||
if cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
preserved_vendors = cfg.get("filament_profiles", "visible_vendors")
|
||||
if cfg.has_section("filament_profiles"):
|
||||
cfg.remove_section("filament_profiles")
|
||||
if profiles or preserved_vendors:
|
||||
cfg["filament_profiles"] = {}
|
||||
if preserved_vendors:
|
||||
cfg["filament_profiles"]["visible_vendors"] = preserved_vendors
|
||||
for slot_idx in sorted(profiles.keys()):
|
||||
entry = profiles[slot_idx] or {}
|
||||
if entry.get("vendor"):
|
||||
cfg["filament_profiles"][f"slot_{slot_idx}_vendor"] = entry["vendor"]
|
||||
if entry.get("name"):
|
||||
cfg["filament_profiles"][f"slot_{slot_idx}_name"] = entry["name"]
|
||||
if entry.get("id"):
|
||||
cfg["filament_profiles"][f"slot_{slot_idx}_id"] = entry["id"]
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
cfg.write(f)
|
||||
return True
|
||||
|
||||
|
||||
def list_visible_vendors() -> list[str]:
|
||||
"""Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini.
|
||||
|
||||
Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A).
|
||||
Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren).
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return []
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
return []
|
||||
raw = cfg.get("filament_profiles", "visible_vendors")
|
||||
return [v.strip() for v in raw.split(",") if v.strip()]
|
||||
|
||||
|
||||
def save_visible_vendors(vendors: list[str]) -> bool:
|
||||
"""Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings
|
||||
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder."""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_section("filament_profiles"):
|
||||
cfg.add_section("filament_profiles")
|
||||
clean = [v.strip() for v in (vendors or []) if v and v.strip()]
|
||||
if clean:
|
||||
cfg["filament_profiles"]["visible_vendors"] = ", ".join(clean)
|
||||
elif cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
cfg.remove_option("filament_profiles", "visible_vendors")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
cfg.write(f)
|
||||
return True
|
||||
|
||||
|
||||
def get(key: str, default: str = "") -> str:
|
||||
return os.environ.get(key, default)
|
||||
|
||||
@@ -175,3 +317,8 @@ 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")))
|
||||
SPOOLMAN_SERVER = get("SPOOLMAN_SERVER", "")
|
||||
SPOOLMAN_SYNC_RATE = int(get("SPOOLMAN_SYNC_RATE", "0"))
|
||||
|
||||
1465
data/orca_filaments.json
Normal file
1465
data/orca_filaments.json
Normal file
File diff suppressed because it is too large
Load Diff
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
|
||||
29
docker-compose.portainer.yml
Normal file
29
docker-compose.portainer.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# KX-Bridge — Portainer Stack
|
||||
#
|
||||
# Paste this into Portainer → Stacks → Add stack → Web editor
|
||||
#
|
||||
# No configuration needed upfront — just deploy, open http://HOST-IP:7125
|
||||
# and add your printer via the UI (IP only, credentials are fetched automatically).
|
||||
#
|
||||
# All data (config, GCode store, database) is stored in named Docker volumes
|
||||
# managed by Portainer.
|
||||
|
||||
services:
|
||||
kx-bridge:
|
||||
image: gitea.it-drui.de/viewit/kx-bridge:latest
|
||||
volumes:
|
||||
- kx-bridge-config:/app/config
|
||||
- kx-bridge-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"
|
||||
|
||||
volumes:
|
||||
kx-bridge-config:
|
||||
kx-bridge-data:
|
||||
@@ -1,7 +1,9 @@
|
||||
services:
|
||||
kx-bridge:
|
||||
image: kx-bridge:latest
|
||||
build: .
|
||||
image: gitea.it-drui.de/viewit/kx-bridge:latest
|
||||
# Selbst bauen statt das Registry-Image zu pullen?
|
||||
# Dann image-Zeile auskommentieren und folgende aktivieren:
|
||||
# build: .
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- ./data:/app/data
|
||||
|
||||
344
docs/filament-preset-bridge-guide.md
Normal file
344
docs/filament-preset-bridge-guide.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# Eigene Filament-Presets anlegen, prüfen und mit KX-Bridge verknüpfen
|
||||
|
||||
> **Gilt für:** OrcaSlicer-KX v2.4.0-alpha-kx2 oder neuer
|
||||
|
||||
---
|
||||
|
||||
## Was ist die `filament_id` und warum ist sie wichtig?
|
||||
|
||||
Jedes Filament-Preset in OrcaSlicer hat eine interne `filament_id`. Diese ID wird von der KX-Bridge genutzt, um beim AMS-Sync das richtige Preset zuzuordnen.
|
||||
|
||||
- System-Presets (z.B. "Polymaker PolyTerra PLA") haben eine feste ID wie `GFL99` oder `OGFL04`.
|
||||
- **Eigene (User-)Presets** bekommen in OrcaSlicer-KX automatisch eine eindeutige ID, die mit `P` beginnt (z.B. `P3a7f2c1`).
|
||||
|
||||
Ohne eindeutige ID zeigt OrcaSlicer beim Sync immer "Generic PLA" — auch wenn das Preset existiert.
|
||||
|
||||
---
|
||||
|
||||
## 1. Eigenes Filament-Preset anlegen
|
||||
|
||||
1. OrcaSlicer-KX starten
|
||||
2. Rechts oben im **Filament-Dropdown** ein passendes Basis-Preset wählen (z.B. "Generic PLA" oder ein Hersteller-Preset)
|
||||
3. Einstellungen nach Wunsch anpassen (Temperaturen, Kühlung, etc.)
|
||||
4. Auf das **Speichern-Symbol** (Diskette) klicken → **"Save as new preset"**
|
||||
5. Namen eingeben — z.B. `SUNLU PLA+ 2.0`
|
||||
> Der Name muss später exakt so in der Bridge eingetragen werden.
|
||||
6. Drucker auswählen: **Anycubic Kobra X 0.4 nozzle** — wichtig für die Kompatibilität!
|
||||
7. **Speichern** klicken
|
||||
8. OrcaSlicer **einmal neu starten** — erst dann wird die `filament_id` dauerhaft gespeichert.
|
||||
|
||||
---
|
||||
|
||||
## 2. Eindeutige ID prüfen
|
||||
|
||||
Nach dem Neustart prüfen, ob die ID korrekt gesetzt wurde:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
Die Datei öffnen und nach `filament_id` suchen:
|
||||
|
||||
```json
|
||||
{
|
||||
"filament_id": "P3a7f2c1",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✅ Korrekt: ID beginnt mit `P` gefolgt von 7 Hex-Zeichen
|
||||
❌ Fehlt oder leer: OrcaSlicer-KX zu alt — Update auf v2.4.0-alpha-kx2 oder neuer
|
||||
|
||||
---
|
||||
|
||||
## 3. Preset auf einen anderen PC übertragen (Import)
|
||||
|
||||
### Exportieren (Quell-PC)
|
||||
|
||||
Die Preset-Datei einfach kopieren:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
### Importieren (Ziel-PC)
|
||||
|
||||
**Methode A — Datei direkt kopieren:**
|
||||
1. Die `.json`-Datei in das gleiche Verzeichnis auf dem Ziel-PC kopieren
|
||||
2. OrcaSlicer neu starten → Preset erscheint im Dropdown
|
||||
|
||||
**Methode B — OrcaSlicer Import-Funktion:**
|
||||
1. In OrcaSlicer: **File → Import → Import Configs...**
|
||||
2. Die `.json`-Datei auswählen
|
||||
3. OrcaSlicer neu starten
|
||||
|
||||
> **Wichtig:** Die `filament_id` in der Datei bleibt erhalten — das Preset wird auf dem Ziel-PC genauso erkannt wie auf dem Quell-PC.
|
||||
|
||||
---
|
||||
|
||||
## 4. Preset in KX-Bridge verknüpfen
|
||||
|
||||
1. KX-Bridge UI öffnen
|
||||
2. **Filament-Verwaltung** → AMS-Slot auswählen
|
||||
3. Im Feld **Filament-Name** exakt den OrcaSlicer-Preset-Namen eintragen:
|
||||
```
|
||||
SUNLU PLA+ 2.0
|
||||
```
|
||||
4. Speichern
|
||||
|
||||
Die Bridge sendet beim Sync `filament_name: "SUNLU PLA+ 2.0"` → OrcaSlicer findet das Preset anhand von Name und `filament_id` → zeigt es korrekt an.
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
| Was | Warum |
|
||||
|-----|-------|
|
||||
| Name in OrcaSlicer und Bridge müssen **exakt** übereinstimmen | Groß-/Kleinschreibung und Sonderzeichen werden verglichen |
|
||||
| Preset muss für **Anycubic Kobra X 0.4 nozzle** kompatibel sein | Beim Speichern den richtigen Drucker auswählen |
|
||||
| Nach dem ersten Speichern OrcaSlicer **neu starten** | Erst dann wird die `filament_id` persistent geschrieben |
|
||||
| **OrcaSlicer-KX v2.4.0-alpha-kx2** oder neuer verwenden | Ältere Versionen generieren keine eindeutige `filament_id` für User-Presets |
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# How to Create, Verify and Import Custom Filament Presets for KX-Bridge
|
||||
|
||||
> **Requires:** OrcaSlicer-KX v2.4.0-alpha-kx2 or newer
|
||||
|
||||
---
|
||||
|
||||
## What is the `filament_id` and why does it matter?
|
||||
|
||||
Every filament preset in OrcaSlicer has an internal `filament_id`. The KX-Bridge uses this ID to match the correct preset during AMS sync.
|
||||
|
||||
- System presets (e.g. "Polymaker PolyTerra PLA") have a fixed ID like `GFL99` or `OGFL04`.
|
||||
- **Custom (user) presets** automatically receive a unique ID starting with `P` (e.g. `P3a7f2c1`) in OrcaSlicer-KX.
|
||||
|
||||
Without a unique ID, OrcaSlicer will always show "Generic PLA" during sync — even if the preset exists.
|
||||
|
||||
---
|
||||
|
||||
## 1. Create a Custom Filament Preset
|
||||
|
||||
1. Launch OrcaSlicer-KX
|
||||
2. Select a suitable base preset from the **filament dropdown** (e.g. "Generic PLA" or a vendor preset)
|
||||
3. Adjust settings as needed (temperatures, cooling, etc.)
|
||||
4. Click the **save icon** (floppy disk) → **"Save as new preset"**
|
||||
5. Enter a name — e.g. `SUNLU PLA+ 2.0`
|
||||
> This name must be entered in the bridge exactly as typed here.
|
||||
6. Select printer: **Anycubic Kobra X 0.4 nozzle** — required for compatibility!
|
||||
7. Click **Save**
|
||||
8. **Restart OrcaSlicer once** — the `filament_id` is only written permanently after a restart.
|
||||
|
||||
---
|
||||
|
||||
## 2. Verify the Unique ID
|
||||
|
||||
After restarting, check that the ID was set correctly:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
Open the file and look for `filament_id`:
|
||||
|
||||
```json
|
||||
{
|
||||
"filament_id": "P3a7f2c1",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✅ Correct: ID starts with `P` followed by 7 hex characters
|
||||
❌ Missing or empty: Your OrcaSlicer-KX version is too old — update to v2.4.0-alpha-kx2 or newer
|
||||
|
||||
---
|
||||
|
||||
## 3. Transfer a Preset to Another PC (Import)
|
||||
|
||||
### Export (source PC)
|
||||
|
||||
Simply copy the preset file:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
### Import (target PC)
|
||||
|
||||
**Method A — Copy file directly:**
|
||||
1. Copy the `.json` file to the same directory on the target PC
|
||||
2. Restart OrcaSlicer → preset appears in the dropdown
|
||||
|
||||
**Method B — OrcaSlicer import function:**
|
||||
1. In OrcaSlicer: **File → Import → Import Configs...**
|
||||
2. Select the `.json` file
|
||||
3. Restart OrcaSlicer
|
||||
|
||||
> **Note:** The `filament_id` inside the file is preserved — the preset will be recognized on the target PC exactly as on the source PC.
|
||||
|
||||
---
|
||||
|
||||
## 4. Link the Preset in KX-Bridge
|
||||
|
||||
1. Open the KX-Bridge UI
|
||||
2. Go to **Filament Management** → select the AMS slot
|
||||
3. In the **Filament Name** field, enter the OrcaSlicer preset name exactly:
|
||||
```
|
||||
SUNLU PLA+ 2.0
|
||||
```
|
||||
4. Save
|
||||
|
||||
The bridge sends `filament_name: "SUNLU PLA+ 2.0"` during sync → OrcaSlicer matches by name and `filament_id` → displays the preset correctly.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| What | Why |
|
||||
|------|-----|
|
||||
| Name in OrcaSlicer and Bridge must match **exactly** | Case and special characters are compared |
|
||||
| Preset must be compatible with **Anycubic Kobra X 0.4 nozzle** | Select the correct printer when saving |
|
||||
| **Restart OrcaSlicer** after saving for the first time | The `filament_id` is only written persistently after a restart |
|
||||
| Use **OrcaSlicer-KX v2.4.0-alpha-kx2** or newer | Older versions do not generate a unique `filament_id` for user presets |
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# Cómo crear, verificar e importar perfiles de filamento personalizados para KX-Bridge
|
||||
|
||||
> **Requiere:** OrcaSlicer-KX v2.4.0-alpha-kx2 o superior
|
||||
|
||||
---
|
||||
|
||||
## ¿Qué es el `filament_id` y por qué es importante?
|
||||
|
||||
Cada perfil de filamento en OrcaSlicer tiene un `filament_id` interno. KX-Bridge usa este ID para asignar el perfil correcto durante la sincronización AMS.
|
||||
|
||||
- Los perfiles del sistema (p. ej. "Polymaker PolyTerra PLA") tienen un ID fijo como `GFL99` o `OGFL04`.
|
||||
- Los **perfiles personalizados (usuario)** reciben automáticamente un ID único que empieza por `P` (p. ej. `P3a7f2c1`) en OrcaSlicer-KX.
|
||||
|
||||
Sin un ID único, OrcaSlicer mostrará siempre "Generic PLA" durante la sincronización, aunque el perfil exista.
|
||||
|
||||
---
|
||||
|
||||
## 1. Crear un perfil de filamento personalizado
|
||||
|
||||
1. Iniciar OrcaSlicer-KX
|
||||
2. Seleccionar un perfil base adecuado en el **menú desplegable de filamento** (p. ej. "Generic PLA" o un perfil de fabricante)
|
||||
3. Ajustar la configuración según sea necesario (temperaturas, refrigeración, etc.)
|
||||
4. Hacer clic en el **icono de guardar** (disquete) → **"Save as new preset"**
|
||||
5. Introducir un nombre — p. ej. `SUNLU PLA+ 2.0`
|
||||
> Este nombre debe introducirse en la bridge exactamente igual.
|
||||
6. Seleccionar impresora: **Anycubic Kobra X 0.4 nozzle** — ¡necesario para la compatibilidad!
|
||||
7. Hacer clic en **Guardar**
|
||||
8. **Reiniciar OrcaSlicer una vez** — el `filament_id` solo se escribe de forma permanente tras un reinicio.
|
||||
|
||||
---
|
||||
|
||||
## 2. Verificar el ID único
|
||||
|
||||
Tras reiniciar, comprobar que el ID se ha establecido correctamente:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
Abrir el archivo y buscar `filament_id`:
|
||||
|
||||
```json
|
||||
{
|
||||
"filament_id": "P3a7f2c1",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✅ Correcto: el ID empieza por `P` seguido de 7 caracteres hexadecimales
|
||||
❌ Falta o está vacío: la versión de OrcaSlicer-KX es demasiado antigua — actualizar a v2.4.0-alpha-kx2 o superior
|
||||
|
||||
---
|
||||
|
||||
## 3. Transferir un perfil a otro PC (importar)
|
||||
|
||||
### Exportar (PC de origen)
|
||||
|
||||
Simplemente copiar el archivo del perfil:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
### Importar (PC de destino)
|
||||
|
||||
**Método A — Copiar el archivo directamente:**
|
||||
1. Copiar el archivo `.json` al mismo directorio en el PC de destino
|
||||
2. Reiniciar OrcaSlicer → el perfil aparece en el menú desplegable
|
||||
|
||||
**Método B — Función de importación de OrcaSlicer:**
|
||||
1. En OrcaSlicer: **File → Import → Import Configs...**
|
||||
2. Seleccionar el archivo `.json`
|
||||
3. Reiniciar OrcaSlicer
|
||||
|
||||
> **Nota:** El `filament_id` dentro del archivo se conserva — el perfil se reconocerá en el PC de destino exactamente igual que en el de origen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Vincular el perfil en KX-Bridge
|
||||
|
||||
1. Abrir la interfaz de KX-Bridge
|
||||
2. Ir a **Gestión de filamentos** → seleccionar la ranura AMS
|
||||
3. En el campo **Nombre de filamento**, introducir el nombre exacto del perfil de OrcaSlicer:
|
||||
```
|
||||
SUNLU PLA+ 2.0
|
||||
```
|
||||
4. Guardar
|
||||
|
||||
La bridge envía `filament_name: "SUNLU PLA+ 2.0"` durante la sincronización → OrcaSlicer busca por nombre y `filament_id` → muestra el perfil correctamente.
|
||||
|
||||
---
|
||||
|
||||
## Referencia rápida
|
||||
|
||||
| Qué | Por qué |
|
||||
|-----|---------|
|
||||
| El nombre en OrcaSlicer y en Bridge debe coincidir **exactamente** | Se comparan mayúsculas, minúsculas y caracteres especiales |
|
||||
| El perfil debe ser compatible con **Anycubic Kobra X 0.4 nozzle** | Seleccionar la impresora correcta al guardar |
|
||||
| **Reiniciar OrcaSlicer** tras guardar por primera vez | El `filament_id` solo se escribe de forma permanente tras un reinicio |
|
||||
| Usar **OrcaSlicer-KX v2.4.0-alpha-kx2** o superior | Las versiones anteriores no generan un `filament_id` único para perfiles de usuario |
|
||||
@@ -48,3 +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")))
|
||||
|
||||
194
kobrax_client.py
194
kobrax_client.py
@@ -27,6 +27,7 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
@@ -120,6 +121,10 @@ class KobraXClient:
|
||||
self._buf = b""
|
||||
self._pid = 1
|
||||
self._lock = threading.Lock()
|
||||
# Generations-Marker: wird bei jedem Socket-Swap/Close erhöht, damit der
|
||||
# Reader-Thread erkennt wenn _reconnect/_do_connect den Socket unter ihm
|
||||
# ersetzt hat (Issue #53). Schützt gegen recv auf einem stale fd.
|
||||
self._sock_gen = 0
|
||||
self._running = False
|
||||
|
||||
# Pending requests by msgid (for response ACK)
|
||||
@@ -154,63 +159,120 @@ class KobraXClient:
|
||||
# -- Connection ----------------------------------------------------------
|
||||
|
||||
def _do_connect(self):
|
||||
if not os.path.exists(CERT_FILE) or not os.path.exists(KEY_FILE):
|
||||
raise FileNotFoundError(
|
||||
f"TLS-Zertifikate fehlen: anycubic_slicer.crt + anycubic_slicer.key "
|
||||
f"müssen neben der kx-bridge Binary liegen ({_SCRIPT_DIR}/). "
|
||||
f"Lade anycubic-certs.zip vom Gitea-Release herunter und entpacke "
|
||||
f"die Dateien dorthin."
|
||||
)
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
|
||||
ctx.load_cert_chain(CERT_FILE, KEY_FILE)
|
||||
|
||||
raw = socket.create_connection((self.host, self.port), timeout=5)
|
||||
self._sock = ctx.wrap_socket(raw)
|
||||
log.info("TLS connected cipher=%s", self._sock.cipher()[0])
|
||||
# Socket als lokale Variable aufbauen — der Handshake (Connect + CONNACK)
|
||||
# läuft OHNE gehaltenes Lock, damit ein langsamer Connect die Sender nicht
|
||||
# einfriert. Erst der fertige Socket wird unter Lock eingeschwenkt (#53).
|
||||
_ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM)
|
||||
raw = socket.create_connection(_ai[0][4], timeout=5)
|
||||
new_sock = ctx.wrap_socket(raw)
|
||||
log.info("TLS connected cipher=%s", new_sock.cipher()[0])
|
||||
|
||||
self._sock.sendall(_build_connect(self.client_id, self.username, self.password))
|
||||
self._sock.settimeout(3)
|
||||
r = self._sock.recv(64)
|
||||
new_sock.sendall(_build_connect(self.client_id, self.username, self.password))
|
||||
new_sock.settimeout(3)
|
||||
r = new_sock.recv(64)
|
||||
if len(r) < 4 or r[0] != 0x20 or r[3] != 0:
|
||||
try:
|
||||
new_sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
raise RuntimeError(f"CONNACK failed: {r.hex()}")
|
||||
log.info("CONNACK rc=0")
|
||||
|
||||
self._sock.settimeout(0.2)
|
||||
self._buf = b""
|
||||
self._subscribe(self._sub_topic())
|
||||
new_sock.settimeout(0.2)
|
||||
with self._lock:
|
||||
self._sock = new_sock
|
||||
self._sock_gen += 1
|
||||
self._buf = b""
|
||||
self._subscribe(self._sub_topic()) # nimmt das Lock selbst — nicht verschachteln
|
||||
log.debug("MQTT connected to %s:%s", self.host, self.port)
|
||||
|
||||
def connect(self):
|
||||
self._do_connect()
|
||||
self._running = True
|
||||
t = threading.Thread(target=self._read_loop, daemon=True)
|
||||
t.start()
|
||||
self._ensure_reader()
|
||||
time.sleep(0.3)
|
||||
|
||||
def _ensure_reader(self):
|
||||
"""Stellt sicher dass der Reader-Thread lebt. Wenn der Reader nach einer
|
||||
früheren disconnect/reconnect-Sequenz oder einem unbehandelten Fehler
|
||||
gestorben ist, würden empfangene Replies sonst nie ankommen — publish()
|
||||
würde dann zwar senden, aber auf Antworten ewig warten."""
|
||||
if not self._running:
|
||||
return # gewollter disconnect
|
||||
t = getattr(self, "_reader_thread", None)
|
||||
if t is not None and t.is_alive():
|
||||
return
|
||||
self._reader_thread = threading.Thread(
|
||||
target=self._read_loop, daemon=True, name="kobrax-mqtt-reader",
|
||||
)
|
||||
self._reader_thread.start()
|
||||
|
||||
def disconnect(self):
|
||||
self._running = False
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
with self._lock:
|
||||
try:
|
||||
if self._sock is not None:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._sock = None
|
||||
self._sock_gen += 1
|
||||
|
||||
def _reconnect(self):
|
||||
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
|
||||
antwortet oder disconnect() gerufen wurde. Backoff cappt bei 60 s. Die
|
||||
ersten 5 Versuche loggen als WARNING (akute Verbindungsstörung), danach
|
||||
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
|
||||
ausgeschaltet) zu vermeiden."""
|
||||
log.warning("Verbindung verloren – reconnect…")
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
for delay in [2, 4, 8, 15, 30]:
|
||||
# Close + Invalidierung unter Lock, damit kein Sender mitten im sendall
|
||||
# auf den gerade geschlossenen Socket trifft (Issue #53).
|
||||
with self._lock:
|
||||
try:
|
||||
if self._sock is not None:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._sock = None
|
||||
self._sock_gen += 1
|
||||
delays = [2, 4, 8, 15, 30, 60]
|
||||
attempt = 0
|
||||
while self._running:
|
||||
delay = delays[min(attempt, len(delays) - 1)]
|
||||
try:
|
||||
self._do_connect()
|
||||
log.info("Reconnect erfolgreich")
|
||||
log.info("Reconnect erfolgreich (nach %d Versuchen)", attempt + 1)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.warning("Reconnect fehlgeschlagen (%s), warte %ss…", e, delay)
|
||||
time.sleep(delay)
|
||||
return False
|
||||
attempt += 1
|
||||
lvl = log.warning if attempt <= 5 else log.debug
|
||||
lvl("Reconnect fehlgeschlagen (%s, Versuch %d), warte %ss…", e, attempt, delay)
|
||||
# Geteiltes Sleep damit disconnect() den Loop schneller bricht.
|
||||
slept = 0.0
|
||||
while slept < delay and self._running:
|
||||
time.sleep(min(0.5, delay - slept))
|
||||
slept += 0.5
|
||||
return False # nur wenn disconnect() gerufen wurde
|
||||
|
||||
def _subscribe(self, topic: str):
|
||||
with self._lock:
|
||||
pid = self._pid
|
||||
self._pid += 1
|
||||
self._sock.sendall(_build_subscribe(topic, pid))
|
||||
if self._sock is not None:
|
||||
self._sock.sendall(_build_subscribe(topic, pid))
|
||||
log.info("SUB %s", topic)
|
||||
|
||||
# -- Read loop -----------------------------------------------------------
|
||||
@@ -220,17 +282,52 @@ class KobraXClient:
|
||||
_empty_count = 0
|
||||
while self._running:
|
||||
if time.time() - last_ping > 30:
|
||||
ping_ok = False
|
||||
with self._lock:
|
||||
try:
|
||||
self._sock.sendall(_build_pingreq())
|
||||
if self._sock is not None:
|
||||
self._sock.sendall(_build_pingreq())
|
||||
ping_ok = True
|
||||
except Exception:
|
||||
if self._running and not self._reconnect():
|
||||
break
|
||||
last_ping = time.time()
|
||||
continue
|
||||
ping_ok = False
|
||||
# _reconnect() AUSSERHALB des Locks aufrufen — es nimmt das Lock
|
||||
# selbst, und threading.Lock ist nicht reentrant (sonst Deadlock).
|
||||
if not ping_ok:
|
||||
if self._running and not self._reconnect():
|
||||
break
|
||||
last_ping = time.time()
|
||||
# Aktuellen Socket + Generation unter Lock greifen, damit ein
|
||||
# paralleler _reconnect/_do_connect-Swap uns nicht auf einem stale
|
||||
# fd pollen lässt (Issue #53).
|
||||
with self._lock:
|
||||
sock = self._sock
|
||||
gen = self._sock_gen
|
||||
if sock is None:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
# Idle-Wartezeit OHNE Lock — select probt nur die Bereitschaft, so
|
||||
# blockiert der Reader während Leerlauf nie das gemeinsame Lock.
|
||||
try:
|
||||
data = self._sock.recv(65536)
|
||||
ready, _, _ = select.select([sock], [], [], 0.2)
|
||||
except (OSError, ValueError):
|
||||
# fd geschlossen/ungültig (Reconnect oder Disconnect mitten im select)
|
||||
if not self._running:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
if not ready:
|
||||
continue # Leerlauf, kein Lock gehalten
|
||||
|
||||
# Daten liegen an: Lock kurz greifen für das eine recv, serialisiert
|
||||
# gegen alle sendall-Caller. recv blockiert nicht lange (select sagte
|
||||
# ready, Socket-Timeout ist 0.2s).
|
||||
try:
|
||||
with self._lock:
|
||||
# Socket könnte zwischen select und hier ersetzt worden sein.
|
||||
if self._sock_gen != gen or self._sock is not sock:
|
||||
continue
|
||||
data = sock.recv(65536)
|
||||
if not data:
|
||||
# Windows SSL kann kurzzeitig b"" liefern ohne echten EOF
|
||||
_empty_count += 1
|
||||
@@ -239,7 +336,7 @@ class KobraXClient:
|
||||
continue
|
||||
_empty_count = 0
|
||||
self._buf += data
|
||||
self._drain()
|
||||
self._drain() # außerhalb des Locks — Dispatch/event.set() bleibt prompt
|
||||
except ssl.SSLWantReadError:
|
||||
continue
|
||||
except socket.timeout:
|
||||
@@ -348,6 +445,9 @@ class KobraXClient:
|
||||
# -- Publish + request/response ------------------------------------------
|
||||
|
||||
def publish(self, msg_type: str, action: str, data=None, timeout: float = 5.0) -> dict | None:
|
||||
# Falls Reader-Thread aus historischen Gründen tot ist, wiederbeleben —
|
||||
# sonst würden Replies nie ankommen und event.wait() läuft ins Timeout.
|
||||
self._ensure_reader()
|
||||
msgid = str(uuid.uuid4())
|
||||
payload = json.dumps({
|
||||
"type": msg_type,
|
||||
@@ -371,9 +471,12 @@ class KobraXClient:
|
||||
report_registered = True
|
||||
|
||||
topic = self._pub_topic(msg_type)
|
||||
log.info("TX %-25s action=%-12s data=%s",
|
||||
f"{msg_type}/request", action,
|
||||
json.dumps(data, ensure_ascii=False) if data else "null")
|
||||
# Status-Poll-TX (query/getInfo) ist reines Rauschen (alle paar Sekunden) →
|
||||
# auf DEBUG. Aktions-TX (start/set/control/move/…) bleibt INFO sichtbar.
|
||||
_tx_level = logging.DEBUG if action in ("query", "getInfo") else logging.INFO
|
||||
log.log(_tx_level, "TX %-25s action=%-12s data=%s",
|
||||
f"{msg_type}/request", action,
|
||||
json.dumps(data, ensure_ascii=False) if data else "null")
|
||||
try:
|
||||
with self._lock:
|
||||
self._sock.sendall(_build_publish(topic, payload))
|
||||
@@ -410,6 +513,7 @@ class KobraXClient:
|
||||
|
||||
def publish_web(self, msg_type: str, action: str, data=None) -> None:
|
||||
"""Fire-and-forget publish on the web/printer topic (used for runtime updates during print)."""
|
||||
self._ensure_reader()
|
||||
msgid = str(uuid.uuid4())
|
||||
payload = json.dumps({
|
||||
"type": msg_type,
|
||||
@@ -426,7 +530,14 @@ class KobraXClient:
|
||||
with self._lock:
|
||||
self._sock.sendall(_build_publish(topic, payload))
|
||||
except Exception as e:
|
||||
log.error("web send error: %s", e)
|
||||
log.error("web send error: %s, reconnecting…", e)
|
||||
# Reconnect triggern (analog zu publish()); ohne Retry weil
|
||||
# fire-and-forget — der nächste Aufruf wird auf den frischen Socket
|
||||
# treffen.
|
||||
try:
|
||||
self._reconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# -- High-level commands -------------------------------------------------
|
||||
|
||||
@@ -542,9 +653,16 @@ class KobraXClient:
|
||||
f"Connection: close\r\n\r\n"
|
||||
).encode()
|
||||
|
||||
sock = socket.create_connection((self.host, 18910), timeout=30)
|
||||
# Connect-Timeout kurz (LAN). Während sendall() darf der Socket so
|
||||
# lange brauchen wie nötig — bei großen Dateien (>100 MB) und
|
||||
# langsamerem WLAN am Drucker dauert das Schieben sonst >30 s und
|
||||
# würde den Connect-Timeout fälschlich auslösen. Read-Timeout danach
|
||||
# generös (Drucker verarbeitet die Datei bevor er antwortet).
|
||||
_ai = socket.getaddrinfo(self.host, 18910, socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock = socket.create_connection(_ai[0][4], timeout=10)
|
||||
sock.settimeout(None) # blocking während Send
|
||||
sock.sendall(headers + body)
|
||||
sock.settimeout(120) # große GCode-Dateien brauchen Zeit bis der Drucker antwortet
|
||||
sock.settimeout(180)
|
||||
response = b""
|
||||
try:
|
||||
while True:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas = [("web", "web")]
|
||||
datas = [("web", "web"), ("data", "static"), ("VERSION", ".")] # bridge/data/ → static/ im _MEIPASS
|
||||
binaries = []
|
||||
hiddenimports = []
|
||||
|
||||
|
||||
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
|
||||
147
orca_filaments.py
Normal file
147
orca_filaments.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""OrcaSlicer Filament-Profil Parser.
|
||||
|
||||
Geteilt zwischen dem Generator (tools/gen_orca_filament_list.py) und dem
|
||||
Custom-Profile-Import-Endpoint (bridge/kobrax_moonraker_bridge.py).
|
||||
|
||||
Liest Orca-Filament-JSON-Dateien (System- oder User-Profile) und gibt
|
||||
sie als normalisierte Liste mit (id, name, vendor, type, color) zurück.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
def first_str(value, default: str = "") -> str:
|
||||
"""Orca-Profile speichern manche Felder als ['wert']. Liefert erstes
|
||||
Element als String."""
|
||||
if isinstance(value, list):
|
||||
return str(value[0]) if value else default
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return default
|
||||
|
||||
|
||||
def clean_name(raw: str) -> str:
|
||||
"""Strippt printer-/varianten-spezifische Suffixe:
|
||||
'PolyTerra PLA @base' → 'PolyTerra PLA'
|
||||
'Anycubic PLA @Anycubic Kobra X 0.4 nozzle' → 'Anycubic PLA'
|
||||
'Anker Generic PLA 0.4 nozzle' → 'Anker Generic PLA'
|
||||
"""
|
||||
name = re.sub(r"\s*@.*$", "", raw).strip()
|
||||
name = re.sub(r"\s+\d+(\.\d+)?\s*nozzle\s*$", "", name, flags=re.IGNORECASE).strip()
|
||||
return name or raw
|
||||
|
||||
|
||||
def parse_profile(data: dict, by_name: dict | None = None,
|
||||
path_vendor: str | None = None,
|
||||
source_path: str = "",
|
||||
system_index: list | None = None) -> dict | None:
|
||||
"""Parsed ein einzelnes Orca-Filament-Profil zum Bridge-Schema.
|
||||
|
||||
`by_name` ist optional ein {name: [profile, …]}-Index für Inherits-Resolve
|
||||
aus dem rohen Source-Tree (Generator). Bei Single-File-Import (User-Datei
|
||||
aus OrcaSlicer-User-Dir) reichen wir stattdessen `system_index` rein —
|
||||
die fertige System-Profile-Liste aus orca_filaments.json. Damit können
|
||||
wir filament_id/vendor/type/color über die `inherits`-Kette aus dem
|
||||
System-Parent ableiten, auch wenn das User-Profil diese Felder nicht
|
||||
selbst setzt (typisch: User-Override-Profile haben nur Tweaks).
|
||||
|
||||
Liefert {id, name, vendor, type, color} oder None wenn das Profil
|
||||
keine filament_id hat (z.B. abstrakte @base-Templates).
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
# User-Profile aus dem OrcaSlicer-User-Dir setzen oft KEIN "type"-Feld —
|
||||
# das kommt vom System-Parent. Wir akzeptieren das wenn entweder "type"
|
||||
# explizit "filament" ist ODER ein "inherits" auf ein anderes Profil zeigt.
|
||||
if data.get("type") not in (None, "filament") and not data.get("inherits"):
|
||||
return None
|
||||
if data.get("type") == "filament" and data.get("inherits") is None and not data.get("filament_id"):
|
||||
# type=filament aber kein parent + keine ID → wertloses Stub
|
||||
return None
|
||||
inst = data.get("instantiation", "true")
|
||||
if isinstance(inst, str) and inst.lower() == "false":
|
||||
return None
|
||||
|
||||
# Build system-name-Index für den fallback-Lookup wenn system_index gesetzt.
|
||||
sys_by_name: dict[str, dict] = {}
|
||||
if system_index:
|
||||
for p in system_index:
|
||||
if isinstance(p, dict) and p.get("name"):
|
||||
sys_by_name[p["name"]] = p
|
||||
|
||||
def _resolve(key: str, depth: int = 5):
|
||||
cur_list = [data]
|
||||
for _ in range(depth):
|
||||
for cur in cur_list:
|
||||
v = cur.get(key)
|
||||
if v not in ("", [], None, [""]) and v is not None:
|
||||
return v
|
||||
# Erst raw-Inherits via by_name (Generator-Pfad)
|
||||
if by_name:
|
||||
next_list: list[dict] = []
|
||||
for cur in cur_list:
|
||||
parent_name = cur.get("inherits")
|
||||
if parent_name and parent_name in by_name:
|
||||
next_list.extend(by_name[parent_name])
|
||||
if next_list:
|
||||
cur_list = next_list
|
||||
continue
|
||||
break
|
||||
return None
|
||||
|
||||
def _resolve_via_system_index(key: str):
|
||||
"""Inherits-Kette über system_index (clean_name-Match)."""
|
||||
parent_raw = data.get("inherits")
|
||||
if not parent_raw or not sys_by_name:
|
||||
return None
|
||||
parent_clean = clean_name(parent_raw)
|
||||
sys_p = sys_by_name.get(parent_clean)
|
||||
if not sys_p:
|
||||
return None
|
||||
# System-JSON benutzt schon das normalisierte Schema
|
||||
mapping = {
|
||||
"filament_id": "id",
|
||||
"filament_vendor": "vendor",
|
||||
"filament_type": "type",
|
||||
"default_filament_colour": "color",
|
||||
}
|
||||
return sys_p.get(mapping.get(key, key))
|
||||
|
||||
def _resolve_full(key: str):
|
||||
v = _resolve(key)
|
||||
if v not in ("", [], None, [""]) and v is not None:
|
||||
return v
|
||||
return _resolve_via_system_index(key)
|
||||
|
||||
fid = _resolve_full("filament_id")
|
||||
if not fid or not isinstance(fid, str):
|
||||
return None
|
||||
|
||||
name_raw = data.get("name", fid)
|
||||
name = clean_name(name_raw)
|
||||
vendor = first_str(_resolve_full("filament_vendor")) or (path_vendor or "Generic")
|
||||
ftype = first_str(_resolve_full("filament_type"), "")
|
||||
color = first_str(_resolve_full("default_filament_colour"), "")
|
||||
|
||||
return {
|
||||
"id": fid,
|
||||
"name": name,
|
||||
"vendor": vendor,
|
||||
"type": ftype,
|
||||
"color": color,
|
||||
}
|
||||
|
||||
|
||||
def parse_profile_bytes(blob: bytes, source_name: str = "",
|
||||
system_index: list | None = None) -> dict | None:
|
||||
"""Liest ein einzelnes Profil aus JSON-Bytes. Für File-Upload-Pfad.
|
||||
`system_index` ist optional die fertige Liste aus orca_filaments.json —
|
||||
wird für die Inherits-Resolve von User-Profilen genutzt die das volle
|
||||
Schema vom System-Parent erben."""
|
||||
try:
|
||||
data = json.loads(blob.decode("utf-8", errors="replace"))
|
||||
except Exception:
|
||||
return None
|
||||
return parse_profile(data, source_path=source_name, system_index=system_index)
|
||||
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
|
||||
@@ -54,6 +54,10 @@ Referenzliste für JavaScript-/DOM-Hooks.
|
||||
| `#logdir-all` | Hook / Selektor |
|
||||
| `#logdir-rx` | Hook / Selektor |
|
||||
| `#logdir-tx` | Hook / Selektor |
|
||||
| `#log-lbl-level` | i18n-Label "Level:" |
|
||||
| `#loglvl-all` | onclick `setLogLevel('all')` |
|
||||
| `#loglvl-err` | onclick `setLogLevel('err')` — nur Fehler |
|
||||
| `#loglvl-warn` | onclick `setLogLevel('warn')` — Fehler + Warnungen |
|
||||
| `#nb-console` | Hook / Selektor |
|
||||
| `#nb-dashboard` | Hook / Selektor |
|
||||
| `#nb-printers` | Hook / Selektor |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,95 +32,23 @@
|
||||
<span id="h-version" style="font-size:11px;opacity:.5;margin-left:6px"></span>
|
||||
<div class="hbadge" id="h-badge"><span class="dot"></span><span id="h-state">Standby</span></div>
|
||||
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
|
||||
<button class="theme-btn" onclick="toggleLang()" id="lang-btn">EN</button>
|
||||
<button class="theme-btn" onclick="openSettings()" id="settings-btn" title="Einstellungen">⚙</button>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<span aria-hidden="true" style="font-size:15px;line-height:1;opacity:.85">🌐</span>
|
||||
<select class="theme-btn" id="lang-select" onchange="setLanguageFromSelect()" style="padding:6px 10px">
|
||||
<option value="de">Deutsch</option>
|
||||
<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>
|
||||
<button class="theme-btn" onclick="showPanel('settings')" id="settings-btn" title="Einstellungen">⚙</button>
|
||||
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
|
||||
</header>
|
||||
|
||||
<!-- ═══ SETTINGS MODAL ═══ -->
|
||||
<div class="modal-overlay" id="settings-modal" onclick="if(event.target===this)closeSettings()">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="modal-title-settings">Einstellungen</span>
|
||||
<button class="modal-close" onclick="closeSettings()">✕</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-field" style="margin-bottom:12px">
|
||||
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
|
||||
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
|
||||
</div>
|
||||
<div class="modal-section" id="modal-sec-connection">Verbindung</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-printer-ip">Drucker-IP</label>
|
||||
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
|
||||
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mqtt-port">MQTT-Port</label>
|
||||
<input type="number" id="s-mqtt-port" placeholder="9883">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-username">MQTT-Benutzername</label>
|
||||
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-password">MQTT-Passwort</label>
|
||||
<input type="password" id="s-password" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-device-id">Device-ID</label>
|
||||
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mode-id">Mode-ID</label>
|
||||
<input type="text" id="s-mode-id" placeholder="20030">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-print">Druckeinstellungen</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
|
||||
<select id="s-default-slot">
|
||||
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
|
||||
<option value="0" id="opt-slot-0">Slot 1</option>
|
||||
<option value="1" id="opt-slot-1">Slot 2</option>
|
||||
<option value="2" id="opt-slot-2">Slot 3</option>
|
||||
<option value="3" id="opt-slot-3">Slot 4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
|
||||
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-poll">Poll-Intervall</div>
|
||||
<div class="poll-btns">
|
||||
<button class="poll-btn" onclick="setPoll(1000)" id="poll-1">1s</button>
|
||||
<button class="poll-btn active" onclick="setPoll(2000)" id="poll-2">2s</button>
|
||||
<button class="poll-btn" onclick="setPoll(5000)" id="poll-5">5s</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-version">Version</div>
|
||||
<div class="update-row">
|
||||
<span id="s-version-label" style="font-size:13px;color:var(--txt)">–</span>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
|
||||
</div>
|
||||
<div class="update-status" id="update-status" style="margin-top:6px"></div>
|
||||
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
|
||||
<span id="lbl-update-apply">Jetzt installieren</span>
|
||||
</button>
|
||||
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
|
||||
</div>
|
||||
|
||||
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings">Speichern & Neustart</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Settings-Modal entfernt — jetzt #panel-settings (Master-Detail im Main-Bereich) -->
|
||||
|
||||
<!-- ═══ AMS SLOT EDIT DIALOG ═══ -->
|
||||
<div class="modal-overlay" id="slot-edit-modal" onclick="if(event.target===this)closeSlotEdit()">
|
||||
@@ -146,11 +74,50 @@
|
||||
oninput="highlightMatBtn(this.value)"
|
||||
style="margin-top:8px;width:100%;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box">
|
||||
</div>
|
||||
<!-- Orca-Filament-Profil-Override (für AMS-Sync) -->
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-profile"></div>
|
||||
<select id="slot-edit-profile"
|
||||
style="width:100%;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box">
|
||||
<option value="" id="slot-profile-default-opt"></option>
|
||||
</select>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-top:4px" id="slot-profile-hint"></div>
|
||||
<a href="#" onclick="event.preventDefault();openProfileImport()"
|
||||
style="display:inline-block;margin-top:6px;font-size:11px;color:var(--accent);text-decoration:none"
|
||||
id="lbl-slot-profile-import">★ Eigene Profile importieren…</a>
|
||||
</div>
|
||||
<button class="btn" id="btn-slot-edit-feed" style="width:100%;margin-bottom:8px" onclick="slotEditFeed()"></button>
|
||||
<button class="modal-save" id="btn-slot-edit-save" onclick="saveSlotEdit()"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ ORCA-PROFILE-IMPORT-DIALOG (Issue #41) ═══ -->
|
||||
<div class="modal-overlay" id="profile-import-modal" onclick="if(event.target===this)closeProfileImport()">
|
||||
<div class="modal-box" style="max-width:480px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||
<div style="font-size:16px;font-weight:600" id="profile-import-title">Eigene OrcaSlicer-Profile importieren</div>
|
||||
<button onclick="closeProfileImport()" style="background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer">×</button>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--txt2);margin-bottom:12px;line-height:1.5" id="profile-import-help">
|
||||
Lade ein <b>ZIP</b> deines OrcaSlicer-Filament-Ordners oder einzelne <b>.json</b>-Files hoch.<br>
|
||||
In OrcaSlicer: <i>Help → Show Configuration Folder → user/<id>/filament/</i>
|
||||
</div>
|
||||
<div id="profile-import-drop" style="border:2px dashed var(--border);border-radius:8px;padding:24px;text-align:center;cursor:pointer;margin-bottom:12px"
|
||||
ondragover="event.preventDefault();this.style.borderColor='var(--accent)'"
|
||||
ondragleave="this.style.borderColor='var(--border)'"
|
||||
ondrop="event.preventDefault();this.style.borderColor='var(--border)';doProfileImportUpload(event.dataTransfer.files)"
|
||||
onclick="document.getElementById('profile-import-file').click()">
|
||||
<div style="font-size:32px;margin-bottom:8px">⬆</div>
|
||||
<div style="font-size:13px;color:var(--txt2)" id="profile-import-dropmsg">Hierher ziehen oder klicken</div>
|
||||
<input type="file" id="profile-import-file" accept=".zip,.json" multiple
|
||||
style="display:none" onchange="doProfileImportUpload(this.files);this.value=''">
|
||||
</div>
|
||||
<div id="profile-import-status" style="font-size:12px;margin-bottom:12px;min-height:18px"></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="profile-import-list-label">Aktuell importiert</div>
|
||||
<div id="profile-import-list" style="max-height:240px;overflow-y:auto;font-size:12px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<nav class="sidebar">
|
||||
<button class="nav-btn active" onclick="showPanel('dashboard')" id="nb-dashboard">
|
||||
@@ -161,6 +128,8 @@
|
||||
<span class="nav-icon">🗂</span><span class="nav-text">Browser</span></button>
|
||||
<button class="nav-btn" onclick="showPanel('console');clearLogBadge()" id="nb-console">
|
||||
<span class="nav-icon">≡</span><span class="nav-text">Konsole</span><span id="log-badge" style="display:none;margin-left:4px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 5px;font-weight:700"></span></button>
|
||||
<button class="nav-btn" onclick="showPanel('settings')" id="nb-settings">
|
||||
<span class="nav-icon">⚙</span><span class="nav-text" id="nav-settings">Einstellungen</span></button>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
@@ -198,9 +167,15 @@
|
||||
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin:8px 0">
|
||||
<div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
|
||||
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0">
|
||||
<div class="time-label" id="d-lbl-layers"></div>
|
||||
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||
<div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
|
||||
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center">
|
||||
<div class="time-label" id="d-lbl-layers"></div>
|
||||
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||
</div>
|
||||
<div class="time-block" style="padding:4px 10px;min-width:72px;text-align:center">
|
||||
<div class="time-label" id="d-lbl-zpos">Z</div>
|
||||
<div class="time-val" style="font-size:13px" id="d-zpos">–</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-grid">
|
||||
@@ -218,12 +193,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="fname" id="d-fname" title="" style="margin-top:6px">–</div>
|
||||
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px">
|
||||
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="printAction('pause')">⏸ Pause</button>
|
||||
<button class="btn btn-resume btn-sm" id="d-btn-resume" onclick="printAction('resume')">▶ Weiter</button>
|
||||
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px;display:none">
|
||||
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="togglePauseResume()">⏸ Pause</button>
|
||||
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none">✂ <span id="d-btn-skip-label">Objekte</span></button>
|
||||
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
|
||||
</div>
|
||||
<!-- Aktionen für eine geladene, aber nicht laufende Datei (Issue #55) -->
|
||||
<div class="ctrl-btns" id="d-idle-btns" style="margin-top:12px;display:none">
|
||||
<button class="btn btn-accent btn-sm" id="d-idle-print" onclick="startIdleFile()">▶ <span id="d-idle-print-lbl">Drucken</span></button>
|
||||
<button class="btn btn-sm" id="d-idle-slots" onclick="startIdleFileWithSlots()" style="background:var(--raised);color:var(--txt)">⚙ <span id="d-idle-slots-lbl">Slots zuordnen</span></button>
|
||||
<button class="btn btn-cancel btn-sm" id="d-idle-clear" onclick="clearIdleFile()">✕ <span id="d-idle-clear-lbl">Leeren</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temperatursteuerung + Verlauf -->
|
||||
@@ -231,7 +211,7 @@
|
||||
<div class="card-title"><span>⊙</span> <span id="d-card-temps">Temperaturen</span></div>
|
||||
<div class="temp-card-inner">
|
||||
<div class="temp-block">
|
||||
<div class="temp-label">Nozzle</div>
|
||||
<div class="temp-label" id="d-lbl-nozzle">Nozzle</div>
|
||||
<div class="temp-row">
|
||||
<div class="temp-val" id="d-nt">–</div>
|
||||
<div class="temp-unit">°C</div>
|
||||
@@ -396,6 +376,16 @@
|
||||
<option value="duration_asc" id="ss-dur">⏱ Druckzeit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="store-upload-zone" onclick="document.getElementById('store-upload-input').click()"
|
||||
ondragover="event.preventDefault();this.classList.add('drag-over')"
|
||||
ondragleave="this.classList.remove('drag-over')"
|
||||
ondrop="event.preventDefault();this.classList.remove('drag-over');uploadGcode(event.dataTransfer.files[0])">
|
||||
<input type="file" id="store-upload-input" accept=".gcode,.bgcode"
|
||||
style="display:none" onchange="uploadGcode(this.files[0]);this.value=''">
|
||||
<span id="store-upload-icon">⬆</span>
|
||||
<span id="store-upload-label"><span id="store-upload-label-prefix">GCode hierher ziehen oder </span><u id="store-upload-label-browse">durchsuchen</u></span>
|
||||
<span id="store-upload-status" style="display:none"></span>
|
||||
</div>
|
||||
<div id="store-empty" style="display:none;color:var(--txt2);text-align:center;padding:40px 0;font-size:14px">
|
||||
</div>
|
||||
<div id="store-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px"></div>
|
||||
@@ -406,7 +396,7 @@
|
||||
<div class="card">
|
||||
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span><span>≡</span> <span id="ptitle-console">Ereignis-Log</span></span>
|
||||
<a id="btn-log-dl" href="/api/log/download" download="kx-bridge.log"
|
||||
<a id="btn-log-dl" href="/api/log/download" download
|
||||
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none">⬇ Download</a>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;align-items:center">
|
||||
@@ -423,6 +413,10 @@
|
||||
<button class="log-dir-btn active" id="logdir-all" onclick="setLogDir('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">RX</button>
|
||||
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">TX</button>
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level">Level:</span>
|
||||
<button class="log-lvl-btn active" id="loglvl-all" onclick="setLogLevel('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⛔ Errors</button>
|
||||
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⚠ Warn</button>
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px">Topic:</span>
|
||||
<button class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">AMS</button>
|
||||
<button class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">print</button>
|
||||
@@ -432,6 +426,198 @@
|
||||
<div class="console" id="console-log" style="height:calc(100vh - 260px);min-height:160px" onscroll="onLogScroll()"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ EINSTELLUNGEN ═══ -->
|
||||
<div class="panel" id="panel-settings">
|
||||
<div class="settings-wrap">
|
||||
<div class="settings-cats">
|
||||
<button class="set-cat active" id="setcat-connection" onclick="showSettingsCat('connection')"><span>🔌</span> <span id="setcat-lbl-connection">Verbindung</span></button>
|
||||
<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>
|
||||
|
||||
<div class="settings-content">
|
||||
<!-- Verbindung -->
|
||||
<div class="set-group active" id="setgrp-connection">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🔌</span> <span id="modal-sec-connection">Verbindung</span></div>
|
||||
<div class="modal-field" style="margin-bottom:12px">
|
||||
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
|
||||
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-printer-ip">Drucker-IP</label>
|
||||
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
|
||||
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mqtt-port">MQTT-Port</label>
|
||||
<input type="number" id="s-mqtt-port" placeholder="9883">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-username">MQTT-Benutzername</label>
|
||||
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-password">MQTT-Passwort</label>
|
||||
<input type="password" id="s-password" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-device-id">Device-ID</label>
|
||||
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mode-id">Mode-ID</label>
|
||||
<input type="text" id="s-mode-id" placeholder="20030">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drucker -->
|
||||
<div class="set-group" id="setgrp-printer">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🖨</span> <span id="modal-sec-print">Druckeinstellungen</span></div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
|
||||
<select id="s-default-slot">
|
||||
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
|
||||
<option value="0" id="opt-slot-0">Slot 1</option>
|
||||
<option value="1" id="opt-slot-1">Slot 2</option>
|
||||
<option value="2" id="opt-slot-2">Slot 3</option>
|
||||
<option value="3" id="opt-slot-3">Slot 4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
|
||||
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-file-ready-mode">Nach Upload: Druckstart-Verhalten</label>
|
||||
<select id="s-file-ready-mode">
|
||||
<option value="1" id="opt-file-ready-dialog">Print-Dialog</option>
|
||||
<option value="0" id="opt-file-ready-banner">Print-Leiste</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
||||
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
|
||||
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Darstellung -->
|
||||
<div class="set-group" id="setgrp-display">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🎨</span> <span id="setcat-lbl-display2">Darstellung</span></div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-set-lang">Sprache</label>
|
||||
<select id="s-lang-select" onchange="setLanguage(this.value)">
|
||||
<option value="de">Deutsch</option>
|
||||
<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>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="toggleTheme()"><span id="lbl-set-theme">Hell / Dunkel umschalten</span></button>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-poll-interval">Poll-Intervall (Sekunden)</label>
|
||||
<input type="number" id="s-poll-interval" min="1" max="60" step="1" placeholder="3" oninput="onPollIntervalInput()">
|
||||
<small style="color:var(--txt2)" id="lbl-poll-hint">Wie oft die Bridge den Drucker-Status abfragt</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filament -->
|
||||
<div class="set-group" id="setgrp-filament">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🧵</span> <span id="modal-sec-orca-profiles">OrcaSlicer-Profile</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="orca-profiles-hint">
|
||||
Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)
|
||||
</div>
|
||||
<div id="orca-profiles-list" style="margin-bottom:8px;font-size:12px;color:var(--txt2)"></div>
|
||||
<button class="btn btn-sm" id="btn-orca-profiles-import" onclick="openProfileImport()"
|
||||
style="background:var(--raised);color:var(--txt)">
|
||||
⬆ <span id="lbl-orca-profiles-import">Profile importieren</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🎯</span> <span id="lbl-filament-mapping">Filament-Profil-Mapping (pro Slot)</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="filament-mapping-hint">
|
||||
Festes Orca-Profil pro AMS-Slot. Beim Slicer-Sync sendet die Bridge dieses Profil statt „Generic".
|
||||
</div>
|
||||
<div id="filament-mapping-list"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveFilamentMapping()"><span id="lbl-filament-mapping-save">Mapping speichern</span></button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span>👁</span> <span id="lbl-visible-vendors">Sichtbare Hersteller (Profil-Dropdown)</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="visible-vendors-hint">
|
||||
Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.
|
||||
</div>
|
||||
<input type="text" id="vendor-filter-search" placeholder="Hersteller suchen…" oninput="renderVendorChecklist()"
|
||||
style="width:100%;padding:6px 10px;margin-bottom:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px">
|
||||
<div id="visible-vendors-list" style="max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveVisibleVendors()"><span id="lbl-visible-vendors-save">Auswahl speichern</span></button>
|
||||
</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">
|
||||
<div class="card-title"><span>⚙</span> <span id="modal-sec-version">Version</span></div>
|
||||
<div class="update-row">
|
||||
<span id="s-version-label" style="font-size:13px;color:var(--txt)">–</span>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
|
||||
</div>
|
||||
<div class="update-status" id="update-status" style="margin-top:6px"></div>
|
||||
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
|
||||
<span id="lbl-update-apply">Jetzt installieren</span>
|
||||
</button>
|
||||
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings" style="margin-top:14px">Speichern & Neustart</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -440,9 +626,26 @@
|
||||
<button class="bnav-btn" onclick="showPanel('printers');loadPrinterTab()" id="bnb-printers"><span class="bnav-icon">🖨</span>Drucker</button>
|
||||
<button class="bnav-btn" onclick="showPanel('store');loadStore()" id="bnb-store"><span class="bnav-icon">🗂</span>Browser</button>
|
||||
<button class="bnav-btn" onclick="showPanel('console');clearLogBadge()" id="bnb-console"><span class="bnav-icon">≡</span>Log<span id="log-badge-bot" style="display:none;margin-left:3px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 4px;font-weight:700"></span></button>
|
||||
<button class="bnav-btn" onclick="showPanel('settings')" id="bnb-settings"><span class="bnav-icon">⚙</span>Setup</button>
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Web-Upload-Verify-Dialog -->
|
||||
<div class="modal-overlay" id="store-web-verify-dialog" onclick="if(event.target===this)closeStoreWebVerifyDialog()">
|
||||
<div class="modal-box" style="max-width:420px;width:100%">
|
||||
<div class="modal-header" style="margin-bottom:14px">
|
||||
<span class="modal-title" id="store-web-verify-title">Datei verifizieren</span>
|
||||
<button onclick="closeStoreWebVerifyDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<p id="store-web-verify-msg" style="font-size:13px;color:var(--txt);margin-bottom:12px">Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde.</p>
|
||||
<div id="store-web-verify-status" style="font-size:12px;color:var(--txt2);min-height:16px;margin-bottom:8px"></div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button id="store-web-verify-abort" onclick="closeStoreWebVerifyDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="store-web-verify-confirm" onclick="confirmStoreWebVerify()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Bestätigen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filament-Slot-Dialog -->
|
||||
<div class="modal-overlay" id="filament-dialog" onclick="if(event.target===this)closeFilamentDialog()">
|
||||
<div class="modal-box" style="max-width:380px;width:100%">
|
||||
@@ -453,9 +656,30 @@
|
||||
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
|
||||
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
|
||||
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
|
||||
<p id="fd-objects-hint" style="font-size:12px;color:var(--txt2);margin-bottom:8px">Objekte überspringen (optional):</p>
|
||||
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
|
||||
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
|
||||
<button type="button" id="fd-objects-toggle" onclick="toggleFdObjects()"
|
||||
style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer;font-size:12px;text-align:left">
|
||||
<span id="fd-objects-arrow" style="font-size:10px;transition:transform .15s">▶</span>
|
||||
<span>✂ <span id="fd-objects-toggle-lbl">Objekte überspringen</span></span>
|
||||
<span id="fd-objects-count" style="margin-left:auto;color:var(--txt2);font-weight:600"></span>
|
||||
</button>
|
||||
<div id="fd-objects-body" style="display:none;margin-top:8px">
|
||||
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
|
||||
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:14px;padding:10px 12px;background:var(--raised);border-radius:8px;border:1px solid var(--border)">
|
||||
<div style="font-size:11px;font-weight:600;color:var(--txt2);margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em" id="fd-options-title">Druckoptionen</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<input type="checkbox" id="fd-auto-leveling" style="width:auto;margin:0">
|
||||
<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>
|
||||
@@ -476,7 +700,7 @@
|
||||
<input type="text" id="apd-name" placeholder="z.B. Kobra X Wohnzimmer" style="width:100%;box-sizing:border-box;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);margin-bottom:6px">
|
||||
<div id="apd-status" style="font-size:12px;margin:8px 0;min-height:16px;color:var(--txt2)"></div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button onclick="closeAddPrinterDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="apd-cancel" onclick="closeAddPrinterDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="apd-confirm" onclick="confirmAddPrinter()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
:root{
|
||||
color-scheme:dark; /* native Form-Controls (select) im Webview dunkel rendern */
|
||||
--bg:#1a1a1f;--card:#24242c;--raised:#2e2e3a;--border:#3a3a4a;
|
||||
--txt:#f0f0f5;--txt2:#8888aa;--accent:#00c8ff;--accent2:#ff6b35;
|
||||
--ok:#4cde80;--err:#ff4d6d;--warn:#ffb020;
|
||||
@@ -6,12 +7,17 @@
|
||||
--mono:"JetBrains Mono","Fira Code",monospace;
|
||||
}
|
||||
[data-theme=light]{
|
||||
color-scheme:light;
|
||||
--bg:#f0f0f5;--card:#fff;--raised:#e8e8f0;--border:#d0d0e0;
|
||||
--txt:#1a1a2e;--txt2:#666680;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:var(--bg);color:var(--txt);font-family:var(--font);font-size:14px;min-height:100vh;display:flex;flex-direction:column}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
/* select/option-Farben explizit setzen — OrcaSlicers Device-Tab-Webview erbt
|
||||
sie sonst nicht und rendert weiße Schrift auf weißem Grund (Issue #29). */
|
||||
select{background:var(--raised)!important;color:var(--txt)!important}
|
||||
select option{background:var(--card)!important;color:var(--txt)!important}
|
||||
|
||||
/* ── HEADER ── */
|
||||
header{background:var(--card);border-bottom:1px solid var(--border);
|
||||
@@ -206,6 +212,40 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
|
||||
.panel{display:none}
|
||||
.panel.active{display:block}
|
||||
|
||||
/* ── SETTINGS (Master-Detail) ── */
|
||||
.settings-wrap{display:grid;grid-template-columns:200px 1fr;gap:16px;align-items:start}
|
||||
.settings-cats{display:flex;flex-direction:column;gap:4px;position:sticky;top:12px}
|
||||
.set-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;
|
||||
border:1px solid transparent;background:var(--raised);color:var(--txt2);cursor:pointer;
|
||||
font-size:13px;text-align:left;transition:background .15s,color .15s}
|
||||
.set-cat:hover{color:var(--txt)}
|
||||
.set-cat.active{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||||
.set-group{display:none}
|
||||
.set-group.active{display:block}
|
||||
.set-group .card{margin-bottom:14px}
|
||||
@media(max-width:768px){
|
||||
.settings-wrap{grid-template-columns:1fr}
|
||||
.settings-cats{flex-direction:row;flex-wrap:wrap;position:static;overflow-x:auto}
|
||||
.set-cat{flex:1;min-width:auto;justify-content:center;padding:8px 6px;font-size:12px}
|
||||
.set-cat .nav-text{display:inline}
|
||||
}
|
||||
|
||||
/* ── FILE BROWSER UPLOAD ZONE ── */
|
||||
#store-upload-zone{
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:6px;padding:18px 12px;margin-bottom:14px;
|
||||
border:2px dashed var(--border);border-radius:10px;
|
||||
background:var(--raised);color:var(--txt2);
|
||||
cursor:pointer;transition:border-color .15s,background .15s;
|
||||
font-size:13px;text-align:center;user-select:none;
|
||||
}
|
||||
#store-upload-zone:hover{border-color:var(--accent);background:rgba(0,200,255,.06);color:var(--txt)}
|
||||
#store-upload-zone.drag-over{border-color:var(--accent);background:rgba(0,200,255,.12);color:var(--accent)}
|
||||
#store-upload-icon{font-size:22px;line-height:1}
|
||||
.upload-status-busy{color:var(--txt2)}
|
||||
.upload-status-ok{color:var(--ok)}
|
||||
.upload-status-err{color:var(--err)}
|
||||
|
||||
/* ── MODAL ── */
|
||||
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);
|
||||
z-index:200;align-items:center;justify-content:center;padding:16px}
|
||||
|
||||
296
web/translations/de.json
Normal file
296
web/translations/de.json
Normal file
@@ -0,0 +1,296 @@
|
||||
{
|
||||
"header_status_standby": "Bereit",
|
||||
"header_status_printing": "Druckt",
|
||||
"header_status_complete": "Fertig",
|
||||
"header_status_error": "Fehler",
|
||||
"kobra_free": "Bereit",
|
||||
"kobra_busy": "Beschäftigt",
|
||||
"kobra_printing": "Druckt",
|
||||
"kobra_preheating": "Aufheizen",
|
||||
"kobra_auto_leveling": "Nivellierung",
|
||||
"kobra_checking": "Prüfung",
|
||||
"kobra_updated": "Aktualisierung",
|
||||
"kobra_init": "Initialisierung",
|
||||
"kobra_pausing": "Pausiert...",
|
||||
"kobra_paused": "Pausiert",
|
||||
"kobra_resuming": "Fortsetzen...",
|
||||
"kobra_resumed": "Fortgesetzt",
|
||||
"kobra_stopping": "Stoppt...",
|
||||
"kobra_stoped": "Gestoppt",
|
||||
"kobra_finished": "Abgeschlossen",
|
||||
"kobra_failed": "Fehler",
|
||||
"kobra_canceled": "Abgebrochen",
|
||||
"kobra_offline": "Offline",
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_print": "Druck",
|
||||
"nav_temps": "Temperaturen",
|
||||
"nav_motion": "Achsen",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "Licht / Lüfter",
|
||||
"nav_console": "Konsole",
|
||||
"card_progress": "Fortschritt",
|
||||
"card_temps": "Temperaturen",
|
||||
"card_light_fan": "Lüfter",
|
||||
"card_speed": "Druckgeschwindigkeit",
|
||||
"card_cam": "Kamera",
|
||||
"lbl_elapsed": "Verstrichen:",
|
||||
"lbl_remaining": "Restzeit:",
|
||||
"lbl_slicer_time": "Slicer-Schätzung:",
|
||||
"lbl_layers": "Layer",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Leise",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"lbl_light": "💡 Licht",
|
||||
"lbl_feed": "Einziehen",
|
||||
"lbl_unload": "Ausziehen",
|
||||
"card_ace_dry": "ACE Trocknung",
|
||||
"ace_dry_dryer": "Trockner",
|
||||
"ace_dry_status_off": "Status: Aus",
|
||||
"ace_dry_status_on": "Status: Aktiv",
|
||||
"ace_dry_status_remaining": "Rest",
|
||||
"ace_dry_humidity": "Luftfeuchte",
|
||||
"ace_dry_current_temp": "Temperatur",
|
||||
"ace_dry_chart": "Verlauf (Temp/Feuchte)",
|
||||
"ace_dry_temp": "Temperatur (°C)",
|
||||
"ace_dry_duration": "Dauer (Min)",
|
||||
"ace_dry_start": "▶ Start",
|
||||
"ace_dry_stop": "■ Stop",
|
||||
"ace_dry_auto_refill": "Auto-Nachschub",
|
||||
"ace_dry_enable": "Trocknung aktivieren",
|
||||
"ace_dry_temp_line": "Trocknungstemperatur",
|
||||
"ace_dry_time_line": "Trocknungszeit",
|
||||
"ace_dry_ui_pending": "(nur UI, Backend folgt)",
|
||||
"ace_dry_dialog_title": "Trockner Temp/Zeit-Einstellungen",
|
||||
"ace_dry_dialog_temp": "Temperatur (30-80°C)",
|
||||
"ace_dry_dialog_time": "Restzeit (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "Bestätigen",
|
||||
"ace_dry_dialog_cancel": "Abbrechen",
|
||||
"ace_dry_dialog_save_restart": "Speichern & Neustart",
|
||||
"ace_dry_dialog_custom_name": "Eigener Name",
|
||||
"ace_dry_dialog_reset_default": "Auf Standard zurücksetzen",
|
||||
"cam_placeholder": "📷 Kamera nicht gestartet",
|
||||
"cam_stream_unavailable": "Stream nicht verfügbar",
|
||||
"btn_cam_start": "▶ Kamera",
|
||||
"btn_cam_stop": "◼ Kamera",
|
||||
"btn_pause": "⏸ Pause",
|
||||
"btn_resume": "▶ Weiter",
|
||||
"btn_cancel": "✕ Stopp",
|
||||
"label_nozzle": "Düse",
|
||||
"label_bed": "Bett",
|
||||
"label_fan": "🌀 Lüfter",
|
||||
"label_light": "💡 Licht",
|
||||
"label_on_off": "Ein / Aus",
|
||||
"label_speed": "Geschwindigkeit",
|
||||
"panel_print_title": "Drucksteuerung",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Fortsetzen",
|
||||
"panel_print_btn_cancel": "✕ Abbrechen",
|
||||
"panel_print_temps_live": "Temperaturen (Live)",
|
||||
"label_set": "Setzen",
|
||||
"label_off": "Aus",
|
||||
"panel_temps_nozzle": "Düse",
|
||||
"panel_temps_bed": "Heizbett",
|
||||
"panel_temps_chart": "Verlauf (letzte 60 Messungen)",
|
||||
"label_target_c": "Ziel:",
|
||||
"panel_motion_xy": "XY-Achsen",
|
||||
"panel_motion_z": "Z-Achse",
|
||||
"label_step": "Schrittweite:",
|
||||
"btn_home_z": "Home Z",
|
||||
"btn_home_xy": "Home XY",
|
||||
"btn_home_all": "Home All",
|
||||
"btn_disable_motors": "Motoren aus",
|
||||
"panel_ams_title": "Filament",
|
||||
"card_ams": "Filament",
|
||||
"ams_no_data": "Keine AMS-Daten empfangen",
|
||||
"label_slot": "Slot",
|
||||
"ams_empty": "Leer",
|
||||
"panel_extras_light": "Licht",
|
||||
"panel_extras_fan": "Lüfter",
|
||||
"panel_extras_camera": "Kamera",
|
||||
"btn_cam_start2": "▶ Start",
|
||||
"btn_cam_stop2": "◼ Stop",
|
||||
"panel_console_title": "Ereignis-Log",
|
||||
"log_light_on": "Licht an",
|
||||
"log_light_off": "Licht aus",
|
||||
"log_fan": "Lüfter →",
|
||||
"log_nozzle": "Düse →",
|
||||
"log_bed": "Bett →",
|
||||
"log_axis": "Achse",
|
||||
"log_home": "Home",
|
||||
"log_home_all": "Home All",
|
||||
"log_cam_start": "Kamera gestartet:",
|
||||
"log_cam_stop": "Kamera gestoppt",
|
||||
"log_poll_error": "Poll-Fehler:",
|
||||
"log_error": "Fehler:",
|
||||
"confirm_cancel": "Druck wirklich abbrechen?",
|
||||
"settings_title": "Einstellungen",
|
||||
"settings_connection": "Verbindung",
|
||||
"settings_print": "Druckeinstellungen",
|
||||
"settings_poll": "Poll-Intervall (Sekunden)",
|
||||
"settings_version": "Version",
|
||||
"nav_settings": "Einstellungen",
|
||||
"settings_cat_display": "Darstellung",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Sprache",
|
||||
"settings_cat_theme": "Hell / Dunkel umschalten",
|
||||
"settings_filament_mapping": "Filament-Profil-Mapping (pro Slot)",
|
||||
"settings_filament_mapping_save": "Mapping speichern",
|
||||
"settings_visible_vendors": "Sichtbare Hersteller (Profil-Dropdown)",
|
||||
"settings_visible_vendors_hint": "Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic\" und eigene Profile sind immer sichtbar.",
|
||||
"settings_visible_vendors_save": "Auswahl speichern",
|
||||
"progress_action_print": "Drucken",
|
||||
"progress_action_slots": "Slots zuordnen",
|
||||
"progress_action_clear": "Leeren",
|
||||
"settings_save": "Speichern & Neustart",
|
||||
"settings_printer_name": "Drucker-Name",
|
||||
"settings_printer_ip": "Drucker-IP",
|
||||
"settings_mqtt_port": "MQTT-Port",
|
||||
"settings_username": "MQTT-Benutzername",
|
||||
"settings_password": "MQTT-Passwort",
|
||||
"settings_device_id": "Device-ID",
|
||||
"settings_mode_id": "Mode-ID",
|
||||
"hint_ip_no_port": "Nur IP-Adresse, kein Port (z.B. 192.168.1.102)",
|
||||
"settings_default_slot": "Standard-Slot (Einfarbdruck)",
|
||||
"settings_slot_auto": "Auto (alle belegten Slots)",
|
||||
"settings_auto_leveling": "Auto-Leveling vor Druck",
|
||||
"settings_camera_on_print": "Kamera bei Druckstart einschalten",
|
||||
"settings_web_upload_warning": "Warnung bei Web-Upload-Druck anzeigen",
|
||||
"update_check": "Auf Updates prüfen",
|
||||
"update_checking": "Prüfe...",
|
||||
"update_available": "verfügbar",
|
||||
"update_none": "Bereits aktuell",
|
||||
"update_apply": "Jetzt installieren",
|
||||
"update_applying": "Lade herunter...",
|
||||
"update_restarting": "Starte neu...",
|
||||
"update_error": "Fehler",
|
||||
"btn_connect": "⚡ Verbinden",
|
||||
"btn_disconnect": "✕ Trennen",
|
||||
"lbl_conn_error": "Verbindungsfehler:",
|
||||
"slot_edit_title": "Slot bearbeiten",
|
||||
"slot_edit_color": "Farbe",
|
||||
"slot_edit_material": "Material",
|
||||
"slot_edit_load": "⬇ Einziehen",
|
||||
"slot_edit_unload": "⬆ Ausziehen",
|
||||
"slot_edit_save": "💾 Speichern",
|
||||
"slot_edit_custom": "z.B. PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "AMS Slot",
|
||||
"slot_edit_profile": "OrcaSlicer-Profil",
|
||||
"slot_edit_profile_hint": "Sendet beim OrcaSlicer-Sync die konkrete Marke statt nur „Generic\"",
|
||||
"slot_edit_profile_default": "— Generic (Default) —",
|
||||
"orca_profile_section": "OrcaSlicer-Profile",
|
||||
"orca_profile_hint": "Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)",
|
||||
"orca_profile_import_btn": "Profile importieren",
|
||||
"orca_profile_import_link": "★ Eigene Profile importieren…",
|
||||
"orca_profile_import_title": "Eigene OrcaSlicer-Profile importieren",
|
||||
"orca_profile_help_html": "Lade ein <b>ZIP</b> deines OrcaSlicer-Filament-Ordners oder einzelne <b>.json</b>-Files hoch.<br>In OrcaSlicer: <i>Help → Show Configuration Folder → user/<id>/filament/</i>",
|
||||
"orca_profile_dropmsg": "Hierher ziehen oder klicken",
|
||||
"orca_profile_list_label": "Aktuell importiert",
|
||||
"orca_profile_user_label": "Eigene Profile",
|
||||
"orca_profile_user_empty": "– keine –",
|
||||
"orca_profile_uploading": "Lade hoch…",
|
||||
"orca_profile_done": "Importiert",
|
||||
"orca_profile_skipped": "übersprungen",
|
||||
"log_dir_all": "Alle",
|
||||
"log_lvl_label": "Level:",
|
||||
"file_ready_btn": "▶ Druck starten",
|
||||
"file_slots_btn": "🎨 Slots wählen",
|
||||
"file_cancel_btn": "✕ Abbrechen",
|
||||
"nav_printers": "Drucker",
|
||||
"skip_title": "✂ Objekte überspringen",
|
||||
"skip_hint": "Objekte abwählen, die nicht weiter gedruckt werden sollen:",
|
||||
"skip_btn_label": "Objekte",
|
||||
"skip_no_objects": "Keine Objekte in diesem Druck.",
|
||||
"skip_already": "übersprungen",
|
||||
"skip_select_at_least_one": "Bitte mindestens ein Objekt wählen.",
|
||||
"skip_sending": "Sende …",
|
||||
"skip_success": "Objekte werden übersprungen.",
|
||||
"fd_objects_hint": "Objekte überspringen (optional):",
|
||||
"fd_objects_toggle": "Objekte überspringen",
|
||||
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
|
||||
"fd_cancel": "Abbrechen",
|
||||
"fd_print": "▶ Drucken",
|
||||
"fd_no_slots_msg": "Keine belegten AMS-Slots.{br}Druck trotzdem starten?",
|
||||
"fd_slot": "Slot",
|
||||
"fd_no_matching_material": "Kein passendes Material",
|
||||
"fd_used": "BELEGT",
|
||||
"add_printer": "Drucker hinzufügen",
|
||||
"apd_lbl_ip": "Drucker-IP",
|
||||
"apd_lbl_name": "Name (optional)",
|
||||
"apd_placeholder_name": "z.B. Kobra X Wohnzimmer",
|
||||
"apd_cancel": "Abbrechen",
|
||||
"apd_confirm": "Hinzufügen",
|
||||
"apd_fetching": "Hole Daten vom Drucker…",
|
||||
"apd_success": "Drucker hinzugefügt, Bridge startet neu…",
|
||||
"apd_err_ip": "Bitte IP-Adresse eingeben",
|
||||
"printers_remove": "Drucker entfernen",
|
||||
"printers_remove_confirm": "Drucker \"{name}\" entfernen? Die Bridge startet neu.",
|
||||
"printers_active": "● aktiv",
|
||||
"printers_switch": "Wechseln →",
|
||||
"printers_current": "Aktueller Drucker",
|
||||
"printers_loading": "Lade…",
|
||||
"printers_none": "Keine Drucker konfiguriert.",
|
||||
"printers_empty_hint": "Noch kein Drucker eingerichtet.",
|
||||
"nav_browser": "Browser",
|
||||
"panel_browser_title": "Datei-Browser",
|
||||
"store_search_placeholder": "🔍 Suche…",
|
||||
"store_empty": "Noch keine Dateien hochgeladen.",
|
||||
"store_refresh": "↻ Aktualisieren",
|
||||
"store_print": "▶ Drucken",
|
||||
"store_download": "⬇ Download",
|
||||
"store_delete_confirm": "Datei löschen?",
|
||||
"store_print_confirm": "Datei drucken?",
|
||||
"store_web_verify_title": "Datei verifizieren",
|
||||
"store_web_verify_msg": "Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde.",
|
||||
"store_web_verify_confirm": "Bestätigen",
|
||||
"store_web_verify_abort": "Abbrechen",
|
||||
"store_no_results": "Keine Dateien gefunden.",
|
||||
"store_never": "noch nicht gedruckt",
|
||||
"store_estimate": "Schätzung",
|
||||
"store_upload_label_prefix": "GCode hierher ziehen oder ",
|
||||
"store_upload_label_browse": "durchsuchen",
|
||||
"store_upload_busy": "⏳ Hochladen…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ Nur GCode-Dateien erlaubt (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "Alle",
|
||||
"sf_ok": "✓ Erfolgreich",
|
||||
"sf_err": "✗ Fehler",
|
||||
"sf_new": "Neu",
|
||||
"ss_date": "↓ Datum",
|
||||
"ss_name": "A–Z Name",
|
||||
"ss_dur": "⏱ Druckzeit",
|
||||
"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": "Custom",
|
||||
"fd_options_title": "Optionen",
|
||||
"print_auto_leveling": "Auto-Leveling für diesen Druck",
|
||||
"settings_file_ready_mode": "Druckdialog starten",
|
||||
"settings_file_ready_banner": "Druckleiste",
|
||||
"settings_file_ready_dialog": "Druckdialog",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Richtung:",
|
||||
"log_lvl_err": "⛔ Fehler",
|
||||
"log_lvl_warn": "⚠ Warnung",
|
||||
"log_topic_label": "Thema:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Druck",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Status",
|
||||
"log_download": "⬇ Download",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Leeren",
|
||||
"log_filter_placeholder": "Filtern…",
|
||||
"skip_cancel": "Abbrechen",
|
||||
"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"
|
||||
}
|
||||
296
web/translations/en.json
Normal file
296
web/translations/en.json
Normal file
@@ -0,0 +1,296 @@
|
||||
{
|
||||
"header_status_standby": "Ready",
|
||||
"header_status_printing": "Printing",
|
||||
"header_status_complete": "Complete",
|
||||
"header_status_error": "Error",
|
||||
"kobra_free": "Ready",
|
||||
"kobra_busy": "Busy",
|
||||
"kobra_printing": "Printing",
|
||||
"kobra_preheating": "Preheating",
|
||||
"kobra_auto_leveling": "Auto Leveling",
|
||||
"kobra_checking": "Checking",
|
||||
"kobra_updated": "Updating",
|
||||
"kobra_init": "Initializing",
|
||||
"kobra_pausing": "Pausing...",
|
||||
"kobra_paused": "Paused",
|
||||
"kobra_resuming": "Resuming...",
|
||||
"kobra_resumed": "Resumed",
|
||||
"kobra_stopping": "Stopping...",
|
||||
"kobra_stoped": "Stopped",
|
||||
"kobra_finished": "Finished",
|
||||
"kobra_failed": "Error",
|
||||
"kobra_canceled": "Cancelled",
|
||||
"kobra_offline": "Offline",
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_print": "Print",
|
||||
"nav_temps": "Temperatures",
|
||||
"nav_motion": "Motion",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "Light / Fan",
|
||||
"nav_console": "Console",
|
||||
"card_progress": "Progress",
|
||||
"card_temps": "Temperatures",
|
||||
"card_light_fan": "Fan",
|
||||
"card_speed": "Print Speed",
|
||||
"card_cam": "Camera",
|
||||
"lbl_elapsed": "Elapsed:",
|
||||
"lbl_remaining": "Remaining:",
|
||||
"lbl_slicer_time": "Slicer estimate:",
|
||||
"lbl_layers": "Layer",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silent",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"lbl_light": "💡 Light",
|
||||
"lbl_feed": "Load",
|
||||
"lbl_unload": "Unload",
|
||||
"card_ace_dry": "ACE Drying",
|
||||
"ace_dry_dryer": "Dryer",
|
||||
"ace_dry_status_off": "Status: Off",
|
||||
"ace_dry_status_on": "Status: Active",
|
||||
"ace_dry_status_remaining": "Remaining",
|
||||
"ace_dry_humidity": "Humidity",
|
||||
"ace_dry_current_temp": "Temperature",
|
||||
"ace_dry_chart": "History (Temp/Humidity)",
|
||||
"ace_dry_temp": "Temperature (°C)",
|
||||
"ace_dry_duration": "Duration (min)",
|
||||
"ace_dry_start": "▶ Start",
|
||||
"ace_dry_stop": "■ Stop",
|
||||
"ace_dry_auto_refill": "Auto Refill",
|
||||
"ace_dry_enable": "Enable Drying",
|
||||
"ace_dry_temp_line": "Drying Temperature",
|
||||
"ace_dry_time_line": "Drying Time",
|
||||
"ace_dry_ui_pending": "(UI only, backend next)",
|
||||
"ace_dry_dialog_title": "Dryer Temp/Time Settings",
|
||||
"ace_dry_dialog_temp": "Temperature (30-80°C)",
|
||||
"ace_dry_dialog_time": "Rem. Time (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "Confirm",
|
||||
"ace_dry_dialog_cancel": "Cancel",
|
||||
"ace_dry_dialog_save_restart": "Save & Restart",
|
||||
"ace_dry_dialog_custom_name": "Custom Name",
|
||||
"ace_dry_dialog_reset_default": "Reset to Default",
|
||||
"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": "Custom",
|
||||
"cam_placeholder": "📷 Camera not started",
|
||||
"cam_stream_unavailable": "Stream unavailable",
|
||||
"btn_cam_start": "▶ Camera",
|
||||
"btn_cam_stop": "◼ Camera",
|
||||
"btn_pause": "⏸ Pause",
|
||||
"btn_resume": "▶ Resume",
|
||||
"btn_cancel": "✕ Stop",
|
||||
"label_nozzle": "Nozzle",
|
||||
"label_bed": "Bed",
|
||||
"label_fan": "🌀 Fan",
|
||||
"label_light": "💡 Light",
|
||||
"label_on_off": "On / Off",
|
||||
"label_speed": "Speed",
|
||||
"panel_print_title": "Print Control",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Resume",
|
||||
"panel_print_btn_cancel": "✕ Cancel",
|
||||
"panel_print_temps_live": "Temperatures (Live)",
|
||||
"label_set": "Set",
|
||||
"label_off": "Off",
|
||||
"panel_temps_nozzle": "Nozzle",
|
||||
"panel_temps_bed": "Heated Bed",
|
||||
"panel_temps_chart": "History (last 60 readings)",
|
||||
"label_target_c": "Target:",
|
||||
"panel_motion_xy": "XY Axes",
|
||||
"panel_motion_z": "Z Axis",
|
||||
"label_step": "Step size:",
|
||||
"btn_home_z": "Home Z",
|
||||
"btn_home_xy": "Home XY",
|
||||
"btn_home_all": "Home All",
|
||||
"btn_disable_motors": "Motors Off",
|
||||
"panel_ams_title": "Filament",
|
||||
"card_ams": "Filament",
|
||||
"ams_no_data": "No AMS data received",
|
||||
"label_slot": "Slot",
|
||||
"ams_empty": "Empty",
|
||||
"panel_extras_light": "Light",
|
||||
"panel_extras_fan": "Fan",
|
||||
"panel_extras_camera": "Camera",
|
||||
"btn_cam_start2": "▶ Start",
|
||||
"btn_cam_stop2": "◼ Stop",
|
||||
"panel_console_title": "Event Log",
|
||||
"log_light_on": "Light on",
|
||||
"log_light_off": "Light off",
|
||||
"log_fan": "Fan →",
|
||||
"log_nozzle": "Nozzle →",
|
||||
"log_bed": "Bed →",
|
||||
"log_axis": "Axis",
|
||||
"log_home": "Home",
|
||||
"log_home_all": "Home All",
|
||||
"log_cam_start": "Camera started:",
|
||||
"log_cam_stop": "Camera stopped",
|
||||
"log_poll_error": "Poll error:",
|
||||
"log_error": "Error:",
|
||||
"confirm_cancel": "Really cancel the print?",
|
||||
"settings_title": "Settings",
|
||||
"settings_connection": "Connection",
|
||||
"settings_print": "Print Settings",
|
||||
"settings_poll": "Poll Interval (seconds)",
|
||||
"nav_settings": "Settings",
|
||||
"settings_cat_display": "Appearance",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Language",
|
||||
"settings_cat_theme": "Toggle light / dark",
|
||||
"settings_filament_mapping": "Filament profile mapping (per slot)",
|
||||
"settings_filament_mapping_save": "Save mapping",
|
||||
"settings_visible_vendors": "Visible vendors (profile dropdown)",
|
||||
"settings_visible_vendors_hint": "Only these vendors appear in the slot profile dropdown. Nothing selected = show all. \"Generic\" and your own profiles are always visible.",
|
||||
"settings_visible_vendors_save": "Save selection",
|
||||
"progress_action_print": "Print",
|
||||
"progress_action_slots": "Map slots",
|
||||
"progress_action_clear": "Clear",
|
||||
"settings_version": "Version",
|
||||
"settings_save": "Save & Restart",
|
||||
"settings_printer_name": "Printer Name",
|
||||
"settings_printer_ip": "Printer IP",
|
||||
"settings_mqtt_port": "MQTT Port",
|
||||
"settings_username": "MQTT Username",
|
||||
"settings_password": "MQTT Password",
|
||||
"settings_device_id": "Device ID",
|
||||
"settings_mode_id": "Mode ID",
|
||||
"hint_ip_no_port": "IP address only, no port (e.g. 192.168.1.102)",
|
||||
"settings_default_slot": "Default Slot (single color)",
|
||||
"settings_slot_auto": "Auto (all loaded slots)",
|
||||
"settings_auto_leveling": "Auto-Leveling Default",
|
||||
"fd_options_title": "Print Options",
|
||||
"print_auto_leveling": "Auto-Leveling",
|
||||
"settings_file_ready_mode": "Start Print Behavior",
|
||||
"settings_file_ready_banner": "Print Bar",
|
||||
"settings_file_ready_dialog": "Print Dialog",
|
||||
"settings_camera_on_print": "Turn camera on at print start",
|
||||
"settings_web_upload_warning": "Show warning when printing web uploads",
|
||||
"update_check": "Check for Updates",
|
||||
"update_checking": "Checking...",
|
||||
"update_available": "available",
|
||||
"update_none": "Already up to date",
|
||||
"update_apply": "Install Now",
|
||||
"update_applying": "Downloading...",
|
||||
"update_restarting": "Restarting...",
|
||||
"update_error": "Error",
|
||||
"btn_connect": "⚡ Connect",
|
||||
"btn_disconnect": "✕ Disconnect",
|
||||
"lbl_conn_error": "Connection error:",
|
||||
"slot_edit_title": "Edit Slot",
|
||||
"slot_edit_color": "Color",
|
||||
"slot_edit_material": "Material",
|
||||
"slot_edit_load": "⬇ Load",
|
||||
"slot_edit_unload": "⬆ Unload",
|
||||
"slot_edit_save": "💾 Save",
|
||||
"slot_edit_custom": "e.g. PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "AMS Slot",
|
||||
"slot_edit_profile": "OrcaSlicer profile",
|
||||
"slot_edit_profile_hint": "Sent on OrcaSlicer sync as the specific brand instead of just \"Generic\"",
|
||||
"slot_edit_profile_default": "— Generic (default) —",
|
||||
"orca_profile_section": "OrcaSlicer Profiles",
|
||||
"orca_profile_hint": "Import your own OrcaSlicer filament profiles (open the user dir via Help → Show Configuration Folder)",
|
||||
"orca_profile_import_btn": "Import profiles",
|
||||
"orca_profile_import_link": "★ Import own profiles…",
|
||||
"orca_profile_import_title": "Import your OrcaSlicer profiles",
|
||||
"orca_profile_help_html": "Upload a <b>ZIP</b> of your OrcaSlicer filament folder or single <b>.json</b> files.<br>In OrcaSlicer: <i>Help → Show Configuration Folder → user/<id>/filament/</i>",
|
||||
"orca_profile_dropmsg": "Drop here or click",
|
||||
"orca_profile_list_label": "Currently imported",
|
||||
"orca_profile_user_label": "Own profiles",
|
||||
"orca_profile_user_empty": "– none –",
|
||||
"orca_profile_uploading": "Uploading…",
|
||||
"orca_profile_done": "Imported",
|
||||
"orca_profile_skipped": "skipped",
|
||||
"log_dir_all": "All",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Dir:",
|
||||
"log_lvl_label": "Level:",
|
||||
"log_lvl_err": "⛔ Errors",
|
||||
"log_lvl_warn": "⚠ Warn",
|
||||
"log_topic_label": "Topic:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Print",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Status",
|
||||
"log_download": "⬇ Download",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Clear",
|
||||
"log_filter_placeholder": "Filter…",
|
||||
"file_ready_btn": "▶ Start Print",
|
||||
"file_slots_btn": "🎨 Select Slots",
|
||||
"file_cancel_btn": "✕ Cancel",
|
||||
"nav_printers": "Printers",
|
||||
"skip_title": "✂ Skip objects",
|
||||
"skip_hint": "Uncheck objects you no longer want to print:",
|
||||
"skip_btn_label": "Objects",
|
||||
"skip_no_objects": "No objects in this print.",
|
||||
"skip_already": "skipped",
|
||||
"skip_cancel": "Cancel",
|
||||
"skip_confirm": "Skip",
|
||||
"skip_select_at_least_one": "Please pick at least one object.",
|
||||
"skip_sending": "Sending …",
|
||||
"skip_success": "Objects will be skipped.",
|
||||
"fd_objects_hint": "Skip objects (optional):",
|
||||
"fd_objects_toggle": "Skip objects",
|
||||
"fd_slots_hint": "Assign GCode channel to AMS slot:",
|
||||
"fd_cancel": "Cancel",
|
||||
"fd_print": "▶ Print",
|
||||
"fd_no_slots_msg": "No loaded AMS slots.{br}Start print anyway?",
|
||||
"fd_slot": "Slot",
|
||||
"fd_no_matching_material": "No matching material",
|
||||
"fd_used": "USED",
|
||||
"add_printer": "Add printer",
|
||||
"apd_lbl_ip": "Printer IP",
|
||||
"apd_lbl_name": "Name (optional)",
|
||||
"apd_placeholder_name": "e.g. Kobra X Living Room",
|
||||
"apd_cancel": "Cancel",
|
||||
"apd_confirm": "Add",
|
||||
"apd_fetching": "Fetching data from printer…",
|
||||
"apd_success": "Printer added, bridge restarting…",
|
||||
"apd_err_ip": "Please enter an IP address",
|
||||
"printers_remove": "Remove printer",
|
||||
"printers_remove_confirm": "Remove printer \"{name}\"? The bridge will restart.",
|
||||
"printers_active": "● active",
|
||||
"printers_switch": "Switch →",
|
||||
"printers_current": "Current printer",
|
||||
"printers_loading": "Loading…",
|
||||
"printers_none": "No printers configured.",
|
||||
"printers_empty_hint": "No printer set up yet.",
|
||||
"nav_browser": "Browser",
|
||||
"panel_browser_title": "File Browser",
|
||||
"store_search_placeholder": "🔍 Search…",
|
||||
"store_empty": "No files uploaded yet.",
|
||||
"store_refresh": "↻ Refresh",
|
||||
"store_print": "▶ Print",
|
||||
"store_download": "⬇ Download",
|
||||
"store_delete_confirm": "Delete file?",
|
||||
"store_print_confirm": "Print file?",
|
||||
"store_web_verify_title": "Verify file",
|
||||
"store_web_verify_msg": "Please verify this file was made for Anycubic Kobra X.",
|
||||
"store_web_verify_confirm": "Confirm",
|
||||
"store_web_verify_abort": "Abort",
|
||||
"store_no_results": "No files found.",
|
||||
"store_never": "never printed",
|
||||
"store_estimate": "Estimate",
|
||||
"store_upload_label_prefix": "Drag GCode here or ",
|
||||
"store_upload_label_browse": "browse",
|
||||
"store_upload_busy": "⏳ Uploading…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ Only GCode files allowed (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "All",
|
||||
"sf_ok": "✓ Completed",
|
||||
"sf_err": "✗ Failed",
|
||||
"sf_new": "New",
|
||||
"ss_date": "↓ Date",
|
||||
"ss_name": "A–Z Name",
|
||||
"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"
|
||||
}
|
||||
296
web/translations/es.json
Normal file
296
web/translations/es.json
Normal file
@@ -0,0 +1,296 @@
|
||||
{
|
||||
"header_status_standby": "Listo",
|
||||
"header_status_printing": "Imprimiendo",
|
||||
"header_status_complete": "Completado",
|
||||
"header_status_error": "Error",
|
||||
"kobra_free": "Listo",
|
||||
"kobra_busy": "Ocupado",
|
||||
"kobra_printing": "Imprimiendo",
|
||||
"kobra_preheating": "Precalentando",
|
||||
"kobra_auto_leveling": "Autonivelado",
|
||||
"kobra_checking": "Comprobando",
|
||||
"kobra_updated": "Actualizando",
|
||||
"kobra_init": "Inicializando",
|
||||
"kobra_pausing": "Pausando...",
|
||||
"kobra_paused": "Pausado",
|
||||
"kobra_resuming": "Reanudando...",
|
||||
"kobra_resumed": "Reanudado",
|
||||
"kobra_stopping": "Deteniendo...",
|
||||
"kobra_stoped": "Detenido",
|
||||
"kobra_finished": "Finalizado",
|
||||
"kobra_failed": "Error",
|
||||
"kobra_canceled": "Cancelado",
|
||||
"kobra_offline": "Desconectada",
|
||||
"nav_dashboard": "Panel",
|
||||
"nav_print": "Impresión",
|
||||
"nav_temps": "Temperaturas",
|
||||
"nav_motion": "Movimiento",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "Luz / Ventilador",
|
||||
"nav_console": "Consola",
|
||||
"card_progress": "Progreso",
|
||||
"card_temps": "Temperaturas",
|
||||
"card_light_fan": "Ventilador",
|
||||
"card_speed": "Velocidad de impresión",
|
||||
"card_cam": "Cámara",
|
||||
"lbl_elapsed": "Transcurrido:",
|
||||
"lbl_remaining": "Restante:",
|
||||
"lbl_slicer_time": "Estimación del slicer:",
|
||||
"lbl_layers": "Capa",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silencioso",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"lbl_light": "💡 Luz",
|
||||
"lbl_feed": "Cargar",
|
||||
"lbl_unload": "Descargar",
|
||||
"card_ace_dry": "Secado ACE",
|
||||
"ace_dry_dryer": "Secador",
|
||||
"ace_dry_status_off": "Estado: Apagado",
|
||||
"ace_dry_status_on": "Estado: Activo",
|
||||
"ace_dry_status_remaining": "Restante",
|
||||
"ace_dry_humidity": "Humedad",
|
||||
"ace_dry_current_temp": "Temperatura",
|
||||
"ace_dry_chart": "Historial (Temp/Humedad)",
|
||||
"ace_dry_temp": "Temperatura (°C)",
|
||||
"ace_dry_duration": "Duración (min)",
|
||||
"ace_dry_start": "▶ Iniciar",
|
||||
"ace_dry_stop": "■ Parar",
|
||||
"ace_dry_auto_refill": "Relleno automático",
|
||||
"ace_dry_enable": "Activar secado",
|
||||
"ace_dry_temp_line": "Temperatura de secado",
|
||||
"ace_dry_time_line": "Tiempo de secado",
|
||||
"ace_dry_ui_pending": "(solo UI, backend después)",
|
||||
"ace_dry_dialog_title": "Ajustes de temp/tiempo del secador",
|
||||
"ace_dry_dialog_temp": "Temperatura (30-80°C)",
|
||||
"ace_dry_dialog_time": "Tiempo restante (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "Confirmar",
|
||||
"ace_dry_dialog_cancel": "Cancelar",
|
||||
"ace_dry_dialog_save_restart": "Guardar y reiniciar",
|
||||
"ace_dry_dialog_custom_name": "Nombre personalizado",
|
||||
"ace_dry_dialog_reset_default": "Restablecer valores predeterminados",
|
||||
"cam_placeholder": "📷 Cámara no iniciada",
|
||||
"cam_stream_unavailable": "Stream no disponible",
|
||||
"btn_cam_start": "▶ Cámara",
|
||||
"btn_cam_stop": "◼ Cámara",
|
||||
"btn_pause": "⏸ Pausa",
|
||||
"btn_resume": "▶ Reanudar",
|
||||
"btn_cancel": "✕ Detener",
|
||||
"label_nozzle": "Boquilla",
|
||||
"label_bed": "Cama",
|
||||
"label_fan": "🌀 Ventilador",
|
||||
"label_light": "💡 Luz",
|
||||
"label_on_off": "Encendido / Apagado",
|
||||
"label_speed": "Velocidad",
|
||||
"panel_print_title": "Control de impresión",
|
||||
"panel_print_btn_pause": "⏸ Pausa",
|
||||
"panel_print_btn_resume": "▶ Reanudar",
|
||||
"panel_print_btn_cancel": "✕ Cancelar",
|
||||
"panel_print_temps_live": "Temperaturas (en vivo)",
|
||||
"label_set": "Set",
|
||||
"label_off": "Apagado",
|
||||
"panel_temps_nozzle": "Boquilla",
|
||||
"panel_temps_bed": "Cama caliente",
|
||||
"panel_temps_chart": "Historial (últimas 60 lecturas)",
|
||||
"label_target_c": "Objetivo:",
|
||||
"panel_motion_xy": "Ejes XY",
|
||||
"panel_motion_z": "Eje Z",
|
||||
"label_step": "Tamaño del paso:",
|
||||
"btn_home_z": "Home Z",
|
||||
"btn_home_xy": "Home XY",
|
||||
"btn_home_all": "Home All",
|
||||
"btn_disable_motors": "Motores apagados",
|
||||
"panel_ams_title": "Filamento",
|
||||
"card_ams": "Filamento",
|
||||
"ams_no_data": "No se recibieron datos de AMS",
|
||||
"label_slot": "Ranura",
|
||||
"ams_empty": "Vacío",
|
||||
"panel_extras_light": "Luz",
|
||||
"panel_extras_fan": "Ventilador",
|
||||
"panel_extras_camera": "Cámara",
|
||||
"btn_cam_start2": "▶ Iniciar",
|
||||
"btn_cam_stop2": "◼ Detener",
|
||||
"panel_console_title": "Registro de eventos",
|
||||
"log_light_on": "Luz encendida",
|
||||
"log_light_off": "Luz apagada",
|
||||
"log_fan": "Ventilador →",
|
||||
"log_nozzle": "Boquilla →",
|
||||
"log_bed": "Cama →",
|
||||
"log_axis": "Eje",
|
||||
"log_home": "Home",
|
||||
"log_home_all": "Home All",
|
||||
"log_cam_start": "Cámara iniciada:",
|
||||
"log_cam_stop": "Cámara detenida",
|
||||
"log_poll_error": "Error de sondeo:",
|
||||
"log_error": "Error:",
|
||||
"confirm_cancel": "¿Realmente cancelar la impresión?",
|
||||
"settings_title": "Configuración",
|
||||
"settings_connection": "Conexión",
|
||||
"settings_print": "Ajustes de impresión",
|
||||
"settings_poll": "Intervalo de sondeo (segundos)",
|
||||
"nav_settings": "Ajustes",
|
||||
"settings_cat_display": "Apariencia",
|
||||
"settings_cat_filament": "Filamento",
|
||||
"settings_cat_language": "Idioma",
|
||||
"settings_cat_theme": "Alternar claro / oscuro",
|
||||
"settings_filament_mapping": "Asignación de perfil de filamento (por ranura)",
|
||||
"settings_filament_mapping_save": "Guardar asignación",
|
||||
"settings_visible_vendors": "Fabricantes visibles (lista de perfiles)",
|
||||
"settings_visible_vendors_hint": "Solo estos fabricantes aparecen en la lista de perfiles de ranura. Nada seleccionado = mostrar todos. «Generic» y tus propios perfiles siempre son visibles.",
|
||||
"settings_visible_vendors_save": "Guardar selección",
|
||||
"progress_action_print": "Imprimir",
|
||||
"progress_action_slots": "Asignar ranuras",
|
||||
"progress_action_clear": "Vaciar",
|
||||
"settings_version": "Versión",
|
||||
"settings_save": "Guardar y reiniciar",
|
||||
"settings_printer_name": "Nombre de impresora",
|
||||
"settings_printer_ip": "IP de impresora",
|
||||
"settings_mqtt_port": "MQTT Port",
|
||||
"settings_username": "Usuario MQTT",
|
||||
"settings_password": "Contraseña MQTT",
|
||||
"settings_device_id": "ID del dispositivo",
|
||||
"settings_mode_id": "ID de modo",
|
||||
"hint_ip_no_port": "Solo dirección IP, sin puerto (p. ej. 192.168.1.102)",
|
||||
"settings_default_slot": "Ranura predeterminada (un color)",
|
||||
"settings_slot_auto": "Auto (todos los slots cargados)",
|
||||
"settings_auto_leveling": "Autonivelado antes de imprimir",
|
||||
"settings_camera_on_print": "Encender cámara al iniciar impresión",
|
||||
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
|
||||
"update_check": "Buscar actualizaciones",
|
||||
"update_checking": "Comprobando...",
|
||||
"update_available": "disponible",
|
||||
"update_none": "Ya actualizado",
|
||||
"update_apply": "Instalar ahora",
|
||||
"update_applying": "Descargando...",
|
||||
"update_restarting": "Reiniciando...",
|
||||
"update_error": "Error",
|
||||
"btn_connect": "⚡ Conectar",
|
||||
"btn_disconnect": "✕ Desconectar",
|
||||
"lbl_conn_error": "Error de conexión:",
|
||||
"slot_edit_title": "Editar slot",
|
||||
"slot_edit_color": "Color",
|
||||
"slot_edit_material": "Material",
|
||||
"slot_edit_load": "⬇ Cargar",
|
||||
"slot_edit_unload": "⬆ Descargar",
|
||||
"slot_edit_save": "💾 Guardar",
|
||||
"slot_edit_custom": "p. ej. PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "Ranura AMS",
|
||||
"slot_edit_profile": "Perfil de OrcaSlicer",
|
||||
"slot_edit_profile_hint": "Envía al sincronizar con OrcaSlicer la marca concreta en lugar de solo \"Generic\"",
|
||||
"slot_edit_profile_default": "— Genérico (Predeterminado) —",
|
||||
"orca_profile_section": "Perfiles de OrcaSlicer",
|
||||
"orca_profile_hint": "Importa tus propios perfiles de filamento de OrcaSlicer (abre el directorio del usuario vía Help → Show Configuration Folder)",
|
||||
"orca_profile_import_btn": "Importar perfiles",
|
||||
"orca_profile_import_link": "★ Importar perfiles propios…",
|
||||
"orca_profile_import_title": "Importar tus perfiles de OrcaSlicer",
|
||||
"orca_profile_help_html": "Sube un <b>ZIP</b> de tu carpeta de filamentos de OrcaSlicer o archivos <b>.json</b> sueltos.<br>En OrcaSlicer: <i>Help → Show Configuration Folder → user/<id>/filament/</i>",
|
||||
"orca_profile_dropmsg": "Suelta aquí o haz clic",
|
||||
"orca_profile_list_label": "Actualmente importados",
|
||||
"orca_profile_user_label": "Perfiles propios",
|
||||
"orca_profile_user_empty": "– ninguno –",
|
||||
"orca_profile_uploading": "Subiendo…",
|
||||
"orca_profile_done": "Importado",
|
||||
"orca_profile_skipped": "omitido",
|
||||
"log_dir_all": "Todos",
|
||||
"log_lvl_label": "Nivel:",
|
||||
"file_ready_btn": "▶ Iniciar impresión",
|
||||
"file_slots_btn": "🎨 Seleccionar ranuras",
|
||||
"file_cancel_btn": "✕ Cancelar",
|
||||
"nav_printers": "Impresoras",
|
||||
"skip_title": "✂ Omitir objetos",
|
||||
"skip_hint": "Deselecciona los objetos que ya no quieras imprimir:",
|
||||
"skip_btn_label": "Objetos",
|
||||
"skip_no_objects": "No hay objetos en esta impresión.",
|
||||
"skip_already": "omitido",
|
||||
"skip_select_at_least_one": "Selecciona al menos un objeto.",
|
||||
"skip_sending": "Enviando …",
|
||||
"skip_success": "Se omitirán los objetos.",
|
||||
"fd_objects_hint": "Omitir objetos (opcional):",
|
||||
"fd_objects_toggle": "Omitir objetos",
|
||||
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
|
||||
"fd_cancel": "Cancelar",
|
||||
"fd_print": "▶ Imprimir",
|
||||
"fd_no_slots_msg": "No hay slots AMS cargados.{br}¿Iniciar impresión de todos modos?",
|
||||
"fd_slot": "Ranura",
|
||||
"fd_no_matching_material": "No hay material compatible",
|
||||
"fd_used": "USADO",
|
||||
"add_printer": "Añadir impresora",
|
||||
"apd_lbl_ip": "IP de impresora",
|
||||
"apd_lbl_name": "Nombre (opcional)",
|
||||
"apd_placeholder_name": "p. ej. Kobra X Sala",
|
||||
"apd_cancel": "Cancelar",
|
||||
"apd_confirm": "Añadir",
|
||||
"apd_fetching": "Obteniendo datos de la impresora…",
|
||||
"apd_success": "Impresora añadida, reiniciando bridge…",
|
||||
"apd_err_ip": "Introduce una dirección IP",
|
||||
"printers_remove": "Eliminar impresora",
|
||||
"printers_remove_confirm": "¿Eliminar impresora \"{name}\"? El bridge se reiniciará.",
|
||||
"printers_active": "● activa",
|
||||
"printers_switch": "Cambiar →",
|
||||
"printers_current": "Impresora actual",
|
||||
"printers_loading": "Cargando…",
|
||||
"printers_none": "No hay impresoras configuradas.",
|
||||
"printers_empty_hint": "Aún no hay impresora configurada.",
|
||||
"nav_browser": "Explorador",
|
||||
"panel_browser_title": "Explorador de archivos",
|
||||
"store_search_placeholder": "🔍 Buscar…",
|
||||
"store_empty": "Aún no hay archivos subidos.",
|
||||
"store_refresh": "↻ Actualizar",
|
||||
"store_print": "▶ Imprimir",
|
||||
"store_download": "⬇ Descargar",
|
||||
"store_delete_confirm": "¿Eliminar archivo?",
|
||||
"store_print_confirm": "¿Imprimir archivo?",
|
||||
"store_web_verify_title": "Verificar archivo",
|
||||
"store_web_verify_msg": "Verifica que este archivo fue creado para Anycubic Kobra X.",
|
||||
"store_web_verify_confirm": "Confirmar",
|
||||
"store_web_verify_abort": "Abortar",
|
||||
"store_no_results": "No se encontraron archivos.",
|
||||
"store_never": "nunca impreso",
|
||||
"store_estimate": "Estimación",
|
||||
"store_upload_label_prefix": "Arrastra el GCode aquí o ",
|
||||
"store_upload_label_browse": "buscar",
|
||||
"store_upload_busy": "⏳ Subiendo…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ Solo se permiten archivos GCode (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "Todos",
|
||||
"sf_ok": "✓ Completado",
|
||||
"sf_err": "✗ Fallido",
|
||||
"sf_new": "Nuevo",
|
||||
"ss_date": "↓ Fecha",
|
||||
"ss_name": "A–Z Nombre",
|
||||
"ss_dur": "⏱ Tiempo de impresión",
|
||||
"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": "Personalizado",
|
||||
"fd_options_title": "Opciones",
|
||||
"print_auto_leveling": "Autonivelado para esta impresión",
|
||||
"settings_file_ready_mode": "Iniciar diálogo de impresión",
|
||||
"settings_file_ready_banner": "Barra de impresión",
|
||||
"settings_file_ready_dialog": "Diálogo de impresión",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Dirección:",
|
||||
"log_lvl_err": "⛔ Errores",
|
||||
"log_lvl_warn": "⚠ Avisos",
|
||||
"log_topic_label": "Tema:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Impresión",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Estado",
|
||||
"log_download": "⬇ Descargar",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Limpiar",
|
||||
"log_filter_placeholder": "Filtrar…",
|
||||
"skip_cancel": "Cancelar",
|
||||
"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"
|
||||
}
|
||||
296
web/translations/fr.json
Normal file
296
web/translations/fr.json
Normal file
@@ -0,0 +1,296 @@
|
||||
{
|
||||
"header_status_standby": "Prêt",
|
||||
"header_status_printing": "Impression",
|
||||
"header_status_complete": "Terminé",
|
||||
"header_status_error": "Erreur",
|
||||
"kobra_free": "Disponible",
|
||||
"kobra_busy": "Occupé",
|
||||
"kobra_printing": "Impression",
|
||||
"kobra_preheating": "Préchauffage",
|
||||
"kobra_auto_leveling": "Mise à niveau auto",
|
||||
"kobra_checking": "Vérification",
|
||||
"kobra_updated": "Mise à jour",
|
||||
"kobra_init": "Initialisation",
|
||||
"kobra_pausing": "Pause en cours…",
|
||||
"kobra_paused": "En pause",
|
||||
"kobra_resuming": "Reprise en cours…",
|
||||
"kobra_resumed": "Repris",
|
||||
"kobra_stopping": "Arrêt en cours…",
|
||||
"kobra_stoped": "Arrêté",
|
||||
"kobra_finished": "Terminé",
|
||||
"kobra_failed": "Erreur",
|
||||
"kobra_canceled": "Annulé",
|
||||
"kobra_offline": "Hors ligne",
|
||||
"nav_dashboard": "Tableau de bord",
|
||||
"nav_print": "Impression",
|
||||
"nav_temps": "Températures",
|
||||
"nav_motion": "Mouvement",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "Lumière / Ventilateur",
|
||||
"nav_console": "Console",
|
||||
"card_progress": "Progression",
|
||||
"card_temps": "Températures",
|
||||
"card_light_fan": "Ventilateur",
|
||||
"card_speed": "Vitesse d'impression",
|
||||
"card_cam": "Caméra",
|
||||
"lbl_elapsed": "Écoulé :",
|
||||
"lbl_remaining": "Restant :",
|
||||
"lbl_slicer_time": "Estimation slicer :",
|
||||
"lbl_layers": "Couche",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silencieux",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"lbl_light": "💡 Lumière",
|
||||
"lbl_feed": "Charger",
|
||||
"lbl_unload": "Décharger",
|
||||
"card_ace_dry": "Séchage ACE",
|
||||
"ace_dry_dryer": "Séchoir",
|
||||
"ace_dry_status_off": "Statut : Arrêté",
|
||||
"ace_dry_status_on": "Statut : Actif",
|
||||
"ace_dry_status_remaining": "Restant",
|
||||
"ace_dry_humidity": "Humidité",
|
||||
"ace_dry_current_temp": "Température",
|
||||
"ace_dry_chart": "Historique (Temp/Humidité)",
|
||||
"ace_dry_temp": "Température (°C)",
|
||||
"ace_dry_duration": "Durée (min)",
|
||||
"ace_dry_start": "▶ Démarrer",
|
||||
"ace_dry_stop": "■ Arrêter",
|
||||
"ace_dry_auto_refill": "Remplissage auto",
|
||||
"ace_dry_enable": "Activer le séchage",
|
||||
"ace_dry_temp_line": "Température de séchage",
|
||||
"ace_dry_time_line": "Durée de séchage",
|
||||
"ace_dry_ui_pending": "(Interface seule, backend suivant)",
|
||||
"ace_dry_dialog_title": "Réglages Temp/Durée du séchoir",
|
||||
"ace_dry_dialog_temp": "Température (30-80°C)",
|
||||
"ace_dry_dialog_time": "Temps restant (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "Confirmer",
|
||||
"ace_dry_dialog_cancel": "Annuler",
|
||||
"ace_dry_dialog_save_restart": "Enregistrer et redémarrer",
|
||||
"ace_dry_dialog_custom_name": "Nom personnalisé",
|
||||
"ace_dry_dialog_reset_default": "Réinitialiser",
|
||||
"cam_placeholder": "📷 Caméra non démarrée",
|
||||
"cam_stream_unavailable": "Flux indisponible",
|
||||
"btn_cam_start": "▶ Caméra",
|
||||
"btn_cam_stop": "◼ Caméra",
|
||||
"btn_pause": "⏸ Pause",
|
||||
"btn_resume": "▶ Reprendre",
|
||||
"btn_cancel": "✕ Arrêter",
|
||||
"label_nozzle": "Buse",
|
||||
"label_bed": "Plateau",
|
||||
"label_fan": "🌀 Ventilateur",
|
||||
"label_light": "💡 Lumière",
|
||||
"label_on_off": "On / Off",
|
||||
"label_speed": "Vitesse",
|
||||
"panel_print_title": "Contrôle impression",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Reprendre",
|
||||
"panel_print_btn_cancel": "✕ Annuler",
|
||||
"panel_print_temps_live": "Températures (en direct)",
|
||||
"label_set": "Définir",
|
||||
"label_off": "Éteint",
|
||||
"panel_temps_nozzle": "Buse",
|
||||
"panel_temps_bed": "Plateau chauffant",
|
||||
"panel_temps_chart": "Historique (60 dernières valeurs)",
|
||||
"label_target_c": "Cible :",
|
||||
"panel_motion_xy": "Axes XY",
|
||||
"panel_motion_z": "Axe Z",
|
||||
"label_step": "Pas :",
|
||||
"btn_home_z": "Origine Z",
|
||||
"btn_home_xy": "Origine XY",
|
||||
"btn_home_all": "Origine Tout",
|
||||
"btn_disable_motors": "Moteurs Off",
|
||||
"panel_ams_title": "Filament",
|
||||
"card_ams": "Filament",
|
||||
"ams_no_data": "Aucune donnée AMS reçue",
|
||||
"label_slot": "Slot",
|
||||
"ams_empty": "Vide",
|
||||
"panel_extras_light": "Lumière",
|
||||
"panel_extras_fan": "Ventilateur",
|
||||
"panel_extras_camera": "Caméra",
|
||||
"btn_cam_start2": "▶ Démarrer",
|
||||
"btn_cam_stop2": "◼ Arrêter",
|
||||
"panel_console_title": "Journal d'événements",
|
||||
"log_light_on": "Lumière allumée",
|
||||
"log_light_off": "Lumière éteinte",
|
||||
"log_fan": "Ventilateur →",
|
||||
"log_nozzle": "Buse →",
|
||||
"log_bed": "Plateau →",
|
||||
"log_axis": "Axe",
|
||||
"log_home": "Origine",
|
||||
"log_home_all": "Origine Tout",
|
||||
"log_cam_start": "Caméra démarrée :",
|
||||
"log_cam_stop": "Caméra arrêtée",
|
||||
"log_poll_error": "Erreur de sondage :",
|
||||
"log_error": "Erreur :",
|
||||
"confirm_cancel": "Vraiment annuler l'impression ?",
|
||||
"settings_title": "Paramètres",
|
||||
"settings_connection": "Connexion",
|
||||
"settings_print": "Paramètres d'impression",
|
||||
"settings_poll": "Intervalle de sondage (secondes)",
|
||||
"nav_settings": "Paramètres",
|
||||
"settings_cat_display": "Apparence",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Langue",
|
||||
"settings_cat_theme": "Basculer clair / sombre",
|
||||
"settings_filament_mapping": "Mappage du profil de filament (par emplacement)",
|
||||
"settings_filament_mapping_save": "Enregistrer le mappage",
|
||||
"settings_visible_vendors": "Fabricants visibles (liste des profils)",
|
||||
"settings_visible_vendors_hint": "Seuls ces fabricants apparaissent dans la liste des profils d'emplacement. Rien de sélectionné = tout afficher. « Generic » et vos propres profils sont toujours visibles.",
|
||||
"settings_visible_vendors_save": "Enregistrer la sélection",
|
||||
"progress_action_print": "Imprimer",
|
||||
"progress_action_slots": "Affecter les emplacements",
|
||||
"progress_action_clear": "Vider",
|
||||
"settings_version": "Version",
|
||||
"settings_save": "Enregistrer et redémarrer",
|
||||
"settings_printer_name": "Nom de l'imprimante",
|
||||
"settings_printer_ip": "IP de l'imprimante",
|
||||
"settings_mqtt_port": "Port MQTT",
|
||||
"settings_username": "Nom d'utilisateur MQTT",
|
||||
"settings_password": "Mot de passe MQTT",
|
||||
"settings_device_id": "ID de l'appareil",
|
||||
"settings_mode_id": "ID du mode",
|
||||
"hint_ip_no_port": "Adresse IP uniquement, sans port (ex. 192.168.1.102)",
|
||||
"settings_default_slot": "Slot par défaut (couleur unique)",
|
||||
"settings_slot_auto": "Auto (tous les slots chargés)",
|
||||
"settings_auto_leveling": "Mise à niveau auto avant impression",
|
||||
"settings_camera_on_print": "Activer la caméra au démarrage de l'impression",
|
||||
"settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
|
||||
"update_check": "Vérifier les mises à jour",
|
||||
"update_checking": "Vérification…",
|
||||
"update_available": "disponible",
|
||||
"update_none": "Déjà à jour",
|
||||
"update_apply": "Installer maintenant",
|
||||
"update_applying": "Téléchargement…",
|
||||
"update_restarting": "Redémarrage…",
|
||||
"update_error": "Erreur",
|
||||
"btn_connect": "⚡ Connecter",
|
||||
"btn_disconnect": "✕ Déconnecter",
|
||||
"lbl_conn_error": "Erreur de connexion :",
|
||||
"slot_edit_title": "Modifier le slot",
|
||||
"slot_edit_color": "Couleur",
|
||||
"slot_edit_material": "Matériau",
|
||||
"slot_edit_load": "⬇ Charger",
|
||||
"slot_edit_unload": "⬆ Décharger",
|
||||
"slot_edit_save": "💾 Enregistrer",
|
||||
"slot_edit_custom": "ex. PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "Slot AMS",
|
||||
"slot_edit_profile": "Profil OrcaSlicer",
|
||||
"slot_edit_profile_hint": "Envoyé lors de la synchronisation OrcaSlicer comme marque spécifique au lieu de \"Générique\"",
|
||||
"slot_edit_profile_default": "— Générique (défaut) —",
|
||||
"orca_profile_section": "Profils OrcaSlicer",
|
||||
"orca_profile_hint": "Importez vos propres profils de filament OrcaSlicer (ouvrez le dossier utilisateur via Aide → Afficher le dossier de configuration)",
|
||||
"orca_profile_import_btn": "Importer des profils",
|
||||
"orca_profile_import_link": "★ Importer mes profils…",
|
||||
"orca_profile_import_title": "Importer vos profils OrcaSlicer",
|
||||
"orca_profile_help_html": "Déposez un <b>ZIP</b> de votre dossier filament OrcaSlicer ou des fichiers <b>.json</b> individuels.<br>Dans OrcaSlicer : <i>Aide → Afficher le dossier de configuration → user/<id>/filament/</i>",
|
||||
"orca_profile_dropmsg": "Déposez ici ou cliquez",
|
||||
"orca_profile_list_label": "Profils importés",
|
||||
"orca_profile_user_label": "Mes profils",
|
||||
"orca_profile_user_empty": "– aucun –",
|
||||
"orca_profile_uploading": "Envoi en cours…",
|
||||
"orca_profile_done": "Importé",
|
||||
"orca_profile_skipped": "ignoré",
|
||||
"log_dir_all": "Tout",
|
||||
"log_lvl_label": "Niveau :",
|
||||
"file_ready_btn": "▶ Lancer l'impression",
|
||||
"file_slots_btn": "🎨 Choisir les slots",
|
||||
"file_cancel_btn": "✕ Annuler",
|
||||
"nav_printers": "Imprimantes",
|
||||
"skip_title": "✂ Ignorer des objets",
|
||||
"skip_hint": "Décochez les objets que vous ne souhaitez plus imprimer :",
|
||||
"skip_btn_label": "Objets",
|
||||
"skip_no_objects": "Aucun objet dans cette impression.",
|
||||
"skip_already": "ignoré",
|
||||
"skip_select_at_least_one": "Veuillez sélectionner au moins un objet.",
|
||||
"skip_sending": "Envoi …",
|
||||
"skip_success": "Les objets seront ignorés.",
|
||||
"fd_objects_hint": "Ignorer des objets (optionnel) :",
|
||||
"fd_objects_toggle": "Ignorer des objets",
|
||||
"fd_slots_hint": "Associer le canal GCode au slot AMS :",
|
||||
"fd_cancel": "Annuler",
|
||||
"fd_print": "▶ Imprimer",
|
||||
"fd_no_slots_msg": "Aucun slot AMS chargé.{br}Lancer l'impression quand même ?",
|
||||
"fd_slot": "Slot",
|
||||
"fd_no_matching_material": "Aucun matériau correspondant",
|
||||
"fd_used": "UTILISÉ",
|
||||
"add_printer": "Ajouter une imprimante",
|
||||
"apd_lbl_ip": "IP de l'imprimante",
|
||||
"apd_lbl_name": "Nom (optionnel)",
|
||||
"apd_placeholder_name": "ex. Kobra X Salon",
|
||||
"apd_cancel": "Annuler",
|
||||
"apd_confirm": "Ajouter",
|
||||
"apd_fetching": "Récupération des données de l'imprimante…",
|
||||
"apd_success": "Imprimante ajoutée, redémarrage du bridge…",
|
||||
"apd_err_ip": "Veuillez saisir une adresse IP",
|
||||
"printers_remove": "Supprimer l'imprimante",
|
||||
"printers_remove_confirm": "Supprimer l'imprimante \"{name}\" ? Le bridge va redémarrer.",
|
||||
"printers_active": "● actif",
|
||||
"printers_switch": "Changer →",
|
||||
"printers_current": "Imprimante actuelle",
|
||||
"printers_loading": "Chargement…",
|
||||
"printers_none": "Aucune imprimante configurée.",
|
||||
"printers_empty_hint": "Aucune imprimante configurée.",
|
||||
"nav_browser": "Navigateur",
|
||||
"panel_browser_title": "Explorateur de fichiers",
|
||||
"store_search_placeholder": "🔍 Rechercher…",
|
||||
"store_empty": "Aucun fichier uploadé.",
|
||||
"store_refresh": "↻ Actualiser",
|
||||
"store_print": "▶ Imprimer",
|
||||
"store_download": "⬇ Télécharger",
|
||||
"store_delete_confirm": "Supprimer le fichier ?",
|
||||
"store_print_confirm": "Imprimer le fichier ?",
|
||||
"store_web_verify_title": "Vérifier le fichier",
|
||||
"store_web_verify_msg": "Veuillez vérifier que ce fichier a été créé pour l'Anycubic Kobra X.",
|
||||
"store_web_verify_confirm": "Confirmer",
|
||||
"store_web_verify_abort": "Annuler",
|
||||
"store_no_results": "Aucun fichier trouvé.",
|
||||
"store_never": "jamais imprimé",
|
||||
"store_estimate": "Estimation",
|
||||
"store_upload_label_prefix": "Déposez un GCode ici ou ",
|
||||
"store_upload_label_browse": "parcourir",
|
||||
"store_upload_busy": "⏳ Envoi en cours…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ Seuls les fichiers GCode sont autorisés (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "Tout",
|
||||
"sf_ok": "✓ Terminés",
|
||||
"sf_err": "✗ Échoués",
|
||||
"sf_new": "Nouveau",
|
||||
"ss_date": "↓ Date",
|
||||
"ss_name": "A–Z Nom",
|
||||
"ss_dur": "⏱ Durée d'impression",
|
||||
"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": "Personnalisé",
|
||||
"fd_options_title": "Options",
|
||||
"print_auto_leveling": "Mise à niveau auto pour cette impression",
|
||||
"settings_file_ready_mode": "Démarrer le dialogue d'impression",
|
||||
"settings_file_ready_banner": "Barre d'impression",
|
||||
"settings_file_ready_dialog": "Dialogue d'impression",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Sens :",
|
||||
"log_lvl_err": "⛔ Erreurs",
|
||||
"log_lvl_warn": "⚠ Avert.",
|
||||
"log_topic_label": "Sujet :",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Impression",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Statut",
|
||||
"log_download": "⬇ Télécharger",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Effacer",
|
||||
"log_filter_placeholder": "Filtrer…",
|
||||
"skip_cancel": "Annuler",
|
||||
"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"
|
||||
}
|
||||
296
web/translations/zh-cn.json
Normal file
296
web/translations/zh-cn.json
Normal file
@@ -0,0 +1,296 @@
|
||||
{
|
||||
"header_status_standby": "就绪",
|
||||
"header_status_printing": "打印中",
|
||||
"header_status_complete": "完成",
|
||||
"header_status_error": "错误",
|
||||
"kobra_free": "就绪",
|
||||
"kobra_busy": "忙碌",
|
||||
"kobra_printing": "打印中",
|
||||
"kobra_preheating": "预热中",
|
||||
"kobra_auto_leveling": "自动调平",
|
||||
"kobra_checking": "检查中",
|
||||
"kobra_updated": "更新中",
|
||||
"kobra_init": "初始化中",
|
||||
"kobra_pausing": "暂停中...",
|
||||
"kobra_paused": "已暂停",
|
||||
"kobra_resuming": "恢复中...",
|
||||
"kobra_resumed": "已恢复",
|
||||
"kobra_stopping": "停止中...",
|
||||
"kobra_stoped": "已停止",
|
||||
"kobra_finished": "已完成",
|
||||
"kobra_failed": "错误",
|
||||
"kobra_canceled": "已取消",
|
||||
"kobra_offline": "离线",
|
||||
"nav_dashboard": "仪表盘",
|
||||
"nav_print": "打印",
|
||||
"nav_temps": "温度",
|
||||
"nav_motion": "运动",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "灯光 / 风扇",
|
||||
"nav_console": "控制台",
|
||||
"card_progress": "进度",
|
||||
"card_temps": "温度",
|
||||
"card_light_fan": "风扇",
|
||||
"card_speed": "打印速度",
|
||||
"card_cam": "相机",
|
||||
"lbl_elapsed": "已用时间:",
|
||||
"lbl_remaining": "剩余时间:",
|
||||
"lbl_slicer_time": "切片预估:",
|
||||
"lbl_layers": "层",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 静音",
|
||||
"speed_normal": "⚡ 标准",
|
||||
"speed_sport": "🚀 运动",
|
||||
"lbl_light": "💡 灯光",
|
||||
"lbl_feed": "进料",
|
||||
"lbl_unload": "退料",
|
||||
"card_ace_dry": "ACE 烘干",
|
||||
"ace_dry_dryer": "烘干机",
|
||||
"ace_dry_status_off": "状态: 关闭",
|
||||
"ace_dry_status_on": "状态: 运行中",
|
||||
"ace_dry_status_remaining": "剩余",
|
||||
"ace_dry_humidity": "湿度",
|
||||
"ace_dry_current_temp": "温度",
|
||||
"ace_dry_chart": "历史 (温度/湿度)",
|
||||
"ace_dry_temp": "温度 (°C)",
|
||||
"ace_dry_duration": "时长 (分钟)",
|
||||
"ace_dry_start": "▶ 启动",
|
||||
"ace_dry_stop": "■ 停止",
|
||||
"ace_dry_auto_refill": "自动补料",
|
||||
"ace_dry_enable": "启用烘干",
|
||||
"ace_dry_temp_line": "烘干温度",
|
||||
"ace_dry_time_line": "烘干时间",
|
||||
"ace_dry_ui_pending": "(仅 UI,后端稍后支持)",
|
||||
"ace_dry_dialog_title": "烘干温度/时间设置",
|
||||
"ace_dry_dialog_temp": "温度 (30-80°C)",
|
||||
"ace_dry_dialog_time": "剩余时间 (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "确认",
|
||||
"ace_dry_dialog_cancel": "取消",
|
||||
"ace_dry_dialog_save_restart": "保存并重启",
|
||||
"ace_dry_dialog_custom_name": "自定义名称",
|
||||
"ace_dry_dialog_reset_default": "恢复默认",
|
||||
"cam_placeholder": "📷 相机未启动",
|
||||
"cam_stream_unavailable": "视频流不可用",
|
||||
"btn_cam_start": "▶ 相机",
|
||||
"btn_cam_stop": "◼ 相机",
|
||||
"btn_pause": "⏸ 暂停",
|
||||
"btn_resume": "▶ 继续",
|
||||
"btn_cancel": "✕ 停止",
|
||||
"label_nozzle": "喷嘴",
|
||||
"label_bed": "热床",
|
||||
"label_fan": "🌀 风扇",
|
||||
"label_light": "💡 灯光",
|
||||
"label_on_off": "开 / 关",
|
||||
"label_speed": "速度",
|
||||
"panel_print_title": "打印控制",
|
||||
"panel_print_btn_pause": "⏸ 暂停",
|
||||
"panel_print_btn_resume": "▶ 继续",
|
||||
"panel_print_btn_cancel": "✕ 取消",
|
||||
"panel_print_temps_live": "温度 (实时)",
|
||||
"label_set": "设置",
|
||||
"label_off": "关闭",
|
||||
"panel_temps_nozzle": "喷嘴",
|
||||
"panel_temps_bed": "热床",
|
||||
"panel_temps_chart": "历史 (最近 60 次读数)",
|
||||
"label_target_c": "目标:",
|
||||
"panel_motion_xy": "XY 轴",
|
||||
"panel_motion_z": "Z 轴",
|
||||
"label_step": "步进:",
|
||||
"btn_home_z": "回零 Z",
|
||||
"btn_home_xy": "回零 XY",
|
||||
"btn_home_all": "全部回零",
|
||||
"btn_disable_motors": "关闭电机",
|
||||
"panel_ams_title": "耗材",
|
||||
"card_ams": "耗材",
|
||||
"ams_no_data": "未收到 AMS 数据",
|
||||
"label_slot": "槽位",
|
||||
"ams_empty": "空",
|
||||
"panel_extras_light": "灯光",
|
||||
"panel_extras_fan": "风扇",
|
||||
"panel_extras_camera": "相机",
|
||||
"btn_cam_start2": "▶ 启动",
|
||||
"btn_cam_stop2": "◼ 停止",
|
||||
"panel_console_title": "事件日志",
|
||||
"log_light_on": "灯光已开",
|
||||
"log_light_off": "灯光已关",
|
||||
"log_fan": "风扇 →",
|
||||
"log_nozzle": "喷嘴 →",
|
||||
"log_bed": "热床 →",
|
||||
"log_axis": "轴",
|
||||
"log_home": "回零",
|
||||
"log_home_all": "全部回零",
|
||||
"log_cam_start": "相机已启动:",
|
||||
"log_cam_stop": "相机已停止",
|
||||
"log_poll_error": "轮询错误:",
|
||||
"log_error": "错误:",
|
||||
"confirm_cancel": "确定要取消打印吗?",
|
||||
"settings_title": "设置",
|
||||
"settings_connection": "连接",
|
||||
"settings_print": "打印设置",
|
||||
"settings_poll": "轮询间隔(秒)",
|
||||
"nav_settings": "设置",
|
||||
"settings_cat_display": "外观",
|
||||
"settings_cat_filament": "耗材",
|
||||
"settings_cat_language": "语言",
|
||||
"settings_cat_theme": "切换浅色 / 深色",
|
||||
"settings_filament_mapping": "耗材配置映射(每槽位)",
|
||||
"settings_filament_mapping_save": "保存映射",
|
||||
"settings_visible_vendors": "可见厂商(配置下拉框)",
|
||||
"settings_visible_vendors_hint": "仅这些厂商会出现在槽位配置下拉框中。未选择 = 显示全部。“Generic”和您自己的配置始终可见。",
|
||||
"settings_visible_vendors_save": "保存选择",
|
||||
"progress_action_print": "打印",
|
||||
"progress_action_slots": "分配槽位",
|
||||
"progress_action_clear": "清除",
|
||||
"settings_version": "版本",
|
||||
"settings_save": "保存并重启",
|
||||
"settings_printer_name": "打印机名称",
|
||||
"settings_printer_ip": "打印机 IP",
|
||||
"settings_mqtt_port": "MQTT 端口",
|
||||
"settings_username": "MQTT 用户名",
|
||||
"settings_password": "MQTT 密码",
|
||||
"settings_device_id": "设备 ID",
|
||||
"settings_mode_id": "模式 ID",
|
||||
"hint_ip_no_port": "仅填写 IP,不要端口 (例如 192.168.1.102)",
|
||||
"settings_default_slot": "默认槽位 (单色)",
|
||||
"settings_slot_auto": "自动 (所有已装载槽位)",
|
||||
"settings_auto_leveling": "打印前自动调平",
|
||||
"settings_camera_on_print": "打印开始时开启相机",
|
||||
"settings_web_upload_warning": "打印网页上传文件时显示警告",
|
||||
"update_check": "检查更新",
|
||||
"update_checking": "检查中...",
|
||||
"update_available": "可用",
|
||||
"update_none": "已是最新版本",
|
||||
"update_apply": "立即安装",
|
||||
"update_applying": "下载中...",
|
||||
"update_restarting": "重启中...",
|
||||
"update_error": "错误",
|
||||
"btn_connect": "⚡ 连接",
|
||||
"btn_disconnect": "✕ 断开",
|
||||
"lbl_conn_error": "连接错误:",
|
||||
"slot_edit_title": "编辑槽位",
|
||||
"slot_edit_color": "颜色",
|
||||
"slot_edit_material": "材料",
|
||||
"slot_edit_load": "⬇ 进料",
|
||||
"slot_edit_unload": "⬆ 退料",
|
||||
"slot_edit_save": "💾 保存",
|
||||
"slot_edit_custom": "例如 PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "AMS 槽位",
|
||||
"slot_edit_profile": "OrcaSlicer 配置",
|
||||
"slot_edit_profile_hint": "在 OrcaSlicer 同步时发送具体品牌,而不仅仅是“Generic”",
|
||||
"slot_edit_profile_default": "— 通用 (默认) —",
|
||||
"orca_profile_section": "OrcaSlicer 配置",
|
||||
"orca_profile_hint": "导入你自己的 OrcaSlicer 耗材配置(在 Help → Show Configuration Folder 打开用户目录)",
|
||||
"orca_profile_import_btn": "导入配置",
|
||||
"orca_profile_import_link": "★ 导入自己的配置…",
|
||||
"orca_profile_import_title": "导入你的 OrcaSlicer 配置",
|
||||
"orca_profile_help_html": "上传 OrcaSlicer 耗材文件夹的 <b>ZIP</b> 或单个 <b>.json</b> 文件。<br>在 OrcaSlicer 中: <i>Help → Show Configuration Folder → user/<id>/filament/</i>",
|
||||
"orca_profile_dropmsg": "拖到此处或点击",
|
||||
"orca_profile_list_label": "已导入",
|
||||
"orca_profile_user_label": "自己的配置",
|
||||
"orca_profile_user_empty": "– 无 –",
|
||||
"orca_profile_uploading": "上传中…",
|
||||
"orca_profile_done": "已导入",
|
||||
"orca_profile_skipped": "跳过",
|
||||
"log_dir_all": "全部",
|
||||
"log_lvl_label": "级别:",
|
||||
"file_ready_btn": "▶ 开始打印",
|
||||
"file_slots_btn": "🎨 选择槽位",
|
||||
"file_cancel_btn": "✕ 取消",
|
||||
"nav_printers": "打印机",
|
||||
"skip_title": "✂ 跳过对象",
|
||||
"skip_hint": "取消勾选不想继续打印的对象:",
|
||||
"skip_btn_label": "对象",
|
||||
"skip_no_objects": "此打印任务没有对象。",
|
||||
"skip_already": "已跳过",
|
||||
"skip_select_at_least_one": "请至少选择一个对象。",
|
||||
"skip_sending": "发送中 …",
|
||||
"skip_success": "对象将被跳过。",
|
||||
"fd_objects_hint": "跳过对象 (可选):",
|
||||
"fd_objects_toggle": "跳过对象",
|
||||
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
|
||||
"fd_cancel": "取消",
|
||||
"fd_print": "▶ 打印",
|
||||
"fd_no_slots_msg": "没有已装载的 AMS 槽位。{br}仍要开始打印吗?",
|
||||
"fd_slot": "槽位",
|
||||
"fd_no_matching_material": "无匹配材料",
|
||||
"fd_used": "已用",
|
||||
"add_printer": "添加打印机",
|
||||
"apd_lbl_ip": "打印机 IP",
|
||||
"apd_lbl_name": "名称 (可选)",
|
||||
"apd_placeholder_name": "例如 Kobra X 客厅",
|
||||
"apd_cancel": "取消",
|
||||
"apd_confirm": "添加",
|
||||
"apd_fetching": "正在从打印机获取数据…",
|
||||
"apd_success": "打印机已添加,Bridge 正在重启…",
|
||||
"apd_err_ip": "请输入 IP 地址",
|
||||
"printers_remove": "移除打印机",
|
||||
"printers_remove_confirm": "移除打印机 \"{name}\"? Bridge 将重启。",
|
||||
"printers_active": "● 活动",
|
||||
"printers_switch": "切换 →",
|
||||
"printers_current": "当前打印机",
|
||||
"printers_loading": "加载中…",
|
||||
"printers_none": "未配置打印机。",
|
||||
"printers_empty_hint": "尚未设置打印机。",
|
||||
"nav_browser": "浏览器",
|
||||
"panel_browser_title": "文件浏览器",
|
||||
"store_search_placeholder": "🔍 搜索…",
|
||||
"store_empty": "尚未上传文件。",
|
||||
"store_refresh": "↻ 刷新",
|
||||
"store_print": "▶ 打印",
|
||||
"store_download": "⬇ 下载",
|
||||
"store_delete_confirm": "删除文件?",
|
||||
"store_print_confirm": "打印文件?",
|
||||
"store_web_verify_title": "验证文件",
|
||||
"store_web_verify_msg": "请确认此文件是为 Anycubic Kobra X 创建的。",
|
||||
"store_web_verify_confirm": "确认",
|
||||
"store_web_verify_abort": "取消",
|
||||
"store_no_results": "未找到文件。",
|
||||
"store_never": "从未打印",
|
||||
"store_estimate": "估算",
|
||||
"store_upload_label_prefix": "将 GCode 拖到这里或 ",
|
||||
"store_upload_label_browse": "浏览",
|
||||
"store_upload_busy": "⏳ 上传中…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ 仅允许 GCode 文件 (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "全部",
|
||||
"sf_ok": "✓ 已完成",
|
||||
"sf_err": "✗ 失败",
|
||||
"sf_new": "新",
|
||||
"ss_date": "↓ 日期",
|
||||
"ss_name": "A–Z 名称",
|
||||
"ss_dur": "⏱ 打印时间",
|
||||
"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": "自定义",
|
||||
"fd_options_title": "选项",
|
||||
"print_auto_leveling": "本次打印自动调平",
|
||||
"settings_file_ready_mode": "开始打印对话框",
|
||||
"settings_file_ready_banner": "打印栏",
|
||||
"settings_file_ready_dialog": "打印对话框",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "方向:",
|
||||
"log_lvl_err": "⛔ 错误",
|
||||
"log_lvl_warn": "⚠ 警告",
|
||||
"log_topic_label": "主题:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "打印",
|
||||
"log_topic_info": "信息",
|
||||
"log_topic_status": "状态",
|
||||
"log_download": "⬇ 下载",
|
||||
"log_auto": "⬇ 自动",
|
||||
"log_clear": "✕ 清空",
|
||||
"log_filter_placeholder": "筛选…",
|
||||
"skip_cancel": "取消",
|
||||
"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