Compare commits

...

9 Commits

Author SHA1 Message Date
Gangoke
fc89dfffa5 fire and forget setDry 2026-05-31 17:27:01 -10:00
ac695ecf36 build: sources for v0.9.18 2026-05-31 19:53:36 +02:00
23b8a69065 merge: PR #40 — Spanish Translation Fixes (@pezfisk)
Native-speaker review der spanischen Übersetzung — fehlende Akzente
(impresión, cámara, después, animación, …) und sprachliche
Korrekturen (Pause→Pausa, Start→Iniciar, Layer→Capa, Stream→Stream).
Zusätzlich neue README.es.md plus Links in README.md + README.de.md.
2026-05-31 18:42:06 +02:00
22dc58258c Spanish translation 2026-05-31 15:47:19 +02:00
e4b4d091f3 Spanish translation 2026-05-31 15:42:28 +02:00
ba209827ce build: sources for v0.9.17 2026-05-30 19:32:39 +02:00
d26b37b332 build: sources for v0.9.17 2026-05-30 19:29:10 +02:00
6f269833d2 merge: PR #37 — Language Refactor (@gangoke)
Sprach-System modernisiert:
- Inline-JS-Translations → web/translations/{de,en,es,zh-cn}.json
- Toggle-Button → Dropdown mit Globe-Icon
- Browser-Locale-Detection mit LocalStorage-Priorität
- Backend-Route /kx/ui/translations/<lang>.json (regioned codes wie zh-cn)
- de.json fd_used 'GENUTZT' → 'BELEGT' (Slot-Kontext lesbarer)
- ES + ZH-CN sind AI-übersetzt (Hinweis im PR)
2026-05-30 18:28:58 +02:00
d808cd3ea8 fix(de): fd_used 'GENUTZT' → 'BELEGT' (klingt im deutschen Slot-Kontext natürlicher) 2026-05-30 18:28:28 +02:00
19 changed files with 8872 additions and 145 deletions

2
.gitignore vendored
View File

