Compare commits

...

22 Commits

Author SHA1 Message Date
ce416f3b9a ci: release.yml nur noch Docker-Build (Release macht release.sh)
All checks were successful
Nightly Build / build (push) Successful in 3m43s
- "Create Gitea Release"-Step entfernt → keine doppelte Release-Erstellung
  mehr (release.sh legt Release + englischen Auto-Changelog + Assets an).
- Image-Tag strippt fuehrendes 'v' (VERSION-Datei hat keins).
- Tag-Pattern auf 'v*' erweitert (vorher matchte v0.9.x.y-Hotfixes nicht).
2026-06-26 23:45:23 +02:00
67c013f4ff nightly: 0.9.27-nightly9
All checks were successful
Nightly Build / build (push) Successful in 3m41s
2026-06-26 23:19:02 +02:00
40f85b1eb6 nightly: 0.9.27-nightly8 2026-06-26 23:10:27 +02:00
54ce101f99 ci: Changelog aus CHANGES.md lesen (von release.sh aus Dev-Repo generiert) 2026-06-26 23:10:22 +02:00
3531cad0ef nightly: 0.9.27-nightly7
All checks were successful
Nightly Build / build (push) Successful in 4m4s
2026-06-26 22:51:32 +02:00
f192a9943d ci: apk/wget-Fallback für curl korrekt klammern
All checks were successful
Nightly Build / build (push) Successful in 3m23s
2026-06-25 23:47:34 +02:00
eb7fd44f68 nightly: 0.9.27-nightly6 2026-06-25 23:41:36 +02:00
e5b2a19192 ci: curl via apk/static-binary statt BusyBox-wget für API-Calls 2026-06-25 23:41:25 +02:00
2f59a2b02b nightly: 0.9.27-nightly5
Some checks failed
Nightly Build / build (push) Failing after 3m48s
2026-06-25 23:29:34 +02:00
bc9bfb58ea ci: TAG aus VERSION statt Datum, curl durch wget ersetzen 2026-06-25 23:29:19 +02:00
ac309d5d3d nightly: 0.9.27-nightly4
Some checks failed
Nightly Build / build (push) Failing after 3m37s
2026-06-25 23:01:34 +02:00
38d98666c4 ci: python3 durch awk ersetzen (nicht im Runner-Image) 2026-06-25 23:01:28 +02:00
e7c978a067 ci: nightly.yml YAML-Syntaxfehler beheben (kein Multiline-Body im YAML) 2026-06-25 22:53:58 +02:00
d7c2dccef5 ci: Gitea Nightly-Release mit Changelog nach erfolgreichem Build 2026-06-25 22:50:08 +02:00
e59550b5a0 nightly: 0.9.27-nightly3 + Kamera-Fix
All checks were successful
Nightly Build / build (push) Successful in 3m33s
2026-06-25 22:38:45 +02:00
5871e851da fix: .runner-token aus Repo entfernen + gitignore 2026-06-25 22:37:52 +02:00
e70e9c82d7 nightly: 0.9.27-nightly3 2026-06-25 22:37:39 +02:00
be110fd766 ci: Docker CLI Installation BusyBox-kompatibel (wget, fixe Versionen) 2026-06-25 12:56:38 +02:00
77fce988d7 ci: Docker CLI als statisches Binary installieren (Alpine-kompatibel) 2026-06-25 12:54:02 +02:00
fe1815c76f ci: Docker CLI im Runner-Container per apt installieren 2026-06-25 12:51:00 +02:00
29a4262a2a ci: Actions auf native Shell-Commands umgestellt (kein Node.js nötig) 2026-06-25 12:46:31 +02:00
e753bcdb03 ci: Builds auf server-runner (Gitea-Server) umgestellt 2026-06-25 12:40:04 +02:00
19 changed files with 427 additions and 124 deletions

View File

