Compare commits
90 Commits
v0.9.1-bet
...
nightly-20
| 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 | |||
| 312b4083d2 | |||
| 534ea41816 | |||
| f1bfab969c | |||
| 81729c37a5 | |||
| fe1ed4b096 | |||
| 33fffa0fc0 | |||
| d040475a62 | |||
| 9f6b6a8518 | |||
| 5eda8a241f | |||
| 2aaaa5bbbe | |||
| 374457fb07 | |||
| 618f1039c3 | |||
| e98a3706be | |||
| e8bd362d34 | |||
| 377a7a4984 | |||
| 9279036c51 | |||
| ce63cc5e7a | |||
| 5c83cc6df0 | |||
| be11217896 | |||
| 0292785fd8 | |||
| 50419fb487 | |||
| f196b8d29a | |||
| 1d3c5a7e1b | |||
| c22296d880 | |||
| d9d3581e22 | |||
| 966d421016 | |||
| 2a12ecca51 | |||
| ae4777187f | |||
| 8ccafb96c4 | |||
| 21f340271b | |||
| 2f56a1f056 | |||
| c3a62a13c5 | |||
| 4f1eaf7e93 | |||
| fc681316fc |
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
|
||||
}
|
||||
}
|
||||
@@ -21,3 +21,4 @@ DEVICE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Modell-ID (Kobra X Standard: 20030)
|
||||
MODE_ID=20030
|
||||
|
||||
|
||||
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
|
||||
}"
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -7,3 +7,18 @@ dist/
|
||||
releases/*/kx-bridge
|
||||
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
|
||||
|
||||
811
CHANGELOG.de.md
Normal file
811
CHANGELOG.de.md
Normal file
@@ -0,0 +1,811 @@
|
||||
# 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
|
||||
- **Theme-System (Community-Beitrag von @hirnwunde, PR #27):** Die Web-UI liegt
|
||||
jetzt in echten Dateien unter `web/themes/<name>/` (`index.html` + `style.css`
|
||||
+ `app.js`) statt im Python-Quelltext eingebettet. Theme umschalten mit
|
||||
`--ui-theme <name>`. Für Theme-Autoren gibt es eine dokumentierte Hook-Referenz
|
||||
(`web/DOC/THEME-CSS-HOOKS.md`, `THEME-JS-ID-HOOKS.md`). Das Default-Theme
|
||||
enthält die komplette aktuelle UI (ACE2, Objekte überspringen, Filament-Dialog).
|
||||
Für Nutzer keine Änderung — Binaries/Docker-Image liefern das Theme eingebettet.
|
||||
- **Neustart über API (Community-Beitrag von @gangoke, PR #28):** neuer Endpoint
|
||||
`POST /api/restart`, um die Bridge per API neu zu starten — z. B. für einen
|
||||
Neustart-Button in der Home-Assistant-Integration.
|
||||
|
||||
### Intern
|
||||
- Vereinheitlichter PyInstaller-Build (`kx-bridge.spec`) für Linux, Windows und
|
||||
Docker — bindet `web/` (Themes) ins Onefile-Binary ein, zur Laufzeit aus
|
||||
`sys._MEIPASS` gelesen. Theme-Einbettung in Linux-Binary und Windows-EXE verifiziert.
|
||||
- `data/` in `.gitignore` aufgenommen.
|
||||
|
||||
## [0.9.13] – 2026-05-20
|
||||
|
||||
============================================================
|
||||
STOPP — VOR DEM DRÜCKEN VON "UPDATE" LESEN
|
||||
============================================================
|
||||
|
||||
Der "Update"-Button ist in 0.9.11 und 0.9.12 KAPUTT.
|
||||
NICHT benutzen. Stattdessen einmalig manuell updaten —
|
||||
ab 0.9.13 funktioniert er wieder.
|
||||
|
||||
>> WINDOWS-.EXE / LINUX-BINARY-Nutzer — GEFAHR:
|
||||
Update ÜBERSCHREIBT deine kx-bridge.exe / kx-bridge mit
|
||||
einer Textdatei. Das Programm STARTET DANN NICHT MEHR
|
||||
und kann sich nicht selbst reparieren.
|
||||
--> Manuell updaten: die 0.9.13
|
||||
kx-bridge-windows.zip / kx-bridge-linux.zip von der
|
||||
Releases-Seite laden und die alte Datei ersetzen.
|
||||
Deine config/- und data/-Ordner bleiben erhalten.
|
||||
|
||||
>> DOCKER-Nutzer:
|
||||
Update führt zur Crash-Loop des Containers
|
||||
(ModuleNotFoundError: No module named '_web_assets').
|
||||
--> Manuell updaten:
|
||||
docker compose pull (oder docker compose up -d --build)
|
||||
config- + data-Volumes bleiben erhalten.
|
||||
|
||||
Ab 0.9.13 ist der In-App-Updater repariert und wieder sicher.
|
||||
============================================================
|
||||
|
||||
### Fixes
|
||||
- **Self-Update war in 0.9.11 und 0.9.12 kaputt (kritisch):** Der In-App-Updater
|
||||
ersetzte nur `kobrax_moonraker_bridge.py`. Zwei Probleme:
|
||||
- **Binary/EXE-Modus:** Er überschrieb die laufende Programmdatei
|
||||
(`sys.executable`) mit einer Python-Textdatei — übrig blieb ein nicht mehr
|
||||
startbares Programm, das sich nicht selbst reparieren kann (manueller
|
||||
Re-Download nötig).
|
||||
- **Python/Docker-Modus:** Seit 0.9.12 importiert die Hauptdatei das
|
||||
ausgelagerte `_web_assets.py` (gebündeltes Frontend), das der Updater nicht
|
||||
mitlud → `ModuleNotFoundError: No module named '_web_assets'` → Crash-Loop.
|
||||
Der Updater lädt jetzt **alle** Bridge-Module (Hauptdatei + `_web_assets.py` +
|
||||
Client + Loader) erst vollständig herunter, ersetzt sie dann atomar und
|
||||
**verweigert das Self-Update im Binary-Modus** (mit Verweis auf den manuellen
|
||||
Download).
|
||||
|
||||
## [0.9.12] – 2026-05-20
|
||||
|
||||
### Fixes
|
||||
- **Pause-Status** wird jetzt korrekt erkannt: Die Bridge las den Geräte-State
|
||||
statt des verschachtelten Druckauftrags-States, dadurch wurde ein pausierter
|
||||
Druck teils noch als „druckend" angezeigt. Layer/Fortschritt/Restzeit kommen
|
||||
jetzt ebenfalls aus dem Auftrags-Report.
|
||||
|
||||
### Intern
|
||||
- Frontend (HTML/CSS/JS) aus der Python-Datei nach `web/index.html` ausgelagert,
|
||||
zur Build-Zeit wieder eingebettet — besser wartbar, für Nutzer keine Änderung.
|
||||
|
||||
### Doku
|
||||
- Community-**Home-Assistant-Integration** von @gangoke verlinkt.
|
||||
|
||||
## [0.9.11] – 2026-05-20
|
||||
|
||||
### Neu
|
||||
- **ACE Pro 2 Support (experimentell, Community-Beitrag von @gangoke, PR #26):** Die Bridge erkennt jetzt die Filament-Hardware automatisch und passt sich an:
|
||||
- **Modi:** `toolhead` (kein ACE, Standard-4-Slot-Box), `ace_direct` (ein ACE Pro 2 direkt am Toolhead), `ace_hub` (bis zu 4 ACE-Units am Slot-4-Hub) — insgesamt bis zu **19 Slots**.
|
||||
- **AMS Auto-Refill** Umschalter.
|
||||
- **Trockner:** Temperatur-/Luftfeuchte-Monitor, Start/Stop/Temp/Dauer-Steuerung, mit Material-Presets in einer neuen Config-Sektion `[ace_dry_presets]` (PLA, PLA+, PETG, TPU, ABS/ASA, PA/PC + 3 Custom).
|
||||
- **UI:** Filament-Sektion skaliert auf 19 Slots, Modus-Label, geladener Slot grün umrandet mit Lade-/Entlade-Puls-Animation, Unload/Load direkt aus dem Slot-Edit-Dialog.
|
||||
- **GCode-Farb-Mapping:** ACE2-fähig, Farbe-aus-GCode-Fix, Hinweis bei Inkonsistenz zwischen Mapping und Objekten, besseres Default-Mapping.
|
||||
|
||||
> **⚠️ Experimentell:** Die ACE-Pro-2-Hardware-Pfade wurden vom Contributor mit einer einzelnen ACE2-Unit entwickelt und getestet; die 2–4-Unit-Hub-Konfigurationen sind theoretisch und auf echter Hardware ungetestet. Wir haben hier ebenfalls keine ACE2-Hardware zur Verifikation. Der Standard-`toolhead`-Pfad (ohne ACE) wurde live gegen einen echten Kobra X getestet. Wer ein Multi-ACE-Setup betreibt: bitte per Issue Rückmeldung geben.
|
||||
|
||||
### Fixes
|
||||
- **Happy-Hare-MMU-Emulation:** Es werden nur belegte Slots gesynct — kein Placeholder für leere Slots (kompatibel mit OrcaSlicer PR #13372).
|
||||
- **GCode-Farb-Dialog** zeigt nach einem neuen Upload nicht mehr die Daten der vorherigen Datei.
|
||||
|
||||
---
|
||||
|
||||
## [0.9.10] – 2026-05-17
|
||||
|
||||
> **Hinweis:** Mit diesem Release wird der Fokus von neuen Features auf
|
||||
> **Stabilisierung und Bugfixing** verlagert. Die Kern-Workflows
|
||||
> (Multi-Printer, Drucker hinzufügen/entfernen, Filament-Dialog,
|
||||
> Skip-Objekte, Standalone-Binaries) sind funktional ausreichend — ab jetzt
|
||||
> steht Robustheit vor neuen Features. Größere Feature-Wünsche (ACE Pro 2,
|
||||
> Home-Assistant-Integration vervollständigen, …) bleiben vorerst im Backlog.
|
||||
|
||||
### Neu
|
||||
- **Objekte überspringen (vor und während des Drucks):** Aus dem AnycubicSlicerNext-Workbench-Bundle rekonstruiert — der Kobra X kann das nativ über sein Protokoll, der Anycubic-Slicer bietet es bloß nicht im UI an. Die Bridge schon, in zwei Varianten:
|
||||
- **Vor dem Druck:** beim Starten eines Multi-Object-Drucks aus dem Browser-Tab hat der Filament-Dialog jetzt einen zusätzlichen Abschnitt „Objekte". Einzelne Objekte abwählen (oder Polygon direkt im Build-Plate-SVG anklicken) — sie werden vor dem Druck herausgenommen.
|
||||
- **Während des Drucks:** neuer ✂-Button im Dashboard (nur während eines laufenden Drucks sichtbar). Öffnet einen Dialog mit derselben interaktiven SVG-Vorschau — Teil anklicken, bestätigen, der Drucker druckt es nicht weiter. Bereits übersprungene Teile bleiben ausgegraut, der Dialog aktualisiert sich live damit man sieht welche schon weg sind.
|
||||
- **Filament-Dialog – farbige Kanal- und Slot-Marker (Issue #23):** Die GCode-Kanal-Nummer sitzt jetzt in einer farbigen Box links (Hintergrund = Kanal-Farbe, Auto-Kontrast-Text statt des alten kleinen Punktes), der zugewiesene AMS-Slot bekommt rechts neben dem Dropdown denselben Look — aktualisiert sich live wenn man die Auswahl ändert. Funktioniert mit 4 Kanälen; das Layout iteriert sauber für mehr, aber >4 echte Filament-Slots brauchen eine ACE-Pro-2-Box und sind ohne entsprechende Hardware nicht durchgängig testbar (geparkt als Feature-Request, Issues #22 und #23).
|
||||
|
||||
### Intern
|
||||
- Neue Helfer `kobrax_client.skip_objects(names)` / `query_skip_objects()`.
|
||||
- Neue Endpunkte: `GET /kx/files/{id}/objects`, `POST /kx/skip`, `POST /kx/skip/query`, `GET /kx/skip/state`.
|
||||
- SQLite-Schema: `gcode_files` bekommt die Spalten `objects_skip_parts` und `svg_image` (Auto-Migration auf bestehenden DBs).
|
||||
- `_on_file` extrahiert die vom Drucker gelieferte Objektliste + SVG-Vorschau und speichert sie pro Datei.
|
||||
- `_on_skip`-Callback verfolgt, welche Objekte der Drucker aktuell als übersprungen meldet.
|
||||
|
||||
---
|
||||
|
||||
## [0.9.9] – 2026-05-14
|
||||
|
||||
### Fixes
|
||||
- **„Failed to fetch"-Schleife in der UI (Issue #21):** Wenn die Web-UI über die LAN-IP geöffnet wurde, lieferte `/kx/printers` `bridge_url: http://localhost:7125` zurück. Der Browser machte daraufhin Cross-Origin-Requests von der LAN-IP nach `localhost` — die wurden vom Browser blockiert und produzierten eine Flut aus `TypeError: Failed to fetch`-Poll-Fehlern. Die Bridge liefert jetzt im Einzel-Drucker-Modus eine leere `bridge_url`, sodass das Frontend relative Pfade gegen dieselbe Origin wie die UI nutzt. Im Multi-Printer-Modus werden `localhost`/`127.0.0.1` als Bridge-Hosts herausgefiltert.
|
||||
- **Windows-EXE crasht beim Start (Issue #21):** Die v0.9.8-`kx-bridge.exe` wurde mit einer veralteten `config_loader.py` aus einem früheren Release gebaut und stürzte mit `AttributeError: module 'config_loader' has no attribute 'list_printers'` ab. `release.sh` synct jetzt `config_loader.py` zusammen mit den anderen Quellen ins Windows-Build-Repository.
|
||||
|
||||
---
|
||||
|
||||
## [0.9.8] – 2026-05-12
|
||||
|
||||
### Neu
|
||||
- **Multi-Printer in einer Bridge-Instanz:** Ein Prozess verwaltet jetzt mehrere Drucker gleichzeitig — N MQTT-Verbindungen + N HTTP-Listener (Ports 7125, 7126, …), geteilte SQLite + GCode-Store. Konfiguration über `[printer_1]`-, `[printer_2]`-… Sektionen in `config.ini`. Einzel-Modus (`[connection]`) funktioniert unverändert weiter. `docker-compose.yml` exposed einen Port-Range `7125-7130`.
|
||||
- **Drucker per UI hinzufügen:** „+ Drucker hinzufügen"-Button im Drucker-Tab — nur die IP eingeben, Zugangsdaten (Username, Passwort, Device-ID) werden automatisch vom Drucker geholt und entschlüsselt. Weitere Drucker bekommen den nächsten freien Port (7126, 7127, …).
|
||||
- **Drucker per UI entfernen:** „✕"-Button auf jeder Drucker-Karte mit Bestätigung — entfernt die `[printer_N]`-Sektion und nummeriert die übrigen um. Beim Entfernen des letzten Druckers wird auch `[connection]` geleert (leerer Zustand).
|
||||
- **GCode Store:** Hochgeladene Dateien werden in SQLite gespeichert, inkl. Thumbnail-Extraktion. Neue `/kx/files`-API.
|
||||
- **Browser-Tab:** Grid-Ansicht aller hochgeladenen Dateien — Thumbnail, Status-Badge (✓/✗), letzte Druckdauer, plus Suche, Filter und Sortierung.
|
||||
- **Druckhistorie:** Druckaufträge (Start/Ende/Status) werden in SQLite protokolliert, Status pro Datei im Browser-Tab sichtbar.
|
||||
- **Filament-Dialog:** Per-Kanal-Remapping vor dem Druckstart — jeder GCode-Farbkanal wird einem physischen AMS-Slot zugewiesen (wie im Anycubic Slicer). Verfügbar im Browser-Tab und im Upload-Banner.
|
||||
- **MMU-Emulation:** `GET /printer/objects/query?mmu` liefert eine Happy-Hare-kompatible Struktur, damit OrcaSlicers Filament-Sync die AMS-Slots erkennt.
|
||||
- **Drucker-Tab:** Live-Status aller Drucker-Instanzen, IP auf jeder Karte, „Wechseln →"-Button.
|
||||
- **Editierbarer Drucker-Name:** Eigener Name in den Einstellungen (gespeichert in `[bridge] printer_name`, hat Vorrang vor dem vom Drucker gemeldeten Namen).
|
||||
- **Standalone-tauglich:** Linux-Binary / Windows-EXE laufen ohne Docker — `config/` und `data/` liegen neben dem Programm (portabel). Erststart ohne konfigurierten Drucker zeigt den Drucker-Tab mit „+ Drucker hinzufügen" statt des Einstellungs-Dialogs.
|
||||
- **i18n:** Alle neuen UI-Elemente auf Deutsch und Englisch.
|
||||
|
||||
### Fixes
|
||||
- **CORS:** CORS-Middleware auf allen Endpunkten — Cross-Instance-Fetches in der Multi-Printer-UI funktionieren zuverlässig.
|
||||
- **Einstellungen / Update-Check** zeigen im Multi-Printer-Modus jetzt die aktive Bridge-Instanz (via `_apiUrl`).
|
||||
- **Bridge-Neustart:** Config-abhängige Umgebungsvariablen werden vor einem Neustart gelöscht (der Config-Loader cachte sie, wodurch Config-Änderungen erst nach einem Kaltstart sichtbar wurden). Der Neustart ist jetzt plattformabhängig: Docker/systemd → Prozess-Exit (Supervisor startet neu), Linux standalone → `os.execv`, Windows → detachter Subprozess.
|
||||
- **`--data-dir`-Default** ist jetzt plattformabhängig — der `/app/data`-Default greift nur in Docker (per `ENV` gesetzt), Standalone-Binaries nutzen `<exe-dir>/data`. Behebt einen Startup-Crash beim Ausführen ohne Docker.
|
||||
|
||||
---
|
||||
|
||||
## [0.9.7] – 2026-05-08
|
||||
|
||||
### Neu
|
||||
- **fetch_credentials-Tool:** Ruft MQTT-Credentials direkt vom Drucker per HTTP ab — kein laufender Anycubic Slicer nötig, nur die Drucker-IP. Linux-Binary und Windows-EXE im Release enthalten. (Beitrag von bebu, PR #19)
|
||||
|
||||
### Fixes
|
||||
- **Upload großer GCode-Dateien:** Dateien >1 MB wurden mit HTTP 413 abgelehnt — aiohttp `client_max_size` auf 256 MB erhöht
|
||||
- **Upload-Timeout:** Socket-Timeout nach GCode-Upload von 10s auf 120s erhöht — große Dateien führten zu einem Absturz der Bridge mit leerer Antwort während der Drucker noch verarbeitete
|
||||
|
||||
---
|
||||
|
||||
## [0.9.6] – 2026-05-02
|
||||
|
||||
### Neu
|
||||
- **Licht-Status-Synchronisierung:** Ein/Aus-Zustand und Helligkeit des Druckerlichts werden jetzt live über `light/report` MQTT gelesen — der Licht-Toggle in der UI spiegelt den echten Druckerstatus wider
|
||||
- **Zeit-Minicards:** Fortschritts-Panel zeigt jetzt drei Karten — Verstrichen, Restzeit und Slicer-Schätzung — sowie einen Layer-Badge neben dem Fortschrittsbalken
|
||||
- **Slicer-Schätzzeit aus GCode:** Geschätzte Druckzeit wird direkt aus der hochgeladenen GCode-Datei gelesen (OrcaSlicer: `; total estimated time:` am Dateiende, PrusaSlicer: `; estimated printing time` im Header)
|
||||
- **Erweiterte Druckerstatus-Strings:** `pausing`, `paused`, `resuming`, `resumed`, `stopping`, `stopped` hinzugefügt — fehlten bisher und ließen die UI rohe Status-Codes bei Pause/Fortsetzen/Stopp anzeigen
|
||||
|
||||
### Fixes
|
||||
- **file_ready-Banner:** Upload-Banner wird nach Stopp oder Abbruch eines Drucks nicht mehr angezeigt
|
||||
- **Zeitanzeige bei Stopp/Abbruch:** Verstrichen-, Restzeit- und Slicer-Schätzung werden auf null zurückgesetzt wenn ein Druck gestoppt oder abgebrochen wird
|
||||
- **start.sh:** `config/`-Verzeichnis und `config.ini.example` werden beim ersten Start automatisch angelegt wenn sie fehlen (Issue #15)
|
||||
|
||||
---
|
||||
|
||||
## [0.9.5] – 2026-05-01
|
||||
|
||||
### Neu
|
||||
- **Upload-Banner:** Nach „Nur hochladen" erscheint ein grüner Banner mit Dateiname — „▶ Druck starten" startet den Druck direkt, „✕ Abbrechen" schließt den Banner
|
||||
|
||||
### Fixes
|
||||
- **Auto-Print:** `auto_print` wurde nach dem Multipart-Loop immer auf `False` zurückgesetzt — OrcaSlicer „Hochladen und drucken" startete den Druck nie automatisch
|
||||
- **Thumbnail:** Vorschaubild wird jetzt auch bei „Nur hochladen" angezeigt — Bridge fragt `fileDetails` direkt nach dem Upload an
|
||||
- **Log Auto-Scroll:** Scroll-Position bleibt erhalten wenn Auto-Scroll deaktiviert ist — kein ungewollter Sprung nach oben mehr
|
||||
|
||||
---
|
||||
|
||||
## [0.9.4] – 2026-05-01
|
||||
|
||||
### Neu
|
||||
- **AMS-Slot-Editor:** Slot im AMS-Panel anklicken → Dialog mit Farbpicker und Material-Auswahl (Schnellbuttons: PLA/PETG/ABS/ASA/TPU/PA/PC/HIPS oder Freitext) direkt im Browser
|
||||
- **Verbessertes Log-Panel:** Vollständige MQTT-Payloads (keine Kürzung mehr), Richtungsfilter (Alle/RX/TX) und Topic-Schnellfilter (AMS / print / info / status)
|
||||
|
||||
### Fixes
|
||||
- **i18n:** Kamera-Placeholder-Text und Log-Richtungs-Button „Alle" werden jetzt korrekt beim Sprachwechsel übersetzt
|
||||
|
||||
---
|
||||
|
||||
## [0.9.3] – 2026-05-01
|
||||
|
||||
### Fixes
|
||||
- **Update-Check:** Stable-User erhalten keine Dev-Pre-Releases mehr — `STABLE_RELEASE_API` hatte `pre-release=true`, wodurch stabile Installationen Dev-Builds statt stabiler Releases fanden (Issue #14)
|
||||
- **Version nach Update:** `VERSION`-Datei wird jetzt im Docker-Image mitgeliefert (`COPY VERSION .`) — `_write_version()` benötigt eine vorhandene Datei, ohne die wurde die Version nach dem Self-Update nie aktualisiert (Issue #14)
|
||||
|
||||
### Neu
|
||||
- **Version im Header:** Laufende Version wird im Web-UI-Header neben dem Druckernamen angezeigt — kein Öffnen der Einstellungen nötig (Issue #14)
|
||||
|
||||
---
|
||||
|
||||
## [0.9.2] – 2026-04-29
|
||||
|
||||
### ⚠️ Breaking Change: Konfiguration wechselt von `.env` zu `config/config.ini`
|
||||
|
||||
**Migration erfolgt automatisch** beim ersten Start — keine manuelle Aktion nötig.
|
||||
|
||||
- Einstellungen werden ab sofort aus `config/config.ini` gelesen statt aus `.env`
|
||||
- Beim ersten Start ohne `config.ini` wird die Datei automatisch aus `.env` erstellt
|
||||
- **Docker:** Volume `./config:/app/config` in `docker-compose.yml` ist der persistente Speicherort — Einstellungen überleben `docker-compose restart` und Updates
|
||||
- **Standalone:** `config/config.ini` liegt neben der Binary und wird bei Updates nicht überschrieben
|
||||
- `.env` bleibt als read-only Fallback gemountet — kann liegen bleiben
|
||||
- Zum manuellen Anlegen einer `config.ini`: Vorlage unter `config/config.ini.example`
|
||||
|
||||
### Neu
|
||||
- **Persistente Einstellungen:** `config/config.ini` ersetzt `.env` — Einstellungen gehen nach `docker-compose restart` nicht mehr verloren (Issue #9)
|
||||
- **Verbindungsfehler-Banner:** Roter Banner oben in der Web-UI wenn MQTT-Verbindung fehlschlägt (z.B. falsches Passwort, Drucker nicht erreichbar) (Issue #11)
|
||||
- **Slicer-Schätzzeit:** Geschätzte Gesamtdruckzeit aus dem GCode-Header wird im Fortschritts-Panel angezeigt
|
||||
|
||||
### Fixes
|
||||
- README: OrcaSlicer-Verbindung explizit mit `http://` und Port `:7125` dokumentiert (Issue #12)
|
||||
- README: Direkter Download-Link für `extract_credentials` auf Gitea-Releases (Issue #13)
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-dev] – laufend (dev-Branch)
|
||||
|
||||
### Neu
|
||||
- **Dev-Branch-Infrastruktur:** Versionsschema `0.9.1-dev+<hash>` — jeder Build eindeutig identifizierbar
|
||||
- **Separater Update-Kanal:** Dev-Versionen prüfen auf Gitea Pre-Releases mit `-dev+` im Tag
|
||||
- **AMS-Slot-Auswahl:** Einstellung „Standard-Slot (Einfarbdruck)" im Settings-Modal — fixiert einen bestimmten AMS-Kanal oder Auto (alle belegten Slots)
|
||||
- **Auto-Leveling:** Checkbox im Settings-Modal — steuert `task_settings.auto_leveling` beim Druckstart
|
||||
- **MQTT-Logging:** Strukturiertes TX/RX-Log mit Duplikat-Filter (`kobrax.mqtt` Logger)
|
||||
- **Server-Log im Browser:** Live-Stream via SSE (`/api/log/stream`) — alle Server-Logs erscheinen im Log-Tab der UI
|
||||
- **Log-Tab Verbesserungen:**
|
||||
- Auto-Scroll ein/aus — deaktiviert sich beim manuellen Hochscrollen, Button zum Reaktivieren
|
||||
- Textfilter — Live-Filterung der Log-Einträge
|
||||
- Error-Badge — roter Zähler im Tab-Button bei Fehlern/Warnungen
|
||||
- Clear-Button — Buffer leeren
|
||||
- Download-Button — letzte 500 Einträge als `kx-bridge.log`
|
||||
- Log-Fenster füllt den gesamten verfügbaren Platz (statt fester Höhe 160px)
|
||||
- **Log-Buffer:** 500 Einträge (Server + Browser vereinheitlicht)
|
||||
- **Changelog im Update-Dialog:** Release-Notes aus Gitea werden direkt im Update-Dialog angezeigt
|
||||
- **Slicer-Schätzzeit:** Geschätzte Gesamtdruckzeit aus dem GCode-Header im Fortschritts-Panel
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta15] – 2026-04-26
|
||||
|
||||
### Fixes
|
||||
- AMS: Leere Slots werden beim Druckstart übersprungen — kein `filament runout` mehr bei unbelegten Kanälen (Issue #5)
|
||||
- AMS: Material-Typ wird jetzt korrekt aus dem Drucker-Protokoll gelesen (Feld `type` statt `material_type`)
|
||||
- AMS UI: Leere Slots werden grau/transparent mit „Leer"-Label dargestellt
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta14] – 2026-04-26
|
||||
|
||||
### Fixes
|
||||
- Z-Achse: ▲ fährt jetzt aufwärts (Z+), ▼ abwärts (Z−) — Pfeile waren vertauscht (Issue #4)
|
||||
- Home All: korrekter Achsen-Code 5 — homed alle Achsen XYZ (Issue #4)
|
||||
- Neuer Button „Home XY" (axis=4) in der UI
|
||||
- Neuer Button „Motoren aus" (axis turnOff) in der UI
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta13] – 2026-04-26
|
||||
|
||||
### Fixes (Windows)
|
||||
- Self-Update / Settings-Neustart: `os.execv` funktioniert jetzt korrekt in der PyInstaller-Binary
|
||||
- Kamera: `ffmpeg nicht gefunden` crasht nicht mehr — saubere 503-Antwort wenn ffmpeg fehlt
|
||||
- Reconnect-Loop: Kurze leere TCP-Reads unter Windows lösen keine sofortigen Reconnects mehr aus
|
||||
|
||||
### Struktur
|
||||
- `bridge/`: Bridge-Dateien aus `05_scripts/` herausgelöst
|
||||
- `tools/`: `extract_credentials.py` als eigenständiges Tool mit eigenem README
|
||||
- `_archive/`: RE-Forschungsordner, Analyse-Tools und alte Release-Checksums archiviert
|
||||
- README komplett neu: klarer 3-Schritte-Schnellstart
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta12] – 2026-04-25
|
||||
|
||||
### Fixes
|
||||
- Falsche MQTT-Zugangsdaten zeigen jetzt eine verständliche Fehlermeldung statt des kryptischen `CONNACK failed: 20020005`
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta11] – 2026-04-25
|
||||
|
||||
### Fixes
|
||||
- Drucker-IP wird automatisch bereinigt wenn der Nutzer versehentlich den Port miteingibt (z.B. `192.168.1.102:9883` → `192.168.1.102`)
|
||||
- Settings-Modal: Hinweis erscheint wenn ein `:` in der IP erkannt wird
|
||||
- `docker-compose.yml`: `.env` als Volume gemountet — Einstellungen bleiben nach `docker-compose restart` erhalten
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta10] – 2026-04-25
|
||||
|
||||
### Neu
|
||||
- `start.sh` — startet die Bridge per Docker, baut das Image automatisch beim ersten Aufruf
|
||||
- Tests: pytest-Suite (19 Tests) für API-State, Moonraker-Endpunkte, Settings; Installations-Smoke-Test (`test_install.sh`)
|
||||
- Settings-Modal öffnet sich beim ersten Start automatisch wenn keine Zugangsdaten hinterlegt sind
|
||||
|
||||
### Geändert
|
||||
- README: Schnellstart zeigt jetzt `./start.sh` statt manuellem `docker build`
|
||||
- README: LAN-Modus korrekt als Drucker-Menüoption beschrieben
|
||||
- README: Versionsnummer wird ab jetzt automatisch bei jedem Release aktualisiert
|
||||
- `extract_credentials`: `--write-env` nicht mehr empfohlen — Werte im ⚙-Menü eintragen
|
||||
- Dockerfile im Release-Repo: Pfade ohne `05_scripts/`-Präfix
|
||||
- `release.sh`: Dockerfile für Release-Repo automatisch per `sed` angepasst
|
||||
|
||||
### Fixes
|
||||
- Restdruckzeit (`remain_time`) wird jetzt korrekt aus `print/report` übernommen und in der UI angezeigt
|
||||
- Übersetzungen: „Schrittweite" und „Ziel"-Placeholder in Temperatureingaben korrekt übersetzt
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta9] – 2026-04-25
|
||||
|
||||
### Neu
|
||||
- OrcaSlicer-Profil (`kobra_x_orcaslicer_preset.zip`) als Release-Asset
|
||||
- `release.sh`: OrcaSlicer-Profil wird automatisch ins Release-Repo kopiert und hochgeladen
|
||||
|
||||
### Geändert
|
||||
- README: `extract_credentials` ohne `--write-env`, Werte manuell im ⚙-Menü eintragen
|
||||
- README: Docker-Schnellstart vereinfacht
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta8] – 2026-04-25
|
||||
|
||||
### Neu
|
||||
- Restdruckzeit-Anzeige in der UI (≈ Xh Ym verbleibend) aus `remain_time`-Feld
|
||||
- Settings-Modal: Verbindungseinstellungen und Self-Update direkt im Browser
|
||||
- Self-Update: Bridge prüft Gitea-Release-API auf neue Versionen und aktualisiert sich selbst
|
||||
|
||||
### Geändert
|
||||
- Bridge startet im Offline-Modus wenn Drucker nicht erreichbar (kein Absturz)
|
||||
- Verbinden/Trennen-Button im Header
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta7] – 2026-04-22
|
||||
|
||||
### Neu
|
||||
- Offline-Start: Bridge läuft auch ohne MQTT-Verbindung, verbindet automatisch sobald Drucker erreichbar
|
||||
- Verbinden/Trennen-Button im Header
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta6] – 2026-04-20
|
||||
|
||||
### Neu
|
||||
- Release-ZIPs: `kx-bridge-linux.zip`, `kx-bridge-windows.zip`, `anycubic-certs.zip` mit Zertifikaten
|
||||
|
||||
### Fixes
|
||||
- PyInstaller frozen-Binary: `__file__` durch `sys.executable`-Pfad ersetzt (Cert-Pfad-Fix)
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta5] – 2026-04-19
|
||||
|
||||
### Neu
|
||||
- `kx-bridge.exe` (Windows) wird automatisch via GitHub Actions gebaut
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta4] – 2026-04-18
|
||||
|
||||
### Neu
|
||||
- `release.sh`: baut Linux-Binary und Windows-EXE, lädt alle Assets auf Gitea hoch
|
||||
- Englische README (`README.en.md`)
|
||||
|
||||
### Fixes
|
||||
- `progress` und `filename` werden bei `stoped`/`canceled` korrekt auf 0 zurückgesetzt
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta3] – 2026-04-17
|
||||
|
||||
### Neu
|
||||
- Druckgeschwindigkeit-Karte (Leise / Normal / Sport)
|
||||
- Übersetzungen (DE/EN) vervollständigt
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta2] – 2026-04-17
|
||||
|
||||
### Fixes
|
||||
- Temperatursteuerung während eines laufenden Drucks
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta1] – 2026-04-17
|
||||
|
||||
### Neu
|
||||
- UI-Komplettüberarbeitung: Settings-Modal, Self-Update, Dashboard, Responsive Design
|
||||
- Neue Drucker-Zustände: `pausing`, `paused`, `resuming`, `resumed`, `stopping`
|
||||
- `release.sh`: Version-Bump und Release-Sync Skript
|
||||
|
||||
---
|
||||
|
||||
## [0.9.0-beta1] – 2026-04-10
|
||||
|
||||
### Neu
|
||||
- Erster öffentlicher Release
|
||||
- Docker-Deployment, Linux-Binary, `extract_credentials`-Tool
|
||||
- Moonraker-kompatible HTTP/WebSocket-Bridge für den Anycubic Kobra X
|
||||
- AMS Einziehen/Ausziehen, Licht- und Lüftersteuerung
|
||||
- Web-UI mit Dashboard, Temperaturkarten, Achsensteuerung
|
||||
824
CHANGELOG.md
824
CHANGELOG.md
@@ -1,132 +1,844 @@
|
||||
# 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
|
||||
- **Theme system (community contribution by @hirnwunde, PR #27):** the web UI now
|
||||
lives in real files under `web/themes/<name>/` (`index.html` + `style.css` +
|
||||
`app.js`) instead of being embedded in the Python source. Switch themes with
|
||||
`--ui-theme <name>`. Theme authors get a documented hook reference
|
||||
(`web/DOC/THEME-CSS-HOOKS.md`, `THEME-JS-ID-HOOKS.md`). The default theme
|
||||
carries the full current UI (ACE2, skip objects, filament dialog). No change
|
||||
for users — the bundled binaries/Docker image ship the theme embedded.
|
||||
- **Restart over API (community contribution by @gangoke, PR #28):** new
|
||||
`POST /api/restart` endpoint to restart the bridge remotely — e.g. a restart
|
||||
button in the Home Assistant integration.
|
||||
|
||||
### Internal
|
||||
- Unified PyInstaller build (`kx-bridge.spec`) for Linux, Windows and Docker —
|
||||
embeds `web/` (themes) into the one-file binary, read at runtime from
|
||||
`sys._MEIPASS`. Verified the theme ships in the Linux binary and the Windows EXE.
|
||||
- `data/` added to `.gitignore`.
|
||||
|
||||
## [0.9.13] – 2026-05-20
|
||||
|
||||
============================================================
|
||||
STOP — READ THIS BEFORE PRESSING "UPDATE"
|
||||
============================================================
|
||||
|
||||
The in-app "Update" button is BROKEN in 0.9.11 and 0.9.12.
|
||||
Do NOT use it. Update manually instead (one time), then it
|
||||
works again from 0.9.13 onward.
|
||||
|
||||
>> WINDOWS .EXE / LINUX BINARY users — DANGER:
|
||||
Pressing Update OVERWRITES your kx-bridge.exe / kx-bridge
|
||||
with a text file. The program will NOT start anymore.
|
||||
It cannot repair itself.
|
||||
--> Update manually: download the 0.9.13
|
||||
kx-bridge-windows.zip / kx-bridge-linux.zip from the
|
||||
Releases page and replace your old file.
|
||||
Your config/ and data/ folders are kept.
|
||||
|
||||
>> DOCKER users:
|
||||
Pressing Update makes the container crash-loop
|
||||
(ModuleNotFoundError: No module named '_web_assets').
|
||||
--> Update manually:
|
||||
docker compose pull (or docker compose up -d --build)
|
||||
Your config + data volumes are kept.
|
||||
|
||||
From 0.9.13 on, the in-app updater is fixed and safe again.
|
||||
============================================================
|
||||
|
||||
### Fixes
|
||||
- **Self-update was broken in 0.9.11 and 0.9.12 (critical):** the in-app updater
|
||||
only replaced `kobrax_moonraker_bridge.py`. Two problems:
|
||||
- **Binary/EXE mode:** it overwrote the running executable (`sys.executable`)
|
||||
with a Python text file, leaving an unstartable program that can't recover
|
||||
itself — manual re-download required.
|
||||
- **Python/Docker mode:** since 0.9.12 the main file imports the extracted
|
||||
`_web_assets.py` (bundled frontend), which the updater didn't fetch →
|
||||
`ModuleNotFoundError: No module named '_web_assets'` → crash loop.
|
||||
The updater now downloads **all** bridge modules (main file + `_web_assets.py`
|
||||
+ client + loaders) fully, then swaps them atomically, and **refuses to
|
||||
self-update in binary mode** (pointing you to the manual download instead).
|
||||
|
||||
## [0.9.12] – 2026-05-20
|
||||
|
||||
### Fixes
|
||||
- **Pause state** is now read correctly: the bridge was looking at the device-level
|
||||
state instead of the nested print-job state, so a paused print sometimes still
|
||||
showed as printing. Layer/progress/remaining-time are now also taken from the
|
||||
job report.
|
||||
|
||||
### Internal
|
||||
- Frontend (HTML/CSS/JS) extracted from the Python file into `web/index.html`,
|
||||
bundled back in at build time — easier to maintain, no change for users.
|
||||
|
||||
### Docs
|
||||
- Linked the community **Home Assistant integration** by @gangoke.
|
||||
|
||||
## [0.9.11] – 2026-05-20
|
||||
|
||||
### New
|
||||
- **ACE Pro 2 support (experimental, community contribution by @gangoke, PR #26):** the bridge now auto-detects the filament hardware and adapts:
|
||||
- **Modes:** `toolhead` (no ACE, stock 4-slot box), `ace_direct` (one ACE Pro 2 directly on the toolhead), `ace_hub` (up to 4 ACE units on the slot-4 hub) — up to **19 slots** total.
|
||||
- **AMS auto-refill** toggle.
|
||||
- **Dryer:** temperature/humidity monitor, start/stop/temp/duration control, with material presets configurable in a new `[ace_dry_presets]` config section (PLA, PLA+, PETG, TPU, ABS/ASA, PA/PC + 3 custom).
|
||||
- **UI:** filament section scales to 19 slots, mode label, loaded slot is green-outlined with a load/unload pulse animation, unload/load straight from the slot-edit dialog.
|
||||
- **GCode color mapping:** ACE2-aware, color-from-GCode fix, inconsistency notifier when the mapping doesn't match the objects, better default mapping.
|
||||
|
||||
> **⚠️ Experimental:** the ACE Pro 2 hardware paths were developed and tested by the contributor with a single ACE2 unit; the 2–4 unit hub configurations are theoretical and untested on real hardware. We don't have ACE2 hardware to verify against either. The standard `toolhead` (no-ACE) path was verified live against a real Kobra X here. If you run a multi-ACE setup, please report back via Issues.
|
||||
|
||||
### Fixes
|
||||
- **Happy Hare MMU emulation:** only populated slots are now synced — no placeholder for empty slots (aligns with OrcaSlicer PR #13372).
|
||||
- **GCode color dialog** no longer shows the previously-uploaded file's data after a new upload.
|
||||
|
||||
---
|
||||
|
||||
## [0.9.10] – 2026-05-17
|
||||
|
||||
> **Heads-up:** with this release the focus shifts from new features to
|
||||
> **stabilization and bug-fixing**. The core flows (multi-printer, add/remove,
|
||||
> filament dialog, skip objects, standalone binaries) are feature-complete
|
||||
> enough — from now on the priority is making them rock-solid before adding
|
||||
> more on top. Bigger feature requests (ACE Pro 2, Home Assistant integration
|
||||
> completeness, …) stay on the backlog for now.
|
||||
|
||||
### New
|
||||
- **Skip objects (pre-print and mid-print):** Reverse-engineered from the AnycubicSlicerNext Workbench bundle — the Kobra X actually supports object skipping over its native protocol, but the Anycubic slicer doesn't expose it. The bridge does now, in both flavors:
|
||||
- **Pre-print:** when starting a multi-object print from the Browser tab, the filament dialog now has an additional "Objects" section. Uncheck individual objects (or click the polygon directly on the build-plate SVG preview) and they're stripped from the print before it starts.
|
||||
- **Mid-print:** new ✂ button on the dashboard (only visible during an active print). Opens a dialog with the same interactive SVG preview — click a part to mark it for skipping, hit confirm, and the printer drops it from the rest of the run. Already-skipped parts stay greyed out and the dialog refreshes live so you can see which ones are gone.
|
||||
- **Filament dialog – colored channel and slot markers (Issue #23):** the GCode channel number now sits in a colored box on the left (background = channel color, auto-contrast text instead of the old tiny dot), and the assigned AMS slot gets the same treatment on the right of the dropdown — updates live as you change the selection. Plays well with 4 channels; the layout iterates so more channels render correctly, but >4 actual filament slots still need an ACE Pro 2 hub to be testable end-to-end (parked as a feature request, Issues #22 and #23).
|
||||
|
||||
### Internal
|
||||
- New `kobrax_client.skip_objects(names)` / `query_skip_objects()` helpers.
|
||||
- New endpoints: `GET /kx/files/{id}/objects`, `POST /kx/skip`, `POST /kx/skip/query`, `GET /kx/skip/state`.
|
||||
- SQLite schema: `gcode_files` gained `objects_skip_parts` and `svg_image` columns (auto-migrates on existing DBs).
|
||||
- `_on_file` now extracts the printer-provided object list + SVG preview and persists them per file.
|
||||
- `_on_skip` callback tracks which objects the printer reports as currently skipped.
|
||||
|
||||
---
|
||||
|
||||
## [0.9.9] – 2026-05-14
|
||||
|
||||
### Fixes
|
||||
- **"Failed to fetch" loop in the UI (Issue #21):** When the web UI was opened via the LAN IP, `/kx/printers` was returning `bridge_url: http://localhost:7125`, which caused the browser to fire cross-origin requests from the LAN IP to `localhost` — these were silently blocked, producing a flood of `TypeError: Failed to fetch` poll errors. The bridge now sends an empty `bridge_url` in single-printer mode so the frontend uses relative paths against the same origin as the UI. In multi-printer mode, `localhost`/`127.0.0.1` are filtered out as bridge hosts.
|
||||
- **Windows EXE startup crash (Issue #21):** The v0.9.8 `kx-bridge.exe` was built with a stale `config_loader.py` from an earlier release and crashed on startup with `AttributeError: module 'config_loader' has no attribute 'list_printers'`. `release.sh` now syncs `config_loader.py` into the Windows build repository together with the other source files.
|
||||
|
||||
---
|
||||
|
||||
## [0.9.8] – 2026-05-12
|
||||
|
||||
### New
|
||||
- **Multi-printer in a single bridge instance:** One process now manages multiple printers — N MQTT connections + N HTTP listeners (ports 7125, 7126, …), shared SQLite + GCode store. Configure via `[printer_1]`, `[printer_2]` … sections in `config.ini`. Single-printer mode (`[connection]` only) keeps working unchanged. `docker-compose.yml` exposes a port range `7125-7130`.
|
||||
- **Add printer from the UI:** "+ Add printer" button in the Printers tab — just enter the printer IP, the credentials (username, password, device ID) are fetched and decrypted from the printer automatically. Adding more printers assigns the next free port (7126, 7127, …).
|
||||
- **Remove printer from the UI:** "✕" button on each printer card with a confirmation dialog — removes the `[printer_N]` section and renumbers the rest. Removing the last printer clears `[connection]` too, leaving an empty state.
|
||||
- **GCode Store:** Uploaded files are persisted in SQLite with thumbnail extraction. New `/kx/files` API.
|
||||
- **Browser tab:** Grid view of all uploaded files — thumbnail, status badge (✓/✗), last print duration, plus search, filter and sort.
|
||||
- **Print history:** Print jobs (start/end/status) are recorded in SQLite, status shown per file in the Browser tab.
|
||||
- **Filament dialog:** Per-channel remapping before print start — assign each GCode color channel to a physical AMS slot (like the Anycubic Slicer does). Available in the Browser tab and the upload banner.
|
||||
- **MMU emulation:** `GET /printer/objects/query?mmu` returns a Happy-Hare-compatible structure so OrcaSlicer's filament sync detects the AMS slots.
|
||||
- **Printers tab:** Live status of all printer instances, IP shown on each card, "Switch →" button.
|
||||
- **Editable printer name:** Set a custom name in Settings (stored in `[bridge] printer_name`, takes precedence over the MQTT-reported name).
|
||||
- **Standalone friendly:** Linux binary / Windows EXE run without Docker — `config/` and `data/` are placed next to the executable (portable). First start with no printer configured shows the Printers tab with "+ Add printer" instead of the settings modal.
|
||||
- **i18n:** All new UI elements available in German and English.
|
||||
|
||||
### Fixes
|
||||
- **CORS:** CORS middleware added to all endpoints — cross-instance fetches in the multi-printer UI work reliably.
|
||||
- **Settings / update check** now reflect the active bridge instance in multi-printer mode (via `_apiUrl`).
|
||||
- **Bridge restart:** Config-dependent environment variables are cleared before a restart (the config loader cached them, which made config changes invisible until the next cold start). Restart is now platform-aware: Docker/systemd → process exit (supervisor restarts), Linux standalone → `os.execv`, Windows → detached subprocess.
|
||||
- **`--data-dir` default** is now platform-dependent — the `/app/data` default only applies inside Docker (set via `ENV`), standalone binaries use `<exe-dir>/data`. Fixes a startup crash when running the binary without Docker.
|
||||
|
||||
---
|
||||
|
||||
## [0.9.7] – 2026-05-08
|
||||
|
||||
### New
|
||||
- **fetch_credentials tool:** Fetches and decrypts MQTT credentials directly from the printer via HTTP — no running Anycubic Slicer required, only the printer IP needed. Linux binary and Windows EXE included in release. (Contributed by bebu, PR #19)
|
||||
|
||||
### Fixes
|
||||
- **Large GCode upload:** Files >1 MB were rejected with HTTP 413 — aiohttp `client_max_size` raised to 256 MB
|
||||
- **Upload timeout:** Socket timeout after GCode upload raised from 10s to 120s — large files caused the bridge to crash with an empty response while the printer was still processing
|
||||
|
||||
---
|
||||
|
||||
## [0.9.6] – 2026-05-02
|
||||
|
||||
### New
|
||||
- **Light status sync:** Light on/off state and brightness are now read live from the printer via `light/report` MQTT message — the light toggle in the UI reflects the actual printer state
|
||||
- **Time mini-cards:** Progress panel now shows three cards — Elapsed, Remaining and Slicer estimate — with a layer counter badge next to the progress bar
|
||||
- **Slicer estimate from GCode:** Estimated print time is parsed directly from the uploaded GCode file (OrcaSlicer: `; total estimated time:` at end of file, PrusaSlicer: `; estimated printing time` in header)
|
||||
- **Extended printer status strings:** Added `pausing`, `paused`, `resuming`, `resumed`, `stopping`, `stopped` states — previously missing, causing the UI to show raw status codes during pause/resume/stop transitions
|
||||
|
||||
### Fixes
|
||||
- **file_ready banner:** Upload banner is no longer shown after print stop or cancel
|
||||
- **Timers on stop/cancel:** Elapsed, remaining and slicer estimate times are reset to zero when a print is stopped or cancelled
|
||||
- **start.sh:** `config/` directory and `config.ini.example` are now created automatically on first run if missing (Issue #15)
|
||||
|
||||
---
|
||||
|
||||
## [0.9.5] – 2026-05-01
|
||||
|
||||
### New
|
||||
- **Upload banner:** After "Upload only", a green banner appears with the filename — "▶ Start Print" starts the print directly, "✕ Cancel" dismisses the banner
|
||||
|
||||
### Fixes
|
||||
- **Auto-print:** `auto_print` was always reset to `False` after the multipart loop — OrcaSlicer "Upload and print" never started the print automatically
|
||||
- **Thumbnail:** Preview image is now shown after "Upload only" — bridge requests `fileDetails` immediately after upload
|
||||
- **Log auto-scroll:** Scroll position is preserved when auto-scroll is disabled — no more unwanted jump to top
|
||||
|
||||
---
|
||||
|
||||
## [0.9.4] – 2026-05-01
|
||||
|
||||
### New
|
||||
- **AMS slot editor:** Click any slot in the AMS panel to open an edit dialog — set color (color picker) and material (preset buttons: PLA/PETG/ABS/ASA/TPU/PA/PC/HIPS or free text) directly from the browser
|
||||
- **Improved log panel:** Full MQTT payloads (no truncation), direction filter (All/RX/TX) and topic quick-filter buttons (AMS / print / info / status)
|
||||
|
||||
### Fixes
|
||||
- **i18n:** Camera placeholder text and log direction "All" button now correctly translated on language switch
|
||||
|
||||
---
|
||||
|
||||
## [0.9.3] – 2026-05-01
|
||||
|
||||
### Fixes
|
||||
- **Update check:** Stable users no longer receive dev pre-releases — `STABLE_RELEASE_API` had `pre-release=true` which caused stable installs to find dev builds instead of stable releases (Issue #14)
|
||||
- **Version after update:** `VERSION` file is now included in the Docker image (`COPY VERSION .`) — `_write_version()` requires the file to exist, without it the version was never updated after self-update (Issue #14)
|
||||
|
||||
### New
|
||||
- **Version in header:** Running version shown in the Web-UI header next to the printer name — no need to open Settings to check (Issue #14)
|
||||
|
||||
---
|
||||
|
||||
## [0.9.2] – 2026-04-29
|
||||
|
||||
### ⚠️ Breaking Change: Configuration moves from `.env` to `config/config.ini`
|
||||
|
||||
**Migration is automatic** on first start — no manual action required.
|
||||
|
||||
- Settings are now read from `config/config.ini` instead of `.env`
|
||||
- On first start without `config.ini`, the file is created automatically from `.env`
|
||||
- **Docker:** Volume `./config:/app/config` in `docker-compose.yml` is the persistent storage — settings survive `docker-compose restart` and updates
|
||||
- **Standalone:** `config/config.ini` sits next to the binary and is not overwritten on update
|
||||
- `.env` stays mounted read-only as a migration source — you can leave it in place
|
||||
- To create a `config.ini` manually: copy `config/config.ini.example`
|
||||
|
||||
### New
|
||||
- **Persistent settings:** `config/config.ini` replaces `.env` — settings no longer lost after `docker-compose restart` (Issue #9)
|
||||
- **Connection error banner:** Red banner at the top of the Web-UI when MQTT connection fails (e.g. wrong password, printer unreachable) (Issue #11)
|
||||
- **Slicer estimated time:** Estimated total print time from GCode header shown in the progress panel
|
||||
|
||||
### Fixes
|
||||
- README: OrcaSlicer connection documented explicitly with `http://` and port `:7125` (Issue #12)
|
||||
- README: Direct download link for `extract_credentials` pointing to Gitea releases (Issue #13)
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-dev] – ongoing (dev branch)
|
||||
|
||||
### New
|
||||
- **Dev branch infrastructure:** Version scheme `0.9.1-dev+<hash>` — every build uniquely identifiable
|
||||
- **Separate update channel:** Dev versions check for Gitea pre-releases with `-dev+` in the tag
|
||||
- **AMS slot selection:** Setting "Default slot (single color)" in the Settings modal — pins a specific AMS channel or Auto (all loaded slots)
|
||||
- **Auto-leveling:** Checkbox in Settings modal — controls `task_settings.auto_leveling` on print start
|
||||
- **MQTT logging:** Structured TX/RX log with duplicate filter (`kobrax.mqtt` logger)
|
||||
- **Server log in browser console:** Live stream via SSE (`/api/log/stream`) — all server logs appear in the Log tab
|
||||
- **Log tab improvements:**
|
||||
- Auto-scroll on/off — disables automatically on manual scroll-up, button to re-enable
|
||||
- Text filter — live filtering of log entries
|
||||
- Error badge — red counter on the tab button when errors/warnings occur while on another tab
|
||||
- Clear button — empty the buffer
|
||||
- Download button — last 500 entries as `kx-bridge.log`
|
||||
- Log window now fills all available space (instead of fixed 160px height)
|
||||
- **Log buffer:** 500 entries (server + browser unified)
|
||||
- **Changelog in update dialog:** Release notes from Gitea loaded and shown directly in the update dialog
|
||||
- **Slicer estimated time:** Estimated total print time from GCode header shown in the progress panel
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta15] – 2026-04-26
|
||||
|
||||
### Fixes
|
||||
- AMS: Empty slots are skipped on print start — no more `filament runout` for unloaded channels (Issue #5)
|
||||
- AMS: Material type is now correctly read from the printer protocol (field `type` instead of `material_type`)
|
||||
- AMS UI: Empty slots shown grey/transparent with "Empty" label
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta14] – 2026-04-26
|
||||
|
||||
### Fixes
|
||||
- Z axis: ▲ now moves up (Z+), ▼ moves down (Z−) — arrows were reversed (Issue #4)
|
||||
- Home All: correct axis code 5 — homes all axes XYZ (Issue #4)
|
||||
- New "Home XY" button (axis=4) in the UI
|
||||
- New "Motors Off" button (axis turnOff) in the UI
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta13] – 2026-04-26
|
||||
|
||||
### Fixes (Windows)
|
||||
- Self-update / Settings restart: `os.execv` now works correctly in PyInstaller binary
|
||||
- Camera: `ffmpeg not found` no longer crashes — clean 503 response when ffmpeg is not installed
|
||||
- Reconnect loop: Short empty TCP reads on Windows no longer trigger immediate reconnects
|
||||
|
||||
### Structure
|
||||
- `bridge/`: Bridge files extracted from `05_scripts/`
|
||||
- `tools/`: `extract_credentials.py` as standalone tool with its own README
|
||||
- `_archive/`: RE research folders, analysis tools and old release checksums archived
|
||||
- README fully rewritten: clear 3-step quick start
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta12] – 2026-04-25
|
||||
|
||||
### Fixes
|
||||
- Fehlermeldung bei falschen MQTT-Zugangsdaten ist jetzt verständlich: `Falsche MQTT-Zugangsdaten (falscher Benutzername, Passwort oder Device-ID)` statt kryptischem `CONNACK failed: 20020005`
|
||||
- Wrong MQTT credentials now shows a human-readable error instead of cryptic `CONNACK failed: 20020005`
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta11] – 2026-04-25
|
||||
|
||||
### Fixes
|
||||
- Drucker-IP wird automatisch bereinigt wenn der Nutzer versehentlich den Port miteingibt (z.B. `192.168.1.102:9883` → `192.168.1.102`)
|
||||
- Settings-Modal: Hinweis erscheint wenn ein `:` in der IP erkannt wird
|
||||
- `docker-compose.yml`: `.env` wird als Volume in den Container gemountet – Einstellungen bleiben nach `docker-compose restart` erhalten
|
||||
- Printer IP is automatically cleaned if the user accidentally includes the port (e.g. `192.168.1.102:9883` → `192.168.1.102`)
|
||||
- Settings modal: hint shown when `:` is detected in the IP field
|
||||
- `docker-compose.yml`: `.env` mounted as volume into the container — settings persist after `docker-compose restart`
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta10] – 2026-04-25
|
||||
|
||||
### Neu
|
||||
- `start.sh` – startet die Bridge per Docker, baut das Image automatisch beim ersten Aufruf
|
||||
- Tests: pytest-Suite (19 Tests) für API-State, Moonraker-Endpunkte, Settings; Installations-Smoke-Test (`test_install.sh`)
|
||||
- Settings-Modal öffnet sich beim ersten Start automatisch wenn keine Zugangsdaten hinterlegt sind
|
||||
### New
|
||||
- `start.sh` — starts the bridge via Docker, builds the image automatically on first run
|
||||
- Tests: pytest suite (19 tests) for API state, Moonraker endpoints, settings; install smoke test (`test_install.sh`)
|
||||
- Settings modal opens automatically on first start when no credentials are configured
|
||||
|
||||
### Geändert
|
||||
- README (DE + EN): Schnellstart zeigt jetzt `./start.sh` statt manuellem `docker build`
|
||||
- README: LAN-Modus korrekt als Drucker-Menüoption beschrieben (kein WLAN-Bezug)
|
||||
- README: Versionsnummer wird ab jetzt automatisch bei jedem Release aktualisiert
|
||||
- `extract_credentials`: kein `--write-env` mehr empfohlen – Werte im ⚙-Menü eintragen
|
||||
- Dockerfile im Release-Repo: Pfade ohne `05_scripts/`-Präfix (direkt aus Repo-Root)
|
||||
- `release.sh`: Dockerfile für Release-Repo automatisch per `sed` angepasst
|
||||
### Changed
|
||||
- README: Quick start now shows `./start.sh` instead of manual `docker build`
|
||||
- README: LAN mode correctly described as a printer menu option
|
||||
- README: Version number now updated automatically on each release
|
||||
- `extract_credentials`: `--write-env` no longer recommended — enter values in the ⚙ menu
|
||||
- Dockerfile in release repo: paths without `05_scripts/` prefix
|
||||
- `release.sh`: Dockerfile for release repo automatically patched via `sed`
|
||||
|
||||
### Fixes
|
||||
- Restdruckzeit (`remain_time`) wird jetzt korrekt aus `print/report` übernommen und in der UI angezeigt
|
||||
- Übersetzung: „Schrittweite" und „Ziel"-Placeholder in Temperatureingaben werden jetzt korrekt übersetzt
|
||||
- Remaining print time (`remain_time`) now correctly taken from `print/report` and shown in UI
|
||||
- Translation: "Step size" and "Target" placeholders in temperature inputs now correctly translated
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta9] – 2026-04-25
|
||||
|
||||
### Neu
|
||||
- OrcaSlicer-Profil (`kobra_x_orcaslicer_preset.zip`) als Release-Asset
|
||||
- `release.sh`: OrcaSlicer-Profil wird automatisch ins Release-Repo kopiert und hochgeladen
|
||||
### New
|
||||
- OrcaSlicer profile (`kobra_x_orcaslicer_preset.zip`) as release asset
|
||||
- `release.sh`: OrcaSlicer profile automatically copied to release repo and uploaded
|
||||
|
||||
### Geändert
|
||||
- README: `extract_credentials` ohne `--write-env`, Werte manuell ins ⚙-Menü eintragen
|
||||
- README: Docker-Schnellstart vereinfacht (kein `.env` anlegen vor dem Start nötig)
|
||||
### Changed
|
||||
- README: `extract_credentials` without `--write-env`, values entered manually in the ⚙ menu
|
||||
- README: Docker quick start simplified
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta8] – 2026-04-25
|
||||
|
||||
### Neu
|
||||
- Restdruckzeit-Anzeige in der UI (≈ Xh Ym verbleibend) aus `remain_time`-Feld des Druckers
|
||||
- Settings-Modal: Verbindungseinstellungen und Self-Update direkt im Browser
|
||||
- Self-Update: Bridge prüft Gitea-Release-API auf neue Versionen und aktualisiert sich selbst
|
||||
### New
|
||||
- Remaining print time display in UI (≈ Xh Ym remaining) from `remain_time` field
|
||||
- Settings modal: connection settings and self-update directly in the browser
|
||||
- Self-update: bridge checks Gitea release API for new versions and updates itself
|
||||
|
||||
### Geändert
|
||||
- Bridge startet im Offline-Modus wenn Drucker nicht erreichbar (kein Absturz)
|
||||
- Verbinden/Trennen-Button im Header
|
||||
### Changed
|
||||
- Bridge starts in offline mode when printer is unreachable (no crash)
|
||||
- Connect/Disconnect button in header
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta7] – 2026-04-22
|
||||
|
||||
### Neu
|
||||
- Offline-Start: Bridge läuft auch ohne MQTT-Verbindung, verbindet automatisch sobald Drucker erreichbar
|
||||
- Verbinden/Trennen-Button im Header
|
||||
### New
|
||||
- Offline start: bridge runs without MQTT connection, reconnects automatically when printer is reachable
|
||||
- Connect/Disconnect button in header
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta6] – 2026-04-20
|
||||
|
||||
### Neu
|
||||
- Release-ZIPs: `kx-bridge-linux.zip`, `kx-bridge-windows.zip`, `anycubic-certs.zip` mit Zertifikaten
|
||||
### New
|
||||
- Release ZIPs: `kx-bridge-linux.zip`, `kx-bridge-windows.zip`, `anycubic-certs.zip` with certificates
|
||||
|
||||
### Fixes
|
||||
- PyInstaller frozen-Binary: `__file__` durch `sys.executable`-Pfad ersetzt (Cert-Pfad-Fix)
|
||||
- PyInstaller frozen binary: `__file__` replaced with `sys.executable` path (cert path fix)
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta5] – 2026-04-19
|
||||
|
||||
### Neu
|
||||
- `kx-bridge.exe` (Windows) wird automatisch via GitHub Actions gebaut
|
||||
### New
|
||||
- `kx-bridge.exe` (Windows) built automatically via GitHub Actions
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta4] – 2026-04-18
|
||||
|
||||
### Neu
|
||||
- `release.sh`: baut Linux-Binary und Windows-EXE, lädt alle Assets auf Gitea hoch
|
||||
- Englische README (`README.en.md`)
|
||||
### New
|
||||
- `release.sh`: builds Linux binary and Windows EXE, uploads all assets to Gitea
|
||||
- English README (`README.en.md`)
|
||||
|
||||
### Fixes
|
||||
- `progress` und `filename` werden bei `stoped`/`canceled` korrekt auf 0 zurückgesetzt
|
||||
- `progress` and `filename` correctly reset to 0 on `stoped`/`canceled`
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta3] – 2026-04-17
|
||||
|
||||
### Neu
|
||||
- Print-Speed-Card (Leise / Normal / Sport)
|
||||
- Übersetzungen (DE/EN) vervollständigt
|
||||
### New
|
||||
- Print speed card (Silent / Normal / Sport)
|
||||
- Translations (DE/EN) completed
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta2] – 2026-04-17
|
||||
|
||||
### Fixes
|
||||
- Temperatursteuerung während eines laufenden Drucks
|
||||
- Temperature control during an active print
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1-beta1] – 2026-04-17
|
||||
|
||||
### Neu
|
||||
- UI-Komplettüberarbeitung: Settings-Modal, Self-Update, Dashboard, Responsive Design
|
||||
- Neue Drucker-Zustände: `pausing`, `paused`, `resuming`, `resumed`, `stopping`
|
||||
- `release.sh`: Version-Bump und Release-Sync Skript
|
||||
### New
|
||||
- Complete UI overhaul: Settings modal, self-update, dashboard, responsive design
|
||||
- New printer states: `pausing`, `paused`, `resuming`, `resumed`, `stopping`
|
||||
- `release.sh`: version bump and release sync script
|
||||
|
||||
---
|
||||
|
||||
## [0.9.0-beta1] – 2026-04-10
|
||||
|
||||
### Neu
|
||||
- Erster öffentlicher Release
|
||||
- Docker-Deployment, Linux-Binary, `extract_credentials`-Tool
|
||||
- Moonraker-kompatible HTTP/WebSocket-Bridge für den Anycubic Kobra X
|
||||
- AMS Einziehen/Ausziehen, Licht- und Lüftersteuerung
|
||||
- Web-UI mit Dashboard, Temperaturkarten, Achsensteuerung
|
||||
### New
|
||||
- First public release
|
||||
- Docker deployment, Linux binary, `extract_credentials` tool
|
||||
- Moonraker-compatible HTTP/WebSocket bridge for the Anycubic Kobra X
|
||||
- AMS load/unload, light and fan control
|
||||
- Web-UI with dashboard, temperature cards, motion control
|
||||
|
||||
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.
|
||||
22
Dockerfile
22
Dockerfile
@@ -1,16 +1,34 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
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 .
|
||||
COPY config/config.ini.example /app/config/config.ini.example
|
||||
|
||||
# config/ ist ein Volume-Mountpoint – beim Start wird config.ini aus .env migriert
|
||||
# falls noch keine config.ini vorhanden ist.
|
||||
RUN mkdir -p /app/config && mkdir -p /app/data
|
||||
|
||||
# Daten-Verzeichnis fest auf /app/data (sonst würde der Binary-Default <exe-dir>/data greifen)
|
||||
# und Container-Erkennung für den Bridge-Restart (Supervisor startet neu statt subprocess).
|
||||
ENV KX_DATA_DIR=/app/data
|
||||
ENV KX_IN_DOCKER=1
|
||||
|
||||
EXPOSE 7125
|
||||
|
||||
|
||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
68
NOTICE.md
Normal file
68
NOTICE.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# NOTICE
|
||||
|
||||
This repository contains code licensed under the **GNU General Public License
|
||||
v3.0** (see [LICENSE](LICENSE)) and material that is **not** covered by that
|
||||
license. Read this file before forking, distributing, or building from source.
|
||||
|
||||
## What is GPLv3-licensed
|
||||
|
||||
The original work in this repository:
|
||||
|
||||
- `bridge/` — Bridge daemon, MQTT client, web UI, configuration loader,
|
||||
protocol implementation
|
||||
- `tools/` — `extract_credentials`, `fetch_credentials` utilities
|
||||
- `_archive/tools/kx_printer_emulator.py` — printer emulator
|
||||
- `Dockerfile`, `docker-compose.yml`, `release.sh`, build scripts
|
||||
- Documentation files (`README*`, `CHANGELOG*`, `_archive/RE/06_docs/*`)
|
||||
- `kobra_x_orcaslicer_preset.zip` — slicer profile derived from public OrcaSlicer
|
||||
presets, adapted for the Kobra X
|
||||
|
||||
You are free to use, modify and redistribute this code under the terms of
|
||||
GPLv3 — including for commercial purposes — provided downstream forks remain
|
||||
under GPLv3 and source is made available to recipients.
|
||||
|
||||
## What is **not** GPLv3-licensed
|
||||
|
||||
The following items are **third-party material**, included here only for
|
||||
**interoperability purposes** as permitted under §69e UrhG (German Copyright
|
||||
Act; equivalent: EU Software Directive Art. 6, US fair-use for reverse
|
||||
engineering for interoperability):
|
||||
|
||||
- **`bridge/anycubic_slicer.crt`** and **`bridge/anycubic_slicer.key`** —
|
||||
TLS client certificate and private key extracted from Anycubic Slicer Next
|
||||
binaries. Copyright remains with Anycubic / their respective authors.
|
||||
Included solely to enable the bridge to authenticate against the LAN MQTT
|
||||
broker that runs on a Kobra X printer the end-user already owns. No
|
||||
ownership claim is made.
|
||||
|
||||
- **MQTT protocol structures, payload formats and signature algorithms** —
|
||||
reverse-engineered from `Workbench.dll` / `cloud_mqtt.dll` of the Anycubic
|
||||
Slicer Next application. The protocol itself is documented in
|
||||
`_archive/RE/06_docs/` for transparency. Any reproduction of code or
|
||||
protocol details serves interoperability only.
|
||||
|
||||
- **AMS material naming, slot numbering, GCode markers (`EXCLUDE_OBJECT_*`)**
|
||||
— follow conventions established by Anycubic and the wider Klipper
|
||||
ecosystem; usage here is purely interoperability-driven.
|
||||
|
||||
## What this means for forks
|
||||
|
||||
If you fork KX-Bridge:
|
||||
|
||||
1. **You must keep this `NOTICE.md` and `LICENSE` file** in your fork.
|
||||
2. Your modifications and additions to the bridge code, UI, tools and docs
|
||||
inherit GPLv3 — they must be made available under the same license if you
|
||||
distribute them.
|
||||
3. The third-party material listed above is **not** something you can
|
||||
relicense; it stays under the original (implicit) rights of its owners.
|
||||
4. Removing the certificates and rebuilding them from your own Anycubic
|
||||
Slicer installation is the safest path for redistribution if you are
|
||||
unsure about the §69e situation in your jurisdiction.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This project is independent, non-commercial reverse-engineering work. It is
|
||||
**not** affiliated with, endorsed by, or supported by Anycubic Technology
|
||||
Co., Ltd. or any of their subsidiaries.
|
||||
|
||||
All trademarks are property of their respective owners.
|
||||
254
README.de.md
Normal file
254
README.de.md
Normal file
@@ -0,0 +1,254 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="knlogo.png" alt="KX-Bridge" width="160"/>
|
||||
|
||||
# KX-Bridge
|
||||
|
||||
**Steuere deinen Anycubic Kobra X mit OrcaSlicer — ohne Klipper, ohne Raspberry Pi.**
|
||||
|
||||
Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
||||
|
||||
<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>
|
||||
|
||||
[](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>Gefällt dir KX-Bridge? Ein Kaffee auf <a href="https://ko-fi.com/viewitde">Ko-fi</a> hält das Projekt am Leben. ☕</sub>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ✨ Was kann KX-Bridge?
|
||||
|
||||
| | Feature |
|
||||
|---|---|
|
||||
| 🖨️ | **Druckersteuerung** — Start, Pause, Resume, Abbruch, Temperaturen, Druckgeschwindigkeit |
|
||||
| 📊 | **Live-Status** — Temperatur, Fortschritt, Layer, Restzeit, Kamera-Stream |
|
||||
| 🎨 | **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); für korrekten Vendor-Match pro Slot den [OrcaSlicer-KX-Build](#-empfohlener-slicer) nutzen |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Schnellstart
|
||||
|
||||
### 1. Drucker vorbereiten
|
||||
|
||||
LAN-Modus am Kobra X aktivieren:
|
||||
**Drucker-Display → Einstellungen → LAN-Modus aktivieren**
|
||||
|
||||
### 2. Bridge starten
|
||||
|
||||
**Docker (empfohlen):**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Linux-Binary (kein Docker):**
|
||||
```bash
|
||||
chmod +x kx-bridge && ./kx-bridge
|
||||
```
|
||||
|
||||
**Windows-EXE (kein Docker):**
|
||||
```
|
||||
kx-bridge.exe
|
||||
```
|
||||
> `config\` und `data\` werden neben der EXE angelegt — portabel.
|
||||
|
||||
> Bei Linux- und Windows-Binary liegen `config/` und `data/` (Einstellungen, SQLite,
|
||||
> GCode-Store) jeweils neben dem Programm. Einfach den ganzen Ordner kopieren = umziehen.
|
||||
|
||||
**Python direkt:**
|
||||
```bash
|
||||
pip install -r bridge/requirements.txt
|
||||
python bridge/kobrax_moonraker_bridge.py
|
||||
```
|
||||
|
||||
### 3. Drucker einrichten
|
||||
|
||||
Web-UI öffnen: **`http://BRIDGE-IP:7125`**
|
||||
|
||||
Beim Erststart erscheint der **Drucker-Tab** mit *„+ Drucker hinzufügen"* — einfach die
|
||||
IP-Adresse des Druckers eingeben, der Rest (Username, Passwort, Device-ID) wird automatisch
|
||||
vom Drucker geholt und entschlüsselt. Fertig.
|
||||
|
||||
> Mehrere Drucker? Einfach mehrfach *„+ Drucker hinzufügen"* — jeder bekommt seinen eigenen
|
||||
> Port (7125, 7126, …) und ist im Header-Dropdown auswählbar.
|
||||
|
||||
### 4. OrcaSlicer verbinden
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## 🎨 Empfohlener Slicer
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🏠 Community & Integrationen
|
||||
|
||||
- **[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.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Zugangsdaten manuell ermitteln
|
||||
|
||||
Normalerweise nicht nötig — *„+ Drucker hinzufügen"* macht das automatisch. Falls doch:
|
||||
|
||||
```bash
|
||||
fetch_credentials --ip 192.168.x.x --write-config
|
||||
```
|
||||
Holt die Zugangsdaten per HTTP direkt vom Drucker und schreibt sie in `config/config.ini`.
|
||||
Nur die Drucker-IP nötig, kein Slicer.
|
||||
|
||||
Alternativ (wenn die IP unbekannt ist): AnycubicSlicerNext öffnen, Drucker verbinden,
|
||||
dann `extract_credentials` ausführen → gibt Username, Passwort, Device-ID und IP aus.
|
||||
|
||||
> **Downloads:** [Releases](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases) → `fetch_credentials` / `extract_credentials` (Linux & Windows)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Nützliche Befehle
|
||||
|
||||
```bash
|
||||
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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🩹 Troubleshooting
|
||||
|
||||
<details>
|
||||
<summary><b>"Falsche MQTT-Zugangsdaten" beim Start</b></summary>
|
||||
|
||||
- Drucker über *„+ Drucker hinzufügen"* erneut hinzufügen, oder
|
||||
`fetch_credentials --ip <ip> --write-config` ausführen und Bridge neu starten
|
||||
- Nur die IP-Adresse eingeben, ohne Port (✗ `192.168.1.102:9883` → ✓ `192.168.1.102`)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Drucker nicht gefunden / kein LAN-Modus</b></summary>
|
||||
|
||||
- Am Drucker-Display: Einstellungen → LAN-Modus aktivieren
|
||||
- Drucker und Bridge müssen im selben Netzwerk sein
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Docker: Permission denied</b></summary>
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER # danach aus- und wieder einloggen
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Upgrade von 0.9.1 oder älter</b></summary>
|
||||
|
||||
Ab 0.9.2 speichert KX-Bridge die Einstellungen in `config/config.ini` statt `.env`.
|
||||
Die Migration läuft automatisch beim ersten Start nach dem Upgrade — keine Aktion nötig.
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Sicherheit
|
||||
|
||||
- Die Bridge ist im lokalen Netzwerk unter `http://<host-IP>:7125` erreichbar — **nicht** ins Internet exposen
|
||||
- `config/config.ini` enthält Drucker-Zugangsdaten — nicht öffentlich teilen
|
||||
- Die Zugangsdaten geben **keinen** Zugriff auf Anycubic-Cloud-Dienste
|
||||
|
||||
---
|
||||
|
||||
## 📄 Lizenz
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
KX-Bridge steht unter der **GNU General Public License v3.0** ([LICENSE](LICENSE)).
|
||||
Forks und Erweiterungen müssen bei Weitergabe ebenfalls unter GPLv3 stehen.
|
||||
|
||||
Die MQTT-Protokoll-Implementierung ist das Ergebnis unabhängiger
|
||||
Reverse-Engineering-Arbeit zur Herstellung der Interoperabilität (§69e UrhG /
|
||||
EU-Softwarerichtlinie Art. 6). Drittmaterial im Repository (Anycubic-
|
||||
TLS-Zertifikate) fällt **nicht** unter die GPLv3 und ist ausschließlich
|
||||
enthalten, um die Authentifizierung am eigenen Drucker zu ermöglichen.
|
||||
Details + Disclaimer in [NOTICE.md](NOTICE.md).
|
||||
|
||||
Dieses Projekt ist unabhängig und steht in keinem Zusammenhang mit Anycubic.
|
||||
|
||||
<div align="center">
|
||||
<br>
|
||||
|
||||
**Wenn dir KX-Bridge hilft, freut sich das Projekt über Unterstützung:**
|
||||
|
||||
[](https://ko-fi.com/viewitde)
|
||||
|
||||
</div>
|
||||
256
README.en.md
256
README.en.md
@@ -1,256 +0,0 @@
|
||||
# KX-Bridge – Anycubic Kobra X Moonraker Bridge
|
||||
|
||||
**Version:** 0.9.1-beta10
|
||||
**Status:** Public Beta – suitable for home users, feedback welcome
|
||||
|
||||
KX-Bridge is a Moonraker-compatible HTTP/WebSocket bridge for the **Anycubic Kobra X** 3D printer. It allows you to control the printer through OrcaSlicer and other Moonraker-compatible software — no Klipper, no Raspberry Pi required.
|
||||
|
||||
---
|
||||
|
||||
## What's supported?
|
||||
|
||||
- Printer status (temperature, progress, state)
|
||||
- File transfer and print start
|
||||
- Print control: pause, resume, cancel
|
||||
- Temperature control during an active print
|
||||
- Print speed (Silent / Normal / Sport)
|
||||
- AMS filament change (load / unload)
|
||||
- Light and fan control
|
||||
- Web UI with dashboard, temperature cards, axis control, and camera view
|
||||
- Settings and self-update directly in the browser (⚙ menu)
|
||||
- OrcaSlicer connection (Moonraker protocol)
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- Anycubic Kobra X on your local network, with **LAN mode enabled** (printer menu → enable LAN mode)
|
||||
- Printer MQTT credentials (→ see [Extracting credentials](#extracting-credentials))
|
||||
- Docker **or** Python 3.9+ **or** the pre-built Linux binary
|
||||
|
||||
---
|
||||
|
||||
## Quick start – Docker (recommended)
|
||||
|
||||
```bash
|
||||
# 1. Start the bridge
|
||||
./start.sh
|
||||
```
|
||||
|
||||
`start.sh` builds the Docker image automatically on first run and starts the bridge.
|
||||
|
||||
```
|
||||
# 2. Open the web UI: http://BRIDGE-IP:7125
|
||||
# → Settings (⚙) open automatically on first start
|
||||
# → Enter your credentials (→ see Extracting credentials)
|
||||
|
||||
# 3. In OrcaSlicer: add printer → "Moonraker" → http://BRIDGE-IP:7125
|
||||
```
|
||||
|
||||
Check logs:
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
Stop:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick start – Binary (Linux)
|
||||
|
||||
```bash
|
||||
chmod +x kx-bridge
|
||||
./kx-bridge
|
||||
```
|
||||
|
||||
Open the web UI: `http://localhost:7125`
|
||||
→ Settings (⚙) open automatically and guide you through the initial setup.
|
||||
|
||||
---
|
||||
|
||||
## Quick start – Python directly
|
||||
|
||||
```bash
|
||||
pip install aiohttp
|
||||
python kobrax_moonraker_bridge.py
|
||||
```
|
||||
|
||||
Open the web UI: `http://localhost:7125`
|
||||
→ Settings (⚙) open automatically on first start.
|
||||
|
||||
---
|
||||
|
||||
## Extracting credentials
|
||||
|
||||
The MQTT credentials are printer-specific and are generated on first connection with AnycubicSlicerNext. The `extract_credentials` tool reads them from the memory of the running slicer.
|
||||
|
||||
**Requirement:** AnycubicSlicerNext must be running and connected to the printer (printer status is shown).
|
||||
|
||||
### Windows
|
||||
|
||||
```
|
||||
extract_credentials.exe
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
chmod +x extract_credentials
|
||||
./extract_credentials
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
```
|
||||
[*] Process found: AnycubicSlicerNext.exe (PID 1234)
|
||||
[*] 1986 memory segments read (738.8 MB)
|
||||
[*] Analyzing ... 100% (739 MB)
|
||||
|
||||
=======================================================
|
||||
RESULTS
|
||||
=======================================================
|
||||
Username userXXXXXXXXXX (hits: 47)
|
||||
Password *************** (hits: 1046)
|
||||
Device-ID xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (hits: 3504)
|
||||
Printer IP 192.168.x.x (hits: 3036)
|
||||
=======================================================
|
||||
```
|
||||
|
||||
Enter the displayed values in the bridge settings:
|
||||
Open web UI → **⚙ Settings** → fill in the fields → **Save & Restart**
|
||||
|
||||
> If the result looks uncertain: `--verbose` shows all found candidates.
|
||||
|
||||
All credentials are **processed locally only** — nothing is sent to external servers.
|
||||
|
||||
---
|
||||
|
||||
## Configuration (.env)
|
||||
|
||||
```env
|
||||
PRINTER_IP=192.168.x.x # Printer IP address
|
||||
MQTT_PORT=9883 # Default, do not change
|
||||
MQTT_USERNAME=userXXXXXXXX # Starts with "user"
|
||||
MQTT_PASSWORD=XXXXXXXXXXXXXX # ~15 characters, mixed case
|
||||
DEVICE_ID=xxxxxxxx... # 32-character hex string
|
||||
MODE_ID=20030 # Kobra X default
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OrcaSlicer setup
|
||||
|
||||
1. Add printer → **Anycubic Kobra X** (or generic Klipper printer)
|
||||
2. Connection type: **Moonraker**
|
||||
3. Host: `http://BRIDGE-HOST:7125`
|
||||
4. Test connection → should show "Online"
|
||||
|
||||
---
|
||||
|
||||
## Web UI
|
||||
|
||||
The bridge serves a web interface at `http://BRIDGE-HOST:7125`:
|
||||
|
||||
| Section | Function |
|
||||
|---------|----------|
|
||||
| Dashboard | Printer status, progress, temperature overview |
|
||||
| Temperatures | Set nozzle and bed temperature directly |
|
||||
| Motion | X/Y/Z movement, motor release |
|
||||
| Print Speed | Silent / Normal / Sport |
|
||||
| Fan / Light | Fan speed and work light |
|
||||
| AMS | Load / unload filament |
|
||||
| Camera | Live preview (if supported by printer) |
|
||||
| ⚙ Settings | MQTT credentials, poll interval, self-update |
|
||||
|
||||
### Self-update
|
||||
|
||||
The ⚙ menu in the web UI lets you check for new versions and update the bridge in place — no reinstallation needed. After the download the bridge restarts automatically with the new version.
|
||||
|
||||
---
|
||||
|
||||
## bridge.sh (Linux service manager)
|
||||
|
||||
```bash
|
||||
./bridge.sh start # Start bridge in background
|
||||
./bridge.sh stop # Stop bridge
|
||||
./bridge.sh restart # Restart
|
||||
./bridge.sh status # Check status and port
|
||||
./bridge.sh log 50 # Show last 50 log lines
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Printer states
|
||||
|
||||
The bridge translates internal Kobra states into Moonraker-compatible states:
|
||||
|
||||
| Kobra state | Meaning |
|
||||
|-------------|---------|
|
||||
| free | Ready |
|
||||
| printing / busy | Printing |
|
||||
| pausing / paused | Paused |
|
||||
| resuming / resumed | Resuming |
|
||||
| stopping / stoped | Stopping |
|
||||
| finished | Complete |
|
||||
| canceled | Cancelled |
|
||||
| failed | Error |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Port 7125 already in use:**
|
||||
```bash
|
||||
./bridge.sh stop # or: fuser -k 7125/tcp
|
||||
./bridge.sh start
|
||||
```
|
||||
|
||||
**Invalid credentials / connection refused:**
|
||||
- Start AnycubicSlicerNext, connect to the printer, then run `extract_credentials` again
|
||||
- If the result looks uncertain: `extract_credentials --verbose` shows all candidates
|
||||
- Manually enter the correct candidate in `.env` and restart the bridge
|
||||
|
||||
**Temperature changes are ignored:**
|
||||
- During an active print, temperature changes are sent via a separate channel — this is normal and the bridge handles it automatically.
|
||||
|
||||
**Docker: Permission denied:**
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
# Log out and back in
|
||||
```
|
||||
|
||||
**Docker: .env not found:**
|
||||
```bash
|
||||
# .env must be in the same directory as docker-compose.yml
|
||||
cp .env.example .env && nano .env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker compose logs -f kx-bridge
|
||||
|
||||
# Binary / Python
|
||||
tail -f /tmp/bridge.log # when using bridge.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security notes
|
||||
|
||||
- The bridge binds to `0.0.0.0:7125` by default — use on your local network only
|
||||
- `.env` contains printer credentials — do not share publicly
|
||||
- The credentials are printer-specific and have no access to Anycubic cloud services
|
||||
|
||||
---
|
||||
|
||||
## License & legal
|
||||
|
||||
This project was created through interoperability research under §69e UrhG (German copyright law).
|
||||
For private, non-commercial use only.
|
||||
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>
|
||||
418
README.md
418
README.md
@@ -1,256 +1,254 @@
|
||||
# KX-Bridge – Anycubic Kobra X Moonraker Bridge
|
||||
<div align="center">
|
||||
|
||||
**Version:** 0.9.1-beta10
|
||||
**Status:** Public Beta – für Heimanwender geeignet, Feedback willkommen
|
||||
<img src="knlogo.png" alt="KX-Bridge" width="160"/>
|
||||
|
||||
KX-Bridge ist eine Moonraker-kompatible HTTP/WebSocket-Bridge für den **Anycubic Kobra X** 3D-Drucker. Sie ermöglicht die Steuerung des Druckers über OrcaSlicer und andere Moonraker-kompatible Software, ohne dass Klipper oder ein Raspberry Pi benötigt wird.
|
||||
# KX-Bridge
|
||||
|
||||
**Control your Anycubic Kobra X with OrcaSlicer — no Klipper, no Raspberry Pi.**
|
||||
|
||||
A Moonraker-compatible bridge that talks directly to the printer.
|
||||
|
||||
<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>
|
||||
|
||||
[](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>Like KX-Bridge? A coffee on <a href="https://ko-fi.com/viewitde">Ko-fi</a> keeps the project alive. ☕</sub>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Was wird unterstützt?
|
||||
## ✨ Features
|
||||
|
||||
- Druckerstatus (Temperatur, Fortschritt, Zustand)
|
||||
- Dateiübertragung und Druckstart
|
||||
- Drucksteuerung: Pause, Fortsetzen, Abbrechen
|
||||
- Temperaturregelung während des laufenden Drucks
|
||||
- Druckgeschwindigkeit (Leise / Normal / Sport)
|
||||
- AMS-Farbwechsel (Einziehen / Ausziehen)
|
||||
- Licht- und Lüftersteuerung
|
||||
- Web-UI mit Dashboard, Temperaturkarten, Achsensteuerung und Kameraansicht
|
||||
- Einstellungen und Self-Update direkt im Browser (⚙-Menü)
|
||||
- OrcaSlicer-Verbindung (Moonraker-Protokoll)
|
||||
| | |
|
||||
|---|---|
|
||||
| 🖨️ | **Printer control** — start, pause, resume, cancel, temperatures, print speed |
|
||||
| 📊 | **Live status** — temperature, progress, layers, remaining time, camera stream |
|
||||
| 🎨 | **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); pair with the [OrcaSlicer-KX build](#-recommended-slicer) for proper per-slot vendor matching |
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
## 🚀 Quick Start
|
||||
|
||||
- Anycubic Kobra X im lokalen Netzwerk, mit aktiviertem **LAN-Modus** (Drucker-Menü → LAN-Modus einschalten)
|
||||
- MQTT-Credentials des Druckers (→ siehe [Credentials extrahieren](#credentials-extrahieren))
|
||||
- Docker **oder** Python 3.9+ **oder** direkt die Linux-Binary
|
||||
### 1. Prepare the printer
|
||||
|
||||
Enable LAN mode on the Kobra X:
|
||||
**Printer display → Settings → Enable LAN mode**
|
||||
|
||||
### 2. Start the bridge
|
||||
|
||||
**Docker (recommended):**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Linux binary (no Docker):**
|
||||
```bash
|
||||
chmod +x kx-bridge && ./kx-bridge
|
||||
```
|
||||
|
||||
**Windows EXE (no Docker):**
|
||||
```
|
||||
kx-bridge.exe
|
||||
```
|
||||
> `config\` and `data\` are created next to the EXE — portable.
|
||||
|
||||
> With the Linux and Windows binaries, `config/` and `data/` (settings, SQLite, GCode store)
|
||||
> live next to the program. Copy the whole folder = move the installation.
|
||||
|
||||
**Python directly:**
|
||||
```bash
|
||||
pip install -r bridge/requirements.txt
|
||||
python bridge/kobrax_moonraker_bridge.py
|
||||
```
|
||||
|
||||
### 3. Set up the printer
|
||||
|
||||
Open the Web UI: **`http://BRIDGE-IP:7125`**
|
||||
|
||||
On first start the **Printers tab** shows *"+ Add printer"* — just enter the printer's IP
|
||||
address, the rest (username, password, device ID) is fetched from the printer and decrypted
|
||||
automatically. Done.
|
||||
|
||||
> More than one printer? Just click *"+ Add printer"* again — each gets its own port
|
||||
> (7125, 7126, …) and is selectable from the header dropdown.
|
||||
|
||||
### 4. Connect OrcaSlicer
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
## 🎨 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.
|
||||
|
||||
---
|
||||
|
||||
## Schnellstart – Docker (empfohlen)
|
||||
## 🏠 Community & Integrations
|
||||
|
||||
- **[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.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Getting credentials manually
|
||||
|
||||
Normally not needed — *"+ Add printer"* does this automatically. If you do need it:
|
||||
|
||||
```bash
|
||||
# 1. Bridge starten
|
||||
./start.sh
|
||||
fetch_credentials --ip 192.168.x.x --write-config
|
||||
```
|
||||
Fetches the credentials directly from the printer via HTTP and writes them to `config/config.ini`.
|
||||
Only the printer IP is required, no slicer.
|
||||
|
||||
`start.sh` baut das Docker-Image automatisch beim ersten Aufruf und startet die Bridge.
|
||||
Alternatively (if the IP is unknown): open AnycubicSlicerNext, connect the printer, then run
|
||||
`extract_credentials` → outputs username, password, device ID and the printer IP.
|
||||
|
||||
```
|
||||
# 2. Web-UI öffnen: http://BRIDGE-IP:7125
|
||||
# → Einstellungen (⚙) öffnen sich automatisch beim ersten Start
|
||||
# → Zugangsdaten eintragen (→ siehe Credentials extrahieren)
|
||||
|
||||
# 3. In OrcaSlicer: Drucker → "Moonraker" → http://BRIDGE-IP:7125
|
||||
```
|
||||
|
||||
Logs prüfen:
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
Stoppen:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
> **Downloads:** [Releases](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases) → `fetch_credentials` / `extract_credentials` (Linux & Windows)
|
||||
|
||||
---
|
||||
|
||||
## Schnellstart – Binary (Linux)
|
||||
## ⚙️ Useful commands
|
||||
|
||||
```bash
|
||||
chmod +x kx-bridge
|
||||
./kx-bridge
|
||||
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)
|
||||
```
|
||||
|
||||
Web-UI öffnen: `http://localhost:7125`
|
||||
→ Einstellungen (⚙) öffnen sich automatisch und führen durch die Erstkonfiguration.
|
||||
|
||||
---
|
||||
|
||||
## Schnellstart – Python direkt
|
||||
## 🩹 Troubleshooting
|
||||
|
||||
<details>
|
||||
<summary><b>"Wrong MQTT credentials" on start</b></summary>
|
||||
|
||||
- Re-add the printer via *"+ Add printer"*, or run
|
||||
`fetch_credentials --ip <ip> --write-config` and restart the bridge
|
||||
- Enter only the IP address, no port (✗ `192.168.1.102:9883` → ✓ `192.168.1.102`)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Printer not found / no LAN mode</b></summary>
|
||||
|
||||
- On the printer display: Settings → Enable LAN mode
|
||||
- Printer and bridge must be on the same network
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Docker: Permission denied</b></summary>
|
||||
|
||||
```bash
|
||||
pip install aiohttp
|
||||
python kobrax_moonraker_bridge.py
|
||||
sudo usermod -aG docker $USER # then log out and back in
|
||||
```
|
||||
</details>
|
||||
|
||||
Web-UI öffnen: `http://localhost:7125`
|
||||
→ Einstellungen (⚙) öffnen sich automatisch beim ersten Start.
|
||||
<details>
|
||||
<summary><b>Upgrading from 0.9.1 or earlier</b></summary>
|
||||
|
||||
Starting with 0.9.2, KX-Bridge stores settings in `config/config.ini` instead of `.env`.
|
||||
Migration runs automatically on first start after the upgrade — no action required.
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Credentials extrahieren
|
||||
## 🔒 Security
|
||||
|
||||
Die MQTT-Zugangsdaten sind druckerspezifisch und werden beim ersten Verbindungsaufbau mit dem AnycubicSlicerNext generiert. Das Tool `extract_credentials` liest sie aus dem RAM des laufenden Slicers aus.
|
||||
|
||||
**Voraussetzung:** AnycubicSlicerNext muss gestartet und mit dem Drucker verbunden sein (Drucker-Status wird angezeigt).
|
||||
|
||||
### Windows
|
||||
|
||||
```
|
||||
extract_credentials.exe
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
chmod +x extract_credentials
|
||||
./extract_credentials
|
||||
```
|
||||
|
||||
### Ausgabe
|
||||
|
||||
```
|
||||
[*] Prozess gefunden: AnycubicSlicerNext.exe (PID 1234)
|
||||
[*] 1986 Speichersegmente gelesen (738.8 MB)
|
||||
[*] Analysiere ... 100% (739 MB)
|
||||
|
||||
=======================================================
|
||||
ERGEBNISSE
|
||||
=======================================================
|
||||
Username userXXXXXXXXXX (Treffer: 47)
|
||||
Password *************** (Treffer: 1046)
|
||||
Device-ID xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (Treffer: 3504)
|
||||
Drucker-IP 192.168.x.x (Treffer: 3036)
|
||||
=======================================================
|
||||
```
|
||||
|
||||
Die angezeigten Werte in die Bridge-Einstellungen übertragen:
|
||||
Web-UI öffnen → **⚙ Einstellungen** → Felder ausfüllen → **Speichern & Neustart**
|
||||
|
||||
> Falls das Ergebnis unsicher wirkt: `--verbose` zeigt alle gefundenen Kandidaten.
|
||||
|
||||
Alle Credentials werden **ausschließlich lokal verarbeitet** — keine Übertragung an externe Server.
|
||||
- The bridge is reachable on the local network at `http://<host-IP>:7125` — **do not** expose it to the internet
|
||||
- `config/config.ini` contains printer credentials — do not share publicly
|
||||
- The credentials do **not** grant access to Anycubic cloud services
|
||||
|
||||
---
|
||||
|
||||
## Konfiguration (.env)
|
||||
## 📄 License
|
||||
|
||||
```env
|
||||
PRINTER_IP=192.168.x.x # IP des Druckers
|
||||
MQTT_PORT=9883 # Standard, nicht ändern
|
||||
MQTT_USERNAME=userXXXXXXXX # Beginnt mit "user"
|
||||
MQTT_PASSWORD=XXXXXXXXXXXXXX # ~15 Zeichen, gemischt
|
||||
DEVICE_ID=xxxxxxxx... # 32-stelliger Hex-String
|
||||
MODE_ID=20030 # Kobra X Standard
|
||||
```
|
||||
[](LICENSE)
|
||||
|
||||
---
|
||||
KX-Bridge is released under the **GNU General Public License v3.0**. See
|
||||
[LICENSE](LICENSE) for the full text. Forks and modifications must remain
|
||||
under GPLv3 if redistributed.
|
||||
|
||||
## OrcaSlicer verbinden
|
||||
The MQTT protocol implementation is the result of independent
|
||||
reverse-engineering for interoperability purposes (§69e UrhG / EU Software
|
||||
Directive Art. 6). Third-party material in the repository (Anycubic TLS
|
||||
certificates) is **not** covered by GPLv3 and is included solely to enable
|
||||
authentication against printers the end-user already owns. See
|
||||
[NOTICE.md](NOTICE.md) for details and disclaimer.
|
||||
|
||||
1. Drucker hinzufügen → **Anycubic Kobra X** (oder generischer Klipper-Drucker)
|
||||
2. Verbindungstyp: **Moonraker**
|
||||
3. IP: `http://BRIDGE-HOST:7125`
|
||||
4. Verbindung testen → sollte "Online" anzeigen
|
||||
This project is independent and not affiliated with Anycubic.
|
||||
|
||||
---
|
||||
<div align="center">
|
||||
<br>
|
||||
|
||||
## Web-UI
|
||||
**If KX-Bridge helps you, the project appreciates your support:**
|
||||
|
||||
Die Bridge stellt unter `http://BRIDGE-HOST:7125` eine Web-Oberfläche bereit:
|
||||
[](https://ko-fi.com/viewitde)
|
||||
|
||||
| Bereich | Funktion |
|
||||
|---------|----------|
|
||||
| Dashboard | Druckerstatus, Fortschritt, Temperaturübersicht |
|
||||
| Temperaturen | Nozzle und Bett direkt setzen |
|
||||
| Achsen | X/Y/Z-Bewegung, Motorfreigabe |
|
||||
| Druckgeschwindigkeit | Leise / Normal / Sport |
|
||||
| Lüfter / Licht | Lüfterdrehzahl und Drucklicht |
|
||||
| AMS | Filament einziehen / ausziehen |
|
||||
| Kamera | Live-Vorschau (falls Drucker unterstützt) |
|
||||
| ⚙ Einstellungen | MQTT-Zugangsdaten, Poll-Intervall, Self-Update |
|
||||
|
||||
### Self-Update
|
||||
|
||||
Über das ⚙-Menü in der Web-UI kann die Bridge auf neue Versionen prüfen und sich selbst aktualisieren — ohne Neuinstallation. Nach dem Download startet die Bridge automatisch mit der neuen Version neu.
|
||||
|
||||
---
|
||||
|
||||
## bridge.sh (Linux Service-Manager)
|
||||
|
||||
```bash
|
||||
./bridge.sh start # Bridge im Hintergrund starten
|
||||
./bridge.sh stop # Bridge beenden
|
||||
./bridge.sh restart # Neustarten
|
||||
./bridge.sh status # Status und Port prüfen
|
||||
./bridge.sh log 50 # Letzte 50 Log-Zeilen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Druckerzustände
|
||||
|
||||
Die Bridge übersetzt die internen Kobra-Zustände in Moonraker-kompatible Zustände:
|
||||
|
||||
| Kobra-Zustand | Bedeutung |
|
||||
|---------------|-----------|
|
||||
| free | Bereit |
|
||||
| printing / busy | Druckt |
|
||||
| pausing / paused | Pausiert |
|
||||
| resuming / resumed | Wird fortgesetzt |
|
||||
| stopping / stoped | Wird gestoppt |
|
||||
| finished | Abgeschlossen |
|
||||
| canceled | Abgebrochen |
|
||||
| failed | Fehler |
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
**Port 7125 bereits belegt:**
|
||||
```bash
|
||||
./bridge.sh stop # oder: fuser -k 7125/tcp
|
||||
./bridge.sh start
|
||||
```
|
||||
|
||||
**Credentials ungültig / Verbindung abgelehnt:**
|
||||
- AnycubicSlicerNext starten, mit Drucker verbinden, `extract_credentials` erneut ausführen
|
||||
- Falls das Ergebnis unsicher wirkt: `extract_credentials --verbose` zeigt alle Kandidaten an
|
||||
- Den richtigen Kandidaten manuell in `.env` eintragen und Bridge neu starten
|
||||
|
||||
**Temperaturänderungen werden ignoriert:**
|
||||
- Während eines laufenden Drucks werden Temperaturänderungen über einen separaten Kanal gesendet — das ist normal und wird von der Bridge automatisch erkannt.
|
||||
|
||||
**Docker: Permission denied:**
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
# Neu einloggen
|
||||
```
|
||||
|
||||
**Docker: .env nicht gefunden:**
|
||||
```bash
|
||||
# .env muss im gleichen Verzeichnis wie docker-compose.yml liegen
|
||||
cp .env.example .env && nano .env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker compose logs -f kx-bridge
|
||||
|
||||
# Binary / Python
|
||||
tail -f /tmp/bridge.log # bei Nutzung von bridge.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sicherheitshinweise
|
||||
|
||||
- Die Bridge bindet standardmäßig auf `0.0.0.0:7125` — nur im lokalen Netzwerk nutzen
|
||||
- `.env` enthält Drucker-Credentials — nicht öffentlich teilen
|
||||
- Die Credentials sind druckerspezifisch und haben keinen Zugang zu Anycubic-Cloud-Diensten
|
||||
|
||||
---
|
||||
|
||||
## Lizenz & Rechtliches
|
||||
|
||||
Dieses Projekt entstand durch Interoperabilitätsforschung gem. §69e UrhG.
|
||||
Ausschließlich für private, nicht-kommerzielle Nutzung.
|
||||
</div>
|
||||
|
||||
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`
|
||||
95
config.ini.example
Normal file
95
config.ini.example
Normal file
@@ -0,0 +1,95 @@
|
||||
# KX-Bridge Konfigurationsdatei
|
||||
# Kopiere diese Datei nach config.ini und trage deine Werte ein:
|
||||
# cp config.ini.example config.ini
|
||||
#
|
||||
# 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
|
||||
printer_ip = 192.168.x.x
|
||||
|
||||
# MQTT-Port (Anycubic Kobra X Standard: 9883)
|
||||
mqtt_port = 9883
|
||||
|
||||
# MQTT-Zugangsdaten (druckerspezifisch, beginnt mit "user")
|
||||
username = userXXXXXXXXXX
|
||||
password = XXXXXXXXXXXXXXX
|
||||
|
||||
# Geräte-ID (32-stelliger Hex-String, druckerspezifisch)
|
||||
device_id = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Modell-ID (Kobra X Standard: 20030)
|
||||
mode_id = 20030
|
||||
|
||||
[print]
|
||||
# Standard-AMS-Slot für Einfarbdruck (auto = alle belegten Slots, 0-3 = fixer Slot)
|
||||
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
|
||||
36
config/config.ini.example
Normal file
36
config/config.ini.example
Normal file
@@ -0,0 +1,36 @@
|
||||
# KX-Bridge Konfigurationsdatei
|
||||
# Kopiere diese Datei nach config.ini und trage deine Werte ein:
|
||||
# cp config.ini.example config.ini
|
||||
#
|
||||
# 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
|
||||
printer_ip = 192.168.x.x
|
||||
|
||||
# MQTT-Port (Anycubic Kobra X Standard: 9883)
|
||||
mqtt_port = 9883
|
||||
|
||||
# MQTT-Zugangsdaten (druckerspezifisch, beginnt mit "user")
|
||||
username = userXXXXXXXXXX
|
||||
password = XXXXXXXXXXXXXXX
|
||||
|
||||
# Geräte-ID (32-stelliger Hex-String, druckerspezifisch)
|
||||
device_id = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Modell-ID (Kobra X Standard: 20030)
|
||||
mode_id = 20030
|
||||
|
||||
[print]
|
||||
# Standard-AMS-Slot für Einfarbdruck (auto = alle belegten Slots, 0-3 = fixer Slot)
|
||||
default_ams_slot = auto
|
||||
|
||||
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
|
||||
auto_leveling = 1
|
||||
|
||||
[bridge]
|
||||
# Poll-Intervall in Sekunden
|
||||
poll_interval = 3
|
||||
324
config_loader.py
Normal file
324
config_loader.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
config_loader.py – lädt Verbindungsparameter aus config/config.ini (primär)
|
||||
oder .env (Fallback / Migration).
|
||||
Umgebungsvariablen haben immer Vorrang.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import pathlib
|
||||
import configparser
|
||||
|
||||
_BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) else pathlib.Path(__file__).parent
|
||||
|
||||
CONFIG_SECTION_CONNECTION = "connection"
|
||||
CONFIG_SECTION_PRINT = "print"
|
||||
CONFIG_SECTION_BRIDGE = "bridge"
|
||||
CONFIG_SECTION_SPOOLMAN = "spoolman"
|
||||
|
||||
|
||||
def _find_config_file() -> pathlib.Path | None:
|
||||
for base in (_BASE, _BASE.parent):
|
||||
p = base / "config" / "config.ini"
|
||||
if p.is_file():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _find_env_file() -> pathlib.Path | None:
|
||||
for base in (_BASE, _BASE.parent):
|
||||
p = base / ".env"
|
||||
if p.is_file():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _load_env_file(path: pathlib.Path):
|
||||
"""Lädt .env-Datei als Fallback – setzt nur Keys die noch nicht in os.environ sind."""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, val = line.partition("=")
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
if key and key not in os.environ:
|
||||
os.environ[key] = val
|
||||
|
||||
|
||||
def _load_config_file(path: pathlib.Path):
|
||||
"""Lädt config.ini und setzt Keys in os.environ (nur wenn nicht bereits gesetzt)."""
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
|
||||
mapping = {
|
||||
"PRINTER_IP": (CONFIG_SECTION_CONNECTION, "printer_ip"),
|
||||
"MQTT_PORT": (CONFIG_SECTION_CONNECTION, "mqtt_port"),
|
||||
"MQTT_USERNAME": (CONFIG_SECTION_CONNECTION, "username"),
|
||||
"MQTT_PASSWORD": (CONFIG_SECTION_CONNECTION, "password"),
|
||||
"MODE_ID": (CONFIG_SECTION_CONNECTION, "mode_id"),
|
||||
"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:
|
||||
try:
|
||||
val = cfg.get(section, option)
|
||||
if val:
|
||||
os.environ[env_key] = val
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
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] = {}
|
||||
with open(env_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, _, v = line.partition("=")
|
||||
env_vals[k.strip()] = v.strip()
|
||||
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg[CONFIG_SECTION_CONNECTION] = {
|
||||
"printer_ip": env_vals.get("PRINTER_IP", ""),
|
||||
"mqtt_port": env_vals.get("MQTT_PORT", "9883"),
|
||||
"username": env_vals.get("MQTT_USERNAME", ""),
|
||||
"password": env_vals.get("MQTT_PASSWORD", ""),
|
||||
"mode_id": env_vals.get("MODE_ID", ""),
|
||||
"device_id": env_vals.get("DEVICE_ID", ""),
|
||||
}
|
||||
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",
|
||||
}
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write("# KX-Bridge Konfigurationsdatei\n")
|
||||
f.write("# Automatisch migriert aus .env\n\n")
|
||||
cfg.write(f)
|
||||
|
||||
|
||||
def find_config_path() -> pathlib.Path:
|
||||
"""Gibt den Pfad zur config.ini zurück (auch wenn sie noch nicht existiert)."""
|
||||
for base in (_BASE, _BASE.parent):
|
||||
config_dir = base / "config"
|
||||
if config_dir.is_dir():
|
||||
return config_dir / "config.ini"
|
||||
return _BASE / "config" / "config.ini"
|
||||
|
||||
|
||||
# ─── Laden ───────────────────────────────────────────────────────────────────
|
||||
|
||||
_config_path = _find_config_file()
|
||||
_env_path = _find_env_file()
|
||||
|
||||
if _config_path:
|
||||
_load_config_file(_config_path)
|
||||
elif _env_path:
|
||||
# Kein config.ini vorhanden → aus .env migrieren
|
||||
_target = find_config_path()
|
||||
migrate_env_to_config(_env_path, _target)
|
||||
_load_config_file(_target)
|
||||
_config_path = _target
|
||||
|
||||
|
||||
def list_printers() -> list[dict]:
|
||||
"""Liest alle [printer_N]-Sektionen aus config.ini.
|
||||
|
||||
Jede Sektion kann folgende Keys haben:
|
||||
name, printer_ip, mqtt_port, username, password, mode_id, device_id,
|
||||
bridge_url, default_ams_slot, auto_leveling
|
||||
|
||||
Gibt eine leere Liste zurück wenn keine [printer_N]-Sektionen vorhanden sind
|
||||
(Single-Printer-Betrieb via [connection]).
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return []
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
printers: list[dict] = []
|
||||
idx = 1
|
||||
while True:
|
||||
section = f"printer_{idx}"
|
||||
if not cfg.has_section(section):
|
||||
break
|
||||
p = dict(cfg[section])
|
||||
p.setdefault("id", str(idx))
|
||||
if "mqtt_port" in p:
|
||||
try:
|
||||
p["mqtt_port"] = int(p["mqtt_port"])
|
||||
except ValueError:
|
||||
p["mqtt_port"] = 9883
|
||||
printers.append(p)
|
||||
idx += 1
|
||||
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)
|
||||
|
||||
|
||||
# Häufig verwendete Shortcuts
|
||||
PRINTER_IP = get("PRINTER_IP", "")
|
||||
MQTT_PORT = int(get("MQTT_PORT", "9883"))
|
||||
USERNAME = get("MQTT_USERNAME", "")
|
||||
PASSWORD = get("MQTT_PASSWORD", "")
|
||||
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,12 +1,15 @@
|
||||
services:
|
||||
kx-bridge:
|
||||
image: kx-bridge:latest
|
||||
build: .
|
||||
env_file: .env
|
||||
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:
|
||||
- ./.env:/app/.env
|
||||
- ./config:/app/config
|
||||
- ./data:/app/data
|
||||
- ./.env:/app/.env:ro
|
||||
ports:
|
||||
- "7125:7125"
|
||||
- "7125-7130:7125-7130"
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
|
||||
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 |
|
||||
@@ -40,9 +40,14 @@ def get(key: str, default: str = "") -> str:
|
||||
|
||||
|
||||
# Häufig verwendete Shortcuts
|
||||
PRINTER_IP = get("PRINTER_IP", "")
|
||||
MQTT_PORT = int(get("MQTT_PORT", "9883"))
|
||||
USERNAME = get("MQTT_USERNAME", "")
|
||||
PASSWORD = get("MQTT_PASSWORD", "")
|
||||
MODE_ID = get("MODE_ID", "")
|
||||
DEVICE_ID = get("DEVICE_ID", "")
|
||||
PRINTER_IP = get("PRINTER_IP", "")
|
||||
MQTT_PORT = int(get("MQTT_PORT", "9883"))
|
||||
USERNAME = get("MQTT_USERNAME", "")
|
||||
PASSWORD = get("MQTT_PASSWORD", "")
|
||||
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")))
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
"""
|
||||
extract_credentials.py – Extrahiert Anycubic LAN-MQTT-Credentials aus dem RAM
|
||||
des laufenden AnycubicSlicerNext-Prozesses.
|
||||
extract_credentials.py – Extracts Anycubic LAN-MQTT credentials from the RAM
|
||||
of the running AnycubicSlicerNext process.
|
||||
|
||||
Voraussetzungen:
|
||||
- AnycubicSlicerNext läuft und ist mit dem Drucker verbunden
|
||||
- Gleiches Benutzerkonto wie der Slicer-Prozess (kein Admin nötig)
|
||||
Requirements:
|
||||
- AnycubicSlicerNext is running and connected to the printer
|
||||
- Same user account as the slicer process (no admin required)
|
||||
|
||||
Verwendung:
|
||||
Usage:
|
||||
python3 extract_credentials.py [--write-env] [--env-file ../.env]
|
||||
|
||||
Funktionsweise:
|
||||
1. Prozess "AnycubicSlicer.exe" (Windows) bzw. "AnycubicSlicer" (Linux) finden
|
||||
2. Speicherseiten des Prozesses durchsuchen (nur r/rw, keine Exec-Pages)
|
||||
3. Nach MQTT-Credential-Patterns suchen:
|
||||
How it works:
|
||||
1. Find process "AnycubicSlicer.exe" (Windows) or "AnycubicSlicer" (Linux)
|
||||
2. Scan memory pages of the process (only r/rw, no exec pages)
|
||||
3. Search for MQTT credential patterns:
|
||||
Username: user[A-Za-z0-9]{8,12}
|
||||
Password: [A-Za-z0-9+/]{13,18}
|
||||
Drucker-IP: d{1,3}.d{1,3}.d{1,3}.d{1,3}
|
||||
4. Kandidaten nach Plausibilität filtern und ausgeben
|
||||
5. Optional: .env-Datei schreiben
|
||||
Printer IP: d{1,3}.d{1,3}.d{1,3}.d{1,3}
|
||||
4. Filter candidates by plausibility and print results
|
||||
5. Optionally write .env file
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -28,48 +28,48 @@ import sys
|
||||
import platform
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plattform-Erkennung
|
||||
# Platform detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
IS_WINDOWS = platform.system() == "Windows"
|
||||
IS_LINUX = platform.system() == "Linux"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pattern
|
||||
# Patterns
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Username: "user" + 8–12 alphanumerische Zeichen (drucker-generiert)
|
||||
# Username: "user" + 8–12 alphanumeric characters (printer-generated)
|
||||
RE_USERNAME = re.compile(rb'user[A-Za-z0-9]{8,12}(?=[^A-Za-z0-9]|$)')
|
||||
|
||||
# Password: 13–20 alphanumerische Zeichen (kein / da kein RTSP-Pfad)
|
||||
# Anycubic-Passwörter: gemischte Groß/Klein/Ziffern, kein Slash
|
||||
# Password: 13–20 alphanumeric characters (no / since no RTSP path)
|
||||
# Anycubic passwords: mixed upper/lower/digits, no slash
|
||||
RE_PASSWORD = re.compile(rb'[A-Za-z0-9]{13,20}(?=[^A-Za-z0-9]|$)')
|
||||
|
||||
# Kontext-Pattern: sucht Passwort das direkt nach "password" im Speicher steht
|
||||
# Context pattern: password directly following "password" in memory
|
||||
RE_PASSWORD_CTX = re.compile(rb'(?:password|passwd|Password)\x00{0,4}([A-Za-z0-9]{10,25})(?=[^A-Za-z0-9]|$)', re.IGNORECASE)
|
||||
|
||||
# Proximity-Pattern: Username gefolgt von Passwort in naher Umgebung (<512 Bytes)
|
||||
# Proximity pattern: username followed by password within close range (<512 bytes)
|
||||
RE_USER_PASS_PROXIMITY = re.compile(
|
||||
rb'(user[A-Za-z0-9]{8,12}).{1,512}?([A-Za-z0-9]{13,20})(?=[^A-Za-z0-9]|$)',
|
||||
re.DOTALL
|
||||
)
|
||||
|
||||
# IPv4-Adresse (kein localhost, kein Broadcast)
|
||||
# IPv4 address (no localhost, no broadcast)
|
||||
RE_IP = re.compile(rb'(?<![.\d])(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?![.\d])')
|
||||
|
||||
# mode_id: 5-stellige Zahl (z.B. 20030)
|
||||
# mode_id: 5-digit number (e.g. 20030)
|
||||
RE_MODE_ID = re.compile(rb'(?<!\d)(2\d{4})(?!\d)')
|
||||
|
||||
# device_id: 32 Hex-Zeichen (MD5-Format)
|
||||
# device_id: 32 hex characters (MD5 format)
|
||||
RE_DEVICE_ID = re.compile(rb'[0-9a-f]{32}(?=[^0-9a-f]|$)')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Windows – Speicher lesen via ctypes / ReadProcessMemory
|
||||
# Windows – read memory via ctypes / ReadProcessMemory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _win_find_pid(name: str) -> "int | None":
|
||||
"""Findet die PID eines Prozesses anhand des Namens (case-insensitive)."""
|
||||
"""Find the PID of a process by name (case-insensitive)."""
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
|
||||
@@ -110,15 +110,15 @@ def _win_find_pid(name: str) -> "int | None":
|
||||
|
||||
|
||||
def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
|
||||
"""Liest alle lesbaren Speicherseiten eines Windows-Prozesses."""
|
||||
"""Read all readable memory pages of a Windows process."""
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
|
||||
PROCESS_VM_READ = 0x0010
|
||||
PROCESS_VM_READ = 0x0010
|
||||
PROCESS_QUERY_INFORMATION = 0x0400
|
||||
MEM_COMMIT = 0x1000
|
||||
PAGE_NOACCESS = 0x01
|
||||
PAGE_GUARD = 0x100
|
||||
MEM_COMMIT = 0x1000
|
||||
PAGE_NOACCESS = 0x01
|
||||
PAGE_GUARD = 0x100
|
||||
|
||||
class MEMORY_BASIC_INFORMATION(ctypes.Structure):
|
||||
_fields_ = [
|
||||
@@ -134,7 +134,7 @@ def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
|
||||
k32 = ctypes.windll.kernel32
|
||||
handle = k32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
|
||||
if not handle:
|
||||
raise PermissionError(f"OpenProcess fehlgeschlagen (PID {pid}): {ctypes.GetLastError()}")
|
||||
raise PermissionError(f"OpenProcess failed (PID {pid}): {ctypes.GetLastError()}")
|
||||
|
||||
chunks = []
|
||||
addr = 0
|
||||
@@ -147,7 +147,7 @@ def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
|
||||
mbi.State != MEM_COMMIT or
|
||||
mbi.Protect & PAGE_NOACCESS or
|
||||
mbi.Protect & PAGE_GUARD or
|
||||
mbi.RegionSize > 256 * 1024 * 1024 # >256 MB überspringen
|
||||
mbi.RegionSize > 256 * 1024 * 1024 # skip >256 MB regions
|
||||
)
|
||||
if not skip:
|
||||
buf = ctypes.create_string_buffer(mbi.RegionSize)
|
||||
@@ -156,7 +156,7 @@ def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
|
||||
buf, mbi.RegionSize, ctypes.byref(read)):
|
||||
chunks.append(bytes(buf[:read.value]))
|
||||
addr += mbi.RegionSize
|
||||
if addr >= 0x7FFFFFFFFFFF: # Ende des User-Space (64-bit)
|
||||
if addr >= 0x7FFFFFFFFFFF: # end of user space (64-bit)
|
||||
break
|
||||
finally:
|
||||
k32.CloseHandle(handle)
|
||||
@@ -165,11 +165,11 @@ def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Linux – Speicher lesen via /proc/{pid}/mem
|
||||
# Linux – read memory via /proc/{pid}/mem
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _linux_find_pid(name: str) -> "int | None":
|
||||
"""Findet PID anhand des Prozessnamens in /proc."""
|
||||
"""Find PID by process name in /proc."""
|
||||
for entry in os.listdir("/proc"):
|
||||
if not entry.isdigit():
|
||||
continue
|
||||
@@ -183,7 +183,7 @@ def _linux_find_pid(name: str) -> "int | None":
|
||||
|
||||
|
||||
def _linux_read_memory(pid: int) -> "list[bytes]":
|
||||
"""Liest lesbare Speichersegmente aus /proc/{pid}/mem."""
|
||||
"""Read readable memory segments from /proc/{pid}/mem."""
|
||||
chunks = []
|
||||
maps_path = f"/proc/{pid}/maps"
|
||||
mem_path = f"/proc/{pid}/mem"
|
||||
@@ -193,8 +193,8 @@ def _linux_read_memory(pid: int) -> "list[bytes]":
|
||||
mem = open(mem_path, "rb")
|
||||
except PermissionError:
|
||||
raise PermissionError(
|
||||
f"Kein Zugriff auf /proc/{pid}/mem — "
|
||||
"Script als gleicher Benutzer wie der Slicer starten."
|
||||
f"No access to /proc/{pid}/mem — "
|
||||
"run the script as the same user as the slicer process."
|
||||
)
|
||||
|
||||
for line in maps:
|
||||
@@ -202,16 +202,16 @@ def _linux_read_memory(pid: int) -> "list[bytes]":
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
perms = parts[1]
|
||||
if "r" not in perms: # nur lesbare Seiten
|
||||
if "r" not in perms: # readable pages only
|
||||
continue
|
||||
if "x" in perms: # Code-Seiten überspringen (keine Strings)
|
||||
if "x" in perms: # skip code pages (no strings)
|
||||
continue
|
||||
try:
|
||||
start, end = [int(x, 16) for x in parts[0].split("-")]
|
||||
except ValueError:
|
||||
continue
|
||||
size = end - start
|
||||
if size > 256 * 1024 * 1024: # >256 MB überspringen
|
||||
if size > 256 * 1024 * 1024: # skip >256 MB regions
|
||||
continue
|
||||
try:
|
||||
mem.seek(start)
|
||||
@@ -226,7 +226,7 @@ def _linux_read_memory(pid: int) -> "list[bytes]":
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pattern-Suche
|
||||
# Pattern search
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _is_valid_ip(ip_bytes: bytes) -> bool:
|
||||
@@ -245,7 +245,7 @@ def _is_valid_ip(ip_bytes: bytes) -> bool:
|
||||
|
||||
|
||||
def search_chunks(chunks: "list[bytes]") -> dict:
|
||||
"""Durchsucht Speicher-Chunks nach Credential-Patterns."""
|
||||
"""Search memory chunks for credential patterns."""
|
||||
usernames = {} # value → count
|
||||
passwords = {}
|
||||
ips = {}
|
||||
@@ -256,12 +256,12 @@ def search_chunks(chunks: "list[bytes]") -> dict:
|
||||
if i % 50 == 0 or i == total - 1:
|
||||
pct = (i + 1) * 100 // total
|
||||
mb_done = sum(len(c) for c in chunks[:i+1]) / 1024 / 1024
|
||||
print(f"\r[*] Analysiere ... {pct:3d}% ({mb_done:.0f} MB)", end="", flush=True)
|
||||
print(f"\r[*] Scanning ... {pct:3d}% ({mb_done:.0f} MB)", end="", flush=True)
|
||||
for m in RE_USERNAME.finditer(chunk):
|
||||
v = m.group().decode("ascii", errors="replace")
|
||||
usernames[v] = usernames.get(v, 0) + 1
|
||||
|
||||
# Proximity: Passwort das innerhalb von 512 Bytes nach einem Username steht
|
||||
# Proximity: password within 512 bytes after a username
|
||||
for m in RE_USER_PASS_PROXIMITY.finditer(chunk):
|
||||
pw = m.group(2).decode("ascii", errors="replace")
|
||||
has_upper = any(c.isupper() for c in pw)
|
||||
@@ -272,7 +272,7 @@ def search_chunks(chunks: "list[bytes]") -> dict:
|
||||
|
||||
for m in RE_PASSWORD.finditer(chunk):
|
||||
v = m.group().decode("ascii", errors="replace")
|
||||
# Filter: mindestens 2 Großbuchstaben + 2 Kleinbuchstaben + 1 Ziffer
|
||||
# Filter: at least 2 uppercase + 2 lowercase + 1 digit
|
||||
has_upper = sum(1 for c in v if c.isupper()) >= 2
|
||||
has_lower = sum(1 for c in v if c.islower()) >= 2
|
||||
has_digit = sum(1 for c in v if c.isdigit()) >= 1
|
||||
@@ -289,9 +289,9 @@ def search_chunks(chunks: "list[bytes]") -> dict:
|
||||
v = m.group().decode("ascii")
|
||||
device_ids[v] = device_ids.get(v, 0) + 1
|
||||
|
||||
print() # Zeilenumbruch nach Fortschrittszeile
|
||||
print() # newline after progress line
|
||||
|
||||
# Nach Häufigkeit sortieren, häufigste zuerst
|
||||
# Sort by frequency, most frequent first
|
||||
def top(d, n=10):
|
||||
return sorted(d.items(), key=lambda x: -x[1])[:n]
|
||||
|
||||
@@ -304,7 +304,7 @@ def search_chunks(chunks: "list[bytes]") -> dict:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hauptprogramm
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SLICER_NAMES = [
|
||||
@@ -359,95 +359,95 @@ def write_env(results: dict, env_path: str,
|
||||
|
||||
with open(env_path, "w") as f:
|
||||
f.writelines(lines)
|
||||
print(f"\n✓ Credentials in '{env_path}' gespeichert.")
|
||||
print(f"\n✓ Credentials saved to '{env_path}'.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extrahiert MQTT-Credentials aus dem RAM des AnycubicSlicer-Prozesses"
|
||||
description="Extract MQTT credentials from the AnycubicSlicer process RAM"
|
||||
)
|
||||
parser.add_argument("--write-env", action="store_true",
|
||||
help="Gefundene Credentials in .env schreiben")
|
||||
help="Write found credentials to .env file")
|
||||
parser.add_argument("--env-file", default=None,
|
||||
help="Pfad zur .env-Datei (Standard: ../. env relativ zu diesem Script)")
|
||||
help="Path to .env file (default: ../.env relative to this script)")
|
||||
parser.add_argument("--pid", type=int, default=None,
|
||||
help="Prozess-PID direkt angeben (überspringt Auto-Erkennung)")
|
||||
help="Specify process PID directly (skips auto-detection)")
|
||||
parser.add_argument("--verbose", action="store_true",
|
||||
help="Alle Kandidaten ausgeben, nicht nur den besten")
|
||||
help="Show all candidates, not just the best match")
|
||||
args = parser.parse_args()
|
||||
|
||||
# .env-Pfad bestimmen
|
||||
# Determine .env path
|
||||
if args.env_file:
|
||||
env_path = args.env_file
|
||||
else:
|
||||
env_path = os.path.join(os.path.dirname(__file__), "..", ".env")
|
||||
env_path = os.path.normpath(env_path)
|
||||
|
||||
# Prozess finden
|
||||
# Find process
|
||||
if args.pid:
|
||||
pid, proc_name = args.pid, f"PID {args.pid}"
|
||||
else:
|
||||
print("[*] Suche AnycubicSlicer-Prozess ...")
|
||||
print("[*] Searching for AnycubicSlicer process ...")
|
||||
pid, proc_name = find_slicer_pid()
|
||||
if not pid:
|
||||
print("✗ AnycubicSlicer nicht gefunden. Bitte den Slicer starten und "
|
||||
"mit dem Drucker verbinden, dann erneut ausführen.")
|
||||
print("✗ AnycubicSlicer not found. Please start the slicer, connect it "
|
||||
"to the printer, then run this script again.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[*] Prozess gefunden: {proc_name} (PID {pid})")
|
||||
print(f"[*] Lese Prozess-Speicher ...")
|
||||
print(f"[*] Process found: {proc_name} (PID {pid})")
|
||||
print(f"[*] Reading process memory ...")
|
||||
|
||||
try:
|
||||
chunks = read_process(pid)
|
||||
except PermissionError as e:
|
||||
print(f"✗ Zugriffsfehler: {e}")
|
||||
print(f"✗ Permission error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
total_mb = sum(len(c) for c in chunks) / 1024 / 1024
|
||||
print(f"[*] {len(chunks)} Speichersegmente gelesen ({total_mb:.1f} MB)")
|
||||
print(f"[*] Durchsuche nach Credentials ...")
|
||||
print(f"[*] {len(chunks)} memory segments read ({total_mb:.1f} MB)")
|
||||
print(f"[*] Searching for credentials ...")
|
||||
|
||||
results = search_chunks(chunks)
|
||||
|
||||
# Ausgabe
|
||||
# Output
|
||||
print("\n" + "="*55)
|
||||
print(" ERGEBNISSE")
|
||||
print(" RESULTS")
|
||||
print("="*55)
|
||||
|
||||
def show(label, items, verbose):
|
||||
if not items:
|
||||
print(f" {label:12s} — nicht gefunden")
|
||||
print(f" {label:12s} — not found")
|
||||
return items[0][0] if items else ""
|
||||
best = items[0][0]
|
||||
print(f" {label:12s} {best} (Treffer: {items[0][1]})")
|
||||
print(f" {label:12s} {best} (matches: {items[0][1]})")
|
||||
if verbose and len(items) > 1:
|
||||
for val, cnt in items[1:]:
|
||||
print(f" {'':12s} {val} (Treffer: {cnt})")
|
||||
print(f" {'':12s} {val} (matches: {cnt})")
|
||||
return best
|
||||
|
||||
best_user = show("Username", results["usernames"], args.verbose)
|
||||
best_pass = show("Password", results["passwords"], args.verbose)
|
||||
best_device = show("Device-ID", results["device_ids"], args.verbose)
|
||||
best_user = show("Username", results["usernames"], args.verbose)
|
||||
best_pass = show("Password", results["passwords"], args.verbose)
|
||||
best_device = show("Device-ID", results["device_ids"], args.verbose)
|
||||
|
||||
# IP: 192.168.x.x bevorzugen
|
||||
# IP: prefer 192.168.x.x
|
||||
lan_ips = [(ip, cnt) for ip, cnt in results["ips"]
|
||||
if ip.startswith("192.168.") or ip.startswith("10.") or ip.startswith("172.")]
|
||||
if not lan_ips:
|
||||
lan_ips = results["ips"]
|
||||
best_ip = show("Drucker-IP", lan_ips, args.verbose)
|
||||
best_ip = show("Printer IP", lan_ips, args.verbose)
|
||||
|
||||
print("="*55)
|
||||
|
||||
if not best_user or not best_pass:
|
||||
print("\n⚠ Keine vollständigen Credentials gefunden.")
|
||||
print(" Stelle sicher dass der Slicer MIT dem Drucker verbunden ist.")
|
||||
print("\n⚠ No complete credentials found.")
|
||||
print(" Make sure the slicer is connected to the printer.")
|
||||
sys.exit(1)
|
||||
|
||||
if args.write_env:
|
||||
write_env(results, env_path, best_user, best_pass, best_ip,
|
||||
device_id=best_device)
|
||||
else:
|
||||
print(f"\nHinweis: --write-env übergeben um Credentials in '{env_path}' zu speichern.")
|
||||
print(f"\nTip: pass --write-env to save credentials to '{env_path}'.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
397
fetch_credentials.py
Normal file
397
fetch_credentials.py
Normal file
@@ -0,0 +1,397 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
fetch_credentials.py – Fetches and decrypts Anycubic Kobra X credentials via HTTP API.
|
||||
|
||||
Original approach by bebu (PR #19, KX-Bridge-Release).
|
||||
Reverse engineered from the Vue project embedded in libWorkbench.so (Anycubic Slicer Next).
|
||||
No running slicer required — only the printer IP in LAN.
|
||||
|
||||
Algorithm: AES-256-CBC
|
||||
Key: token[16:32] from /info response
|
||||
IV: response token from /ctrl response
|
||||
|
||||
Usage:
|
||||
python3 fetch_credentials.py --ip 192.168.x.x
|
||||
python3 fetch_credentials.py --ip 192.168.x.x --write-config
|
||||
python3 fetch_credentials.py --ip 192.168.x.x --write-config --config-file ../config/config.ini
|
||||
python3 fetch_credentials.py --ctrl ctrl.json --info info.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import base64
|
||||
import hashlib
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
|
||||
|
||||
def evp_bytes_to_key(password, salt, key_len, iv_len):
|
||||
"""
|
||||
Derive key and IV from password and salt using OpenSSL EVP_BytesToKey
|
||||
This mimics the CryptoJS default key derivation
|
||||
"""
|
||||
m = []
|
||||
i = 0
|
||||
while len(b''.join(m)) < (key_len + iv_len):
|
||||
md5 = hashlib.md5()
|
||||
data = password + salt
|
||||
if i > 0:
|
||||
data = m[i - 1] + password + salt
|
||||
md5.update(data)
|
||||
m.append(md5.digest())
|
||||
i += 1
|
||||
ms = b''.join(m)
|
||||
return ms[:key_len], ms[key_len:key_len + iv_len]
|
||||
|
||||
|
||||
def generate_signature(token, ts, nonce):
|
||||
"""
|
||||
Generate MD5 signature for /ctrl endpoint
|
||||
Signature = md5(md5(token[0:16]) + ts + nonce)
|
||||
"""
|
||||
# First MD5: token.slice(0, 16)
|
||||
first_md5 = hashlib.md5(token[:16].encode('utf-8')).hexdigest()
|
||||
# Second MD5: first_md5 + ts + nonce
|
||||
signature_data = first_md5 + str(ts) + nonce
|
||||
signature = hashlib.md5(signature_data.encode('utf-8')).hexdigest()
|
||||
return signature
|
||||
|
||||
|
||||
def generate_nonce(length=6):
|
||||
"""Generate a random alphanumeric nonce"""
|
||||
chars = string.ascii_letters + string.digits
|
||||
return ''.join(random.choice(chars) for _ in range(length))
|
||||
|
||||
|
||||
def fetch_from_http(ip, port, endpoint, token=None, did="random", verbose=False):
|
||||
"""
|
||||
Fetch data from HTTP endpoint on the printer
|
||||
|
||||
Args:
|
||||
ip (str): IP address of the printer
|
||||
port (int): Port number (default 18910)
|
||||
endpoint (str): Either 'info' or 'ctrl'
|
||||
token (str): Device token (required for /ctrl endpoint)
|
||||
did (str): Device ID (required for /ctrl endpoint)
|
||||
verbose (bool): Print debug information
|
||||
|
||||
Returns:
|
||||
dict: JSON response data
|
||||
"""
|
||||
try:
|
||||
if endpoint == 'info':
|
||||
url = f"http://{ip}:{port}/info"
|
||||
if verbose:
|
||||
print(f"Fetching: {url}")
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
elif endpoint == 'ctrl':
|
||||
if not token:
|
||||
raise ValueError("Token is required for /ctrl endpoint")
|
||||
|
||||
# Generate signature parameters
|
||||
ts = int(time.time() * 1000) # Current timestamp in ms
|
||||
nonce = generate_nonce(6)
|
||||
signature = generate_signature(token, ts, nonce)
|
||||
|
||||
url = f"http://{ip}:{port}/ctrl"
|
||||
params = {
|
||||
'ts': ts,
|
||||
'nonce': nonce,
|
||||
'sign': signature,
|
||||
'did': did
|
||||
}
|
||||
|
||||
if verbose:
|
||||
print(f"Fetching: {url}")
|
||||
print(f" Parameters:")
|
||||
print(f" ts: {ts}")
|
||||
print(f" nonce: {nonce}")
|
||||
print(f" sign: {signature}")
|
||||
print(f" did: {did}")
|
||||
|
||||
response = requests.post(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown endpoint: {endpoint}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise Exception(f"HTTP request failed for {endpoint}: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
raise Exception(f"Invalid JSON response from {endpoint}: {e}")
|
||||
|
||||
|
||||
def decrypt_text(encrypted_data, key, iv):
|
||||
"""
|
||||
Decrypt data using AES-256-CBC
|
||||
Handles CryptoJS-style encrypted data (OpenSSL format with salt)
|
||||
|
||||
Args:
|
||||
encrypted_data (str): Encrypted data string (CryptoJS format)
|
||||
key (str): Decryption key string
|
||||
iv (str): Initialization vector string
|
||||
|
||||
Returns:
|
||||
dict: Decrypted JSON data
|
||||
"""
|
||||
try:
|
||||
# Convert key and IV to bytes
|
||||
key_bytes = key.encode('utf-8')
|
||||
iv_bytes = iv.encode('utf-8')
|
||||
|
||||
# Decrypt using direct key and IV (as per the original code)
|
||||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
||||
|
||||
# The encrypted_data might be base64 or hex encoded
|
||||
# Try base64 first
|
||||
try:
|
||||
encrypted_bytes = base64.b64decode(encrypted_data)
|
||||
except:
|
||||
try:
|
||||
# Try as hex
|
||||
encrypted_bytes = bytes.fromhex(encrypted_data)
|
||||
except:
|
||||
# If all else fails, encode as UTF-8
|
||||
encrypted_bytes = encrypted_data.encode('utf-8')
|
||||
|
||||
# Decrypt
|
||||
decrypted = cipher.decrypt(encrypted_bytes)
|
||||
|
||||
# Try to unpad
|
||||
try:
|
||||
unpadded = unpad(decrypted, AES.block_size)
|
||||
except ValueError:
|
||||
# If unpadding fails, use as-is
|
||||
unpadded = decrypted
|
||||
|
||||
plaintext = unpadded.decode('utf-8')
|
||||
|
||||
# Parse JSON
|
||||
return json.loads(plaintext)
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e), "error_type": type(e).__name__}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to decrypt printer data"""
|
||||
|
||||
# Parse command-line arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Fetch and decrypt Anycubic Kobra X credentials via HTTP API',
|
||||
)
|
||||
# HTTP mode
|
||||
parser.add_argument('--ip', help='IP address of the printer')
|
||||
parser.add_argument('--port', type=int, default=18910, help='Printer HTTP port (default: 18910)')
|
||||
|
||||
# File mode
|
||||
parser.add_argument('--ctrl', default='ctrl.json', help='Path to ctrl.json (default: ctrl.json)')
|
||||
parser.add_argument('--info', default='info.json', help='Path to info.json (default: info.json)')
|
||||
|
||||
# Output
|
||||
parser.add_argument('--output', default=None, help='Save raw decrypted JSON to file (optional)')
|
||||
parser.add_argument('--write-config', action='store_true',
|
||||
help='Write credentials to config.ini')
|
||||
parser.add_argument('--config-file', default=None,
|
||||
help='Path to config.ini (default: ../config/config.ini relative to this script)')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Determine mode: HTTP or file
|
||||
if args.ip:
|
||||
# HTTP mode: fetch from printer
|
||||
if args.verbose:
|
||||
print("=" * 70)
|
||||
print("Fetching configuration from printer via HTTP")
|
||||
print("=" * 70)
|
||||
print(f"Printer IP: {args.ip}:{args.port}")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Fetch info.json
|
||||
if args.verbose:
|
||||
print("Step 1: Fetching device info...")
|
||||
info = fetch_from_http(args.ip, args.port, 'info', verbose=args.verbose)
|
||||
|
||||
# Get token from info
|
||||
token = info.get('token')
|
||||
if not token:
|
||||
print("Error: No token found in /info response", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Fetch data.json (encrypted config) from /ctrl endpoint
|
||||
if args.verbose:
|
||||
print("\nStep 2: Fetching encrypted configuration from /ctrl...")
|
||||
data = fetch_from_http(args.ip, args.port, 'ctrl', token=token, verbose=args.verbose)
|
||||
|
||||
if args.verbose:
|
||||
print("\nData fetched successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
else:
|
||||
# File mode: load from disk
|
||||
if args.verbose:
|
||||
print("=" * 70)
|
||||
print("Loading configuration from files")
|
||||
print("=" * 70)
|
||||
|
||||
# Check if input files exist
|
||||
if not Path(args.ctrl).exists():
|
||||
print(f"Error: {args.ctrl} not found", file=sys.stderr)
|
||||
return 1
|
||||
if not Path(args.info).exists():
|
||||
print(f"Error: {args.info} not found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Read ctrl.json
|
||||
try:
|
||||
with open(args.ctrl, 'r') as f:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error reading {args.ctrl}: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error reading {args.ctrl}: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Read info.json
|
||||
try:
|
||||
with open(args.info, 'r') as f:
|
||||
info = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error reading {args.info}: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error reading {args.info}: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Extract values
|
||||
try:
|
||||
encrypted_info = data['data']['info']
|
||||
response_token = data['data']['token']
|
||||
full_token = info['token']
|
||||
except KeyError as e:
|
||||
print(f"Error: Missing required key {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Generate decryption key and IV
|
||||
key_part = full_token[16:32]
|
||||
|
||||
if args.verbose:
|
||||
print("=" * 70)
|
||||
print("Printer Configuration Decryption")
|
||||
print("=" * 70)
|
||||
print(f"Input data file: {args.ctrl}")
|
||||
print(f"Input info file: {args.info}")
|
||||
print(f"Output file: {args.output}")
|
||||
print()
|
||||
print("Decryption Parameters:")
|
||||
print(f" Encrypted data length: {len(encrypted_info)} bytes")
|
||||
print(f" Full token: {full_token}")
|
||||
print(f" Full token length: {len(full_token)} characters")
|
||||
print(f" Response token (IV): {response_token}")
|
||||
print(f" Decryption key: {key_part}")
|
||||
print(f" Key length: {len(key_part)} characters")
|
||||
print(f" IV length: {len(response_token)} characters")
|
||||
print()
|
||||
|
||||
# Decrypt
|
||||
if args.verbose:
|
||||
print("Decrypting...")
|
||||
|
||||
result = decrypt_text(encrypted_info, key_part, response_token)
|
||||
|
||||
if 'error' in result:
|
||||
print(f"Error during decryption: {result.get('error')}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Show result
|
||||
print()
|
||||
print("=" * 55)
|
||||
print(" CREDENTIALS")
|
||||
print("=" * 55)
|
||||
print(f" {'Printer IP':12s} {result.get('ip', 'n/a')}")
|
||||
print(f" {'Username':12s} {result.get('username', 'n/a')}")
|
||||
print(f" {'Password':12s} {result.get('password', 'n/a')}")
|
||||
print(f" {'Device-ID':12s} {result.get('deviceId', 'n/a')}")
|
||||
print(f" {'Mode-ID':12s} {result.get('modeId', 'n/a')}")
|
||||
print(f" {'Model':12s} {result.get('modelName', 'n/a')}")
|
||||
print(f" {'Broker':12s} {result.get('broker', 'n/a')}")
|
||||
print("=" * 55)
|
||||
|
||||
if args.verbose:
|
||||
print()
|
||||
print("Full decrypted config:")
|
||||
# Strip certs/keys from verbose output to avoid cluttering terminal
|
||||
display = {k: v for k, v in result.items() if k not in ('devicecrt', 'devicepk')}
|
||||
print(json.dumps(display, indent=2))
|
||||
|
||||
# Optionally save raw JSON
|
||||
if args.output:
|
||||
try:
|
||||
with open(args.output, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
print(f"\nRaw config saved to: {args.output}")
|
||||
except Exception as e:
|
||||
print(f"Error writing to {args.output}: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Write config.ini
|
||||
if args.write_config:
|
||||
if args.config_file:
|
||||
config_path = args.config_file
|
||||
else:
|
||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'..', 'config', 'config.ini')
|
||||
config_path = os.path.normpath(config_path)
|
||||
|
||||
_write_config_ini(result, config_path)
|
||||
else:
|
||||
print(f"\nTip: pass --write-config to write credentials directly to config.ini")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _write_config_ini(result: dict, config_path: str):
|
||||
"""Write fetched credentials into config.ini, preserving existing non-credential keys."""
|
||||
import configparser
|
||||
|
||||
cfg = configparser.ConfigParser()
|
||||
|
||||
if os.path.isfile(config_path):
|
||||
cfg.read(config_path)
|
||||
|
||||
if not cfg.has_section('connection'):
|
||||
cfg.add_section('connection')
|
||||
|
||||
cfg.set('connection', 'printer_ip', result.get('ip', ''))
|
||||
cfg.set('connection', 'mqtt_port', '9883')
|
||||
cfg.set('connection', 'username', result.get('username', ''))
|
||||
cfg.set('connection', 'password', result.get('password', ''))
|
||||
cfg.set('connection', 'device_id', result.get('deviceId', ''))
|
||||
cfg.set('connection', 'mode_id', result.get('modeId', '20030'))
|
||||
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
with open(config_path, 'w') as f:
|
||||
cfg.write(f)
|
||||
|
||||
print(f"\n✓ Credentials written to '{config_path}'.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
BIN
knlogo.png
Normal file
BIN
knlogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
283
kobrax_client.py
283
kobrax_client.py
@@ -14,10 +14,20 @@ Verwendung:
|
||||
info = client.query_info()
|
||||
print(info["data"]["temp"])
|
||||
client.disconnect()
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
Copyright (C) 2026 viewit (KX-Bridge contributors)
|
||||
|
||||
Licensed under GPLv3 — see LICENSE in the project root.
|
||||
Protocol reverse-engineered for interoperability (§69e UrhG / EU Software
|
||||
Directive Art. 6). Not affiliated with Anycubic. See NOTICE.md.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
@@ -28,6 +38,8 @@ from datetime import datetime
|
||||
|
||||
import env_loader
|
||||
|
||||
log = logging.getLogger("kobrax.mqtt")
|
||||
|
||||
_SCRIPT_DIR = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(os.path.abspath(__file__))
|
||||
CERT_FILE = os.path.join(_SCRIPT_DIR, "anycubic_slicer.crt")
|
||||
KEY_FILE = os.path.join(_SCRIPT_DIR, "anycubic_slicer.key")
|
||||
@@ -109,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)
|
||||
@@ -119,6 +135,13 @@ class KobraXClient:
|
||||
# Optional callbacks: topic_suffix → callable(payload_dict)
|
||||
self.callbacks: dict[str, callable] = {}
|
||||
|
||||
# Dedup: last hash per topic suffix to suppress repeated identical messages
|
||||
self._last_rx_hash: dict[str, str] = {}
|
||||
# Fields that change every tick and should be stripped before dedup-hashing
|
||||
_VOLATILE = {"timestamp", "msgid", "progress", "curr_layer",
|
||||
"curr_nozzle_temp", "curr_hotbed_temp",
|
||||
"target_nozzle_temp", "target_hotbed_temp"}
|
||||
|
||||
# -- Topics --------------------------------------------------------------
|
||||
|
||||
def _pub_topic(self, msg_type: str) -> str:
|
||||
@@ -136,92 +159,191 @@ 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)
|
||||
print(f"[kobrax] TLS: {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()}")
|
||||
print(f"[kobrax] CONNACK rc=0")
|
||||
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):
|
||||
print("[kobrax] Verbindung verloren – reconnect…")
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
for delay in [2, 4, 8, 15, 30]:
|
||||
"""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…")
|
||||
# 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()
|
||||
print("[kobrax] Reconnect erfolgreich")
|
||||
log.info("Reconnect erfolgreich (nach %d Versuchen)", attempt + 1)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[kobrax] Reconnect fehlgeschlagen ({e}), warte {delay}s…")
|
||||
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))
|
||||
print(f"[kobrax] SUB {topic}")
|
||||
if self._sock is not None:
|
||||
self._sock.sendall(_build_subscribe(topic, pid))
|
||||
log.info("SUB %s", topic)
|
||||
|
||||
# -- Read loop -----------------------------------------------------------
|
||||
|
||||
def _read_loop(self):
|
||||
last_ping = time.time()
|
||||
_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:
|
||||
raise ConnectionResetError("EOF")
|
||||
# Windows SSL kann kurzzeitig b"" liefern ohne echten EOF
|
||||
_empty_count += 1
|
||||
if _empty_count >= 5:
|
||||
raise ConnectionResetError("EOF")
|
||||
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:
|
||||
continue
|
||||
except Exception as e:
|
||||
if self._running:
|
||||
print(f"[kobrax] reader error: {e}")
|
||||
log.warning("reader error: %s", e)
|
||||
if not self._reconnect():
|
||||
break
|
||||
last_ping = time.time()
|
||||
@@ -260,9 +382,40 @@ class KobraXClient:
|
||||
|
||||
self._buf = buf[idx:]
|
||||
|
||||
def _dedup_hash(self, suffix: str, payload: dict) -> str:
|
||||
"""Hash payload ignoring volatile per-tick fields for dedup check."""
|
||||
stable = {k: v for k, v in payload.items()
|
||||
if k not in {"timestamp", "msgid", "progress", "curr_layer",
|
||||
"curr_nozzle_temp", "curr_hotbed_temp",
|
||||
"target_nozzle_temp", "target_hotbed_temp"}}
|
||||
return hashlib.md5(json.dumps(stable, sort_keys=True).encode(), usedforsecurity=False).hexdigest()
|
||||
|
||||
def _dispatch(self, topic: str, payload: dict):
|
||||
# Resolve by report topic suffix (e.g. "info/report")
|
||||
suffix = "/".join(topic.split("/")[-2:])
|
||||
|
||||
# Structured RX log with dedup suppression
|
||||
h = self._dedup_hash(suffix, payload)
|
||||
is_dup = self._last_rx_hash.get(suffix) == h
|
||||
self._last_rx_hash[suffix] = h
|
||||
if is_dup:
|
||||
log.debug("RX [dup] %-25s state=%-12s", suffix, payload.get("state", ""))
|
||||
else:
|
||||
data = payload.get("data") or {}
|
||||
state = payload.get("state", "")
|
||||
if "progress" in data:
|
||||
log.info("RX %-25s state=%-12s progress=%s%% layer=%s/%s",
|
||||
suffix, state, data["progress"],
|
||||
data.get("curr_layer", "?"), data.get("total_layers", "?"))
|
||||
elif "curr_nozzle_temp" in data:
|
||||
log.info("RX %-25s nozzle=%s°C/%s°C bed=%s°C/%s°C",
|
||||
suffix,
|
||||
data["curr_nozzle_temp"], data.get("target_nozzle_temp", 0),
|
||||
data.get("curr_hotbed_temp", "?"), data.get("target_hotbed_temp", 0))
|
||||
else:
|
||||
log.info("RX %-25s state=%-12s data=%s",
|
||||
suffix, state, json.dumps(payload.get("data"), ensure_ascii=False))
|
||||
|
||||
# Resolve by report topic suffix (e.g. "info/report")
|
||||
if suffix in self._pending_report:
|
||||
entry = self._pending_report[suffix]
|
||||
entry["result"] = payload
|
||||
@@ -276,23 +429,25 @@ class KobraXClient:
|
||||
entry["event"].set()
|
||||
|
||||
# User callbacks by topic suffix (last two path components)
|
||||
suffix = "/".join(topic.split("/")[-2:])
|
||||
if suffix in self.callbacks:
|
||||
try:
|
||||
self.callbacks[suffix](payload)
|
||||
except Exception as e:
|
||||
print(f"[kobrax] callback error for {suffix}: {e}")
|
||||
log.error("callback error for %s: %s", suffix, e)
|
||||
|
||||
# Generic wildcard callback
|
||||
if "*" in self.callbacks:
|
||||
try:
|
||||
self.callbacks["*"](topic, payload)
|
||||
except Exception as e:
|
||||
print(f"[kobrax] wildcard callback error: {e}")
|
||||
log.error("wildcard callback error: %s", e)
|
||||
|
||||
# -- 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,
|
||||
@@ -316,11 +471,17 @@ class KobraXClient:
|
||||
report_registered = True
|
||||
|
||||
topic = self._pub_topic(msg_type)
|
||||
# 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))
|
||||
except Exception as e:
|
||||
print(f"[kobrax] send error: {e}, reconnecting…")
|
||||
log.error("send error: %s, reconnecting…", e)
|
||||
self._pending_msgid.pop(msgid, None)
|
||||
if report_registered:
|
||||
self._pending_report.pop(report_key, None)
|
||||
@@ -352,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,
|
||||
@@ -361,11 +523,21 @@ class KobraXClient:
|
||||
"data": data,
|
||||
}, separators=(",", ":"))
|
||||
topic = self._web_topic(msg_type)
|
||||
log.info("TX(web) %-23s 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))
|
||||
except Exception as e:
|
||||
print(f"[kobrax] web send error: {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 -------------------------------------------------
|
||||
|
||||
@@ -404,6 +576,20 @@ class KobraXClient:
|
||||
def stop_print(self, taskid: str = "-1") -> dict | None:
|
||||
return self.publish("print", "stop", {"taskid": taskid})
|
||||
|
||||
# -- Part-Skip ("Exclude Object") ---------------------------------------
|
||||
|
||||
def query_skip_objects(self) -> dict | None:
|
||||
"""Fragt den Drucker nach der aktuellen Objekt-/Skip-Liste."""
|
||||
return self.publish("skip", "query_obj")
|
||||
|
||||
def skip_objects(self, names: list[str]) -> dict | None:
|
||||
"""Überspringt die genannten Objekte – auch mid-print möglich.
|
||||
|
||||
Namen entsprechen den EXCLUDE_OBJECT_DEFINE NAME=… Einträgen
|
||||
im GCode-Header bzw. file_details.objects_skip_parts.
|
||||
"""
|
||||
return self.publish("skip", "start", {"objects_skip_parts": list(names)})
|
||||
|
||||
# -- G-Code Upload -------------------------------------------------------
|
||||
|
||||
def upload_gcode(self, filepath: str, remote_filename: str | None = None,
|
||||
@@ -467,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(10)
|
||||
sock.settimeout(180)
|
||||
response = b""
|
||||
try:
|
||||
while True:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
45
kx-bridge.spec
Normal file
45
kx-bridge.spec
Normal file
@@ -0,0 +1,45 @@
|
||||
# PyInstaller-Spec für kx-bridge — plattformneutral (Linux + Windows via PyBuilder).
|
||||
# Wird relativ zum Repo-Root ausgeführt (`pyinstaller kx-bridge.spec`), wo
|
||||
# kobrax_moonraker_bridge.py und web/ flach liegen (Release-Repo-Layout).
|
||||
#
|
||||
# Bindet das Web-Theme-System (web/themes/<name>/ + web/DOC/) ins Onefile-Binary
|
||||
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas = [("web", "web"), ("data", "static"), ("VERSION", ".")] # bridge/data/ → static/ im _MEIPASS
|
||||
binaries = []
|
||||
hiddenimports = []
|
||||
|
||||
# pycryptodome vollständig einsammeln (Krypto für die Drucker-Auth)
|
||||
_d, _b, _h = collect_all("pycryptodome")
|
||||
datas += _d
|
||||
binaries += _b
|
||||
hiddenimports += _h
|
||||
|
||||
a = Analysis(
|
||||
["kobrax_moonraker_bridge.py"],
|
||||
pathex=[],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name="kx-bridge",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
console=True,
|
||||
onefile=True,
|
||||
)
|
||||
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
|
||||
@@ -1 +1,4 @@
|
||||
aiohttp>=3.9
|
||||
imageio-ffmpeg>=0.4.9
|
||||
requests>=2.30.0
|
||||
pycryptodome>=3.20.0
|
||||
|
||||
21
start.sh
21
start.sh
@@ -15,6 +15,15 @@ if [[ ! -f .env ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# config/ Verzeichnis und config.ini.example anlegen falls nicht vorhanden
|
||||
mkdir -p config
|
||||
if [[ ! -f config/config.ini ]] && [[ ! -f config/config.ini.example ]]; then
|
||||
if [[ -f config.ini.example ]]; then
|
||||
cp config.ini.example config/config.ini.example
|
||||
echo "[start] config/config.ini.example aus config.ini.example erstellt"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Docker verfügbar?
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo "[start] Docker nicht gefunden – bitte Docker installieren."
|
||||
@@ -34,12 +43,12 @@ else
|
||||
print(int(datetime.datetime.fromisoformat(s).replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null || echo 0)
|
||||
|
||||
for f in Dockerfile \
|
||||
05_scripts/kobrax_moonraker_bridge.py \
|
||||
05_scripts/kobrax_client.py \
|
||||
05_scripts/env_loader.py \
|
||||
05_scripts/requirements.txt \
|
||||
05_scripts/anycubic_slicer.crt \
|
||||
05_scripts/anycubic_slicer.key; do
|
||||
bridge/kobrax_moonraker_bridge.py \
|
||||
bridge/kobrax_client.py \
|
||||
bridge/env_loader.py \
|
||||
bridge/requirements.txt \
|
||||
bridge/anycubic_slicer.crt \
|
||||
bridge/anycubic_slicer.key; do
|
||||
if [[ -f "$f" ]]; then
|
||||
FILE_TS=$(python3 -c "import os; print(int(os.path.getmtime('$f')))" 2>/dev/null || echo 0)
|
||||
if [[ $FILE_TS -gt $IMAGE_TS ]]; then
|
||||
|
||||
136
web/DOC/THEME-CSS-HOOKS.md
Normal file
136
web/DOC/THEME-CSS-HOOKS.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# KX-Bridge Theme – CSS-ID-Hooks
|
||||
|
||||
Referenzliste für CSS-/Layout-Anpassungen.
|
||||
|
||||
| ID | Verwendung |
|
||||
|---|---|
|
||||
| `#ace-dry-dialog-custom-name-label` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-custom-name-row` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-temp-label` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-time-label` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-title` | Hook / Selektor |
|
||||
| `#add-printer-btn-label` | Hook / Selektor |
|
||||
| `#ams-no-data` | Hook / Selektor |
|
||||
| `#apd-ip` | Hook / Selektor |
|
||||
| `#apd-lbl-ip` | Hook / Selektor |
|
||||
| `#apd-lbl-name` | Hook / Selektor |
|
||||
| `#apd-name` | Hook / Selektor |
|
||||
| `#apd-status` | Hook / Selektor |
|
||||
| `#apd-title` | Hook / Selektor |
|
||||
| `#btn-log-dl` | Hook / Selektor |
|
||||
| `#cam-fname` | Hook / Selektor |
|
||||
| `#cam-img` | Hook / Selektor |
|
||||
| `#cam-overlay` | Hook / Selektor |
|
||||
| `#cam-placeholder` | Hook / Selektor |
|
||||
| `#cam-placeholder-txt` | Hook / Selektor |
|
||||
| `#cam-spinner` | Hook / Selektor |
|
||||
| `#cam-wrap` | Hook / Selektor |
|
||||
| `#conn-error-banner` | Hook / Selektor |
|
||||
| `#d-ace-dry-grid` | Hook / Selektor |
|
||||
| `#d-ace-dry-wrap` | Hook / Selektor |
|
||||
| `#d-ams-card` | Hook / Selektor |
|
||||
| `#d-bt-t` | Hook / Selektor |
|
||||
| `#d-btbar` | Hook / Selektor |
|
||||
| `#d-btn-skip-label` | Hook / Selektor |
|
||||
| `#d-card-ams` | Hook / Selektor |
|
||||
| `#d-card-cam` | Hook / Selektor |
|
||||
| `#d-card-lightfan` | Hook / Selektor |
|
||||
| `#d-card-progress` | Hook / Selektor |
|
||||
| `#d-card-speed` | Hook / Selektor |
|
||||
| `#d-card-temps` | Hook / Selektor |
|
||||
| `#d-chart-label` | Hook / Selektor |
|
||||
| `#d-ctrl-btns` | Hook / Selektor |
|
||||
| `#d-elapsed` | Hook / Selektor |
|
||||
| `#d-fname` | Hook / Selektor |
|
||||
| `#d-layers` | Hook / Selektor |
|
||||
| `#d-lbl-bed` | Hook / Selektor |
|
||||
| `#d-lbl-elapsed` | Hook / Selektor |
|
||||
| `#d-lbl-layers` | Hook / Selektor |
|
||||
| `#d-lbl-light` | Hook / Selektor |
|
||||
| `#d-lbl-remain` | Hook / Selektor |
|
||||
| `#d-nt` | Hook / Selektor |
|
||||
| `#d-nt-t` | Hook / Selektor |
|
||||
| `#d-ntbar` | Hook / Selektor |
|
||||
| `#d-pbar` | Hook / Selektor |
|
||||
| `#d-pct` | Hook / Selektor |
|
||||
| `#d-remain` | Hook / Selektor |
|
||||
| `#d-slicer-label` | Hook / Selektor |
|
||||
| `#d-slicer-row` | Hook / Selektor |
|
||||
| `#d-slicer-time` | Hook / Selektor |
|
||||
| `#d-spd-bar` | Hook / Selektor |
|
||||
| `#d-spd-lbl-1` | Hook / Selektor |
|
||||
| `#d-spd-lbl-2` | Hook / Selektor |
|
||||
| `#d-spd-lbl-3` | Hook / Selektor |
|
||||
| `#d-thumbnail` | Hook / Selektor |
|
||||
| `#fd-objects` | Hook / Selektor |
|
||||
| `#fd-objects-hint` | Hook / Selektor |
|
||||
| `#fd-objects-section` | Hook / Selektor |
|
||||
| `#fd-objects-svg` | Hook / Selektor |
|
||||
| `#fd-slots-hint` | Hook / Selektor |
|
||||
| `#fd-title` | Hook / Selektor |
|
||||
| `#file-ready-banner` | Hook / Selektor |
|
||||
| `#file-ready-name` | Hook / Selektor |
|
||||
| `#h-badge` | Hook / Selektor |
|
||||
| `#h-pname` | Hook / Selektor |
|
||||
| `#h-pname-single` | Hook / Selektor |
|
||||
| `#h-state` | Hook / Selektor |
|
||||
| `#h-version` | Hook / Selektor |
|
||||
| `#lbl-auto-leveling` | Hook / Selektor |
|
||||
| `#lbl-default-slot` | Hook / Selektor |
|
||||
| `#lbl-device-id` | Hook / Selektor |
|
||||
| `#lbl-ip-hint` | Hook / Selektor |
|
||||
| `#lbl-mode-id` | Hook / Selektor |
|
||||
| `#lbl-mqtt-port` | Hook / Selektor |
|
||||
| `#lbl-password` | Hook / Selektor |
|
||||
| `#lbl-printer-ip` | Hook / Selektor |
|
||||
| `#lbl-printer-name` | Hook / Selektor |
|
||||
| `#lbl-slot-color` | Hook / Selektor |
|
||||
| `#lbl-slot-material` | Hook / Selektor |
|
||||
| `#lbl-update-apply` | Hook / Selektor |
|
||||
| `#lbl-update-check` | Hook / Selektor |
|
||||
| `#lbl-username` | Hook / Selektor |
|
||||
| `#log-badge` | Hook / Selektor |
|
||||
| `#log-badge-bot` | Hook / Selektor |
|
||||
| `#modal-sec-connection` | Hook / Selektor |
|
||||
| `#modal-sec-poll` | Hook / Selektor |
|
||||
| `#modal-sec-print` | Hook / Selektor |
|
||||
| `#modal-sec-version` | Hook / Selektor |
|
||||
| `#modal-title-settings` | Hook / Selektor |
|
||||
| `#opt-slot-0` | Hook / Selektor |
|
||||
| `#opt-slot-1` | Hook / Selektor |
|
||||
| `#opt-slot-2` | Hook / Selektor |
|
||||
| `#opt-slot-3` | Hook / Selektor |
|
||||
| `#opt-slot-auto` | Hook / Selektor |
|
||||
| `#printer-dropdown-menu` | Hook / Selektor |
|
||||
| `#printer-dropdown-wrap` | Hook / Selektor |
|
||||
| `#printers-panel-title` | Hook / Selektor |
|
||||
| `#ptitle-console` | Hook / Selektor |
|
||||
| `#ptitle-motion-xy` | Hook / Selektor |
|
||||
| `#ptitle-motion-z` | Hook / Selektor |
|
||||
| `#s-auto-leveling` | Hook / Selektor |
|
||||
| `#s-default-slot` | Hook / Selektor |
|
||||
| `#s-device-id` | Hook / Selektor |
|
||||
| `#s-mode-id` | Hook / Selektor |
|
||||
| `#s-mqtt-port` | Hook / Selektor |
|
||||
| `#s-password` | Hook / Selektor |
|
||||
| `#s-printer-name` | Hook / Selektor |
|
||||
| `#s-username` | Hook / Selektor |
|
||||
| `#s-version-label` | Hook / Selektor |
|
||||
| `#sf-all` | Hook / Selektor |
|
||||
| `#sf-err` | Hook / Selektor |
|
||||
| `#sf-new` | Hook / Selektor |
|
||||
| `#sf-ok` | Hook / Selektor |
|
||||
| `#skip-hint` | Hook / Selektor |
|
||||
| `#skip-list` | Hook / Selektor |
|
||||
| `#skip-status` | Hook / Selektor |
|
||||
| `#skip-svg` | Hook / Selektor |
|
||||
| `#skip-title` | Hook / Selektor |
|
||||
| `#slot-edit-title` | Hook / Selektor |
|
||||
| `#ss-date` | Hook / Selektor |
|
||||
| `#ss-dur` | Hook / Selektor |
|
||||
| `#ss-name` | Hook / Selektor |
|
||||
| `#step-display` | Hook / Selektor |
|
||||
| `#store-empty` | Hook / Selektor |
|
||||
| `#store-panel-title` | Hook / Selektor |
|
||||
| `#update-changelog` | Hook / Selektor |
|
||||
| `#update-status` | Hook / Selektor |
|
||||
90
web/DOC/THEME-JS-ID-HOOKS.md
Normal file
90
web/DOC/THEME-JS-ID-HOOKS.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# KX-Bridge Theme – JavaScript-ID-Hooks
|
||||
|
||||
Referenzliste für JavaScript-/DOM-Hooks.
|
||||
|
||||
| ID | Verwendung |
|
||||
|---|---|
|
||||
| `#ace-dry-dialog` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-cancel` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-confirm` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-custom-name` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-h` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-m` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-reset-default` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-s` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-save-preset` | Hook / Selektor |
|
||||
| `#ace-dry-dialog-temp` | Hook / Selektor |
|
||||
| `#add-printer-dialog` | Hook / Selektor |
|
||||
| `#ams-slots` | Hook / Selektor |
|
||||
| `#apd-confirm` | Hook / Selektor |
|
||||
| `#bnb-console` | Hook / Selektor |
|
||||
| `#bnb-dashboard` | Hook / Selektor |
|
||||
| `#bnb-printers` | Hook / Selektor |
|
||||
| `#bnb-store` | Hook / Selektor |
|
||||
| `#btn-autoscroll` | Hook / Selektor |
|
||||
| `#btn-save-settings` | Hook / Selektor |
|
||||
| `#btn-slot-edit-feed` | Hook / Selektor |
|
||||
| `#btn-slot-edit-save` | Hook / Selektor |
|
||||
| `#btn-update-apply` | Hook / Selektor |
|
||||
| `#btn-update-check` | Hook / Selektor |
|
||||
| `#cam-toggle-btn` | Hook / Selektor |
|
||||
| `#conn-btn` | Hook / Selektor |
|
||||
| `#console-log` | Hook / Selektor |
|
||||
| `#d-bt` | Hook / Selektor |
|
||||
| `#d-btn-cancel` | Hook / Selektor |
|
||||
| `#d-btn-pause` | Hook / Selektor |
|
||||
| `#d-btn-resume` | Hook / Selektor |
|
||||
| `#d-btn-skip` | Hook / Selektor |
|
||||
| `#d-chart` | Hook / Selektor |
|
||||
| `#d-fan` | Hook / Selektor |
|
||||
| `#d-fan-val` | Hook / Selektor |
|
||||
| `#d-light-toggle` | Hook / Selektor |
|
||||
| `#d-spd-1` | Hook / Selektor |
|
||||
| `#d-spd-2` | Hook / Selektor |
|
||||
| `#d-spd-3` | Hook / Selektor |
|
||||
| `#fd-cancel` | Hook / Selektor |
|
||||
| `#fd-print` | Hook / Selektor |
|
||||
| `#fd-slots` | Hook / Selektor |
|
||||
| `#filament-dialog` | Hook / Selektor |
|
||||
| `#file-cancel-btn` | Hook / Selektor |
|
||||
| `#file-ready-btn` | Hook / Selektor |
|
||||
| `#file-slots-btn` | Hook / Selektor |
|
||||
| `#lang-btn` | Hook / Selektor |
|
||||
| `#log-filter` | Hook / Selektor |
|
||||
| `#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 |
|
||||
| `#nb-store` | Hook / Selektor |
|
||||
| `#p-bed-inp` | Hook / Selektor |
|
||||
| `#p-nozzle-inp` | Hook / Selektor |
|
||||
| `#panel-console` | Hook / Selektor |
|
||||
| `#panel-dashboard` | Hook / Selektor |
|
||||
| `#panel-printers` | Hook / Selektor |
|
||||
| `#panel-store` | Hook / Selektor |
|
||||
| `#poll-1` | Hook / Selektor |
|
||||
| `#poll-2` | Hook / Selektor |
|
||||
| `#poll-5` | Hook / Selektor |
|
||||
| `#printer-dropdown-btn` | Hook / Selektor |
|
||||
| `#printers-grid` | Hook / Selektor |
|
||||
| `#s-printer-ip` | Hook / Selektor |
|
||||
| `#settings-btn` | Hook / Selektor |
|
||||
| `#settings-modal` | Hook / Selektor |
|
||||
| `#skip-confirm` | Hook / Selektor |
|
||||
| `#skip-dialog` | Hook / Selektor |
|
||||
| `#slot-edit-color` | Hook / Selektor |
|
||||
| `#slot-edit-mat` | Hook / Selektor |
|
||||
| `#slot-edit-modal` | Hook / Selektor |
|
||||
| `#slot-edit-preview` | Hook / Selektor |
|
||||
| `#slot-mat-btns` | Hook / Selektor |
|
||||
| `#store-filter` | Hook / Selektor |
|
||||
| `#store-grid` | Hook / Selektor |
|
||||
| `#store-refresh-btn` | Hook / Selektor |
|
||||
| `#store-search` | Hook / Selektor |
|
||||
| `#store-sort` | Hook / Selektor |
|
||||
3117
web/themes/default/app.js
Normal file
3117
web/themes/default/app.js
Normal file
File diff suppressed because it is too large
Load Diff
784
web/themes/default/index.html
Normal file
784
web/themes/default/index.html
Normal file
@@ -0,0 +1,784 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>KX-Bridge</title>
|
||||
<link rel="stylesheet" href="/kx/ui/style.css">
|
||||
<body>
|
||||
|
||||
<div id="conn-error-banner" style="display:none;background:#c0392b;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:999;"></div>
|
||||
<div id="file-ready-banner" style="display:none;background:#1a6e3c;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:998;display:none;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap">
|
||||
<span>📄 <span id="file-ready-name"></span></span>
|
||||
<button id="file-ready-btn" onclick="startReadyFile()"
|
||||
style="padding:5px 16px;background:#fff;color:#1a6e3c;border:none;border-radius:6px;font-weight:700;cursor:pointer;font-size:13px"></button>
|
||||
<button id="file-slots-btn" onclick="startReadyFileWithSlots()"
|
||||
style="padding:5px 16px;background:rgba(255,255,255,0.15);color:#fff;border:1px solid rgba(255,255,255,0.5);border-radius:6px;font-weight:700;cursor:pointer;font-size:13px"></button>
|
||||
<button id="file-cancel-btn" onclick="cancelReadyFile()"
|
||||
style="padding:5px 16px;background:rgba(255,255,255,0.15);color:#fff;border:1px solid rgba(255,255,255,0.5);border-radius:6px;font-weight:700;cursor:pointer;font-size:13px"></button>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div class="logo">⬡ KX-Bridge</div>
|
||||
<div style="flex:1"></div>
|
||||
<div id="printer-dropdown-wrap" style="display:none;position:relative">
|
||||
<button id="printer-dropdown-btn" onclick="togglePrinterDropdown()" style="background:var(--raised);border:1px solid var(--border);border-radius:6px;padding:4px 10px;color:var(--txt);cursor:pointer;font-size:13px;display:flex;align-items:center;gap:6px">
|
||||
<span id="h-pname">Anycubic Kobra X</span><span style="opacity:.5">▾</span>
|
||||
</button>
|
||||
<div id="printer-dropdown-menu" style="display:none;position:absolute;top:calc(100% + 4px);right:0;background:var(--card);border:1px solid var(--border);border-radius:8px;min-width:200px;z-index:200;box-shadow:0 4px 16px #0006;overflow:hidden">
|
||||
</div>
|
||||
</div>
|
||||
<div id="h-pname-single" class="hname">Anycubic Kobra X</div>
|
||||
<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>
|
||||
<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 ═══ -->
|
||||
<!-- 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()">
|
||||
<div class="modal-box" style="max-width:340px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||
<span class="modal-title" id="slot-edit-title"></span>
|
||||
<button onclick="closeSlotEdit()" style="background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer;line-height:1">✕</button>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
|
||||
<div id="slot-edit-preview" style="width:56px;height:56px;border-radius:50%;border:3px solid rgba(255,255,255,.2);flex-shrink:0"></div>
|
||||
<div style="flex:1">
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:4px" id="lbl-slot-color"></div>
|
||||
<input type="color" id="slot-edit-color"
|
||||
oninput="document.getElementById('slot-edit-preview').style.background=this.value"
|
||||
style="width:100%;height:36px;border:1px solid var(--border);border-radius:6px;background:var(--raised);cursor:pointer;padding:2px">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-material"></div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px" id="slot-mat-btns">
|
||||
</div>
|
||||
<input type="text" id="slot-edit-mat"
|
||||
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">
|
||||
<span class="nav-icon">⊞</span><span class="nav-text">Dashboard</span></button>
|
||||
<button class="nav-btn" onclick="showPanel('printers');loadPrinterTab()" id="nb-printers">
|
||||
<span class="nav-icon">🖨</span><span class="nav-text">Drucker</span></button>
|
||||
<button class="nav-btn" onclick="showPanel('store');loadStore()" id="nb-store">
|
||||
<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>
|
||||
<!-- ═══ DASHBOARD ═══ -->
|
||||
<div class="panel active" id="panel-dashboard">
|
||||
<div class="grid">
|
||||
<!-- Kamera -->
|
||||
<div class="card" style="grid-column:1/-1">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
|
||||
<div class="card-title" style="margin-bottom:0"><span>📷</span> <span id="d-card-cam">Kamera</span></div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span id="d-lbl-light" style="font-size:12px;color:var(--txt2)">💡 Licht</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="d-light-toggle" onchange="setLight()">
|
||||
<span class="toggle-track"></span>
|
||||
<span class="toggle-thumb"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cam-wrap" id="cam-wrap">
|
||||
<div class="cam-placeholder" id="cam-placeholder"><span id="cam-placeholder-txt">📷 Kamera nicht gestartet</span></div>
|
||||
<div class="cam-spinner" id="cam-spinner"></div>
|
||||
<img id="cam-img" style="display:none;width:100%;height:auto" alt="Kamera">
|
||||
<div class="cam-overlay" id="cam-overlay" style="display:none">
|
||||
<div style="font-size:12px;color:#fff" id="cam-fname"></div>
|
||||
</div>
|
||||
<button class="cam-toggle" onclick="toggleCam()" id="cam-toggle-btn">▶ Kamera</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fortschritt -->
|
||||
<div class="card" style="grid-column:1/-1">
|
||||
<div class="card-title"><span>◉</span> <span id="d-card-progress">Fortschritt</span></div>
|
||||
<img id="d-thumbnail" src="" alt="" style="display:none;width:100%;max-height:160px;object-fit:contain;border-radius:8px;background:#111;margin-bottom:10px">
|
||||
<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 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">
|
||||
<div class="time-block">
|
||||
<div class="time-label" id="d-lbl-elapsed"></div>
|
||||
<div class="time-val" id="d-elapsed">–</div>
|
||||
</div>
|
||||
<div class="time-block" id="d-slicer-row" style="display:none">
|
||||
<div class="time-label" id="d-slicer-label"></div>
|
||||
<div class="time-val" id="d-slicer-time">–</div>
|
||||
</div>
|
||||
<div class="time-block" style="color:var(--acc)">
|
||||
<div class="time-label" id="d-lbl-remain"></div>
|
||||
<div class="time-val" id="d-remain" style="color:var(--acc)">–</div>
|
||||
</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;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 -->
|
||||
<div class="card" style="grid-column:1/-1">
|
||||
<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" id="d-lbl-nozzle">Nozzle</div>
|
||||
<div class="temp-row">
|
||||
<div class="temp-val" id="d-nt">–</div>
|
||||
<div class="temp-unit">°C</div>
|
||||
</div>
|
||||
<div class="temp-target">→ <span id="d-nt-t">0</span>°C</div>
|
||||
<div class="progress-bar" style="margin:8px 0 0">
|
||||
<div class="progress-fill" id="d-ntbar" style="width:0%;background:linear-gradient(90deg,var(--accent2),#ffb020)"></div>
|
||||
</div>
|
||||
<div class="temp-edit" style="margin-top:10px">
|
||||
<input type="number" class="temp-input" id="p-nozzle-inp" placeholder="Ziel" min="0" max="300" style="flex:1">
|
||||
<button class="btn btn-sm btn-accent" onclick="setNozzle()"><span class="lbl-set">Set</span></button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="document.getElementById('p-nozzle-inp').value=0;setNozzle()"><span class="lbl-off">Aus</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="temp-block">
|
||||
<div class="temp-label" id="d-lbl-bed">Bett</div>
|
||||
<div class="temp-row">
|
||||
<div class="temp-val" id="d-bt">–</div>
|
||||
<div class="temp-unit">°C</div>
|
||||
</div>
|
||||
<div class="temp-target">→ <span id="d-bt-t">0</span>°C</div>
|
||||
<div class="progress-bar" style="margin:8px 0 0">
|
||||
<div class="progress-fill" id="d-btbar" style="width:0%;background:linear-gradient(90deg,#ff6b35,var(--warn))"></div>
|
||||
</div>
|
||||
<div class="temp-edit" style="margin-top:10px">
|
||||
<input type="number" class="temp-input" id="p-bed-inp" placeholder="Ziel" min="0" max="120" style="flex:1">
|
||||
<button class="btn btn-sm btn-accent" onclick="setBed()"><span class="lbl-set">Set</span></button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="document.getElementById('p-bed-inp').value=0;setBed()"><span class="lbl-off">Aus</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:14px">
|
||||
<div style="font-size:10px;color:var(--txt2);margin-bottom:4px" id="d-chart-label">Verlauf (letzte 60 Messungen)</div>
|
||||
<canvas id="d-chart" width="800" height="120" style="width:100%;height:120px;background:var(--raised);border-radius:8px"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Achsensteuerung -->
|
||||
<div class="card">
|
||||
<div class="card-title"><span>✛</span> <span id="ptitle-motion-xy">XY-Achsen</span></div>
|
||||
<div class="joypad">
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,1,getStep())" title="Y+">▲</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(0,-1,getStep())" title="X−">◀</button>
|
||||
<button class="joy home" onclick="homeAll()" title="Home All">⌂</button>
|
||||
<button class="joy" onclick="move(0,1,getStep())" title="X+">▶</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,-1,getStep())" title="Y−">▼</button>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="step-btns">
|
||||
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
|
||||
<button class="step-btn active" onclick="setStep(this,1)">1</button>
|
||||
<button class="step-btn" onclick="setStep(this,5)">5</button>
|
||||
<button class="step-btn" onclick="setStep(this,10)">10 mm</button>
|
||||
</div>
|
||||
<div class="home-btns">
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
|
||||
<button class="btn btn-sm btn-accent" onclick="homeAll()"><span class="lbl-home-all">Home All</span></button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="disableMotors()"><span class="lbl-disable-motors">Motors Off</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span>↕</span> <span id="ptitle-motion-z">Z-Achse</span></div>
|
||||
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
|
||||
<button class="joy" onclick="move(2,1,getStep())" title="Z+">▲</button>
|
||||
<button class="joy" onclick="move(2,-1,getStep())" title="Z−">▼</button>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)"><span class="lbl-step">Schrittweite:</span> <span id="step-display">1</span> mm</div>
|
||||
</div>
|
||||
|
||||
<!-- Print Speed -->
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🏎</span> <span id="d-card-speed">Druckgeschwindigkeit</span></div>
|
||||
<div style="display:flex;gap:8px;margin-top:4px">
|
||||
<button class="spd-btn" id="d-spd-1" onclick="setSpeed(1)">
|
||||
<span class="spd-icon">🐢</span>
|
||||
<span id="d-spd-lbl-1">Leise</span>
|
||||
</button>
|
||||
<button class="spd-btn spd-active" id="d-spd-2" onclick="setSpeed(2)">
|
||||
<span class="spd-icon">⚡</span>
|
||||
<span id="d-spd-lbl-2">Normal</span>
|
||||
</button>
|
||||
<button class="spd-btn" id="d-spd-3" onclick="setSpeed(3)">
|
||||
<span class="spd-icon">🚀</span>
|
||||
<span id="d-spd-lbl-3">Sport</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="spd-bar" style="margin-top:12px">
|
||||
<div class="spd-bar-fill" id="d-spd-bar" style="width:50%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lüfter -->
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🌀</span> <span id="d-card-lightfan">Lüfter</span></div>
|
||||
<div class="slider-row">
|
||||
<input type="range" class="slider" min="0" max="100" value="0" id="d-fan" oninput="document.getElementById('d-fan-val').textContent=this.value" onchange="setFan()">
|
||||
<span class="slider-val" id="d-fan-val">0</span>
|
||||
</div>
|
||||
<div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(0)">Aus</button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(25)">25%</button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(50)">50%</button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(75)">75%</button>
|
||||
<button class="btn btn-sm btn-accent" onclick="quickFan(100)">100%</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="d-ace-dry-wrap" style="display:none">
|
||||
<div id="d-ace-dry-grid" style="display:contents"></div>
|
||||
</div>
|
||||
|
||||
<!-- AMS -->
|
||||
<div class="card" style="grid-column:1/-1" id="d-ams-card">
|
||||
<div class="card-title"><span>◫</span> <span id="d-card-ams">Filament</span></div>
|
||||
<div class="ams-slots" id="ams-slots">
|
||||
<div style="grid-column:1/-1;text-align:center;color:var(--txt2);padding:20px" id="ams-no-data">Keine AMS-Daten empfangen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ CONSOLE ═══ -->
|
||||
<!-- ═══ DRUCKER ═══ -->
|
||||
<div class="panel" id="panel-printers">
|
||||
<div class="card">
|
||||
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<span id="printers-panel-title">🖨 Drucker</span>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button onclick="openAddPrinterDialog()" style="font-size:12px;padding:4px 12px;background:var(--accent);border:none;border-radius:6px;color:#fff;cursor:pointer;font-weight:600">+ <span id="add-printer-btn-label">Drucker hinzufügen</span></button>
|
||||
<button onclick="loadPrinterTab()" style="font-size:12px;padding:4px 12px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="printers-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ GCODE STORE ═══ -->
|
||||
<div class="panel" id="panel-store">
|
||||
<div class="card">
|
||||
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
|
||||
<span id="store-panel-title">🗂 Datei-Browser</span>
|
||||
<button id="store-refresh-btn" onclick="loadStore()" style="font-size:12px;padding:4px 12px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">↻ Aktualisieren</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
|
||||
<input id="store-search" type="text" placeholder="🔍 Suche…" oninput="renderStore()"
|
||||
style="flex:1;min-width:140px;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
|
||||
<select id="store-filter" onchange="renderStore()"
|
||||
style="padding:6px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
|
||||
<option value="all" id="sf-all">Alle</option>
|
||||
<option value="completed" id="sf-ok">✓ Erfolgreich</option>
|
||||
<option value="failed" id="sf-err">✗ Fehler</option>
|
||||
<option value="never" id="sf-new">Neu</option>
|
||||
</select>
|
||||
<select id="store-sort" onchange="renderStore()"
|
||||
style="padding:6px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
|
||||
<option value="date_desc" id="ss-date">↓ Datum</option>
|
||||
<option value="name_asc" id="ss-name">A–Z Name</option>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-console">
|
||||
<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
|
||||
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">
|
||||
<input id="log-filter" type="text" placeholder="Filter…"
|
||||
oninput="renderLog()"
|
||||
style="flex:1;min-width:120px;padding:5px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;font-family:var(--mono)">
|
||||
<button id="btn-autoscroll" onclick="toggleAutoScroll()"
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap">⬇ Auto</button>
|
||||
<button onclick="consoleLogs=[];renderLog()"
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">✕ Clear</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap">
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-right:2px">Dir:</span>
|
||||
<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>
|
||||
<button class="log-topic-btn" data-topic="info" onclick="setLogTopic('info')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">info</button>
|
||||
<button class="log-topic-btn" data-topic="status" onclick="setLogTopic('status')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">status</button>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<nav class="bottom-nav">
|
||||
<button class="bnav-btn active" onclick="showPanel('dashboard')" id="bnb-dashboard"><span class="bnav-icon">⊞</span>Dashboard</button>
|
||||
<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%">
|
||||
<div class="modal-header" style="margin-bottom:14px">
|
||||
<span class="modal-title" id="fd-title" style="font-size:14px;word-break:break-all"></span>
|
||||
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
<button id="fd-print" onclick="confirmFilamentPrint()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">▶ Drucken</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Drucker-hinzufügen-Dialog -->
|
||||
<div class="modal-overlay" id="add-printer-dialog" onclick="if(event.target===this)closeAddPrinterDialog()">
|
||||
<div class="modal-box" style="max-width:380px;width:100%">
|
||||
<div class="modal-header" style="margin-bottom:14px">
|
||||
<span class="modal-title" id="apd-title">Drucker hinzufügen</span>
|
||||
<button onclick="closeAddPrinterDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<label id="apd-lbl-ip" style="display:block;font-size:12px;color:var(--txt2);margin-bottom:4px">Drucker-IP</label>
|
||||
<input type="text" id="apd-ip" placeholder="192.168.1.100" 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:10px">
|
||||
<label id="apd-lbl-name" style="display:block;font-size:12px;color:var(--txt2);margin-bottom:4px">Name (optional)</label>
|
||||
<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 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>
|
||||
</div>
|
||||
<!-- Mid-Print Skip-Dialog -->
|
||||
<div class="modal-overlay" id="skip-dialog" onclick="if(event.target===this)closeSkipDialog()">
|
||||
<div class="modal-box" style="max-width:420px;width:100%">
|
||||
<div class="modal-header" style="margin-bottom:14px">
|
||||
<span class="modal-title" id="skip-title">✂ Objekte überspringen</span>
|
||||
<button onclick="closeSkipDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<p id="skip-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">Objekte abwählen, die nicht weiter gedruckt werden sollen:</p>
|
||||
<div id="skip-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:10px;text-align:center"></div>
|
||||
<div id="skip-list" style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;margin-bottom:12px"></div>
|
||||
<div id="skip-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 onclick="closeSkipDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="skip-confirm" onclick="confirmSkip()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Überspringen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ACE Dryer Temp/Time Settings Dialog -->
|
||||
<div class="modal-overlay" id="ace-dry-dialog" onclick="if(event.target===this)closeAceDryDialog()">
|
||||
<div class="modal-box" style="max-width:560px;width:100%">
|
||||
<div class="modal-header" style="margin-bottom:10px">
|
||||
<span class="modal-title" id="ace-dry-dialog-title">Dryer Temp/Time Settings</span>
|
||||
<button onclick="closeAceDryDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
|
||||
<label id="ace-dry-dialog-temp-label" style="min-width:190px;font-size:12px;color:var(--txt)">Temperature (30-80°C)</label>
|
||||
<input id="ace-dry-dialog-temp" type="number" min="30" max="80" step="1"
|
||||
oninput="aceDryDialogInputsChanged()"
|
||||
style="width:130px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center" value="45">
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
|
||||
<label id="ace-dry-dialog-time-label" style="min-width:190px;font-size:12px;color:var(--txt)">Rem. Time (h:m:s)</label>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<input id="ace-dry-dialog-h" type="number" min="0" max="24" step="1" value="4"
|
||||
oninput="aceDryDialogInputsChanged()"
|
||||
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
|
||||
<span style="color:var(--txt2)">:</span>
|
||||
<input id="ace-dry-dialog-m" type="number" min="0" max="59" step="1" value="0"
|
||||
oninput="aceDryDialogInputsChanged()"
|
||||
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
|
||||
<span style="color:var(--txt2)">:</span>
|
||||
<input id="ace-dry-dialog-s" type="number" min="0" max="59" step="1" value="0"
|
||||
oninput="aceDryDialogInputsChanged()"
|
||||
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:8px">
|
||||
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA+</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PETG</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">TPU</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">ABS / ASA</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PA / PC</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 1</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 2</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 3</button>
|
||||
</div>
|
||||
<div id="ace-dry-dialog-custom-name-row" style="display:none;align-items:center;gap:12px;margin-bottom:14px">
|
||||
<label id="ace-dry-dialog-custom-name-label" style="min-width:190px;font-size:12px;color:var(--txt)">Custom Name</label>
|
||||
<input id="ace-dry-dialog-custom-name" type="text" maxlength="32" oninput="aceDryDialogInputsChanged()"
|
||||
style="width:220px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt)">
|
||||
</div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:8px">
|
||||
<button id="ace-dry-dialog-reset-default" onclick="resetAceDryPresetToDefault()" style="display:none;padding:8px 14px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Reset to Default</button>
|
||||
<button id="ace-dry-dialog-save-preset" onclick="saveAceDryPresetAndRestart()" style="display:none;padding:8px 14px;background:var(--warn);border:1px solid transparent;border-radius:8px;color:#fff;cursor:pointer">Save & Restart</button>
|
||||
<button id="ace-dry-dialog-cancel" onclick="closeAceDryDialog()" style="padding:8px 14px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Cancel</button>
|
||||
<button id="ace-dry-dialog-confirm" onclick="confirmAceDryDialog()" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/kx/ui/app.js"></script></head>
|
||||
<footer style="text-align:center;padding:12px;font-size:11px;color:var(--txt2);border-top:1px solid var(--border);margin-top:auto">
|
||||
© ViewIT 2026
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
333
web/themes/default/style.css
Normal file
333
web/themes/default/style.css
Normal file
@@ -0,0 +1,333 @@
|
||||
: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;
|
||||
--font:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
--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);
|
||||
display:flex;align-items:center;gap:12px;padding:0 20px;height:52px;
|
||||
position:sticky;top:0;z-index:100}
|
||||
.logo{font-size:18px;font-weight:700;color:var(--accent);letter-spacing:-.02em}
|
||||
.hname{font-size:13px;color:var(--txt2)}
|
||||
.hbadge{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600;
|
||||
padding:4px 10px;border-radius:20px;background:var(--raised);color:var(--txt2);
|
||||
text-transform:uppercase;letter-spacing:.04em}
|
||||
.hbadge.printing{background:#0d2d1a;color:var(--ok)}
|
||||
.hbadge.complete{background:#0d1f38;color:#60b0ff}
|
||||
.hbadge.error{background:#2d0d0d;color:var(--err)}
|
||||
.hbadge .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
|
||||
.hbadge.printing .dot{animation:pulse 1.4s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
||||
.theme-btn{background:none;border:1px solid var(--border);color:var(--txt2);
|
||||
border-radius:8px;padding:6px 10px;cursor:pointer;font-size:13px;transition:.15s}
|
||||
.theme-btn:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.conn-btn{border-radius:8px;padding:6px 12px;cursor:pointer;font-size:13px;
|
||||
font-weight:600;border:none;transition:.15s}
|
||||
.conn-btn.disconnected{background:var(--accent);color:#fff}
|
||||
.conn-btn.disconnected:hover{opacity:.85}
|
||||
.conn-btn.connected{background:transparent;border:1px solid var(--border);color:var(--txt2)}
|
||||
.conn-btn.connected:hover{border-color:#e05;color:#e05}
|
||||
|
||||
/* ── LAYOUT ── */
|
||||
.layout{display:flex;flex:1;min-height:0}
|
||||
nav.sidebar{width:200px;background:var(--card);border-right:1px solid var(--border);
|
||||
display:flex;flex-direction:column;padding:12px 8px;gap:2px;flex-shrink:0}
|
||||
.nav-btn{background:none;border:none;color:var(--txt2);text-align:left;
|
||||
padding:9px 12px;border-radius:8px;cursor:pointer;font-size:13px;
|
||||
display:flex;align-items:center;gap:10px;transition:.12s;width:100%}
|
||||
.nav-btn:hover{background:var(--raised);color:var(--txt)}
|
||||
.nav-btn.active{background:var(--raised);color:var(--accent)}
|
||||
.nav-icon{font-size:16px;width:20px;text-align:center}
|
||||
main{flex:1;overflow-y:auto;padding:20px}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px}
|
||||
|
||||
/* ── CARD ── */
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;
|
||||
padding:18px;transition:box-shadow .15s,transform .15s}
|
||||
.card:hover{box-shadow:0 4px 20px rgba(0,0,0,.3);transform:translateY(-1px)}
|
||||
.card-title{font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:var(--txt2);
|
||||
margin-bottom:14px;display:flex;align-items:center;gap:8px}
|
||||
.card-title span{font-size:14px}
|
||||
|
||||
/* ── HERO ── */
|
||||
.hero{grid-column:1/-1;display:grid;grid-template-columns:1fr 320px;gap:16px}
|
||||
@media(max-width:900px){.hero{grid-template-columns:1fr}}
|
||||
.cam-wrap{background:#0a0a0e;border-radius:10px;overflow:hidden;
|
||||
min-height:180px;max-height:320px;display:flex;align-items:center;justify-content:center;position:relative}
|
||||
.cam-wrap img,.cam-wrap video{width:100%;max-height:320px;height:auto;display:block;object-fit:contain}
|
||||
.cam-placeholder{color:var(--txt2);font-size:13px;text-align:center;padding:20px}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.cam-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.15);
|
||||
border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;display:none}
|
||||
.cam-overlay{position:absolute;bottom:0;left:0;right:0;
|
||||
background:linear-gradient(transparent,rgba(0,0,0,.75));padding:14px}
|
||||
.cam-toggle{position:absolute;top:10px;right:10px;background:rgba(0,0,0,.5);
|
||||
border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:8px;
|
||||
padding:6px 10px;cursor:pointer;font-size:12px;backdrop-filter:blur(4px)}
|
||||
.cam-toggle:hover{background:rgba(0,0,0,.7)}
|
||||
|
||||
/* ── PROGRESS ── */
|
||||
.hero-info{display:flex;flex-direction:column;gap:12px}
|
||||
.pct-big{font-size:52px;font-weight:700;line-height:1;color:var(--txt)}
|
||||
.pct-big small{font-size:20px;font-weight:400;color:var(--txt2)}
|
||||
.progress-bar{height:8px;background:var(--raised);border-radius:4px;overflow:hidden;margin:4px 0}
|
||||
.progress-fill{height:100%;background:linear-gradient(90deg,var(--accent),#0080cc);
|
||||
border-radius:4px;transition:width .6s ease}
|
||||
.meta-row{display:flex;justify-content:space-between;font-size:12px;color:var(--txt2)}
|
||||
.layer-badge{background:var(--raised);border-radius:6px;padding:4px 8px;
|
||||
font-family:var(--mono);font-size:12px;color:var(--txt)}
|
||||
.fname{font-size:12px;color:var(--txt2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||||
background:var(--raised);border-radius:6px;padding:6px 8px}
|
||||
|
||||
/* ── PRINT CONTROLS ── */
|
||||
.ctrl-btns{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.btn{border:none;border-radius:8px;padding:10px 16px;font-size:13px;font-weight:600;
|
||||
cursor:pointer;transition:opacity .15s,transform .1s;white-space:nowrap}
|
||||
.btn:hover{opacity:.85;transform:translateY(-1px)}
|
||||
.btn:active{transform:translateY(0)}
|
||||
.btn-start{background:var(--ok);color:#0d2010}
|
||||
.btn-pause{background:var(--raised);color:var(--txt);border:1px solid var(--border)}
|
||||
.btn-resume{background:#0d2d1a;color:var(--ok);border:1px solid var(--ok)}
|
||||
.btn-skip{background:var(--raised);color:var(--warn);border:1px solid var(--warn)}
|
||||
.btn-cancel{background:#2d0d0d;color:var(--err);border:1px solid var(--err)}
|
||||
.btn-accent{background:var(--accent);color:#001a24}
|
||||
.btn-sm{padding:7px 12px;font-size:12px}
|
||||
.spd-btn{flex:1;border:1.5px solid var(--border);background:var(--raised);color:var(--txt);
|
||||
border-radius:10px;padding:14px 8px;font-size:13px;font-weight:600;cursor:pointer;
|
||||
transition:all .15s;display:flex;flex-direction:column;align-items:center;gap:4px}
|
||||
.spd-btn:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.spd-btn.spd-active{border-color:var(--accent);background:rgba(0,200,255,.12);color:var(--accent)}
|
||||
.spd-btn .spd-icon{font-size:22px}
|
||||
.spd-bar{height:4px;border-radius:2px;background:var(--border);margin-top:10px;overflow:hidden}
|
||||
.spd-bar-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--accent2),var(--accent));transition:width .3s}
|
||||
|
||||
/* ── TIME CARDS ── */
|
||||
.time-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:8px}
|
||||
.time-block{background:var(--raised);border-radius:10px;padding:10px 12px}
|
||||
.time-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--txt2);margin-bottom:4px}
|
||||
.time-val{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--txt)}
|
||||
/* ── TEMPS ── */
|
||||
.temp-pair{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.temp-card-inner{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.temp-block{background:var(--raised);border-radius:10px;padding:14px;position:relative}
|
||||
.temp-label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--txt2);margin-bottom:6px}
|
||||
.temp-row{display:flex;align-items:baseline;gap:6px}
|
||||
.temp-val{font-size:30px;font-weight:700;font-family:var(--mono)}
|
||||
.temp-unit{font-size:14px;color:var(--txt2)}
|
||||
.temp-target{font-size:11px;color:var(--txt2);margin-top:2px}
|
||||
.temp-arc{position:absolute;top:12px;right:12px}
|
||||
.temp-edit{display:flex;gap:6px;margin-top:10px}
|
||||
.temp-input{background:var(--bg);border:1px solid var(--border);color:var(--txt);
|
||||
border-radius:6px;padding:5px 8px;font-size:13px;font-family:var(--mono);width:70px}
|
||||
.temp-input:focus{outline:none;border-color:var(--accent)}
|
||||
.chart-wrap{margin-top:12px}
|
||||
canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:var(--raised)}
|
||||
|
||||
/* ── MOTION ── */
|
||||
.joypad{display:grid;grid-template-columns:repeat(3,52px);
|
||||
grid-template-rows:repeat(3,52px);gap:6px;justify-content:center;margin:8px auto}
|
||||
.joy{background:var(--raised);border:1px solid var(--border);color:var(--txt);
|
||||
border-radius:10px;font-size:18px;cursor:pointer;transition:.12s;
|
||||
display:flex;align-items:center;justify-content:center}
|
||||
.joy:hover{background:var(--accent);color:#001a24;border-color:var(--accent)}
|
||||
.joy:active{transform:scale(.93)}
|
||||
.joy.home{font-size:14px;background:var(--bg)}
|
||||
.step-btns{display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin-top:10px}
|
||||
.step-btn{background:var(--raised);border:1px solid var(--border);color:var(--txt2);
|
||||
border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer;transition:.12s}
|
||||
.step-btn.active,.step-btn:hover{background:var(--accent);color:#001a24;border-color:var(--accent)}
|
||||
.home-btns{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px;justify-content:center}
|
||||
|
||||
/* ── AMS ── */
|
||||
.ams-slots{display:flex;flex-direction:column;gap:12px}
|
||||
.ams-box-group{}
|
||||
.ams-box-label{font-size:11px;font-weight:700;color:var(--txt2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px;padding-left:2px}
|
||||
.ams-box-slots{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
||||
.ams-slot{background:var(--raised);border-radius:10px;padding:10px;text-align:center;
|
||||
border:2px solid transparent;transition:.2s;position:relative}
|
||||
.ams-slot.active{border-color:var(--slot-color,var(--accent));
|
||||
box-shadow:0 0 12px rgba(var(--slot-rgb,0,200,255),.3)}
|
||||
.ams-slot.loaded{border-color:var(--ok)!important;
|
||||
box-shadow:0 0 0 2px rgba(64,220,120,.35),0 0 14px rgba(64,220,120,.35)}
|
||||
.ams-slot.loading{border-color:var(--ok)!important;animation:amsPulseGreen 1s ease-in-out infinite}
|
||||
.ams-slot.unloading{border-color:var(--err)!important;animation:amsPulseRed 1s ease-in-out infinite}
|
||||
@keyframes amsPulseGreen{0%{box-shadow:0 0 0 0 rgba(64,220,120,.55)}50%{box-shadow:0 0 0 4px rgba(64,220,120,.25),0 0 18px rgba(64,220,120,.45)}100%{box-shadow:0 0 0 0 rgba(64,220,120,.55)}}
|
||||
@keyframes amsPulseRed{0%{box-shadow:0 0 0 0 rgba(230,80,80,.55)}50%{box-shadow:0 0 0 4px rgba(230,80,80,.25),0 0 18px rgba(230,80,80,.45)}100%{box-shadow:0 0 0 0 rgba(230,80,80,.55)}}
|
||||
.ams-slot-bridge{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
|
||||
border:1px dashed var(--border);background:linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,.01));
|
||||
color:var(--txt2);min-height:106px}
|
||||
.ams-slot-bridge .bridge-chip{width:58px;height:58px;border:1px solid rgba(255,255,255,.14);border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.04);color:var(--txt2);
|
||||
font-size:13px;font-weight:700;letter-spacing:.04em}
|
||||
.slot-circle{width:36px;height:36px;border-radius:50%;margin:0 auto 6px;border:2px solid rgba(255,255,255,.15)}
|
||||
.slot-label{font-size:11px;color:var(--txt2);font-family:var(--mono)}
|
||||
.slot-material{font-size:12px;font-weight:600;margin-bottom:2px}
|
||||
|
||||
/* ── LIGHT + FAN ── */
|
||||
.toggle-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
|
||||
.toggle-label{font-size:13px;font-weight:600}
|
||||
.toggle{position:relative;width:44px;height:24px;cursor:pointer}
|
||||
.toggle input{opacity:0;width:0;height:0;position:absolute}
|
||||
.toggle-track{width:44px;height:24px;background:var(--raised);border-radius:12px;
|
||||
border:1px solid var(--border);transition:.25s;display:block}
|
||||
.toggle input:checked+.toggle-track{background:var(--accent)}
|
||||
.toggle-thumb{position:absolute;top:3px;left:3px;width:18px;height:18px;
|
||||
background:#fff;border-radius:50%;transition:.25s;pointer-events:none}
|
||||
.toggle input:checked~.toggle-thumb{transform:translateX(20px)}
|
||||
.slider-row{display:flex;align-items:center;gap:10px;margin-top:8px}
|
||||
.slider-label{font-size:12px;color:var(--txt2);width:80px}
|
||||
.slider{flex:1;-webkit-appearance:none;height:4px;border-radius:2px;
|
||||
background:var(--raised);outline:none;cursor:pointer}
|
||||
.slider::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;
|
||||
border-radius:50%;background:var(--accent);cursor:pointer;transition:.1s}
|
||||
.slider::-webkit-slider-thumb:hover{transform:scale(1.2)}
|
||||
.slider-val{font-family:var(--mono);font-size:12px;color:var(--txt);width:30px;text-align:right}
|
||||
|
||||
/* ── CONSOLE ── */
|
||||
.console{background:#0a0a0e;border-radius:8px;padding:10px;font-family:var(--mono);
|
||||
font-size:11px;color:#8888aa;overflow-y:auto;line-height:1.6}
|
||||
.console .ts{color:#444;margin-right:6px}
|
||||
.console .msg-info{color:#8888aa}
|
||||
.console .msg-ok{color:var(--ok)}
|
||||
.console .msg-err{color:var(--err)}
|
||||
.console .msg-warn{color:var(--warn)}
|
||||
|
||||
/* ── PANELS ── */
|
||||
.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}
|
||||
.modal-overlay.open{display:flex}
|
||||
.modal-box{background:var(--card);border:1px solid var(--border);border-radius:14px;
|
||||
width:100%;max-width:480px;max-height:90vh;overflow-y:auto;padding:24px;
|
||||
display:flex;flex-direction:column;gap:18px}
|
||||
.modal-header{display:flex;align-items:center;justify-content:space-between}
|
||||
.modal-title{font-size:15px;font-weight:700;color:var(--txt)}
|
||||
.modal-close{background:none;border:none;color:var(--txt2);font-size:20px;
|
||||
cursor:pointer;padding:4px 8px;border-radius:6px}
|
||||
.modal-close:hover{background:var(--raised);color:var(--txt)}
|
||||
.modal-section{font-size:10px;text-transform:uppercase;letter-spacing:.1em;
|
||||
color:var(--txt2);margin-bottom:6px;margin-top:4px}
|
||||
.modal-field{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}
|
||||
.modal-field label{font-size:12px;color:var(--txt2)}
|
||||
.modal-field input{background:var(--raised);border:1px solid var(--border);
|
||||
border-radius:7px;color:var(--txt);padding:7px 10px;font-size:13px;width:100%}
|
||||
.modal-field input:focus{outline:none;border-color:var(--accent)}
|
||||
.poll-btns{display:flex;gap:8px}
|
||||
.poll-btn{flex:1;padding:7px;background:var(--raised);border:1px solid var(--border);
|
||||
border-radius:7px;color:var(--txt2);cursor:pointer;font-size:13px;transition:all .15s}
|
||||
.poll-btn.active{background:var(--accent);border-color:var(--accent);color:#000;font-weight:600}
|
||||
.update-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.update-status{font-size:12px;color:var(--txt2);flex:1;min-width:0}
|
||||
.modal-save{width:100%;padding:10px;background:var(--accent);border:none;
|
||||
border-radius:8px;color:#000;font-weight:700;font-size:14px;cursor:pointer;margin-top:4px}
|
||||
.modal-save:hover{opacity:.88}
|
||||
|
||||
/* ── BOTTOM NAV (mobile) ── */
|
||||
nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
|
||||
background:var(--card);border-top:1px solid var(--border);
|
||||
justify-content:space-around;padding:8px 0 max(8px,env(safe-area-inset-bottom))}
|
||||
.bnav-btn{background:none;border:none;color:var(--txt2);display:flex;
|
||||
flex-direction:column;align-items:center;gap:3px;cursor:pointer;font-size:10px;padding:4px 8px}
|
||||
.bnav-btn.active{color:var(--accent)}
|
||||
.bnav-icon{font-size:20px}
|
||||
|
||||
/* ── Tablet (769–1100px): schmale Sidebar ── */
|
||||
@media(min-width:769px) and (max-width:1100px){
|
||||
nav.sidebar{width:52px;padding:12px 4px}
|
||||
.nav-btn .nav-text{display:none}
|
||||
.nav-btn{justify-content:center;padding:10px}
|
||||
.nav-icon{width:auto}
|
||||
.grid{grid-template-columns:repeat(2,1fr)}
|
||||
.hero{grid-template-columns:1fr}
|
||||
}
|
||||
|
||||
/* ── Mobile (≤768px): Bottom-Nav, 1-Spalte ── */
|
||||
@media(max-width:768px){
|
||||
nav.sidebar{display:none}
|
||||
nav.bottom-nav{display:flex}
|
||||
main{padding:10px;padding-bottom:72px}
|
||||
|
||||
/* Header kompakt */
|
||||
header{padding:0 12px;gap:8px}
|
||||
.hname{display:none}
|
||||
|
||||
/* 1-Spalten-Grid, full-width spans funktionieren weiterhin */
|
||||
.grid{grid-template-columns:1fr;gap:12px}
|
||||
|
||||
/* Hero: Kamera über Info */
|
||||
.hero{grid-template-columns:1fr}
|
||||
.cam-wrap{max-height:220px}
|
||||
|
||||
/* Temp-Pair und Temp-Card übereinander */
|
||||
.temp-pair{grid-template-columns:1fr}
|
||||
.temp-card-inner{grid-template-columns:1fr}
|
||||
|
||||
/* AMS: 2 Spalten auf kleinen Screens */
|
||||
.ams-box-slots{grid-template-columns:repeat(2,1fr)}
|
||||
|
||||
/* Joypad etwas kleiner */
|
||||
.joypad{grid-template-columns:repeat(3,44px);grid-template-rows:repeat(3,44px);gap:5px}
|
||||
.joy{font-size:16px}
|
||||
|
||||
/* Buttons größere Touch-Targets */
|
||||
.btn{padding:10px 14px;font-size:13px}
|
||||
.btn-sm{padding:8px 12px}
|
||||
.step-btn{padding:8px 12px;font-size:13px}
|
||||
|
||||
/* Modal vollbreite auf kleinen Screens */
|
||||
.modal-box{padding:16px;border-radius:10px}
|
||||
.poll-btns{gap:6px}
|
||||
}
|
||||
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