@@ -15,3 +15,5 @@ config/config.ini
config/*.ini
!config/config.ini.example
data/
!data/orca_filaments.json

View File

@@ -1,5 +1,133 @@
# Changelog
## [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 aktiv geladene Slot kann
während des Drucks nicht umgeschrieben werden — vorher ausziehen.
- **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

View File

@@ -1,5 +1,131 @@
# Changelog
## [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 currently loaded slot can not
be overwritten during a print — unload it first.
- **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 12 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

View File

@@ -7,6 +7,9 @@ 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 .

View File

@@ -8,7 +8,7 @@
Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
<sub>🇬🇧 <a href="README.md">English version</a></sub>
<sub>🇬🇧 <a href="README.md">English version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
<br>

224
README.es.md Normal file
View File

@@ -0,0 +1,224 @@
<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>🇬🇧 <a href="README.md">English version</a> · 🇩🇪 <a href="README.de.md">Deutsche Version</a></sub>
<br>
[![Ko-fi](https://img.shields.io/badge/Ko--fi-Apoya%20este%20proyecto-FF5E5B?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/viewitde)
&nbsp;
[![Releases](https://img.shields.io/badge/Descargar-Lanzamientos-2EA043?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Downloads](https://img.shields.io/badge/Descargas-800%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Video](https://img.shields.io/badge/YouTube-Tutorial-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](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 de filamento, reasignación por canal, emulación MMU para sincronización de filamento en OrcaSlicer |
| 🗂️ | **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 |
| 🔄 | **Actualización automática** — instala nuevas versiones directamente desde el navegador |
| 🌐 | **OrcaSlicer** — protocolo Moonraker completo (HTTP + WebSocket), interfaz EN/ES |
---
## 🚀 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 && ./kx-bridge
```
**EXE Windows (sin Docker):**
```
kx-bridge.exe
```
> `config\` y `data\` se crean junto al EXE — instalación portátil.
> 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.
---
## 📺 Vídeo tutorial
[![Configuración y uso de KX-Bridge](https://img.youtube.com/vi/1Ql4wfH27fM/hqdefault.jpg)](https://www.youtube.com/watch?v=1Ql4wfH27fM)
---
## 🎨 Slicer recomendado
Para la mejor experiencia con KX-Bridge ofrecemos una **versión modificada de OrcaSlicer** que
incluye tres PRs abiertos de SoftFever/OrcaSlicer: el perfil de la impresora Anycubic Kobra
X, una corrección de GCode multicolor y — lo más importante — una corrección de sincronización de
filamento Moonraker/Happy-Hare que mantiene las posiciones de las ranuras AMS intactas incluso con una ranura vacía.
**[Lanzamientos de OrcaSlicer-KX](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
OrcaSlicer estándar también funciona; la versión modificada mejora principalmente el manejo de AMS.
Es una versión basada en [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0);
el código fuente está disponible a través de los PRs enlazados.
---
## 🏠 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.
> 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: GPL v3](https://img.shields.io/badge/License-GPL_v3-blue.svg)](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:**
[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/viewitde)
</div>

View File

@@ -8,7 +8,7 @@
A Moonraker-compatible bridge that talks directly to the printer.
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a></sub>
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
<br>

View File

@@ -1 +1 @@
0.9.16
0.9.18

View File

@@ -166,6 +166,84 @@ def list_printers() -> list[dict]:
return printers
def list_filament_profiles() -> dict[int, dict]:
"""Liest die [filament_profiles]-Sektion aus config.ini.
Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird
aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt
(als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit
derselben filament_id wie 'OGFL99', d.h. die ID ist nicht eindeutig):
[filament_profiles]
slot_0_vendor = Polymaker
slot_0_name = PolyTerra PLA
slot_0_id = OGFL01
Gibt einen Dict {slot_index: {"id": ..., "vendor": ..., "name": ...}}
zurück. Leere/fehlende Slots werden NICHT aufgenommen — das Default-Mapping
(per filament_type) in der Bridge bleibt dann aktiv.
Backwards-Kompat: alte Configs mit nur (vendor, id) bleiben lesbar; `name`
fehlt dann und der Aufrufer kann optional aus der orca_filaments.json
rekonstruieren.
"""
path = _find_config_file()
if not path:
return {}
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
if not cfg.has_section("filament_profiles"):
return {}
result: dict[int, dict] = {}
for key, value in cfg.items("filament_profiles"):
# Erwartet: slot_<idx>_id oder slot_<idx>_vendor oder slot_<idx>_name
if not key.startswith("slot_"):
continue
parts = key.split("_", 2)
if len(parts) < 3:
continue
try:
slot_idx = int(parts[1])
except ValueError:
continue
field = parts[2]
if field not in ("id", "vendor", "name"):
continue
if not value.strip():
continue
result.setdefault(slot_idx, {})[field] = value.strip()
return result
def save_filament_profiles(profiles: dict[int, dict]) -> bool:
"""Schreibt die übergebenen Slot-Profile in die [filament_profiles]-
Sektion der config.ini. Existierende Einträge werden komplett ersetzt.
profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker", "name": "PolyTerra PLA"}}
Mindestens vendor+name müssen gesetzt sein; id ist optional (Hint).
"""
path = _find_config_file()
if not path:
return False
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
if cfg.has_section("filament_profiles"):
cfg.remove_section("filament_profiles")
if profiles:
cfg["filament_profiles"] = {}
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 get(key: str, default: str = "") -> str:
return os.environ.get(key, default)

7016
data/orca_filaments.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -179,10 +179,24 @@ class KobraXClient:
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:
@@ -191,20 +205,34 @@ class KobraXClient:
pass
def _reconnect(self):
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
antwortet oder disconnect() gerufen wurde. Backoff cappt bei 60 s. Die
ersten 5 Versuche loggen als WARNING (akute Verbindungsstörung), danach
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
ausgeschaltet) zu vermeiden."""
log.warning("Verbindung verloren reconnect…")
try:
self._sock.close()
except Exception:
pass
for delay in [2, 4, 8, 15, 30]:
delays = [2, 4, 8, 15, 30, 60]
attempt = 0
while self._running:
delay = delays[min(attempt, len(delays) - 1)]
try:
self._do_connect()
log.info("Reconnect erfolgreich")
log.info("Reconnect erfolgreich (nach %d Versuchen)", attempt + 1)
return True
except Exception as e:
log.warning("Reconnect fehlgeschlagen (%s), warte %ss…", e, delay)
time.sleep(delay)
return False
attempt += 1
lvl = log.warning if attempt <= 5 else log.debug
lvl("Reconnect fehlgeschlagen (%s, Versuch %d), warte %ss…", e, attempt, delay)
# Geteiltes Sleep damit disconnect() den Loop schneller bricht.
slept = 0.0
while slept < delay and self._running:
time.sleep(min(0.5, delay - slept))
slept += 0.5
return False # nur wenn disconnect() gerufen wurde
def _subscribe(self, topic: str):
with self._lock:
@@ -348,6 +376,9 @@ class KobraXClient:
# -- Publish + request/response ------------------------------------------
def publish(self, msg_type: str, action: str, data=None, timeout: float = 5.0) -> dict | None:
# Falls Reader-Thread aus historischen Gründen tot ist, wiederbeleben —
# sonst würden Replies nie ankommen und event.wait() läuft ins Timeout.
self._ensure_reader()
msgid = str(uuid.uuid4())
payload = json.dumps({
"type": msg_type,
@@ -413,6 +444,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,
@@ -429,7 +461,14 @@ class KobraXClient:
with self._lock:
self._sock.sendall(_build_publish(topic, payload))
except Exception as e:
log.error("web send error: %s", e)
log.error("web send error: %s, reconnecting…", e)
# Reconnect triggern (analog zu publish()); ohne Retry weil
# fire-and-forget — der nächste Aufruf wird auf den frischen Socket
# treffen.
try:
self._reconnect()
except Exception:
pass
# -- High-level commands -------------------------------------------------
@@ -545,9 +584,15 @@ 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).
sock = socket.create_connection((self.host, 18910), timeout=10)
sock.settimeout(None) # blocking während Send
sock.sendall(headers + body)
sock.settimeout(120) # große GCode-Dateien brauchen Zeit bis der Drucker antwortet
sock.settimeout(180)
response = b""
try:
while True:

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
from PyInstaller.utils.hooks import collect_all
datas = [("web", "web")]
datas = [("web", "web"), ("data", "static")] # bridge/data/ → static/ im _MEIPASS
binaries = []
hiddenimports = []

View File

@@ -285,9 +285,9 @@ function applyLang(){
setText('d-lbl-light',T.lbl_light);
setText('d-lbl-nozzle',T.label_nozzle);
setText('d-lbl-bed',T.label_bed);
// Dashboard buttons
setText('d-btn-pause',T.btn_pause);
setText('d-btn-resume',T.btn_resume);
// Dashboard buttons — Pause-Button wird zur Toggle-Action; Resume-Beschriftung
// wird in updatePauseResumeBtn() je nach Druckerzustand gesetzt.
updatePauseResumeBtn();
setText('d-btn-cancel',T.btn_cancel);
setText('cam-toggle-btn',camOn?T.btn_cam_stop:T.btn_cam_start);
setText('cam-placeholder-txt',T.cam_placeholder);
@@ -361,6 +361,10 @@ function applyLang(){
// Slot-Edit-Dialog
setText('lbl-slot-color',T.slot_edit_color);
setText('lbl-slot-material',T.slot_edit_material);
setText('lbl-slot-profile',T.slot_edit_profile);
setText('slot-profile-hint',T.slot_edit_profile_hint);
var defOpt=document.getElementById('slot-profile-default-opt');
if(defOpt) defOpt.textContent=T.slot_edit_profile_default;
setText('btn-slot-edit-save',T.slot_edit_save);
updateSlotEditFeedButton();
var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom);
@@ -370,7 +374,11 @@ function applyLang(){
setText('file-ready-btn',T.file_ready_btn);
setText('file-slots-btn',T.file_slots_btn);
setText('file-cancel-btn',T.file_cancel_btn);
setText('file-cancel-btn',T.file_cancel_btn);
// GCode-Browser-Karten: Texte sind via innerHTML eingebacken,
// bei Sprachwechsel komplett neu rendern.
if(typeof renderStore==='function' && typeof storeFiles!=='undefined'){
try{ renderStore(); }catch(e){}
}
}
function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;}
@@ -604,11 +612,14 @@ function applyState(){
}else{frb.style.display='none';}
}
// skip-button (mid-print) nur sichtbar wenn aktuell gedruckt wird
var printing=(s.print_state==='printing'||s.print_state==='paused');
var skipBtn=document.getElementById('d-btn-skip');
if(skipBtn){
var printing=(s.print_state==='printing'||s.print_state==='paused');
skipBtn.style.display=printing?'':'none';
}
if(skipBtn) skipBtn.style.display=printing?'':'none';
// Pause/Stopp-Buttons nur bei aktivem Druck zeigen (sonst verwirrend wenn
// der Drucker idle ist). Pause-Button rendert sich passend zum State um.
var ctrlBtns=document.getElementById('d-ctrl-btns');
if(ctrlBtns) ctrlBtns.style.display=printing?'':'none';
updatePauseResumeBtn();
// header
var b=document.getElementById('h-badge');
@@ -770,10 +781,17 @@ function applyState(){
var activity=(slot.activity||'');
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'');
var slotLabel=T.label_slot+' '+(globalIdx+1);
var profile=(window._slotProfileMap||{})[globalIdx];
var vendorBadge='';
if(!empty && profile && profile.vendor){
var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':'');
vendorBadge='<div class="slot-label" style="font-size:9px;color:var(--accent);font-weight:600;margin-top:1px" title="'+tt+'">'+profile.vendor+'</div>';
}
html+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(empty?' empty':'')
+'" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
+'<div class="slot-circle" style="background:'+col+'"></div>'
+'<div class="slot-material">'+(empty?'':(slot.type||slot.material_type||''))+'</div>'
+vendorBadge
+'<div class="slot-label">'+slotLabel+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
@@ -904,6 +922,52 @@ function updateSlotEditFeedButton(){
btn.style.display='';
btn.textContent=_slotEditLoaded?tr('slot_edit_unload'):tr('slot_edit_load');
}
var _orcaFilamentCache=null; // [{id,name,vendor,type,color}, …]
function _loadOrcaFilaments(cb){
if(_orcaFilamentCache){ cb(_orcaFilamentCache); return; }
fetch(_apiUrl('/kx/filament/profiles')).then(function(r){return r.json();}).then(function(d){
_orcaFilamentCache=d.result||[];
cb(_orcaFilamentCache);
}).catch(function(){ cb([]); });
}
function _profileKey(vendor, name){
// Eindeutiger Selector: (vendor, name). IDs aus orca_filaments.json sind
// NICHT eindeutig (z.B. 136 Profile mit OGFL99). Wir kodieren beide in den
// <option>-Value-String mit | als Trenner.
return (vendor||'')+'|'+(name||'');
}
function _fillSlotProfileDropdown(material, currentVendor, currentName){
var sel=document.getElementById('slot-edit-profile');
if(!sel) return;
var wantKey=_profileKey(currentVendor, currentName);
_loadOrcaFilaments(function(profiles){
// Type-Filter: nur Profile vom passenden material zeigen (z.B. PLA → alle PLA-Varianten)
var matU=(material||'').toUpperCase().trim();
var matched=profiles.filter(function(p){
var pt=(p.type||'').toUpperCase();
// PLA-CF, PLA-SILK etc. zählen auch zu PLA
return matU==='' || pt===matU || pt.startsWith(matU+'-') || pt.startsWith(matU+' ');
});
sel.innerHTML='<option value="">'+tr('slot_edit_profile_default')+'</option>';
// Gruppieren nach Vendor
var byVendor={};
matched.forEach(function(p){ (byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p); });
Object.keys(byVendor).sort().forEach(function(v){
var g=document.createElement('optgroup'); g.label=v;
byVendor[v].forEach(function(p){
var o=document.createElement('option');
o.value=_profileKey(p.vendor, p.name);
o.dataset.vendor=p.vendor;
o.dataset.name=p.name;
o.dataset.id=p.id || '';
o.textContent=p.name;
if(o.value===wantKey) o.selected=true;
g.appendChild(o);
});
sel.appendChild(g);
});
});
}
function openSlotEdit(i){
var slot=(window._amsSlots||[])[i]||{};
var globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i);
@@ -923,6 +987,19 @@ function openSlotEdit(i){
+'style="padding:4px 10px;border-radius:6px;border:1px solid var(--border);cursor:pointer;font-size:12px;'
+(m===mat?'background:var(--accent);color:#fff':'background:var(--raised);color:var(--txt2)')+'">'+m+'</button>';
}).join('');
// OrcaSlicer-Profil-Dropdown: aktuellen User-Override für diesen Slot
// aus /kx/filament/slots holen (enthält vendor+name+id).
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}).then(function(d){
var arr=d.result||[];
var entry=arr.find(function(x){return x.slot_index===globalIdx;})||{};
window._slotProfileMap=window._slotProfileMap||{};
window._slotProfileMap[globalIdx]={
id: entry.filament_id||'',
vendor:entry.filament_vendor||'',
name: entry.filament_name||'',
};
_fillSlotProfileDropdown(mat, entry.filament_vendor||'', entry.filament_name||'');
}).catch(function(){ _fillSlotProfileDropdown(mat,'',''); });
updateSlotEditFeedButton();
document.getElementById('slot-edit-modal').classList.add('open');
}
@@ -967,6 +1044,9 @@ function cancelReadyFile(){
function selectMatPreset(m){
document.getElementById('slot-edit-mat').value=m;
highlightMatBtn(m);
// Filament-Profile-Dropdown an neues Material anpassen
// (vorherige Selektion zurücksetzen — andere Material-Profile passen nicht)
_fillSlotProfileDropdown(m, '', '');
}
function highlightMatBtn(val){
document.querySelectorAll('.mat-preset-btn').forEach(function(b){
@@ -974,6 +1054,8 @@ function highlightMatBtn(val){
b.style.background=on?'var(--accent)':'var(--raised)';
b.style.color=on?'#fff':'var(--txt2)';
});
// Auch bei manueller Eingabe ins Material-Textfeld: Dropdown refreshen.
if(val) _fillSlotProfileDropdown(val, '', '');
}
function hexToRgb(hex){
var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16);
@@ -983,13 +1065,56 @@ function saveSlotEdit(){
var hex=document.getElementById('slot-edit-color').value;
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
var color=hexToRgb(hex);
post('/api/ams/set_slot',{index:_slotEditIndex,type:mat,color:color})
.then(function(r){return r.json();})
.then(function(r){
closeSlotEdit();
clog(tr('slot_edit_ok')+' '+(_slotEditIndex+1)+': '+mat+' '+hex,'msg-ok');
})
.catch(function(e){clog('Fehler: '+e,'msg-err');});
var slotIdx=_slotEditIndex;
var profSel=document.getElementById('slot-edit-profile');
var sel=profSel && profSel.selectedOptions && profSel.selectedOptions[0];
// Primärer Selector: (vendor, name). id ist nur Hint (aus dem JSON-data-attr
// mitgegeben — Backend lookt sie nochmal selber nach um veraltete Hints zu
// korrigieren).
var newProfVendor=sel?(sel.dataset.vendor||''):'';
var newProfName =sel?(sel.dataset.name ||''):'';
var newProfId =sel?(sel.dataset.id ||''):'';
// Sequenziell speichern: erst Profil-Override (config.ini), dann Material/Farbe
// (MQTT zum Drucker). Sonst können beide Pfade sich überholen und der Slot-State
// ist beim nächsten Re-Open inkonsistent.
fetch(_apiUrl('/kx/filament/slots/'+slotIdx+'/profile'),{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({vendor:newProfVendor, name:newProfName, id:newProfId})
})
.then(function(r){return r.json();})
.then(function(){
window._slotProfileMap=window._slotProfileMap||{};
if(newProfVendor && newProfName){
window._slotProfileMap[slotIdx]={id:newProfId, vendor:newProfVendor, name:newProfName};
} else {
delete window._slotProfileMap[slotIdx];
}
return post('/api/ams/set_slot',{index:slotIdx,type:mat,color:color});
})
.then(function(r){return r?r.json():null;})
.then(function(){
// Slot-Map refreshen damit die Karte sofort den Vendor zeigt.
return fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();});
})
.then(function(d){
var arr=(d && d.result)||[];
window._slotProfileMap={};
arr.forEach(function(e){
if(e.filament_vendor && e.filament_name){
window._slotProfileMap[e.slot_index]={
id: e.filament_id||'',
vendor:e.filament_vendor,
name: e.filament_name,
};
}
});
closeSlotEdit();
var profSuffix=newProfName?(' ['+newProfVendor+' '+newProfName+']'):'';
clog(tr('slot_edit_ok')+' '+(slotIdx+1)+': '+mat+' '+hex+profSuffix,'msg-ok');
if(typeof poll==='function') poll();
})
.catch(function(e){clog('Fehler: '+e,'msg-err');});
}
document.addEventListener('DOMContentLoaded',function(){
document.getElementById('s-printer-ip').addEventListener('input',function(){
@@ -1086,6 +1211,21 @@ var pollTimer;
(function(){
var ms=parseInt(localStorage.getItem('pollInterval')||'2000');
initPrinters();
// Slot-Profile-Map initial laden, sonst zeigen die Karten beim ersten
// Render keine Vendor-Badge obwohl in der config.ini ein Override steht.
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}).then(function(d){
var arr=(d && d.result)||[];
window._slotProfileMap={};
arr.forEach(function(e){
if(e.filament_vendor && e.filament_name){
window._slotProfileMap[e.slot_index]={
id: e.filament_id||'',
vendor:e.filament_vendor,
name: e.filament_name,
};
}
});
}).catch(function(){});
poll();pollTimer=setInterval(poll,ms);
})();
@@ -1094,7 +1234,28 @@ function printAction(a){
post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()})
.catch(function(e){clog('Fehler: '+e,'msg-err')});
}
function confirmCancel(){if(confirm('Druck wirklich abbrechen?'))printAction('cancel')}
function togglePauseResume(){
// Druckt → pause; Pausiert → resume. Status kommt aus dem zuletzt gepollten
// print_state in S; bei Unklarheit (kein State) Pause als Default.
var state=(S && S.print_state)||'';
if(state==='paused') printAction('resume');
else printAction('pause');
}
function updatePauseResumeBtn(){
var btn=document.getElementById('d-btn-pause');
if(!btn) return;
var state=(S && S.print_state)||'';
if(state==='paused'){
btn.textContent=T.btn_resume||'▶ Weiter';
btn.classList.add('btn-resume');
btn.classList.remove('btn-pause');
} else {
btn.textContent=T.btn_pause||'⏸ Pause';
btn.classList.add('btn-pause');
btn.classList.remove('btn-resume');
}
}
function confirmCancel(){if(confirm(T.confirm_cancel||'Druck wirklich abbrechen?'))printAction('cancel')}
// ── Axis motion ──
// axis codes: 0=X, 1=Y, 2=Z

View File

@@ -162,6 +162,15 @@
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>
</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>
@@ -234,9 +243,8 @@
</div>
</div>
<div class="fname" id="d-fname" title="" style="margin-top:6px"></div>
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px">
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="printAction('pause')">⏸ Pause</button>
<button class="btn btn-resume btn-sm" id="d-btn-resume" onclick="printAction('resume')">▶ Weiter</button>
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px;display:none">
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="togglePauseResume()">⏸ Pause</button>
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none"><span id="d-btn-skip-label">Objekte</span></button>
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
</div>

View File

@@ -161,6 +161,9 @@
"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) —",
"log_dir_all": "Alle",
"log_lvl_label": "Level:",
"file_ready_btn": "▶ Druck starten",
@@ -182,7 +185,7 @@
"fd_no_slots_msg": "Keine belegten AMS-Slots.{br}Druck trotzdem starten?",
"fd_slot": "Slot",
"fd_no_matching_material": "Kein passendes Material",
"fd_used": "GENUTZT",
"fd_used": "BELEGT",
"add_printer": "Drucker hinzufügen",
"apd_lbl_ip": "Drucker-IP",
"apd_lbl_name": "Name (optional)",

View File

@@ -161,6 +161,9 @@
"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) —",
"log_dir_all": "All",
"log_lvl_label": "Level:",
"file_ready_btn": "▶ Start Print",

View File

@@ -20,9 +20,9 @@
"kobra_finished": "Finalizado",
"kobra_failed": "Error",
"kobra_canceled": "Cancelado",
"kobra_offline": "Offline",
"kobra_offline": "Desconectada",
"nav_dashboard": "Panel",
"nav_print": "Impresion",
"nav_print": "Impresión",
"nav_temps": "Temperaturas",
"nav_motion": "Movimiento",
"nav_ams": "AMS",
@@ -31,12 +31,12 @@
"card_progress": "Progreso",
"card_temps": "Temperaturas",
"card_light_fan": "Ventilador",
"card_speed": "Velocidad de impresion",
"card_cam": "Camara",
"card_speed": "Velocidad de impresión",
"card_cam": "Cámara",
"lbl_elapsed": "Transcurrido:",
"lbl_remaining": "Restante:",
"lbl_slicer_time": "Estimacion del slicer:",
"lbl_layers": "Layer",
"lbl_slicer_time": "Estimación del slicer:",
"lbl_layers": "Capa",
"speed_silent": "🐢 Silencioso",
"speed_normal": "⚡ Normal",
"speed_sport": "🚀 Sport",
@@ -52,14 +52,14 @@
"ace_dry_current_temp": "Temperatura",
"ace_dry_chart": "Historial (Temp/Humedad)",
"ace_dry_temp": "Temperatura (°C)",
"ace_dry_duration": "Duracion (min)",
"ace_dry_start": "▶ Start",
"ace_dry_stop": "■ Stop",
"ace_dry_auto_refill": "Relleno automatico",
"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 despues)",
"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)",
@@ -67,12 +67,12 @@
"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 por defecto",
"cam_placeholder": "📷 Camara no iniciada",
"ace_dry_dialog_reset_default": "Restablecer valores predeterminados",
"cam_placeholder": "📷 Cámara no iniciada",
"cam_stream_unavailable": "Stream no disponible",
"btn_cam_start": "▶ Camara",
"btn_cam_stop": "◼ Camara",
"btn_pause": "⏸ Pause",
"btn_cam_start": "▶ Cámara",
"btn_cam_stop": "◼ Cámara",
"btn_pause": "⏸ Pausa",
"btn_resume": "▶ Reanudar",
"btn_cancel": "✕ Detener",
"label_nozzle": "Boquilla",
@@ -81,20 +81,20 @@
"label_light": "💡 Luz",
"label_on_off": "Encendido / Apagado",
"label_speed": "Velocidad",
"panel_print_title": "Control de impresion",
"panel_print_btn_pause": "⏸ Pause",
"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": "Off",
"label_off": "Apagado",
"panel_temps_nozzle": "Boquilla",
"panel_temps_bed": "Cama caliente",
"panel_temps_chart": "Historial (ultimas 60 lecturas)",
"panel_temps_chart": "Historial (últimas 60 lecturas)",
"label_target_c": "Objetivo:",
"panel_motion_xy": "Ejes XY",
"panel_motion_z": "Eje Z",
"label_step": "Tamano del paso:",
"label_step": "Tamaño del paso:",
"btn_home_z": "Home Z",
"btn_home_xy": "Home XY",
"btn_home_all": "Home All",
@@ -103,11 +103,11 @@
"card_ams": "Filamento",
"ams_no_data": "No se recibieron datos de AMS",
"label_slot": "Ranura",
"ams_empty": "Vacio",
"ams_empty": "Vacío",
"panel_extras_light": "Luz",
"panel_extras_fan": "Ventilador",
"panel_extras_camera": "Camara",
"btn_cam_start2": "▶ Start",
"panel_extras_camera": "Cámara",
"btn_cam_start2": "▶ Iniciar",
"btn_cam_stop2": "◼ Detener",
"panel_console_title": "Registro de eventos",
"log_light_on": "Luz encendida",
@@ -118,29 +118,29 @@
"log_axis": "Eje",
"log_home": "Home",
"log_home_all": "Home All",
"log_cam_start": "Camara iniciada:",
"log_cam_stop": "Camara detenida",
"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 impresion?",
"settings_title": "Configuracion",
"settings_connection": "Conexion",
"settings_print": "Ajustes de impresion",
"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",
"settings_version": "Version",
"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": "Contrasena MQTT",
"settings_password": "Contraseña MQTT",
"settings_device_id": "ID del dispositivo",
"settings_mode_id": "Mode ID",
"hint_ip_no_port": "Solo direccion IP, sin puerto (p. ej. 192.168.1.102)",
"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 camara al iniciar impresion",
"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...",
@@ -152,7 +152,7 @@
"update_error": "Error",
"btn_connect": "⚡ Conectar",
"btn_disconnect": "✕ Desconectar",
"lbl_conn_error": "Error de conexion:",
"lbl_conn_error": "Error de conexión:",
"slot_edit_title": "Editar slot",
"slot_edit_color": "Color",
"slot_edit_material": "Material",
@@ -161,62 +161,65 @@
"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) —",
"log_dir_all": "Todos",
"log_lvl_label": "Level:",
"file_ready_btn": "▶ Iniciar impresion",
"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": "Desmarca objetos que ya no quieras imprimir:",
"skip_hint": "Deselecciona los objetos que ya no quieras imprimir:",
"skip_btn_label": "Objetos",
"skip_no_objects": "No hay objetos en esta impresion.",
"skip_no_objects": "No hay objetos en esta impresión.",
"skip_already": "omitido",
"skip_select_at_least_one": "Elige al menos un objeto.",
"skip_select_at_least_one": "Selecciona al menos un objeto.",
"skip_sending": "Enviando …",
"skip_success": "Se omitiran los objetos.",
"skip_success": "Se omitirán los objetos.",
"fd_objects_hint": "Omitir objetos (opcional):",
"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 impresion de todos modos?",
"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": "Agregar impresora",
"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": "Agregar",
"apd_confirm": "Añadir",
"apd_fetching": "Obteniendo datos de la impresora…",
"apd_success": "Impresora agregada, reiniciando bridge…",
"apd_err_ip": "Introduce una direccion IP",
"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 reiniciara.",
"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": "Aun no hay impresora configurada.",
"printers_empty_hint": "Aún no hay impresora configurada.",
"nav_browser": "Explorador",
"panel_browser_title": "Explorador de archivos",
"store_search_placeholder": "🔍 Buscar…",
"store_empty": "Aun no hay archivos subidos.",
"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_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": "Estimacion",
"store_upload_label_prefix": "Arrastra GCode aqui o ",
"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}",
@@ -227,5 +230,5 @@
"sf_new": "Nuevo",
"ss_date": "↓ Fecha",
"ss_name": "AZ Nombre",
"ss_dur": "⏱ Tiempo de impresion"
"ss_dur": "⏱ Tiempo de impresión"
}

View File

@@ -161,6 +161,9 @@
"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": "— 通用 (默认) —",
"log_dir_all": "全部",
"log_lvl_label": "级别:",
"file_ready_btn": "▶ 开始打印",