@@ -16,46 +16,117 @@ on:
jobs:
build:
runs-on: self-hosted
runs-on: server-runner
steps:
- name: Checkout
uses: actions/checkout@v4
with:
clean: true
run: |
if [ -d .git ]; then
git fetch origin nightly
git reset --hard origin/nightly
git clean -fd
else
git clone --depth=1 --branch nightly https://gitea.it-drui.de/viewit/KX-Bridge-Release.git .
fi
- name: Install Docker CLI
run: |
if ! command -v docker >/dev/null 2>&1; then
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
DARCH="x86_64"
BARCH="amd64"
else
DARCH="aarch64"
BARCH="arm64"
fi
wget -qO- "https://download.docker.com/linux/static/stable/${DARCH}/docker-27.5.1.tgz" \
| tar xz --strip-components=1 -C /usr/local/bin docker/docker
chmod +x /usr/local/bin/docker
mkdir -p /usr/local/lib/docker/cli-plugins
wget -qO /usr/local/lib/docker/cli-plugins/docker-buildx \
"https://github.com/docker/buildx/releases/download/v0.23.0/buildx-v0.23.0.linux-${BARCH}"
chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
fi
docker version --format '{{.Client.Version}}'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
run: |
docker run --rm --privileged tonistiigi/binfmt:latest --install all
- name: Set up buildx
uses: docker/setup-buildx-action@v3
run: |
docker buildx inspect kxbuilder 2>/dev/null || \
docker buildx create --name kxbuilder --use
docker buildx use kxbuilder
- name: Login to Gitea registry
uses: docker/login-action@v3
with:
registry: gitea.it-drui.de
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | \
docker login gitea.it-drui.de -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- 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"
VERSION=$(cat VERSION)
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
--provenance=false \
--no-cache \
$(echo "$TAGS" | tr ',' '\n' | sed 's/^/-t /') \
-t "gitea.it-drui.de/viewit/kx-bridge:nightly" \
-t "gitea.it-drui.de/viewit/kx-bridge:nightly-${VERSION}" \
.
- name: Set nightly tag
- name: Create Gitea Nightly Release
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
DATE=$(date +%Y%m%d)
TAG="nightly-$DATE"
VERSION=$(cat VERSION)
TAG="nightly-${VERSION}"
git config user.name "gitea-actions"
git config user.email "actions@it-drui.de"
# Changelog aus CHANGES.md lesen (wird von release.sh aus dem Dev-Repo generiert)
BODY_FILE=$(mktemp)
if [ -f CHANGES.md ]; then
cat CHANGES.md > "$BODY_FILE"
else
# Fallback falls CHANGES.md fehlt
printf '## KX-Bridge %s -- Nightly Build\n\n' "$VERSION" > "$BODY_FILE"
printf '[experimentell] Ungetestete Features, nur fuer Tester geeignet.\n\n' >> "$BODY_FILE"
printf '- Automatischer Nightly-Build\n\n---\n\n' >> "$BODY_FILE"
printf '### Docker-Image aktualisieren\n\n```bash\ndocker compose pull && docker compose up -d\n```\n\n' >> "$BODY_FILE"
printf 'Image-Tag: `gitea.it-drui.de/viewit/kx-bridge:nightly`\n' >> "$BODY_FILE"
fi
# Tag setzen
git tag -f "$TAG"
git push origin "$TAG" --force
git push https://gitea-actions:${GITEA_TOKEN}@gitea.it-drui.de/viewit/KX-Bridge-Release.git "$TAG" --force
# curl installieren (BusyBox wget kann kein DELETE/POST mit Headers)
if ! command -v curl >/dev/null 2>&1; then
if ! apk add --no-cache curl 2>/dev/null; then
wget -qO /usr/local/bin/curl \
"https://github.com/moparisthebest/static-curl/releases/download/v8.6.0/curl-amd64"
chmod +x /usr/local/bin/curl
fi
fi
# Altes Release loeschen falls vorhanden
curl -s -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases/tags/${TAG}" \
2>/dev/null || true
# Release erstellen (JSON-Body via awk escapen)
BODY_JSON=$(awk '{
gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); gsub(/\t/, "\\t");
printf "%s\\n", $0
}' "$BODY_FILE" | awk 'BEGIN{printf "\""} {printf "%s", $0} END{printf "\""}')
JSON_PAYLOAD="{\"tag_name\":\"${TAG}\",\"name\":\"KX-Bridge ${VERSION} Nightly\",\"body\":${BODY_JSON},\"draft\":false,\"prerelease\":true}"
printf '%s' "$JSON_PAYLOAD" > /tmp/release_body.json
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @/tmp/release_body.json \
"https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases"
rm -f "$BODY_FILE" /tmp/release_body.json

View File

@@ -7,9 +7,17 @@ on:
jobs:
lint-and-test:
runs-on: self-hosted
runs-on: server-runner
steps:
- uses: actions/checkout@v4
- name: Checkout
run: |
if [ -d .git ]; then
git fetch origin
git reset --hard origin/nightly
git clean -fd
else
git clone --depth=1 --branch nightly https://gitea.it-drui.de/viewit/KX-Bridge-Release.git .
fi
- name: Dependencies installieren
run: pip3 install -r requirements.txt

View File

@@ -3,33 +3,63 @@ name: Stable Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v*'
jobs:
release:
runs-on: self-hosted
runs-on: server-runner
steps:
- name: Checkout
uses: actions/checkout@v4
with:
clean: true
run: |
TAG="${GITHUB_REF#refs/tags/}"
if [ -d .git ]; then
git fetch --tags origin
git checkout "$TAG"
git clean -fd
else
git clone --depth=1 --branch "$TAG" https://gitea.it-drui.de/viewit/KX-Bridge-Release.git .
fi
- name: Install Docker CLI
run: |
if ! command -v docker >/dev/null 2>&1; then
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
DARCH="x86_64"
BARCH="amd64"
else
DARCH="aarch64"
BARCH="arm64"
fi
wget -qO- "https://download.docker.com/linux/static/stable/${DARCH}/docker-27.5.1.tgz" \
| tar xz --strip-components=1 -C /usr/local/bin docker/docker
chmod +x /usr/local/bin/docker
mkdir -p /usr/local/lib/docker/cli-plugins
wget -qO /usr/local/lib/docker/cli-plugins/docker-buildx \
"https://github.com/docker/buildx/releases/download/v0.23.0/buildx-v0.23.0.linux-${BARCH}"
chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
fi
docker version --format '{{.Client.Version}}'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
run: |
docker run --rm --privileged tonistiigi/binfmt:latest --install all
- name: Set up buildx
uses: docker/setup-buildx-action@v3
run: |
docker buildx inspect kxbuilder 2>/dev/null || \
docker buildx create --name kxbuilder --use
docker buildx use kxbuilder
- name: Login to Gitea registry
uses: docker/login-action@v3
with:
registry: gitea.it-drui.de
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | \
docker login gitea.it-drui.de -u "${{ secrets.REGISTRY_USER }}" --password-stdin
# Strip fuehrendes 'v' fuer den Image-Tag (VERSION-Datei hat kein 'v').
- name: Build & push (amd64 + arm64)
run: |
VERSION="${GITHUB_REF#refs/tags/}"
VERSION="${GITHUB_REF#refs/tags/v}"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
@@ -39,20 +69,6 @@ jobs:
-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
}"
# Hinweis: Das Gitea-Release (inkl. englischem Auto-Changelog + Binaries als
# Assets) erstellt release.sh synchron, da es die lokal via CodeBuilder
# gebauten Binaries direkt hochlaedt. Dieser Workflow baut nur das Docker-Image.

4
.gitignore vendored
View File

@@ -17,8 +17,4 @@ config/*.ini
data/
!data/orca_filaments.json
# Sensitive files — never commit
.runner-token
secrets/
*.token

33
CHANGES.md Normal file
View File

@@ -0,0 +1,33 @@
## KX-Bridge 0.9.27-nightly9 — Nightly Build
[experimentell] Ungetestete Features, nur für Tester geeignet.
### Änderungen seit `v0.9.26`
- fix(spoolman): Status-Dot beim Seitenload initialisieren
- chore: CHANGES.md mit echtem Changelog aus Dev-Repo ins Release-Repo schreiben
- fix(spoolman): SPOOLMAN_* env-Cache bei Restart leeren, Status-Dot nach Save aktualisieren
- chore: README.es.md + CONTRIBUTING.md in release.sh-Sync aufnehmen
- docs: Nightly-Sektion, Wartungshinweis + CONTRIBUTING.md, FR/IT-Sprachen, Downloads-Badge
- fix(update): Nightly-Vergleich auf Versions-String umstellen (statt Datum)
- feat(update): Nightly-Track vom Stable-Track trennen
- fix(release): nightly immer auf nightly-Branch pushen, kein master-Push
- fix(camera): exponentielles Backoff bei ffmpeg-Fehler + /api/camera/reset + ↺-Button
- fix(release): Nightly-Release vom nightly-Branch erlauben
- feat(i18n): fehlende UI-Übersetzungen ergänzt + Keys alphabetisch sortiert (PR #70 @fenopy)
- fix: config/config.ini.example beim Release-Sync mitübertragen (Issue #72)
- feat(ui): Integrationen-Tab in Settings (Spoolman + Obico-Hinweis)
- feat(stack): KobraX Full Stack Compose für Portainer (KX-Bridge + Obico + Spoolman)
- feat(release): Nightly/Stable Release-Workflow mit eigenem Docker-Tag
- feat(spoolman): optionale Spoolman-Filamentverbrauch-Integration (PR #65, @p2l)
- fix(release): Artifact-Download per HTTP statt lokalem Dateipfad
---
### Docker-Image aktualisieren
```bash
docker compose pull && docker compose up -d
```
Image-Tag: `gitea.it-drui.de/viewit/kx-bridge:nightly`

View File

@@ -63,13 +63,13 @@ The PR template will be pre-filled. Set the target branch to **`nightly`**.
## Branch model
```
main ← stable releases only (merged by maintainer)
master ← 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.
Your PR always targets `nightly`. The maintainer periodically merges `nightly → master` for a new stable release.
---

View File

@@ -21,7 +21,7 @@ Feedback willkommen.</sub>
&nbsp;
[![Releases](https://img.shields.io/badge/Download-Releases-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/Downloads-800%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
[![Downloads](https://img.shields.io/badge/Downloads-3.1k%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)
@@ -29,6 +29,11 @@ Feedback willkommen.</sub>
</div>
> [!CAUTION]
> **Laufende Wartungsarbeiten** — Wir strukturieren das Repository um (Branch-Modell, CI-Workflows, Beitragsprozess). Es kann zu Änderungen bei Branch-Namen, PR-Templates und der Art der Veröffentlichungen kommen. Wir entschuldigen uns für etwaige Unannehmlichkeiten. Handhabung, Workflow und langfristige Wartbarkeit werden dadurch deutlich verbessert.
>
> 👉 Möchtest du beitragen? Bitte zuerst [CONTRIBUTING.md](CONTRIBUTING.md) lesen.
---
## ✨ Was kann KX-Bridge?
@@ -45,7 +50,7 @@ Feedback willkommen.</sub>
| 🧩 | **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 |
| 🌐 | **Mehrsprachiges UI** — DE / EN / ES / FR / IT / 中文, 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 |
@@ -184,6 +189,24 @@ docker compose up -d --build # lokal selber bauen (statt zu pullen)
---
## 🌙 Nightly-Builds
Nightly-Builds enthalten die neuesten unveröffentlichten Features und werden automatisch bei jedem Entwicklungs-Push gebaut.
Sie können instabil sein — für Tests oder frühen Zugriff auf neue Funktionen geeignet.
```bash
docker compose -f docker-compose.yml -f docker-compose.nightly.yml pull
docker compose -f docker-compose.yml -f docker-compose.nightly.yml up -d
```
Zurück zum stabilen Release:
```bash
docker compose pull && docker compose up -d
```
---
## 🩹 Troubleshooting
<details>

View File

@@ -14,22 +14,13 @@ 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>
[![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)
[![Downloads](https://img.shields.io/badge/Descargas-3.1k%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)
@@ -37,6 +28,11 @@ ninguna está oficialmente probada ni soportada. Se agradece el feedback.</sub>
</div>
> [!CAUTION]
> **Trabajos de mantenimiento en curso** — 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 las molestias. El manejo, el flujo de trabajo y la mantenibilidad a largo plazo mejorarán considerablemente.
>
> 👉 ¿Quieres contribuir? Por favor lee [CONTRIBUTING.md](CONTRIBUTING.md) primero.
---
## ✨ Características
@@ -53,7 +49,7 @@ ninguna está oficialmente probada ni soportada. Se agradece el feedback.</sub>
| 🧩 | **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 |
| 🌐 | **Interfaz multilingüe** — DE / EN / ES / FR / IT / 中文, 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 |
@@ -75,43 +71,17 @@ docker compose up -d
**Binario Linux (sin Docker):**
```bash
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
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.
> ⚠️ **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.
> 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
@@ -139,6 +109,11 @@ Impresora → Tipo de conexión **Moonraker** → Host: `http://IP-DEL-PUENTE:71
---
## 📺 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
@@ -212,6 +187,24 @@ docker compose up -d --build # recompilar localmente (en lugar de desc
---
## 🌙 Builds nocturnos (Nightly)
Los builds nocturnos contienen las últimas funciones no publicadas y se generan automáticamente en cada push de desarrollo.
Pueden ser inestables — úsalos para pruebas o acceso anticipado a nuevas funciones.
```bash
docker compose -f docker-compose.yml -f docker-compose.nightly.yml pull
docker compose -f docker-compose.yml -f docker-compose.nightly.yml up -d
```
Volver a la versión estable:
```bash
docker compose pull && docker compose up -d
```
---
## 🩹 Solución de problemas
<details>

View File

@@ -20,7 +20,7 @@ officially tested or supported. Feedback welcome.</sub>
&nbsp;
[![Releases](https://img.shields.io/badge/Download-Releases-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/Downloads-800%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
[![Downloads](https://img.shields.io/badge/Downloads-3.1k%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)
@@ -28,6 +28,11 @@ officially tested or supported. Feedback welcome.</sub>
</div>
> [!CAUTION]
> **Ongoing maintenance work** — 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.
---
## ✨ Features
@@ -44,7 +49,7 @@ officially tested or supported. Feedback welcome.</sub>
| 🧩 | **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 |
| 🌐 | **Multi-language UI** — DE / EN / ES / FR / IT / 中文, 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 |
@@ -182,6 +187,24 @@ docker compose up -d --build # rebuild locally (instead of pulling)
---
## 🌙 Nightly Builds
Nightly builds contain the latest unreleased features and are built automatically on every development push.
They may be unstable — use them for testing or early access to new functionality.
```bash
docker compose -f docker-compose.yml -f docker-compose.nightly.yml pull
docker compose -f docker-compose.yml -f docker-compose.nightly.yml up -d
```
To go back to the stable release:
```bash
docker compose pull && docker compose up -d
```
---
## 🩹 Troubleshooting
<details>

View File

@@ -1 +1 @@
0.9.27-nightly2
0.9.27-nightly9

View File

@@ -602,10 +602,23 @@ class CameraCache:
self._task_jpeg: "asyncio.Task | None" = None
self._task_h264: "asyncio.Task | None" = None
self._lock = asyncio.Lock()
self._fail_count_jpeg: int = 0
self._fail_count_h264: int = 0
def set_url(self, url: str):
self._url = url
def reset(self):
"""Backoff-Zähler zurücksetzen und laufende ffmpeg-Prozesse killen."""
self._fail_count_jpeg = 0
self._fail_count_h264 = 0
for proc in (self._proc_jpeg, self._proc_h264):
if proc is not None:
try:
proc.kill()
except Exception:
pass
async def ensure_running(self):
if self._proc_jpeg is None or self._proc_jpeg.returncode is not None:
self._task_jpeg = asyncio.create_task(self._run_jpeg_loop())
@@ -629,13 +642,13 @@ class CameraCache:
continue
try:
self._proc_jpeg = await asyncio.create_subprocess_exec(
_find_ffmpeg(), "-loglevel", "quiet",
_find_ffmpeg(), "-loglevel", "warning",
*self._input_args(url), "-i", url,
"-vf", "fps=2",
"-f", "image2pipe", "-vcodec", "mjpeg", "-q:v", "3",
"-flush_packets", "1", "pipe:1",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.PIPE,
)
except Exception as e:
log.warning(f"CameraCache: ffmpeg-jpeg start fehlgeschlagen: {e}")
@@ -643,6 +656,7 @@ class CameraCache:
continue
buf = b""
rc = None
try:
while True:
chunk = await self._proc_jpeg.stdout.read(self.TS_CHUNK)
@@ -676,8 +690,23 @@ class CameraCache:
await self._proc_jpeg.wait()
except Exception:
pass
rc = self._proc_jpeg.returncode
if rc:
try:
err = await self._proc_jpeg.stderr.read(500)
if err:
log.warning(f"CameraCache: ffmpeg-jpeg stderr: {err.decode(errors='replace').strip()}")
except Exception:
pass
self._proc_jpeg = None
await asyncio.sleep(2.0) # restart delay
if rc:
self._fail_count_jpeg += 1
delay = min(2.0 * (2 ** self._fail_count_jpeg), 300.0)
log.warning(f"CameraCache: ffmpeg-jpeg exit {rc}, retry in {delay:.0f}s (Versuch {self._fail_count_jpeg})")
await asyncio.sleep(delay)
else:
self._fail_count_jpeg = 0
await asyncio.sleep(2.0)
async def _run_h264_loop(self):
"""Hält einen ffmpeg-Prozess am Leben der MPEG-TS an alle Subscriber fanoutet."""
@@ -688,18 +717,19 @@ class CameraCache:
continue
try:
self._proc_h264 = await asyncio.create_subprocess_exec(
_find_ffmpeg(), "-loglevel", "quiet",
_find_ffmpeg(), "-loglevel", "warning",
*self._input_args(url), "-i", url,
"-c:v", "copy", "-an",
"-f", "mpegts", "pipe:1",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.PIPE,
)
except Exception as e:
log.warning(f"CameraCache: ffmpeg-h264 start fehlgeschlagen: {e}")
await asyncio.sleep(3.0)
continue
rc = None
try:
while True:
chunk = await self._proc_h264.stdout.read(self.TS_CHUNK)
@@ -729,8 +759,23 @@ class CameraCache:
await self._proc_h264.wait()
except Exception:
pass
rc = self._proc_h264.returncode
if rc:
try:
err = await self._proc_h264.stderr.read(500)
if err:
log.warning(f"CameraCache: ffmpeg-h264 stderr: {err.decode(errors='replace').strip()}")
except Exception:
pass
self._proc_h264 = None
await asyncio.sleep(2.0)
if rc:
self._fail_count_h264 += 1
delay = min(2.0 * (2 ** self._fail_count_h264), 300.0)
log.warning(f"CameraCache: ffmpeg-h264 exit {rc}, retry in {delay:.0f}s (Versuch {self._fail_count_h264})")
await asyncio.sleep(delay)
else:
self._fail_count_h264 = 0
await asyncio.sleep(2.0)
class SpoolmanClient:
@@ -3863,6 +3908,16 @@ class KobraXBridge:
self._camera_user_stopped = True
return web.json_response({"result": "ok"})
async def handle_api_camera_reset(self, request):
"""Backoff-Zähler zurücksetzen und ffmpeg sofort neu starten.
Nützlich nach 429-Sperre (Retry-After abgelaufen) oder nach Drucker-Neustart."""
self.camera_cache.reset()
url = self._state.get("camera_url", "")
if url:
self.camera_cache.set_url(url)
await self.camera_cache.ensure_running()
return web.json_response({"result": "ok"})
async def handle_api_camera_snapshot(self, request):
"""Letzter JPEG-Frame aus dem CameraCache — instant aus dem RAM,
keine eigene ffmpeg-Instanz mehr (verhindert Single-Client-429 am
@@ -4433,7 +4488,8 @@ class KobraXBridge:
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "PRINT_START_DIALOG",
"FILE_READY_DIALOG", "BRIDGE_PRINTER_NAME"):
"FILE_READY_DIALOG", "BRIDGE_PRINTER_NAME",
"SPOOLMAN_SERVER", "SPOOLMAN_SYNC_RATE"):
os.environ.pop(_k, None)
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
@@ -4469,9 +4525,10 @@ class KobraXBridge:
# ─── Update ──────────────────────────────────────────────────────────────
STABLE_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=1"
DEV_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=10&pre-release=true"
GITEA_RAW_BASE = "https://gitea.it-drui.de/viewit/KX-Bridge-Release/raw/tag"
STABLE_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=1"
NIGHTLY_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=5&pre-release=true"
DEV_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=10&pre-release=true"
GITEA_RAW_BASE = "https://gitea.it-drui.de/viewit/KX-Bridge-Release/raw/tag"
def _read_version(self) -> str:
# PyInstaller-Onefile entpackt VERSION (per kx-bridge.spec datas) nach
@@ -4546,8 +4603,14 @@ class KobraXBridge:
async def handle_api_update_check(self, request):
current = self._read_version()
is_nightly = "nightly" in current
is_dev = "-dev+" in current
api_url = self.DEV_RELEASE_API if is_dev else self.STABLE_RELEASE_API
if is_nightly:
api_url = self.NIGHTLY_RELEASE_API
elif is_dev:
api_url = self.DEV_RELEASE_API
else:
api_url = self.STABLE_RELEASE_API
try:
async with aiohttp.ClientSession() as session:
async with session.get(api_url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
@@ -4556,14 +4619,37 @@ class KobraXBridge:
releases = await resp.json(content_type=None)
if not releases:
return web.json_response({"error": "Keine Releases gefunden"}, status=404)
# Dev: neuestes Release mit "-dev+" im Tag suchen
if is_dev:
if is_nightly:
# Neuestes Prerelease mit nightly-Tag suchen
nightly_releases = [r for r in releases if r.get("prerelease") and "nightly" in r.get("tag_name", "")]
if not nightly_releases:
return web.json_response({"error": "Keine Nightly-Releases gefunden"}, status=404)
data = nightly_releases[0]
tag = data.get("tag_name", "")
# Tag-Format: "nightly-0.9.27-nightly4", current: "0.9.27-nightly4"
tag_version = tag[len("nightly-"):] if tag.startswith("nightly-") else tag
update_available = tag_version != current
latest = tag
return web.json_response({
"current": current,
"latest": latest,
"update_available": update_available,
"tag": tag,
"docker_only": True,
"changelog": data.get("body", ""),
})
elif is_dev:
dev_releases = [r for r in releases if "-dev+" in r.get("tag_name", "")]
if not dev_releases:
return web.json_response({"error": "Keine Dev-Releases gefunden"}, status=404)
data = dev_releases[0]
else:
data = releases[0]
# Stable: nur non-prerelease nehmen
stable_releases = [r for r in releases if not r.get("prerelease")]
if not stable_releases:
return web.json_response({"error": "Keine Stable-Releases gefunden"}, status=404)
data = stable_releases[0]
tag = data.get("tag_name", "")
latest = tag.lstrip("v")
if is_dev:
@@ -4577,6 +4663,7 @@ class KobraXBridge:
"update_available": update_available,
"tag": tag,
"download_url": download_url,
"docker_only": False,
"changelog": data.get("body", ""),
})
except Exception as e:
@@ -4597,6 +4684,10 @@ class KobraXBridge:
async def handle_api_update_apply(self, request):
data = await request.json()
new_tag = data.get("tag", "")
if "nightly" in self._read_version():
return web.json_response(
{"error": "Nightly-Updates laufen über Docker: "
"docker compose pull && docker compose up -d"}, status=400)
if getattr(sys, "frozen", False):
return web.json_response(
{"error": "Self-Update wird im Binary-Modus nicht unterstützt "
@@ -5132,6 +5223,7 @@ def build_app(bridge: KobraXBridge) -> web.Application:
r.add_get("/api/camera/snapshot", bridge.handle_api_camera_snapshot)
r.add_post("/api/camera/start", bridge.handle_api_camera_start)
r.add_post("/api/camera/stop", bridge.handle_api_camera_stop)
r.add_post("/api/camera/reset", bridge.handle_api_camera_reset)
r.add_get("/api/state", bridge.handle_api_state)
r.add_get("/api/settings", bridge.handle_api_settings_get)
r.add_post("/api/settings", bridge.handle_api_settings_post)

View File

@@ -550,6 +550,7 @@ function ensureAceDryCards(){
// defer until DOM ready
window.addEventListener('DOMContentLoaded',function(){
setLanguage(currentLang).catch(function(){});
_loadSpoolmanStatus();
// Kein Drucker konfiguriert? → direkt in den Drucker-Tab (zeigt "+ Drucker hinzufügen")
fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){
if(!d.result||!d.result.length){showPanel('printers');loadPrinterTab();}
@@ -1654,6 +1655,7 @@ function saveSettings(){
btn.disabled=false;
setText('btn-save-settings',T.settings_save);
closeSettings();
_loadSpoolmanStatus();
poll();
},4000);
}).catch(function(e){
@@ -1661,21 +1663,33 @@ function saveSettings(){
clog('Settings-Fehler: '+e,'msg-err');
});
}
var _updateDockerOnly=false;
function checkUpdate(){
var sb=document.getElementById('update-status');
sb.textContent=T.update_checking;
document.getElementById('btn-update-apply').style.display='none';
_updateTag='';_updateUrl='';
_updateTag='';_updateUrl='';_updateDockerOnly=false;
fetch(_apiUrl('/api/update/check')).then(function(r){return r.json()}).then(function(d){
if(d.error){sb.textContent=T.update_error+': '+d.error;return;}
var cl=document.getElementById('update-changelog');
if(d.changelog&&d.changelog.trim()){cl.textContent=d.changelog;cl.style.display='block';}
else{cl.style.display='none';}
if(d.changelog&&d.changelog.trim()){
// Changelog als Markdown-Text (pre-formatiert) anzeigen
cl.textContent=d.changelog;cl.style.display='block';
} else {cl.style.display='none';}
_updateDockerOnly=!!d.docker_only;
if(d.update_available){
sb.textContent='v'+d.latest+' '+T.update_available;
sb.textContent=d.latest+' '+T.update_available;
sb.style.color='var(--ok)';
_updateTag=d.tag;_updateUrl=d.download_url;
document.getElementById('btn-update-apply').style.display='inline-block';
_updateTag=d.tag;_updateUrl=d.download_url||'';
var btn=document.getElementById('btn-update-apply');
if(_updateDockerOnly){
btn.textContent=T.update_docker||'docker compose pull';
btn.title='docker compose pull && docker compose up -d';
} else {
btn.textContent=T.update_apply;
btn.title='';
}
btn.style.display='inline-block';
} else {
sb.textContent=T.update_none;
sb.style.color='var(--txt2)';
@@ -1683,9 +1697,21 @@ function checkUpdate(){
}).catch(function(e){sb.textContent=T.update_error+': '+e;});
}
function applyUpdate(){
if(!_updateUrl)return;
var sb=document.getElementById('update-status');
var btn=document.getElementById('btn-update-apply');
if(_updateDockerOnly){
// Nightly: kein Self-Update, Docker-Befehl in Zwischenablage kopieren
var cmd='docker compose pull && docker compose up -d';
if(navigator.clipboard){
navigator.clipboard.writeText(cmd).then(function(){
sb.textContent=T.update_docker_copied||'Befehl kopiert: '+cmd;
});
} else {
sb.textContent=cmd;
}
return;
}
if(!_updateUrl)return;
btn.disabled=true;sb.textContent=T.update_applying;
post('/api/update/apply',{download_url:_updateUrl,tag:_updateTag}).then(function(){
sb.textContent=T.update_restarting;
@@ -1871,6 +1897,7 @@ function camStart(){
post('/api/camera/start',{}).then(function(){
camOn=true;
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_stop');
var rb=document.getElementById('cam-reset-btn');if(rb)rb.style.display='';
clog(tr('log_cam_start'),'msg-ok');
setTimeout(function(){ sp.style.display='none'; img.style.display='block'; },1200);
if(/Android/i.test(navigator.userAgent)){
@@ -1905,6 +1932,7 @@ function camStop(){
camOn=false;
camUserStopped=true; // suppress auto-restart for remainder of this print
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
var rb=document.getElementById('cam-reset-btn');if(rb)rb.style.display='none';
clog(tr('log_cam_stop'),'msg-ok');
}
@@ -2155,6 +2183,13 @@ function aceDryToggle(aceId,on){
function toggleCam(){if(camOn)camStop();else camStart()}
function resetCamera(){
post('/api/camera/reset',{}).then(function(){
var btn=document.getElementById('cam-reset-btn');
if(btn){btn.textContent='↻';setTimeout(function(){btn.textContent='↺';},1000);}
}).catch(function(){});
}
function aceDryStop(aceId){
aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0;
return post('/api/ace/dry',{action:'stop',ace_id:aceId})

View File

@@ -157,6 +157,7 @@
<div style="font-size:12px;color:#fff" id="cam-fname"></div>
</div>
<button class="cam-toggle" onclick="toggleCam()" id="cam-toggle-btn">▶ Kamera</button>
<button class="cam-toggle" onclick="resetCamera()" id="cam-reset-btn" style="display:none;margin-left:4px" title="Kamera-Stream neu verbinden (nach 429-Sperre)"></button>
</div>
</div>

View File

@@ -321,6 +321,8 @@
"update_available": "verfügbar",
"update_check": "Auf Updates prüfen",
"update_checking": "Prüfe...",
"update_docker": "Befehl kopieren",
"update_docker_copied": "Kopiert! Ausführen: docker compose pull && docker compose up -d",
"update_error": "Fehler",
"update_none": "Bereits aktuell",
"update_restarting": "Starte neu..."

View File

@@ -321,6 +321,8 @@
"update_available": "available",
"update_check": "Check for Updates",
"update_checking": "Checking...",
"update_docker": "Copy Command",
"update_docker_copied": "Copied! Run: docker compose pull && docker compose up -d",
"update_error": "Error",
"update_none": "Already up to date",
"update_restarting": "Restarting..."

View File

@@ -321,6 +321,8 @@
"update_available": "disponible",
"update_check": "Buscar actualizaciones",
"update_checking": "Comprobando...",
"update_docker": "Copiar comando",
"update_docker_copied": "Copiado. Ejecutar: docker compose pull && docker compose up -d",
"update_error": "Error",
"update_none": "Ya actualizado",
"update_restarting": "Reiniciando..."

View File

@@ -321,6 +321,8 @@
"update_available": "disponible",
"update_check": "Vérifier les mises à jour",
"update_checking": "Vérification…",
"update_docker": "Copier la commande",
"update_docker_copied": "Copié ! Exécuter : docker compose pull && docker compose up -d",
"update_error": "Erreur",
"update_none": "Déjà à jour",
"update_restarting": "Redémarrage…"

View File

@@ -321,6 +321,8 @@
"update_available": "disponibile",
"update_check": "Controlla aggiornamenti",
"update_checking": "Verifica in corso...",
"update_docker": "Copia comando",
"update_docker_copied": "Copiato! Eseguire: docker compose pull && docker compose up -d",
"update_error": "Errore",
"update_none": "Già aggiornato",
"update_restarting": "Riavvio in corso..."

View File

@@ -321,6 +321,8 @@
"update_available": "可用",
"update_check": "检查更新",
"update_checking": "检查中...",
"update_docker": "复制命令",
"update_docker_copied": "已复制执行docker compose pull && docker compose up -d",
"update_error": "错误",
"update_none": "已是最新版本",
"update_restarting": "重启中..."