Compare commits
37 Commits
master
...
nightly-0.
| Author | SHA1 | Date | |
|---|---|---|---|
| e4bf2c9b95 | |||
| aea6e457f3 | |||
| 0ae8ae59be | |||
| 0322ade606 | |||
| 786fa08ca0 | |||
| cd11542352 | |||
| 51f22947c5 | |||
|
|
a39226d2dd | ||
|
|
2a13f1f0dd | ||
| a16062f44f | |||
| 4f5aa8d126 | |||
| c313e014ad | |||
| 6e9ba0672f | |||
| 44383fabec | |||
| 48bec55611 | |||
| ab44e234be | |||
| 74fc2ddab0 | |||
| 771599be0c | |||
| 0e1d46ee7f | |||
| 15e28244af | |||
| c1a3b9238d | |||
| aad3833301 | |||
| a2f658e701 | |||
| ab2cf6e4ed | |||
| 1b05362c2b | |||
| cfe70430d3 | |||
| 2c8a62f130 | |||
| e4b0716330 | |||
| d9fcc15c53 | |||
| 31dcf4c8fd | |||
| 319a8d5ccb | |||
| 700459085b | |||
| f93c07a971 | |||
| 81906cfffc | |||
| 3f915b058b | |||
| 8b66172ca1 | |||
| cec7cb2a5a |
12
.gitea/make_release_json.py
Normal file
12
.gitea/make_release_json.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys, json
|
||||
tag, version, body_file = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
body = open(body_file).read()
|
||||
payload = json.dumps({
|
||||
"tag_name": tag,
|
||||
"name": "KX-Bridge " + version + " Nightly",
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": True
|
||||
})
|
||||
open("/tmp/release_body.json", "w").write(payload)
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
- 'requirements.txt'
|
||||
- 'web/**'
|
||||
- 'data/**'
|
||||
- '.gitea/workflows/nightly.yml'
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
@@ -21,11 +22,11 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
if [ -d .git ]; then
|
||||
git fetch origin nightly
|
||||
git fetch --tags 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 .
|
||||
git clone --branch nightly https://gitea.it-drui.de/viewit/KX-Bridge-Release.git .
|
||||
fi
|
||||
|
||||
- name: Install Docker CLI
|
||||
@@ -64,11 +65,36 @@ jobs:
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | \
|
||||
docker login gitea.it-drui.de -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: Compute nightly version
|
||||
run: |
|
||||
# Letzten Stable-Tag ermitteln (v0.9.27 → minor=27)
|
||||
LAST_STABLE=$(git tag --list 'v*' --sort=-version:refname \
|
||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' | { read -r line; echo "$line"; cat >/dev/null; } || true)
|
||||
if [ -z "$LAST_STABLE" ]; then
|
||||
echo "ERROR: kein Stable-Tag gefunden" >&2; exit 1
|
||||
fi
|
||||
# Nächste Minor-Version: v0.9.27 → 0.9.28
|
||||
MAJOR=$(echo "$LAST_STABLE" | sed 's/^v//' | cut -d. -f1)
|
||||
MINOR=$(echo "$LAST_STABLE" | sed 's/^v//' | cut -d. -f2)
|
||||
PATCH=$(echo "$LAST_STABLE" | sed 's/^v//' | cut -d. -f3)
|
||||
NEXT_PATCH=$((PATCH + 1))
|
||||
BASE="${MAJOR}.${MINOR}.${NEXT_PATCH}"
|
||||
# Laufende Nummer: Anzahl vorhandener nightly-<BASE>-nightlyX Tags + 1
|
||||
COUNT=$(git tag --list "nightly-${BASE}-nightly*" | wc -l | tr -d ' ')
|
||||
N=$((COUNT + 1))
|
||||
VERSION="${BASE}-nightly${N}"
|
||||
echo "VERSION=${VERSION}" > /tmp/nightly_version.env
|
||||
echo "BASE=${BASE}" >> /tmp/nightly_version.env
|
||||
echo "LAST_STABLE=${LAST_STABLE}" >> /tmp/nightly_version.env
|
||||
echo "Computed nightly version: ${VERSION} (after ${LAST_STABLE})"
|
||||
|
||||
- name: Build & push (amd64 + arm64)
|
||||
run: |
|
||||
VERSION=$(cat VERSION)
|
||||
. /tmp/nightly_version.env
|
||||
# VERSION-Datei im Arbeitsverzeichnis für den Docker-Build setzen (kein Commit)
|
||||
echo "$VERSION" > VERSION
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--push \
|
||||
--provenance=false \
|
||||
--no-cache \
|
||||
@@ -80,50 +106,68 @@ jobs:
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
VERSION=$(cat VERSION)
|
||||
. /tmp/nightly_version.env
|
||||
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)
|
||||
# Letzten Stable-Tag als Changelog-Basis (nur echte vX.Y.Z-Tags)
|
||||
PREV_TAG=$(git tag --list 'v*' --sort=-version:refname \
|
||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
| { read -r line; echo "$line"; cat >/dev/null; } || true)
|
||||
[ -z "$PREV_TAG" ] && PREV_TAG=$(git rev-list --max-parents=0 HEAD)
|
||||
|
||||
# Changelog: NIGHTLY_CHANGELOG.md hat Vorrang (manuell gepflegt),
|
||||
# sonst auto-generiert aus feat/fix-Commits seit letztem Stable-Tag
|
||||
BODY_FILE=$(mktemp)
|
||||
if [ -f CHANGES.md ]; then
|
||||
cat CHANGES.md > "$BODY_FILE"
|
||||
printf '## KX-Bridge %s — Nightly Build\n\n' "$VERSION" > "$BODY_FILE"
|
||||
printf '[experimental] Untested features, for testers only.\n\n' >> "$BODY_FILE"
|
||||
if [ -s NIGHTLY_CHANGELOG.md ]; then
|
||||
cat NIGHTLY_CHANGELOG.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 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
|
||||
printf '### Changes since `%s`\n\n' "$PREV_TAG" >> "$BODY_FILE"
|
||||
git log "${PREV_TAG}..HEAD" --pretty=format:'%s' --no-merges \
|
||||
| grep -E '^(feat|fix)[:(]' \
|
||||
| grep -Ev '^(feat|fix)\((ci|release|build|workflow)\)' \
|
||||
| sed 's/^/- /' \
|
||||
>> "$BODY_FILE" || true
|
||||
if ! grep -q '^- ' "$BODY_FILE"; then
|
||||
printf '- No user-facing changes in this build\n' >> "$BODY_FILE"
|
||||
fi
|
||||
fi
|
||||
printf '\n\n---\n\n### Update Docker image\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"
|
||||
|
||||
# Altes Release loeschen falls vorhanden
|
||||
# Tag setzen
|
||||
git tag "$TAG"
|
||||
git push https://gitea-actions:${GITEA_TOKEN}@gitea.it-drui.de/viewit/KX-Bridge-Release.git "$TAG"
|
||||
|
||||
# curl + jq installieren falls nötig
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
apk add --no-cache curl 2>/dev/null || \
|
||||
{ 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
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
ARCH=$(uname -m)
|
||||
JQ_ARCH="amd64"; [ "$ARCH" = "aarch64" ] && JQ_ARCH="arm64"
|
||||
wget -qO /usr/local/bin/jq \
|
||||
"https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-${JQ_ARCH}"
|
||||
chmod +x /usr/local/bin/jq
|
||||
fi
|
||||
|
||||
# Altes Release löschen 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
|
||||
# Release erstellen — JSON sicher via jq bauen
|
||||
jq -n \
|
||||
--arg tag "$TAG" \
|
||||
--arg name "KX-Bridge ${VERSION} Nightly" \
|
||||
--rawfile body "$BODY_FILE" \
|
||||
'{"tag_name":$tag,"name":$name,"body":$body,"draft":false,"prerelease":true}' \
|
||||
> /tmp/release_body.json
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--push \
|
||||
--provenance=false \
|
||||
--no-cache \
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,9 +4,7 @@ __pycache__/
|
||||
build/
|
||||
dist/
|
||||
*.spec
|
||||
releases/*/kx-bridge
|
||||
releases/*/extract_credentials
|
||||
releases/*/extract_credentials.exe
|
||||
releases/
|
||||
|
||||
!kx-bridge.spec
|
||||
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Filament profiles not isolated between printers in a multi-printer bridge**
|
||||
(issue #74). The slot→profile mapping and `visible_vendors` were stored in a
|
||||
single global `[filament_profiles]` section, so configuring one printer
|
||||
overwrote the other and after a restart both loaded the same mapping. Each
|
||||
printer now persists to its own `[filament_profiles_<id>]` section, with a
|
||||
read-fallback to the legacy global section (single-printer setups unchanged).
|
||||
- **Printer dropdown showed the other printer's filament profiles** (issue #74).
|
||||
The header dropdown and the printers-management "switch" link navigated within
|
||||
the same port (`/printerN`), so viewing another printer pulled its profile
|
||||
names cross-instance from the local origin. The links now point at each
|
||||
printer's own `bridge_url`, so every printer is viewed same-origin on its own
|
||||
port.
|
||||
|
||||
## [0.9.26] – 2026-06-21
|
||||
|
||||
### New
|
||||
|
||||
33
CHANGES.md
33
CHANGES.md
@@ -1,33 +0,0 @@
|
||||
## 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`
|
||||
@@ -2,10 +2,11 @@ FROM python:3.11-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg gcc python3-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||
apt-get purge -y gcc python3-dev && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY kobrax_moonraker_bridge.py .
|
||||
COPY web/ ./web/
|
||||
|
||||
7
NIGHTLY_CHANGELOG.md
Normal file
7
NIGHTLY_CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Changes in this build
|
||||
|
||||
- Refactor: all logs, comments and API error strings translated to English across the codebase (no behavior change)
|
||||
- Refactor: the `print/start` payload was duplicated in three places and had drifted (the new resonance-compensation setting was missing from the upload+print path) — all print start paths now share one payload builder
|
||||
- Fix: a couple of silently swallowed errors (dashboard reprint filament-cache load, filament metadata backfill) now log a warning/debug message instead of failing silently
|
||||
|
||||
**A stable release is coming soon** with the fixes and features from the last several nightly builds. As usual it will be published as ready-to-run binaries (Linux amd64/arm64, Windows) alongside the Docker image.
|
||||
@@ -41,6 +41,21 @@ web_upload_warning = 1
|
||||
# Poll-Intervall in Sekunden
|
||||
poll_interval = 3
|
||||
|
||||
# ─── Spoolman (optional) ───────────────────────────────────────────────────────
|
||||
# Verfolgt den Filamentverbrauch je AMS-Slot und bucht ihn automatisch vom
|
||||
# passenden Spool ab (mm-basiert, wie Moonraker; Spoolman rechnet mm→Gramm).
|
||||
# [spoolman]
|
||||
# # Server-URL der Spoolman-Instanz (aus Sicht des Bridge-Containers erreichbar):
|
||||
# server = http://192.168.x.x:7912
|
||||
# # 0 = nur am Druckende abbuchen, >0 = alle N Sekunden während des Drucks:
|
||||
# sync_rate = 0
|
||||
#
|
||||
# Die AMS-Slot → Spool-Zuordnung wird in der Weboberfläche gesetzt und je Drucker
|
||||
# automatisch persistiert (nicht von Hand eintragen):
|
||||
# Einzeldrucker : [spoolman] slot_spools = 0:42,1:17
|
||||
# Multi-Printer : [spoolman_1] slot_spools = 0:42,1:17
|
||||
# [spoolman_2] slot_spools = 0:5,1:6
|
||||
|
||||
# ─── Multi-Printer (optional) ──────────────────────────────────────────────────
|
||||
# Mehrere Drucker können als [printer_1], [printer_2], … definiert werden.
|
||||
# Jede Bridge-Instanz verbindet sich mit einem Drucker (je eigener Port).
|
||||
|
||||
246
config_loader.py
246
config_loader.py
@@ -1,12 +1,13 @@
|
||||
"""
|
||||
config_loader.py – lädt Verbindungsparameter aus config/config.ini (primär)
|
||||
oder .env (Fallback / Migration).
|
||||
Umgebungsvariablen haben immer Vorrang.
|
||||
config_loader.py - loads connection parameters from config/config.ini (primary)
|
||||
or .env (fallback / migration).
|
||||
Environment variables always take precedence.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import pathlib
|
||||
import configparser
|
||||
from typing import Optional
|
||||
|
||||
_BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) else pathlib.Path(__file__).parent
|
||||
|
||||
@@ -33,7 +34,7 @@ def _find_env_file() -> pathlib.Path | None:
|
||||
|
||||
|
||||
def _load_env_file(path: pathlib.Path):
|
||||
"""Lädt .env-Datei als Fallback – setzt nur Keys die noch nicht in os.environ sind."""
|
||||
"""Loads the .env file as a fallback - only sets keys not yet in os.environ."""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
@@ -47,7 +48,7 @@ def _load_env_file(path: pathlib.Path):
|
||||
|
||||
|
||||
def _load_config_file(path: pathlib.Path):
|
||||
"""Lädt config.ini und setzt Keys in os.environ (nur wenn nicht bereits gesetzt)."""
|
||||
"""Loads config.ini and sets keys in os.environ (only if not already set)."""
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
|
||||
@@ -59,11 +60,13 @@ def _load_config_file(path: pathlib.Path):
|
||||
"MODE_ID": (CONFIG_SECTION_CONNECTION, "mode_id"),
|
||||
"DEVICE_ID": (CONFIG_SECTION_CONNECTION, "device_id"),
|
||||
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||
"VIBRATION_COMPENSATION": (CONFIG_SECTION_PRINT, "vibration_compensation"),
|
||||
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
|
||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||
"BRIDGE_HOST_IP": (CONFIG_SECTION_BRIDGE, "host_ip"),
|
||||
"SPOOLMAN_SERVER": (CONFIG_SECTION_SPOOLMAN, "server"),
|
||||
"SPOOLMAN_SYNC_RATE": (CONFIG_SECTION_SPOOLMAN, "sync_rate"),
|
||||
}
|
||||
@@ -112,8 +115,9 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
||||
}
|
||||
cfg[CONFIG_SECTION_PRINT] = {
|
||||
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
||||
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
||||
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
||||
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
||||
"vibration_compensation": env_vals.get("VIBRATION_COMPENSATION", "0"),
|
||||
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
||||
"web_upload_warning": env_vals.get("WEB_UPLOAD_WARNING", "1"),
|
||||
}
|
||||
cfg[CONFIG_SECTION_BRIDGE] = {
|
||||
@@ -121,12 +125,12 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
||||
}
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write("# KX-Bridge Konfigurationsdatei\n")
|
||||
f.write("# Automatisch migriert aus .env\n\n")
|
||||
f.write("# Automatically migrated from .env\n\n")
|
||||
cfg.write(f)
|
||||
|
||||
|
||||
def find_config_path() -> pathlib.Path:
|
||||
"""Gibt den Pfad zur config.ini zurück (auch wenn sie noch nicht existiert)."""
|
||||
"""Returns the path to config.ini (even if it does not exist yet)."""
|
||||
for base in (_BASE, _BASE.parent):
|
||||
config_dir = base / "config"
|
||||
if config_dir.is_dir():
|
||||
@@ -142,7 +146,7 @@ _env_path = _find_env_file()
|
||||
if _config_path:
|
||||
_load_config_file(_config_path)
|
||||
elif _env_path:
|
||||
# Kein config.ini vorhanden → aus .env migrieren
|
||||
# No config.ini present -> migrate from .env
|
||||
_target = find_config_path()
|
||||
migrate_env_to_config(_env_path, _target)
|
||||
_load_config_file(_target)
|
||||
@@ -150,13 +154,13 @@ elif _env_path:
|
||||
|
||||
|
||||
def list_printers() -> list[dict]:
|
||||
"""Liest alle [printer_N]-Sektionen aus config.ini.
|
||||
"""Reads all [printer_N] sections from config.ini.
|
||||
|
||||
Jede Sektion kann folgende Keys haben:
|
||||
Each section may contain the following keys:
|
||||
name, printer_ip, mqtt_port, username, password, mode_id, device_id,
|
||||
bridge_url, default_ams_slot, auto_leveling
|
||||
|
||||
Gibt eine leere Liste zurück wenn keine [printer_N]-Sektionen vorhanden sind
|
||||
Returns an empty list when no [printer_N] sections exist
|
||||
(Single-Printer-Betrieb via [connection]).
|
||||
"""
|
||||
path = _find_config_file()
|
||||
@@ -182,25 +186,43 @@ def list_printers() -> list[dict]:
|
||||
return printers
|
||||
|
||||
|
||||
def list_filament_profiles() -> dict[int, dict]:
|
||||
"""Liest die [filament_profiles]-Sektion aus config.ini.
|
||||
def _filament_section(printer_id: Optional[str] = None) -> str:
|
||||
"""Section name holding a printer's filament-profile mapping.
|
||||
|
||||
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):
|
||||
Multi-printer (one bridge, N printers): each printer keeps its own
|
||||
``[filament_profiles_<id>]`` section so the mappings cannot overwrite each
|
||||
other. ``printer_id is None`` (single-printer / legacy callers) maps to the
|
||||
original global ``[filament_profiles]`` section — full backward compatibility.
|
||||
"""
|
||||
pid = str(printer_id).strip() if printer_id is not None else ""
|
||||
if pid and pid != "0":
|
||||
return f"filament_profiles_{pid}"
|
||||
return "filament_profiles"
|
||||
|
||||
|
||||
def list_filament_profiles(printer_id: Optional[str] = None) -> dict[int, dict]:
|
||||
"""Reads the [filament_profiles] section from config.ini.
|
||||
|
||||
With ``printer_id`` set, reads the per-printer ``[filament_profiles_<id>]``
|
||||
section and falls back to the legacy global ``[filament_profiles]`` while
|
||||
that printer has no own section yet.
|
||||
|
||||
Format per AMS slot - the primary selector is (vendor, name); the `id` is
|
||||
looked up from orca_filaments.json on save and carried along
|
||||
(as a hint for OrcaSlicer; the Orca data model has ~136 profiles sharing
|
||||
the same filament_id like 'OGFL99', i.e. the ID is not unique):
|
||||
|
||||
[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.
|
||||
Returns a dict {slot_index: {"id": ..., "vendor": ..., "name": ...}}.
|
||||
Empty/missing slots are NOT included - the default mapping
|
||||
(per filament_type) in the bridge then stays active.
|
||||
|
||||
Backwards-Kompat: alte Configs mit nur (vendor, id) bleiben lesbar; `name`
|
||||
fehlt dann und der Aufrufer kann optional aus der orca_filaments.json
|
||||
Backwards compat: old configs with only (vendor, id) stay readable; `name`
|
||||
is then missing and the caller can optionally resolve it from orca_filaments.json
|
||||
rekonstruieren.
|
||||
"""
|
||||
path = _find_config_file()
|
||||
@@ -208,11 +230,14 @@ def list_filament_profiles() -> dict[int, dict]:
|
||||
return {}
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_section("filament_profiles"):
|
||||
section = _filament_section(printer_id)
|
||||
if not cfg.has_section(section):
|
||||
section = "filament_profiles" # fallback: legacy global section
|
||||
if not cfg.has_section(section):
|
||||
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
|
||||
for key, value in cfg.items(section):
|
||||
# Expects: slot_<idx>_id or slot_<idx>_vendor or slot_<idx>_name
|
||||
if not key.startswith("slot_"):
|
||||
continue
|
||||
parts = key.split("_", 2)
|
||||
@@ -231,74 +256,173 @@ def list_filament_profiles() -> dict[int, dict]:
|
||||
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.
|
||||
def save_filament_profiles(profiles: dict[int, dict], printer_id: Optional[str] = None) -> bool:
|
||||
"""Writes the given slot profiles into the [filament_profiles]
|
||||
section of config.ini. Existing entries are completely replaced.
|
||||
|
||||
profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker", "name": "PolyTerra PLA"}}
|
||||
Mindestens vendor+name müssen gesetzt sein; id ist optional (Hint).
|
||||
At least vendor+name must be set; id is optional (hint).
|
||||
|
||||
With ``printer_id`` set, writes the per-printer ``[filament_profiles_<id>]``
|
||||
section only — other printers and the legacy global section are untouched.
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
# visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der
|
||||
# Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren.
|
||||
section = _filament_section(printer_id)
|
||||
# visible_vendors (Issue #41) is not a slot mapping - preserve it when
|
||||
# replacing the section, otherwise the vendor filter is lost on slot save.
|
||||
# First save of a per-printer section inherits the legacy global filter.
|
||||
preserved_vendors = None
|
||||
if cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
if cfg.has_option(section, "visible_vendors"):
|
||||
preserved_vendors = cfg.get(section, "visible_vendors")
|
||||
elif cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
preserved_vendors = cfg.get("filament_profiles", "visible_vendors")
|
||||
if cfg.has_section("filament_profiles"):
|
||||
cfg.remove_section("filament_profiles")
|
||||
if cfg.has_section(section):
|
||||
cfg.remove_section(section)
|
||||
if profiles or preserved_vendors:
|
||||
cfg["filament_profiles"] = {}
|
||||
cfg[section] = {}
|
||||
if preserved_vendors:
|
||||
cfg["filament_profiles"]["visible_vendors"] = preserved_vendors
|
||||
cfg[section]["visible_vendors"] = preserved_vendors
|
||||
for slot_idx in sorted(profiles.keys()):
|
||||
entry = profiles[slot_idx] or {}
|
||||
if entry.get("vendor"):
|
||||
cfg["filament_profiles"][f"slot_{slot_idx}_vendor"] = entry["vendor"]
|
||||
cfg[section][f"slot_{slot_idx}_vendor"] = entry["vendor"]
|
||||
if entry.get("name"):
|
||||
cfg["filament_profiles"][f"slot_{slot_idx}_name"] = entry["name"]
|
||||
cfg[section][f"slot_{slot_idx}_name"] = entry["name"]
|
||||
if entry.get("id"):
|
||||
cfg["filament_profiles"][f"slot_{slot_idx}_id"] = entry["id"]
|
||||
cfg[section][f"slot_{slot_idx}_id"] = entry["id"]
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
cfg.write(f)
|
||||
return True
|
||||
|
||||
|
||||
def list_visible_vendors() -> list[str]:
|
||||
"""Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini.
|
||||
def list_visible_vendors(printer_id: Optional[str] = None) -> list[str]:
|
||||
"""Reads [filament_profiles] visible_vendors (comma-separated) from config.ini.
|
||||
|
||||
Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A).
|
||||
Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren).
|
||||
Vendor visibility filter for the slot profile dropdown (Issue #41 option A).
|
||||
Empty list = no restriction (backwards compatible: all vendors).
|
||||
|
||||
With ``printer_id`` set, reads the per-printer section and falls back to the
|
||||
legacy global ``[filament_profiles]`` filter.
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return []
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
section = _filament_section(printer_id)
|
||||
if not cfg.has_option(section, "visible_vendors"):
|
||||
section = "filament_profiles" # fallback: legacy global section
|
||||
if not cfg.has_option(section, "visible_vendors"):
|
||||
return []
|
||||
raw = cfg.get("filament_profiles", "visible_vendors")
|
||||
raw = cfg.get(section, "visible_vendors")
|
||||
return [v.strip() for v in raw.split(",") if v.strip()]
|
||||
|
||||
|
||||
def save_visible_vendors(vendors: list[str]) -> bool:
|
||||
"""Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings
|
||||
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder."""
|
||||
def save_visible_vendors(vendors: list[str], printer_id: Optional[str] = None) -> bool:
|
||||
"""Writes visible_vendors into [filament_profiles] without touching the
|
||||
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder.
|
||||
|
||||
With ``printer_id`` set, writes the per-printer section. When that section is
|
||||
created here for the first time, the slot mappings are seeded from the legacy
|
||||
global section so they are not orphaned by the read-fallback in
|
||||
``list_filament_profiles``."""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_section("filament_profiles"):
|
||||
cfg.add_section("filament_profiles")
|
||||
section = _filament_section(printer_id)
|
||||
if not cfg.has_section(section):
|
||||
cfg.add_section(section)
|
||||
if section != "filament_profiles" and cfg.has_section("filament_profiles"):
|
||||
for key, value in cfg.items("filament_profiles"):
|
||||
if key.startswith("slot_"):
|
||||
cfg[section][key] = value
|
||||
clean = [v.strip() for v in (vendors or []) if v and v.strip()]
|
||||
if clean:
|
||||
cfg["filament_profiles"]["visible_vendors"] = ", ".join(clean)
|
||||
elif cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
cfg.remove_option("filament_profiles", "visible_vendors")
|
||||
cfg[section]["visible_vendors"] = ", ".join(clean)
|
||||
elif cfg.has_option(section, "visible_vendors"):
|
||||
cfg.remove_option(section, "visible_vendors")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
cfg.write(f)
|
||||
return True
|
||||
|
||||
|
||||
def _spoolman_map_section(printer_id: Optional[str] = None) -> str:
|
||||
"""Section name holding a printer's AMS-slot → Spoolman-spool map.
|
||||
|
||||
Multi-printer (one bridge, N printers): each printer keeps its map in its
|
||||
own ``[spoolman_<id>]`` section so two AMS units cannot overwrite each
|
||||
other's mapping. ``printer_id is None`` (single-printer / legacy callers)
|
||||
uses the original ``[spoolman] slot_spools`` key — full backward
|
||||
compatibility. The global ``[spoolman]`` section keeps ``server`` /
|
||||
``sync_rate`` regardless.
|
||||
"""
|
||||
pid = str(printer_id).strip() if printer_id is not None else ""
|
||||
if pid and pid != "0":
|
||||
return f"{CONFIG_SECTION_SPOOLMAN}_{pid}"
|
||||
return CONFIG_SECTION_SPOOLMAN
|
||||
|
||||
|
||||
def _parse_slot_spools(raw: str) -> dict[int, int]:
|
||||
"""Parse ``"0:42,1:17"`` → ``{0: 42, 1: 17}`` (positive spool ids only)."""
|
||||
result: dict[int, int] = {}
|
||||
for pair in (raw or "").split(","):
|
||||
pair = pair.strip()
|
||||
if ":" not in pair:
|
||||
continue
|
||||
k, _, v = pair.partition(":")
|
||||
k, v = k.strip(), v.strip()
|
||||
if k.isdigit() and v.lstrip("-").isdigit() and int(v) > 0:
|
||||
result[int(k)] = int(v)
|
||||
return result
|
||||
|
||||
|
||||
def list_spool_map(printer_id: Optional[str] = None) -> dict[int, int]:
|
||||
"""Read the AMS-slot → Spoolman-spool-id map from config.ini.
|
||||
|
||||
With ``printer_id`` set, reads the per-printer ``[spoolman_<id>]
|
||||
slot_spools`` key and falls back to the legacy global ``[spoolman]
|
||||
slot_spools`` while that printer has no own section yet. Returns
|
||||
``{slot_index: spool_id}`` (only positive ids).
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return {}
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
section = _spoolman_map_section(printer_id)
|
||||
if cfg.has_option(section, "slot_spools"):
|
||||
return _parse_slot_spools(cfg.get(section, "slot_spools", fallback=""))
|
||||
if cfg.has_option(CONFIG_SECTION_SPOOLMAN, "slot_spools"): # legacy global fallback
|
||||
return _parse_slot_spools(cfg.get(CONFIG_SECTION_SPOOLMAN, "slot_spools", fallback=""))
|
||||
return {}
|
||||
|
||||
|
||||
def save_spool_map(slot_spools: dict[int, int], printer_id: Optional[str] = None) -> bool:
|
||||
"""Persist the AMS-slot → Spoolman-spool-id map to config.ini.
|
||||
|
||||
With ``printer_id`` set, writes only the per-printer ``[spoolman_<id>]``
|
||||
section so other printers and the global ``[spoolman]`` server config stay
|
||||
untouched. An empty map clears the key.
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
section = _spoolman_map_section(printer_id)
|
||||
clean = {int(k): int(v) for k, v in (slot_spools or {}).items() if int(v) > 0}
|
||||
if clean:
|
||||
if not cfg.has_section(section):
|
||||
cfg.add_section(section)
|
||||
cfg[section]["slot_spools"] = ",".join(f"{k}:{v}" for k, v in sorted(clean.items()))
|
||||
elif cfg.has_option(section, "slot_spools"):
|
||||
cfg.remove_option(section, "slot_spools")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
cfg.write(f)
|
||||
return True
|
||||
@@ -308,7 +432,7 @@ def get(key: str, default: str = "") -> str:
|
||||
return os.environ.get(key, default)
|
||||
|
||||
|
||||
# Häufig verwendete Shortcuts
|
||||
# Frequently used shortcuts
|
||||
PRINTER_IP = get("PRINTER_IP", "")
|
||||
MQTT_PORT = int(get("MQTT_PORT", "9883"))
|
||||
USERNAME = get("MQTT_USERNAME", "")
|
||||
@@ -316,9 +440,11 @@ PASSWORD = get("MQTT_PASSWORD", "")
|
||||
MODE_ID = get("MODE_ID", "")
|
||||
DEVICE_ID = get("DEVICE_ID", "")
|
||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
|
||||
VIBRATION_COMPENSATION = int(get("VIBRATION_COMPENSATION", "0"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
SPOOLMAN_SERVER = get("SPOOLMAN_SERVER", "")
|
||||
SPOOLMAN_SYNC_RATE = int(get("SPOOLMAN_SYNC_RATE", "0"))
|
||||
BRIDGE_HOST_IP = get("BRIDGE_HOST_IP", "")
|
||||
|
||||
@@ -10,6 +10,8 @@ services:
|
||||
- ./.env:/app/.env:ro
|
||||
ports:
|
||||
- "7125-7130:7125-7130"
|
||||
# environment:
|
||||
# - BRIDGE_HOST_IP=192.168.1.100 # LAN-IP des Docker-Hosts (für korrekte Log-Anzeige)
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
env_loader.py – lädt Verbindungsparameter aus .env (Repo-Root oder Arbeitsverzeichnis).
|
||||
Umgebungsvariablen haben Vorrang vor .env-Werten.
|
||||
env_loader.py - loads connection parameters from .env (repo root or working directory).
|
||||
Environment variables take precedence over .env values.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
@@ -39,7 +39,7 @@ def get(key: str, default: str = "") -> str:
|
||||
return os.environ.get(key, default)
|
||||
|
||||
|
||||
# Häufig verwendete Shortcuts
|
||||
# Frequently used shortcuts
|
||||
PRINTER_IP = get("PRINTER_IP", "")
|
||||
MQTT_PORT = int(get("MQTT_PORT", "9883"))
|
||||
USERNAME = get("MQTT_USERNAME", "")
|
||||
@@ -47,7 +47,9 @@ PASSWORD = get("MQTT_PASSWORD", "")
|
||||
MODE_ID = get("MODE_ID", "")
|
||||
DEVICE_ID = get("DEVICE_ID", "")
|
||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
|
||||
VIBRATION_COMPENSATION = int(get("VIBRATION_COMPENSATION", "0"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
BRIDGE_HOST_IP = get("BRIDGE_HOST_IP", "")
|
||||
|
||||
116
kobrax_client.py
116
kobrax_client.py
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
kobrax_client.py – Anycubic Kobra X LAN-MQTT-Client
|
||||
|
||||
Protokoll vollständig rekonstruiert via Sniffer 2026-04-17 (953 Nachrichten).
|
||||
Protocol fully reconstructed via sniffer 2026-04-17 (953 messages).
|
||||
|
||||
Voraussetzungen:
|
||||
- /tmp/anycubic_slicer.crt und .key (aus cloud_mqtt.dll @ 0x2ed5b0 / 0x2edce0)
|
||||
- /tmp/anycubic_slicer.crt and .key (from cloud_mqtt.dll @ 0x2ed5b0 / 0x2edce0)
|
||||
- Drucker im LAN-Modus erreichbar auf Port 9883
|
||||
|
||||
Verwendung:
|
||||
@@ -121,9 +121,9 @@ class KobraXClient:
|
||||
self._buf = b""
|
||||
self._pid = 1
|
||||
self._lock = threading.Lock()
|
||||
# Generations-Marker: wird bei jedem Socket-Swap/Close erhöht, damit der
|
||||
# Reader-Thread erkennt wenn _reconnect/_do_connect den Socket unter ihm
|
||||
# ersetzt hat (Issue #53). Schützt gegen recv auf einem stale fd.
|
||||
# Generation marker: incremented on every socket swap/close so the
|
||||
# reader thread notices when _reconnect/_do_connect swapped the socket
|
||||
# underneath it (Issue #53). Protects against recv on a stale fd.
|
||||
self._sock_gen = 0
|
||||
self._running = False
|
||||
|
||||
@@ -162,9 +162,9 @@ class KobraXClient:
|
||||
if not os.path.exists(CERT_FILE) or not os.path.exists(KEY_FILE):
|
||||
raise FileNotFoundError(
|
||||
f"TLS-Zertifikate fehlen: anycubic_slicer.crt + anycubic_slicer.key "
|
||||
f"müssen neben der kx-bridge Binary liegen ({_SCRIPT_DIR}/). "
|
||||
f"Lade anycubic-certs.zip vom Gitea-Release herunter und entpacke "
|
||||
f"die Dateien dorthin."
|
||||
f"must sit next to the kx-bridge binary ({_SCRIPT_DIR}/). "
|
||||
f"Download anycubic-certs.zip from the Gitea release and extract "
|
||||
f"the files there."
|
||||
)
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ctx.check_hostname = False
|
||||
@@ -172,9 +172,9 @@ class KobraXClient:
|
||||
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
|
||||
ctx.load_cert_chain(CERT_FILE, KEY_FILE)
|
||||
|
||||
# Socket als lokale Variable aufbauen — der Handshake (Connect + CONNACK)
|
||||
# läuft OHNE gehaltenes Lock, damit ein langsamer Connect die Sender nicht
|
||||
# einfriert. Erst der fertige Socket wird unter Lock eingeschwenkt (#53).
|
||||
# Build the socket as a local variable - the handshake (connect + CONNACK)
|
||||
# runs WITHOUT holding the lock so a slow connect does not freeze
|
||||
# senders. Only the finished socket is swapped in under the lock (#53).
|
||||
_ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM)
|
||||
raw = socket.create_connection(_ai[0][4], timeout=5)
|
||||
new_sock = ctx.wrap_socket(raw)
|
||||
@@ -196,7 +196,7 @@ class KobraXClient:
|
||||
self._sock = new_sock
|
||||
self._sock_gen += 1
|
||||
self._buf = b""
|
||||
self._subscribe(self._sub_topic()) # nimmt das Lock selbst — nicht verschachteln
|
||||
self._subscribe(self._sub_topic()) # takes the lock itself - do not nest
|
||||
log.debug("MQTT connected to %s:%s", self.host, self.port)
|
||||
|
||||
def connect(self):
|
||||
@@ -206,10 +206,10 @@ class KobraXClient:
|
||||
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."""
|
||||
"""Ensures the reader thread is alive. If the reader died after a
|
||||
previous disconnect/reconnect sequence or an unhandled error,
|
||||
received replies would never arrive - publish()
|
||||
would still send but wait for replies forever."""
|
||||
if not self._running:
|
||||
return # gewollter disconnect
|
||||
t = getattr(self, "_reader_thread", None)
|
||||
@@ -232,13 +232,13 @@ class KobraXClient:
|
||||
self._sock_gen += 1
|
||||
|
||||
def _reconnect(self):
|
||||
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
|
||||
antwortet oder disconnect() gerufen wurde. Backoff cappt bei 60 s. Die
|
||||
ersten 5 Versuche loggen als WARNING (akute Verbindungsstörung), danach
|
||||
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
|
||||
"""Persistent reconnect: keeps retrying forever until the printer is
|
||||
responds or disconnect() was called. Backoff caps at 60 s. The
|
||||
first 5 attempts log as WARNING (acute connection issue), afterwards
|
||||
only DEBUG to avoid log spam during long printer outages (e.g. switched
|
||||
ausgeschaltet) zu vermeiden."""
|
||||
log.warning("Verbindung verloren – reconnect…")
|
||||
# Close + Invalidierung unter Lock, damit kein Sender mitten im sendall
|
||||
log.warning("Connection lost - reconnecting...")
|
||||
# Close + invalidation under the lock so no sender is mid-sendall
|
||||
# auf den gerade geschlossenen Socket trifft (Issue #53).
|
||||
with self._lock:
|
||||
try:
|
||||
@@ -254,18 +254,18 @@ class KobraXClient:
|
||||
delay = delays[min(attempt, len(delays) - 1)]
|
||||
try:
|
||||
self._do_connect()
|
||||
log.info("Reconnect erfolgreich (nach %d Versuchen)", attempt + 1)
|
||||
log.info("Reconnect successful (after %d attempts)", attempt + 1)
|
||||
return True
|
||||
except Exception as e:
|
||||
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.
|
||||
# Split sleep so disconnect() breaks the loop faster.
|
||||
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
|
||||
return False # only when disconnect() was called
|
||||
|
||||
def _subscribe(self, topic: str):
|
||||
with self._lock:
|
||||
@@ -290,15 +290,15 @@ class KobraXClient:
|
||||
ping_ok = True
|
||||
except Exception:
|
||||
ping_ok = False
|
||||
# _reconnect() AUSSERHALB des Locks aufrufen — es nimmt das Lock
|
||||
# selbst, und threading.Lock ist nicht reentrant (sonst Deadlock).
|
||||
# Call _reconnect() OUTSIDE the lock - it takes the lock
|
||||
# itself, and threading.Lock is not reentrant (deadlock otherwise).
|
||||
if not ping_ok:
|
||||
if self._running and not self._reconnect():
|
||||
break
|
||||
last_ping = time.time()
|
||||
# Aktuellen Socket + Generation unter Lock greifen, damit ein
|
||||
# paralleler _reconnect/_do_connect-Swap uns nicht auf einem stale
|
||||
# fd pollen lässt (Issue #53).
|
||||
# Grab the current socket + generation under the lock so a
|
||||
# parallel _reconnect/_do_connect swap does not leave us polling
|
||||
# a stale fd (Issue #53).
|
||||
with self._lock:
|
||||
sock = self._sock
|
||||
gen = self._sock_gen
|
||||
@@ -306,37 +306,37 @@ class KobraXClient:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
# Idle-Wartezeit OHNE Lock — select probt nur die Bereitschaft, so
|
||||
# blockiert der Reader während Leerlauf nie das gemeinsame Lock.
|
||||
# Idle wait WITHOUT the lock - select only probes readiness, so
|
||||
# the reader never blocks the shared lock while idle.
|
||||
try:
|
||||
ready, _, _ = select.select([sock], [], [], 0.2)
|
||||
except (OSError, ValueError):
|
||||
# fd geschlossen/ungültig (Reconnect oder Disconnect mitten im select)
|
||||
# fd closed/invalid (reconnect or disconnect mid-select)
|
||||
if not self._running:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
if not ready:
|
||||
continue # Leerlauf, kein Lock gehalten
|
||||
continue # idle, no lock held
|
||||
|
||||
# Daten liegen an: Lock kurz greifen für das eine recv, serialisiert
|
||||
# gegen alle sendall-Caller. recv blockiert nicht lange (select sagte
|
||||
# ready, Socket-Timeout ist 0.2s).
|
||||
# Data pending: briefly take the lock for the single recv, serialized
|
||||
# against all sendall callers. recv does not block long (select said
|
||||
# ready, socket timeout is 0.2s).
|
||||
try:
|
||||
with self._lock:
|
||||
# Socket könnte zwischen select und hier ersetzt worden sein.
|
||||
# The socket could have been swapped between select and here.
|
||||
if self._sock_gen != gen or self._sock is not sock:
|
||||
continue
|
||||
data = sock.recv(65536)
|
||||
if not data:
|
||||
# Windows SSL kann kurzzeitig b"" liefern ohne echten EOF
|
||||
# Windows SSL can briefly return b"" without a real EOF
|
||||
_empty_count += 1
|
||||
if _empty_count >= 5:
|
||||
raise ConnectionResetError("EOF")
|
||||
continue
|
||||
_empty_count = 0
|
||||
self._buf += data
|
||||
self._drain() # außerhalb des Locks — Dispatch/event.set() bleibt prompt
|
||||
self._drain() # outside the lock - dispatch/event.set() stays prompt
|
||||
except ssl.SSLWantReadError:
|
||||
continue
|
||||
except socket.timeout:
|
||||
@@ -445,8 +445,8 @@ 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.
|
||||
# If the reader thread is dead for historical reasons, revive it -
|
||||
# otherwise replies would never arrive and event.wait() would time out.
|
||||
self._ensure_reader()
|
||||
msgid = str(uuid.uuid4())
|
||||
payload = json.dumps({
|
||||
@@ -471,7 +471,7 @@ class KobraXClient:
|
||||
report_registered = True
|
||||
|
||||
topic = self._pub_topic(msg_type)
|
||||
# Status-Poll-TX (query/getInfo) ist reines Rauschen (alle paar Sekunden) →
|
||||
# Status poll TX (query/getInfo) is pure noise (every few seconds) ->
|
||||
# auf DEBUG. Aktions-TX (start/set/control/move/…) bleibt INFO sichtbar.
|
||||
_tx_level = logging.DEBUG if action in ("query", "getInfo") else logging.INFO
|
||||
log.log(_tx_level, "TX %-25s action=%-12s data=%s",
|
||||
@@ -531,8 +531,8 @@ class KobraXClient:
|
||||
self._sock.sendall(_build_publish(topic, payload))
|
||||
except Exception as 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
|
||||
# Trigger a reconnect (like publish()); no retry because it is
|
||||
# fire-and-forget - the next call will hit the fresh socket
|
||||
# treffen.
|
||||
try:
|
||||
self._reconnect()
|
||||
@@ -579,13 +579,13 @@ class KobraXClient:
|
||||
# -- Part-Skip ("Exclude Object") ---------------------------------------
|
||||
|
||||
def query_skip_objects(self) -> dict | None:
|
||||
"""Fragt den Drucker nach der aktuellen Objekt-/Skip-Liste."""
|
||||
"""Asks the printer for the current object/skip list."""
|
||||
return self.publish("skip", "query_obj")
|
||||
|
||||
def skip_objects(self, names: list[str]) -> dict | None:
|
||||
"""Überspringt die genannten Objekte – auch mid-print möglich.
|
||||
"""Skips the named objects - also possible mid-print.
|
||||
|
||||
Namen entsprechen den EXCLUDE_OBJECT_DEFINE NAME=… Einträgen
|
||||
Names correspond to the EXCLUDE_OBJECT_DEFINE NAME=... entries
|
||||
im GCode-Header bzw. file_details.objects_skip_parts.
|
||||
"""
|
||||
return self.publish("skip", "start", {"objects_skip_parts": list(names)})
|
||||
@@ -653,14 +653,14 @@ class KobraXClient:
|
||||
f"Connection: close\r\n\r\n"
|
||||
).encode()
|
||||
|
||||
# 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).
|
||||
# Short connect timeout (LAN). During sendall() the socket may take
|
||||
# as long as needed - with large files (>100 MB) and slower WiFi
|
||||
# at the printer, pushing otherwise takes >30 s and would falsely
|
||||
# trip the connect timeout. The read timeout afterwards is generous
|
||||
# (the printer processes the file before replying).
|
||||
_ai = socket.getaddrinfo(self.host, 18910, socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock = socket.create_connection(_ai[0][4], timeout=10)
|
||||
sock.settimeout(None) # blocking während Send
|
||||
sock.settimeout(None) # blocking during send
|
||||
sock.sendall(headers + body)
|
||||
sock.settimeout(180)
|
||||
response = b""
|
||||
@@ -717,7 +717,7 @@ if __name__ == "__main__":
|
||||
parser.add_argument("--mode-id", default=env_loader.MODE_ID)
|
||||
parser.add_argument("--device-id", default=env_loader.DEVICE_ID)
|
||||
parser.add_argument("--monitor", action="store_true",
|
||||
help="Dauerhaft mithören und alle Reports ausgeben")
|
||||
help="Listen continuously and print all reports")
|
||||
args = parser.parse_args()
|
||||
|
||||
client = KobraXClient(
|
||||
@@ -741,7 +741,7 @@ if __name__ == "__main__":
|
||||
|
||||
client.callbacks["*"] = on_msg
|
||||
client.connect()
|
||||
print("[kobrax] Monitor-Modus aktiv (Ctrl-C zum Beenden)")
|
||||
print("[kobrax] Monitor mode active (Ctrl-C to stop)")
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
@@ -755,7 +755,7 @@ if __name__ == "__main__":
|
||||
info = client.query_info()
|
||||
if info:
|
||||
d = info.get("data", {})
|
||||
print(f" Drucker: {d.get('printerName')} FW {d.get('version')}")
|
||||
print(f" Printer: {d.get('printerName')} FW {d.get('version')}")
|
||||
print(f" Status: {d.get('state')}")
|
||||
t = d.get("temp", {})
|
||||
print(f" Nozzle: {t.get('curr_nozzle_temp')}°C → {t.get('target_nozzle_temp')}°C")
|
||||
@@ -764,6 +764,6 @@ if __name__ == "__main__":
|
||||
print(f" Upload: {urls.get('fileUploadurl')}")
|
||||
print(f" Kamera: {urls.get('rtspUrl')}")
|
||||
else:
|
||||
print(" Keine Antwort")
|
||||
print(" No response")
|
||||
|
||||
client.disconnect()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
"""OrcaSlicer Filament-Profil Parser.
|
||||
|
||||
Geteilt zwischen dem Generator (tools/gen_orca_filament_list.py) und dem
|
||||
Shared between the generator (tools/gen_orca_filament_list.py) and the
|
||||
Custom-Profile-Import-Endpoint (bridge/kobrax_moonraker_bridge.py).
|
||||
|
||||
Liest Orca-Filament-JSON-Dateien (System- oder User-Profile) und gibt
|
||||
sie als normalisierte Liste mit (id, name, vendor, type, color) zurück.
|
||||
Reads Orca filament JSON files (system or user profiles) and returns
|
||||
them as a normalized list with (id, name, vendor, type, color).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -13,8 +13,8 @@ import re
|
||||
|
||||
|
||||
def first_str(value, default: str = "") -> str:
|
||||
"""Orca-Profile speichern manche Felder als ['wert']. Liefert erstes
|
||||
Element als String."""
|
||||
"""Orca profiles store some fields as ['value']. Returns the first
|
||||
element as a string."""
|
||||
if isinstance(value, list):
|
||||
return str(value[0]) if value else default
|
||||
if isinstance(value, str):
|
||||
@@ -37,34 +37,34 @@ def parse_profile(data: dict, by_name: dict | None = None,
|
||||
path_vendor: str | None = None,
|
||||
source_path: str = "",
|
||||
system_index: list | None = None) -> dict | None:
|
||||
"""Parsed ein einzelnes Orca-Filament-Profil zum Bridge-Schema.
|
||||
"""Parses a single Orca filament profile into the bridge schema.
|
||||
|
||||
`by_name` ist optional ein {name: [profile, …]}-Index für Inherits-Resolve
|
||||
aus dem rohen Source-Tree (Generator). Bei Single-File-Import (User-Datei
|
||||
aus OrcaSlicer-User-Dir) reichen wir stattdessen `system_index` rein —
|
||||
die fertige System-Profile-Liste aus orca_filaments.json. Damit können
|
||||
wir filament_id/vendor/type/color über die `inherits`-Kette aus dem
|
||||
System-Parent ableiten, auch wenn das User-Profil diese Felder nicht
|
||||
selbst setzt (typisch: User-Override-Profile haben nur Tweaks).
|
||||
`by_name` is optionally a {name: [profile, ...]} index for inherits resolution
|
||||
from the raw source tree (generator). For single-file imports (user file
|
||||
from the OrcaSlicer user dir) we pass `system_index` instead -
|
||||
the finished system profile list from orca_filaments.json. This lets
|
||||
us derive filament_id/vendor/type/color via the `inherits` chain from
|
||||
the system parent even when the user profile does not set these
|
||||
fields itself (typically: user override profiles only contain tweaks).
|
||||
|
||||
Liefert {id, name, vendor, type, color} oder None wenn das Profil
|
||||
keine filament_id hat (z.B. abstrakte @base-Templates).
|
||||
Returns {id, name, vendor, type, color} or None when the profile
|
||||
has no filament_id (e.g. abstract @base templates).
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
# User-Profile aus dem OrcaSlicer-User-Dir setzen oft KEIN "type"-Feld —
|
||||
# das kommt vom System-Parent. Wir akzeptieren das wenn entweder "type"
|
||||
# explizit "filament" ist ODER ein "inherits" auf ein anderes Profil zeigt.
|
||||
# User profiles from the OrcaSlicer user dir often set NO "type" field -
|
||||
# it comes from the system parent. We accept that when either "type"
|
||||
# is explicitly "filament" OR an "inherits" points to another profile.
|
||||
if data.get("type") not in (None, "filament") and not data.get("inherits"):
|
||||
return None
|
||||
if data.get("type") == "filament" and data.get("inherits") is None and not data.get("filament_id"):
|
||||
# type=filament aber kein parent + keine ID → wertloses Stub
|
||||
# type=filament but no parent + no ID -> worthless stub
|
||||
return None
|
||||
inst = data.get("instantiation", "true")
|
||||
if isinstance(inst, str) and inst.lower() == "false":
|
||||
return None
|
||||
|
||||
# Build system-name-Index für den fallback-Lookup wenn system_index gesetzt.
|
||||
# Build the system name index for the fallback lookup when system_index is set.
|
||||
sys_by_name: dict[str, dict] = {}
|
||||
if system_index:
|
||||
for p in system_index:
|
||||
@@ -92,7 +92,7 @@ def parse_profile(data: dict, by_name: dict | None = None,
|
||||
return None
|
||||
|
||||
def _resolve_via_system_index(key: str):
|
||||
"""Inherits-Kette über system_index (clean_name-Match)."""
|
||||
"""Inherits chain via system_index (clean_name match)."""
|
||||
parent_raw = data.get("inherits")
|
||||
if not parent_raw or not sys_by_name:
|
||||
return None
|
||||
@@ -100,7 +100,7 @@ def parse_profile(data: dict, by_name: dict | None = None,
|
||||
sys_p = sys_by_name.get(parent_clean)
|
||||
if not sys_p:
|
||||
return None
|
||||
# System-JSON benutzt schon das normalisierte Schema
|
||||
# The system JSON already uses the normalized schema
|
||||
mapping = {
|
||||
"filament_id": "id",
|
||||
"filament_vendor": "vendor",
|
||||
@@ -136,10 +136,10 @@ def parse_profile(data: dict, by_name: dict | None = None,
|
||||
|
||||
def parse_profile_bytes(blob: bytes, source_name: str = "",
|
||||
system_index: list | None = None) -> dict | None:
|
||||
"""Liest ein einzelnes Profil aus JSON-Bytes. Für File-Upload-Pfad.
|
||||
`system_index` ist optional die fertige Liste aus orca_filaments.json —
|
||||
wird für die Inherits-Resolve von User-Profilen genutzt die das volle
|
||||
Schema vom System-Parent erben."""
|
||||
"""Reads a single profile from JSON bytes. For the file upload path.
|
||||
`system_index` is optionally the finished list from orca_filaments.json -
|
||||
used for the inherits resolution of user profiles that do not carry the full
|
||||
schema from the system parent."""
|
||||
try:
|
||||
data = json.loads(blob.decode("utf-8", errors="replace"))
|
||||
except Exception:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fb4bf06b0cfb5bcac81e2faf99d8ace1c15771ea009837802a08a4dd5ba77a8f /home/coding/Source/kobrax/releases/0.9.0-beta1/extract_credentials
|
||||
68f9bf800d1df0e71423edd35e90a8f5f7fb6e9e5220a8c12ed98cc6c4fb4833 /home/coding/Source/kobrax/releases/0.9.0-beta1/extract_credentials.exe
|
||||
7c1a99953e21fc3881f60df444940d66a4689b009e9a17ec936396857a6b9dc0 /home/coding/Source/kobrax/releases/0.9.0-beta1/kx-bridge
|
||||
@@ -1,11 +0,0 @@
|
||||
06873a3b6773e6e755deff33dba468dee3af09859f9b026b14a600501c8285cb anycubic-certs.zip
|
||||
68e08c6b6badb2bf86f61e2164e96b902944c885f083a598ea4e3c854c432e91 extract_credentials
|
||||
8d4d78bdc887e2512790805b711b941420536d5f0f30043b5f3bc74d4fa893d3 extract_credentials.exe
|
||||
9b79a5f8edc734018fc547ac232ddd822ec5fee050e99e3305ccf8f38b177c9e fetch_credentials
|
||||
b79615bc2f9df33bccd5471bfcd83c9bdc5291e857fe5b04927b171342bc1b48 fetch_credentials.exe
|
||||
37ae8e3f4dc7bb6cf434de980db4aed9c1047d995063ce289bdc268f7764380e kx-bridge.exe
|
||||
4f636f91fef7eb1ea1ad1ae8c7f7eaa64b725e4dc2e74db42a53ce5dc5808ba3 kx-bridge-linux-amd64
|
||||
b0ff6fa222cfbf79e0ab609aac4c9d3b1acd80b5c4df10c7847e0c7d125913bc kx-bridge-linux-amd64.zip
|
||||
0cd9448ceec03cc7f545f9c62a21b7075a866dba5ee396ec26b48816f927db1a kx-bridge-linux-arm64
|
||||
99c4641bbdcc000b2cf2142ee01c16942b13da47ea837885fa1be68e984f6101 kx-bridge-linux-arm64.zip
|
||||
31659f76913e1c785cfa9e0a9e87f893a483a6a55248006b046ac07b25b40870 kx-bridge-windows.zip
|
||||
Binary file not shown.
@@ -1,24 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEDTCCAvWgAwIBAgICAZAwDQYJKoZIhvcNAQEFBQAwgZsxCzAJBgNVBAYTAkNO
|
||||
MRIwEAYDVQQIDAlHdWFuZ2RvbmcxETAPBgNVBAcMCFNoZW56aGVuMREwDwYDVQQK
|
||||
DAhBbnljdWJpYzERMA8GA1UECwwIQW55Y3ViaWMxEzARBgNVBAMMCkFDIFJvb3Qg
|
||||
Q0ExKjAoBgkqhkiG9w0BCQEWG2FueWN1YmljX2Nsb3VkQGFueWN1YmljLmNvbTAg
|
||||
Fw0yMzA3MjAwMzI3NTFaGA8yMTIzMDcyMTAzMjc1MVowgZ8xCzAJBgNVBAYTAkNO
|
||||
MRIwEAYDVQQIDAlHdWFuZ2RvbmcxETAPBgNVBAcMCFNoZW56aGVuMREwDwYDVQQK
|
||||
DAhBbnljdWJpYzERMA8GA1UECwwIQW55Y3ViaWMxFzAVBgNVBAMMDkFueWN1Ymlj
|
||||
U2xpY2VyMSowKAYJKoZIhvcNAQkBFhthbnljdWJpY19jbG91ZEBhbnljdWJpYy5j
|
||||
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdoQ7g2F/yecfpdlqT
|
||||
b8W/84r3vQ4ZEWx2PbSTBcGD55HmzJp2lwABHFHbn4CltT9YzoJWpOiVMHYnyPep
|
||||
43tkNUIcGm7z0jrTD5djyYjVAzEitkNzJspKK/xcVmZe/V7Q3IAWXtzgWCd0YpVk
|
||||
K3J0HqoqJvcTSnYe4VXxbIGwbpeYyji9W/DuG1M4Z+sFiPDWeR9xo5IXRU5ZwaTP
|
||||
8OiCCLSBbeKgf0UFWTIZdJ1JXJ7efbbstZOjf5L9LhBIC0hLdL4jlMpF7r0ThecJ
|
||||
cTx9Bnw/hhy+i32rJTRzZDIaLhKg/bka9ZrORZdxxQRiPoMjLjoxtr4+AUaeLWkI
|
||||
ajSJAgMBAAGjUzBRMB0GA1UdDgQWBBRI4P3/uKdYYFPEcFIwYxdv1p9gETAfBgNV
|
||||
HSMEGDAWgBQlkDqpFERfr3u1rR9gNbNKtgrHIjAPBgNVHRMBAf8EBTADAQH/MA0G
|
||||
CSqGSIb3DQEBBQUAA4IBAQBP3ws80Y9eBR2lpjYP3rVvH8kA6+LnEXT4PpHj+fSw
|
||||
jciaNskzpiwNvBy00m32ACR5YKlMUjevlQuyyw+LQbTUwAEOwyy9SDQpiXdjL6q3
|
||||
SPQ4aB4A57nFXOGrthc/nb9yFcteWrZrKbwvVUu2vqU7U8n7lJKjhVuFRWSXS3SV
|
||||
sPc9JZ21kpPYWKbGtfD6jUlW0Ip+PurLw9FrbVwnEcOMf/ezSlrH5c8mfJyo8pVk
|
||||
aC/6PpReqijusOSRZ5oLyhPvtgddXseJFByun1Ud0CDlFA05nGGPmnVcXD+GMnHH
|
||||
i6baCTeifwp5Jpdzv4imcCPvayKUNuX32vYNfNkWC/R5
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdoQ7g2F/yecfp
|
||||
dlqTb8W/84r3vQ4ZEWx2PbSTBcGD55HmzJp2lwABHFHbn4CltT9YzoJWpOiVMHYn
|
||||
yPep43tkNUIcGm7z0jrTD5djyYjVAzEitkNzJspKK/xcVmZe/V7Q3IAWXtzgWCd0
|
||||
YpVkK3J0HqoqJvcTSnYe4VXxbIGwbpeYyji9W/DuG1M4Z+sFiPDWeR9xo5IXRU5Z
|
||||
waTP8OiCCLSBbeKgf0UFWTIZdJ1JXJ7efbbstZOjf5L9LhBIC0hLdL4jlMpF7r0T
|
||||
hecJcTx9Bnw/hhy+i32rJTRzZDIaLhKg/bka9ZrORZdxxQRiPoMjLjoxtr4+AUae
|
||||
LWkIajSJAgMBAAECggEASwRkC9lRiLqN30kvWW5g6hsec8KrTfLm2pMCVy2AlgxB
|
||||
B3VD51YvKzERyBwSKITT/1RPK9K/4xe3NrpAkmGsd3vLd8W+vorvXFePr7gct7VP
|
||||
4Wb+J7D+keKXlg2sswRiHqI0PN45Nzq/iBaCaJiIMiPbB0+PHBl9J/Cv7XsD3tq+
|
||||
9WKhvXf2g1g9GMrLaCCcWXWCqcu0LlbqJnw3yMnJLSltmyFTmlVLjDHM75bMVz97
|
||||
4emQzOlnRN2yA5cWWCaM+mgjNM2aWwUsXBZzCgwSqSaj1QD4B/epCuDBORWHS9D6
|
||||
jL15w8xjly9q8OS+4d6beR5h9GiPyMK4Ff2wXImCXQKBgQDwXxtrL+kVZrQ/qftj
|
||||
24F3+QDN0j5Z3lUMTfZPn6ng/E/aBfn8KcWJHj2vYkKZdB5wOXJr56BYe3Hukzfp
|
||||
QF0E2+g1WAGskF1mb/vVab54geox5Y6CA+ionRn2kcCwybVkktR/0JK2UV9Qjb/z
|
||||
k1WU+RUhNrW/GDBqYulaadnR+wKBgQDsCf2/yKGPxj4pIvAtn5RFSlfscddgkSnc
|
||||
ouBkDXEp5ta+5PGrlrdzS/F0vFhvBPbfbVJxVwRnM/Oqj8c0/bj7oc5RpPxirciO
|
||||
AaovKVPTiORaviytnB2HgkflkJfy5vdXv4ZQahAV/UwtSmLwBshe+Ya68MAFrQRa
|
||||
7M4z6k4QSwKBgQCm7OVVoofzXMeADsONrTpT3pA4XvD95/CYAuwyj2ah35Z0igH4
|
||||
o+mSN3YO/eXSO1mIBdz4Inqv98o/K+2ABjqSzUSNBvjipb63DL2Oj0i+1zmUPR6i
|
||||
G6TOs4r8OGvgWbOmjHEV8fpwskHG5ymONZsRQYjy79N3SY0V1GrJZwjlUQKBgD0x
|
||||
AeWcP7YkMK09b4KEYk3sTgrwIGPafj3Cw+VsTrAMNhPbCoPvWLO9NmWLBmoRoWae
|
||||
0sarRmry3vKSv5QPSsuBURl9aiiy4NFfwRzk2+R1Eq4rqy1+0XD152muKJZCJlFL
|
||||
R6jFNlJdDkiXhjqvp3ZnvfPswfs2tXBU/8gZsA8tAoGBALXfc5m9I5R1l1zN7tpa
|
||||
ncA0S3EKzqmuCc3KzlS6OS0e9Lz1MsmfEsvxvW3w4SrdfTbwQpEy9RNg89dlgPtc
|
||||
rdId1QdN2eWPY5M4lz9n9EYdzi9ufoKAEYu2a0lP+qz690JwmL1Jx49bvQEn5Nu0
|
||||
4swn72uwBRlhjAw46MF77SBQ
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,89 +0,0 @@
|
||||
pyinstaller --onefile --name extract_credentials /src/extract_credentials.py && cp /src/dist/extract_credentials.exe /out/ && pyinstaller --onefile --name fetch_credentials /src/fetch_credentials.py && cp /src/dist/fetch_credentials.exe /out/
|
||||
114 INFO: PyInstaller: 3.6
|
||||
114 INFO: Python: 3.7.5
|
||||
114 INFO: Platform: Windows-7-6.1.7601-SP1
|
||||
115 INFO: wrote Z:\src\extract_credentials.spec
|
||||
120 INFO: UPX is not available.
|
||||
121 INFO: Extending PYTHONPATH with paths
|
||||
['Z:\\src', 'Z:\\src']
|
||||
121 INFO: checking Analysis
|
||||
121 INFO: Building Analysis because Analysis-00.toc is non existent
|
||||
121 INFO: Initializing module dependency graph...
|
||||
124 INFO: Caching module graph hooks...
|
||||
130 INFO: Analyzing base_library.zip ...
|
||||
2142 INFO: Caching module dependency graph...
|
||||
2226 INFO: running Analysis Analysis-00.toc
|
||||
2234 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
|
||||
required by c:\Python37\python.exe
|
||||
2467 INFO: Analyzing \src\extract_credentials.py
|
||||
2497 INFO: Processing module hooks...
|
||||
2497 INFO: Loading module hook "hook-encodings.py"...
|
||||
2649 INFO: Loading module hook "hook-pydoc.py"...
|
||||
2650 INFO: Loading module hook "hook-xml.py"...
|
||||
2788 INFO: Looking for ctypes DLLs
|
||||
2791 INFO: Analyzing run-time hooks ...
|
||||
2794 INFO: Looking for dynamic libraries
|
||||
2909 INFO: Looking for eggs
|
||||
2909 INFO: Using Python library c:\Python37\python37.dll
|
||||
2909 INFO: Found binding redirects:
|
||||
[]
|
||||
2910 INFO: Warnings written to Z:\src\build\extract_credentials\warn-extract_credentials.txt
|
||||
2927 INFO: Graph cross-reference written to Z:\src\build\extract_credentials\xref-extract_credentials.html
|
||||
2939 INFO: checking PYZ
|
||||
2939 INFO: Building PYZ because PYZ-00.toc is non existent
|
||||
2939 INFO: Building PYZ (ZlibArchive) Z:\src\build\extract_credentials\PYZ-00.pyz
|
||||
3199 INFO: Building PYZ (ZlibArchive) Z:\src\build\extract_credentials\PYZ-00.pyz completed successfully.
|
||||
3203 INFO: checking PKG
|
||||
3203 INFO: Building PKG because PKG-00.toc is non existent
|
||||
3203 INFO: Building PKG (CArchive) PKG-00.pkg
|
||||
4599 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
|
||||
4601 INFO: Bootloader c:\Python37\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe
|
||||
4601 INFO: checking EXE
|
||||
4601 INFO: Building EXE because EXE-00.toc is non existent
|
||||
4601 INFO: Building EXE from EXE-00.toc
|
||||
4601 INFO: Appending archive to EXE Z:\src\dist\extract_credentials.exe
|
||||
4604 INFO: Building EXE from EXE-00.toc completed successfully.
|
||||
77 INFO: PyInstaller: 3.6
|
||||
77 INFO: Python: 3.7.5
|
||||
77 INFO: Platform: Windows-7-6.1.7601-SP1
|
||||
77 INFO: wrote Z:\src\fetch_credentials.spec
|
||||
83 INFO: UPX is not available.
|
||||
84 INFO: Extending PYTHONPATH with paths
|
||||
['Z:\\src', 'Z:\\src']
|
||||
84 INFO: checking Analysis
|
||||
84 INFO: Building Analysis because Analysis-00.toc is non existent
|
||||
84 INFO: Initializing module dependency graph...
|
||||
88 INFO: Caching module graph hooks...
|
||||
94 INFO: Analyzing base_library.zip ...
|
||||
2079 INFO: Caching module dependency graph...
|
||||
2168 INFO: running Analysis Analysis-00.toc
|
||||
2170 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
|
||||
required by c:\Python37\python.exe
|
||||
2425 INFO: Analyzing \src\fetch_credentials.py
|
||||
2501 INFO: Processing module hooks...
|
||||
2501 INFO: Loading module hook "hook-encodings.py"...
|
||||
2630 INFO: Loading module hook "hook-pydoc.py"...
|
||||
2631 INFO: Loading module hook "hook-xml.py"...
|
||||
2765 INFO: Looking for ctypes DLLs
|
||||
2765 INFO: Analyzing run-time hooks ...
|
||||
2768 INFO: Looking for dynamic libraries
|
||||
2878 INFO: Looking for eggs
|
||||
2878 INFO: Using Python library c:\Python37\python37.dll
|
||||
2878 INFO: Found binding redirects:
|
||||
[]
|
||||
2880 INFO: Warnings written to Z:\src\build\fetch_credentials\warn-fetch_credentials.txt
|
||||
2896 INFO: Graph cross-reference written to Z:\src\build\fetch_credentials\xref-fetch_credentials.html
|
||||
2901 INFO: checking PYZ
|
||||
2901 INFO: Building PYZ because PYZ-00.toc is non existent
|
||||
2901 INFO: Building PYZ (ZlibArchive) Z:\src\build\fetch_credentials\PYZ-00.pyz
|
||||
3159 INFO: Building PYZ (ZlibArchive) Z:\src\build\fetch_credentials\PYZ-00.pyz completed successfully.
|
||||
3163 INFO: checking PKG
|
||||
3163 INFO: Building PKG because PKG-00.toc is non existent
|
||||
3163 INFO: Building PKG (CArchive) PKG-00.pkg
|
||||
4428 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
|
||||
4430 INFO: Bootloader c:\Python37\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe
|
||||
4430 INFO: checking EXE
|
||||
4430 INFO: Building EXE because EXE-00.toc is non existent
|
||||
4430 INFO: Building EXE from EXE-00.toc
|
||||
4430 INFO: Appending archive to EXE Z:\src\dist\fetch_credentials.exe
|
||||
4434 INFO: Building EXE from EXE-00.toc completed successfully.
|
||||
@@ -1,56 +0,0 @@
|
||||
32 INFO: PyInstaller: 6.19.0, contrib hooks: 2026.4
|
||||
32 INFO: Python: 3.12.3
|
||||
33 INFO: Platform: Linux-6.17.0-35-generic-x86_64-with-glibc2.39
|
||||
33 INFO: Python environment: /usr
|
||||
33 INFO: wrote /home/coding/Source/KX-Bridge-Release/build/extract_credentials.spec
|
||||
34 INFO: Module search paths (PYTHONPATH):
|
||||
['/usr/lib/python312.zip',
|
||||
'/usr/lib/python3.12',
|
||||
'/usr/lib/python3.12/lib-dynload',
|
||||
'/home/coding/.local/lib/python3.12/site-packages',
|
||||
'/usr/local/lib/python3.12/dist-packages',
|
||||
'/usr/lib/python3/dist-packages',
|
||||
'/usr/lib/python3.12/dist-packages',
|
||||
'/home/coding/Source/KX-Bridge-Release']
|
||||
352 INFO: checking Analysis
|
||||
352 INFO: Building Analysis because Analysis-00.toc is non existent
|
||||
352 INFO: Looking for Python shared library...
|
||||
364 WARNING: Unrecognised line of output 'Der Cache wurde generiert von: ldconfig (Ubuntu GLIBC 2.39-0ubuntu8.7) stable release version 2.39' from ldconfig
|
||||
364 INFO: Using Python shared library: /lib/x86_64-linux-gnu/libpython3.12.so.1.0
|
||||
364 INFO: Running Analysis Analysis-00.toc
|
||||
364 INFO: Target bytecode optimization level: 0
|
||||
364 INFO: Initializing module dependency graph...
|
||||
366 INFO: Initializing module graph hook caches...
|
||||
380 INFO: Analyzing modules for base_library.zip ...
|
||||
1005 INFO: Processing standard module hook 'hook-encodings.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
1923 INFO: Processing standard module hook 'hook-pickle.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
2491 INFO: Processing standard module hook 'hook-heapq.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
2739 INFO: Caching module dependency graph...
|
||||
2764 INFO: Analyzing /home/coding/Source/KX-Bridge-Release/extract_credentials.py
|
||||
2790 INFO: Processing standard module hook 'hook-platform.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
2801 INFO: Processing standard module hook 'hook-_ctypes.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
2808 INFO: Processing module hooks (post-graph stage)...
|
||||
2812 INFO: Performing binary vs. data reclassification (1 entries)
|
||||
2818 INFO: Looking for ctypes DLLs
|
||||
2825 INFO: Analyzing run-time hooks ...
|
||||
2825 INFO: Including run-time hook 'pyi_rth_inspect.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
|
||||
2829 INFO: Creating base_library.zip...
|
||||
2841 INFO: Looking for dynamic libraries
|
||||
2987 INFO: Warnings written to /home/coding/Source/KX-Bridge-Release/build/pyinstaller/extract_credentials/warn-extract_credentials.txt
|
||||
2993 INFO: Graph cross-reference written to /home/coding/Source/KX-Bridge-Release/build/pyinstaller/extract_credentials/xref-extract_credentials.html
|
||||
3000 INFO: checking PYZ
|
||||
3000 INFO: Building PYZ because PYZ-00.toc is non existent
|
||||
3000 INFO: Building PYZ (ZlibArchive) /home/coding/Source/KX-Bridge-Release/build/pyinstaller/extract_credentials/PYZ-00.pyz
|
||||
3099 INFO: Building PYZ (ZlibArchive) /home/coding/Source/KX-Bridge-Release/build/pyinstaller/extract_credentials/PYZ-00.pyz completed successfully.
|
||||
3105 INFO: checking PKG
|
||||
3105 INFO: Building PKG because PKG-00.toc is non existent
|
||||
3105 INFO: Building PKG (CArchive) extract_credentials.pkg
|
||||
5376 INFO: Building PKG (CArchive) extract_credentials.pkg completed successfully.
|
||||
5376 INFO: Bootloader /home/coding/.local/lib/python3.12/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run
|
||||
5376 INFO: checking EXE
|
||||
5376 INFO: Building EXE because EXE-00.toc is non existent
|
||||
5376 INFO: Building EXE from EXE-00.toc
|
||||
5376 INFO: Copying bootloader EXE to /home/coding/Source/KX-Bridge-Release/releases/0.9.27/extract_credentials
|
||||
5376 INFO: Appending PKG archive to custom ELF section in EXE
|
||||
5386 INFO: Building EXE from EXE-00.toc completed successfully.
|
||||
5386 INFO: Build complete! The results are available in: /home/coding/Source/KX-Bridge-Release/releases/0.9.27
|
||||
@@ -1,91 +0,0 @@
|
||||
35 INFO: PyInstaller: 6.19.0, contrib hooks: 2026.4
|
||||
35 INFO: Python: 3.12.3
|
||||
36 INFO: Platform: Linux-6.17.0-35-generic-x86_64-with-glibc2.39
|
||||
36 INFO: Python environment: /usr
|
||||
36 INFO: wrote /home/coding/Source/KX-Bridge-Release/build/fetch_credentials.spec
|
||||
37 WARNING: collect_data_files - skipping data collection for module 'pycryptodome' as it is not a package.
|
||||
37 WARNING: collect_dynamic_libs - skipping library collection for module 'pycryptodome' as it is not a package.
|
||||
117 INFO: Module search paths (PYTHONPATH):
|
||||
['/usr/lib/python312.zip',
|
||||
'/usr/lib/python3.12',
|
||||
'/usr/lib/python3.12/lib-dynload',
|
||||
'/home/coding/.local/lib/python3.12/site-packages',
|
||||
'/usr/local/lib/python3.12/dist-packages',
|
||||
'/usr/lib/python3/dist-packages',
|
||||
'/usr/lib/python3.12/dist-packages',
|
||||
'/home/coding/Source/KX-Bridge-Release']
|
||||
458 INFO: checking Analysis
|
||||
458 INFO: Building Analysis because Analysis-00.toc is non existent
|
||||
458 INFO: Looking for Python shared library...
|
||||
475 WARNING: Unrecognised line of output 'Der Cache wurde generiert von: ldconfig (Ubuntu GLIBC 2.39-0ubuntu8.7) stable release version 2.39' from ldconfig
|
||||
475 INFO: Using Python shared library: /lib/x86_64-linux-gnu/libpython3.12.so.1.0
|
||||
475 INFO: Running Analysis Analysis-00.toc
|
||||
475 INFO: Target bytecode optimization level: 0
|
||||
476 INFO: Initializing module dependency graph...
|
||||
476 INFO: Initializing module graph hook caches...
|
||||
491 INFO: Analyzing modules for base_library.zip ...
|
||||
1017 INFO: Processing standard module hook 'hook-encodings.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
1871 INFO: Processing standard module hook 'hook-pickle.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
2469 INFO: Processing standard module hook 'hook-heapq.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
2692 INFO: Caching module dependency graph...
|
||||
2717 INFO: Analyzing /home/coding/Source/KX-Bridge-Release/fetch_credentials.py
|
||||
2750 INFO: Processing standard module hook 'hook-urllib3.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||
2817 INFO: Processing pre-safe-import-module hook 'hook-typing_extensions.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||
2818 INFO: SetuptoolsInfo: initializing cached setuptools info...
|
||||
3154 INFO: Processing standard module hook 'hook-multiprocessing.util.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
3216 INFO: Processing standard module hook 'hook-xml.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
3402 INFO: Processing standard module hook 'hook-_ctypes.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
4175 INFO: Processing standard module hook 'hook-chardet.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||
4759 INFO: Processing standard module hook 'hook-cryptography.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||
5142 INFO: hook-cryptography: cryptography uses dynamically-linked OpenSSL: '/lib/x86_64-linux-gnu/libssl.so.3'
|
||||
5351 INFO: Processing standard module hook 'hook-bcrypt.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||
5426 INFO: Processing standard module hook 'hook-certifi.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||
5502 INFO: Processing standard module hook 'hook-Crypto.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||
5706 INFO: Processing standard module hook 'hook-pycparser.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||
5802 INFO: Processing standard module hook 'hook-setuptools.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
5806 INFO: Processing pre-safe-import-module hook 'hook-distutils.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||
5861 INFO: Processing standard module hook 'hook-sysconfig.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
5896 INFO: Processing standard module hook 'hook-platform.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
5906 INFO: Processing standard module hook 'hook-_osx_support.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
6028 INFO: Processing pre-safe-import-module hook 'hook-importlib_metadata.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||
6141 INFO: Processing pre-safe-import-module hook 'hook-packaging.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||
6502 INFO: Processing standard module hook 'hook-pkg_resources.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
7089 INFO: Processing standard module hook 'hook-xml.dom.domreg.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
7323 INFO: Processing standard module hook 'hook-sqlite3.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
7648 INFO: Processing standard module hook 'hook-difflib.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
7995 INFO: Processing module hooks (post-graph stage)...
|
||||
7997 WARNING: Hidden import "pycparser.lextab" not found!
|
||||
7997 WARNING: Hidden import "pycparser.yacctab" not found!
|
||||
8231 INFO: Processing pre-safe-import-module hook 'hook-platformdirs.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||
8237 INFO: Processing standard module hook 'hook-platformdirs.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||
8304 INFO: Processing standard module hook 'hook-setuptools._vendor.importlib_metadata.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
8391 INFO: Processing standard module hook 'hook-setuptools._vendor.jaraco.text.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
|
||||
8490 INFO: Performing binary vs. data reclassification (51 entries)
|
||||
8605 INFO: Looking for ctypes DLLs
|
||||
8618 INFO: Analyzing run-time hooks ...
|
||||
8620 INFO: Including run-time hook 'pyi_rth_inspect.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
|
||||
8622 INFO: Including run-time hook 'pyi_rth_pkgutil.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
|
||||
8623 INFO: Including run-time hook 'pyi_rth_multiprocessing.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
|
||||
8625 INFO: Including run-time hook 'pyi_rth_setuptools.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
|
||||
8626 INFO: Including run-time hook 'pyi_rth_pkgres.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
|
||||
8628 INFO: Including run-time hook 'pyi_rth_cryptography_openssl.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/rthooks'
|
||||
8635 INFO: Creating base_library.zip...
|
||||
8664 INFO: Looking for dynamic libraries
|
||||
9293 INFO: Warnings written to /home/coding/Source/KX-Bridge-Release/build/pyinstaller/fetch_credentials/warn-fetch_credentials.txt
|
||||
9315 INFO: Graph cross-reference written to /home/coding/Source/KX-Bridge-Release/build/pyinstaller/fetch_credentials/xref-fetch_credentials.html
|
||||
9327 INFO: checking PYZ
|
||||
9327 INFO: Building PYZ because PYZ-00.toc is non existent
|
||||
9327 INFO: Building PYZ (ZlibArchive) /home/coding/Source/KX-Bridge-Release/build/pyinstaller/fetch_credentials/PYZ-00.pyz
|
||||
9755 INFO: Building PYZ (ZlibArchive) /home/coding/Source/KX-Bridge-Release/build/pyinstaller/fetch_credentials/PYZ-00.pyz completed successfully.
|
||||
9766 INFO: checking PKG
|
||||
9766 INFO: Building PKG because PKG-00.toc is non existent
|
||||
9766 INFO: Building PKG (CArchive) fetch_credentials.pkg
|
||||
14335 INFO: Building PKG (CArchive) fetch_credentials.pkg completed successfully.
|
||||
14336 INFO: Bootloader /home/coding/.local/lib/python3.12/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run
|
||||
14336 INFO: checking EXE
|
||||
14336 INFO: Building EXE because EXE-00.toc is non existent
|
||||
14336 INFO: Building EXE from EXE-00.toc
|
||||
14336 INFO: Copying bootloader EXE to /home/coding/Source/KX-Bridge-Release/releases/0.9.27/fetch_credentials
|
||||
14336 INFO: Appending PKG archive to custom ELF section in EXE
|
||||
14360 INFO: Building EXE from EXE-00.toc completed successfully.
|
||||
14361 INFO: Build complete! The results are available in: /home/coding/Source/KX-Bridge-Release/releases/0.9.27
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
67
tests/test_filament_profiles_per_printer.py
Normal file
67
tests/test_filament_profiles_per_printer.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Per-printer filament-profile isolation (config_loader).
|
||||
|
||||
Regression test for the multi-printer bug (issue #74): the slot->profile mapping
|
||||
and ``visible_vendors`` lived in a single global ``[filament_profiles]`` section,
|
||||
so configuring one printer overwrote the other and after a restart both loaded
|
||||
the same map. Each printer now uses its own ``[filament_profiles_<id>]`` section,
|
||||
with a read-fallback to the legacy global section for backward compatibility.
|
||||
"""
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) # repo root
|
||||
import config_loader # noqa: E402
|
||||
|
||||
BASE_INI = (
|
||||
"[printer_1]\nname = K1\n\n"
|
||||
"[printer_2]\nname = K2\n\n"
|
||||
"[filament_profiles]\n"
|
||||
"visible_vendors = Anycubic, SUNLU\n"
|
||||
"slot_0_vendor = Anycubic\nslot_0_name = Anycubic PLA+\nslot_0_id = GFPLA+\n"
|
||||
)
|
||||
|
||||
|
||||
def _use_ini(monkeypatch, tmp_path, text=BASE_INI):
|
||||
path = tmp_path / "config.ini"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
monkeypatch.setattr(config_loader, "_find_config_file", lambda: path)
|
||||
return path
|
||||
|
||||
|
||||
def test_legacy_global_still_works(tmp_path, monkeypatch):
|
||||
"""No printer_id -> original global section (single-printer back-compat)."""
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
assert config_loader.list_filament_profiles()[0]["name"] == "Anycubic PLA+"
|
||||
assert config_loader.list_visible_vendors() == ["Anycubic", "SUNLU"]
|
||||
|
||||
|
||||
def test_read_falls_back_to_global_until_first_save(tmp_path, monkeypatch):
|
||||
"""Before any per-printer save, both printers see the global mapping."""
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
assert config_loader.list_filament_profiles("1")[0]["name"] == "Anycubic PLA+"
|
||||
assert config_loader.list_filament_profiles("2")[0]["name"] == "Anycubic PLA+"
|
||||
|
||||
|
||||
def test_saving_one_printer_does_not_touch_the_other(tmp_path, monkeypatch):
|
||||
"""Core regression: configuring printer 1 must not change printer 2."""
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
config_loader.save_filament_profiles(
|
||||
{0: {"vendor": "KINGROON", "name": "KINGROON PLA Basic", "id": "Pc0b8a01"}}, "1")
|
||||
assert config_loader.list_filament_profiles("1")[0]["name"] == "KINGROON PLA Basic"
|
||||
assert config_loader.list_filament_profiles("2")[0]["name"] == "Anycubic PLA+"
|
||||
# legacy global section preserved untouched
|
||||
assert config_loader.list_filament_profiles()[0]["name"] == "Anycubic PLA+"
|
||||
|
||||
|
||||
def test_visible_vendors_isolated_per_printer(tmp_path, monkeypatch):
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
config_loader.save_visible_vendors(["KINGROON"], "1")
|
||||
assert config_loader.list_visible_vendors("1") == ["KINGROON"]
|
||||
assert config_loader.list_visible_vendors("2") == ["Anycubic", "SUNLU"]
|
||||
|
||||
|
||||
def test_save_visible_vendors_keeps_slot_fallback(tmp_path, monkeypatch):
|
||||
"""Creating a per-printer section only for vendors must not orphan slots."""
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
config_loader.save_visible_vendors(["KINGROON"], "1")
|
||||
assert config_loader.list_filament_profiles("1")[0]["name"] == "Anycubic PLA+"
|
||||
100
tests/test_spoolman_slot_map.py
Normal file
100
tests/test_spoolman_slot_map.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Per-printer Spoolman slot-map isolation + persistence (config_loader).
|
||||
|
||||
Regression test for two bugs in the Spoolman slot→spool persistence:
|
||||
|
||||
1. The bridge referenced ``config_loader`` while the module alias is
|
||||
``env_loader`` → ``NameError`` swallowed by a bare ``except``, so the map
|
||||
was never loaded nor saved (persistence looked implemented but was dead).
|
||||
2. The map lived in a single global ``[spoolman] slot_spools`` key, so two
|
||||
printers/two AMS units overwrote each other (same class as issue #74/#75).
|
||||
|
||||
Each printer now uses its own ``[spoolman_<id>]`` section, with a read-fallback
|
||||
to the legacy global key for backward compatibility. The global ``[spoolman]``
|
||||
section keeps ``server`` / ``sync_rate``.
|
||||
"""
|
||||
import sys
|
||||
import pathlib
|
||||
import configparser
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) # repo root
|
||||
import config_loader # noqa: E402
|
||||
|
||||
BASE_INI = (
|
||||
"[printer_1]\nname = K1\n\n"
|
||||
"[printer_2]\nname = K2\n\n"
|
||||
"[spoolman]\n"
|
||||
"server = http://192.168.3.200:7912\n"
|
||||
"sync_rate = 0\n"
|
||||
"slot_spools = 0:1,1:2\n"
|
||||
)
|
||||
|
||||
|
||||
def _use_ini(monkeypatch, tmp_path, text=BASE_INI):
|
||||
path = tmp_path / "config.ini"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
monkeypatch.setattr(config_loader, "_find_config_file", lambda: path)
|
||||
return path
|
||||
|
||||
|
||||
def test_legacy_global_read(tmp_path, monkeypatch):
|
||||
"""No printer_id -> original global [spoolman] slot_spools (back-compat)."""
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
assert config_loader.list_spool_map() == {0: 1, 1: 2}
|
||||
|
||||
|
||||
def test_read_falls_back_to_global_until_first_save(tmp_path, monkeypatch):
|
||||
"""Before any per-printer save, both printers see the global mapping."""
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
assert config_loader.list_spool_map("1") == {0: 1, 1: 2}
|
||||
assert config_loader.list_spool_map("2") == {0: 1, 1: 2}
|
||||
|
||||
|
||||
def test_saving_one_printer_does_not_touch_the_other(tmp_path, monkeypatch):
|
||||
"""Core regression: mapping printer 1 must not change printer 2."""
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
config_loader.save_spool_map({0: 42, 1: 17}, "1")
|
||||
assert config_loader.list_spool_map("1") == {0: 42, 1: 17}
|
||||
# printer 2 has no own section yet -> still the global fallback
|
||||
assert config_loader.list_spool_map("2") == {0: 1, 1: 2}
|
||||
# legacy global key preserved untouched
|
||||
assert config_loader.list_spool_map() == {0: 1, 1: 2}
|
||||
|
||||
|
||||
def test_both_printers_isolated_after_each_saves(tmp_path, monkeypatch):
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
config_loader.save_spool_map({0: 42, 1: 17}, "1")
|
||||
config_loader.save_spool_map({0: 5, 1: 6}, "2")
|
||||
assert config_loader.list_spool_map("1") == {0: 42, 1: 17}
|
||||
assert config_loader.list_spool_map("2") == {0: 5, 1: 6}
|
||||
|
||||
|
||||
def test_save_preserves_server_and_sync_rate(tmp_path, monkeypatch):
|
||||
"""Writing a per-printer map must not clobber [spoolman] server/sync_rate."""
|
||||
path = _use_ini(monkeypatch, tmp_path)
|
||||
config_loader.save_spool_map({0: 42}, "1")
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
assert cfg.get("spoolman", "server") == "http://192.168.3.200:7912"
|
||||
assert cfg.get("spoolman", "sync_rate") == "0"
|
||||
assert cfg.get("spoolman_1", "slot_spools") == "0:42"
|
||||
|
||||
|
||||
def test_persistence_round_trips(tmp_path, monkeypatch):
|
||||
"""Save then read back (simulates a bridge restart) — the map survives."""
|
||||
_use_ini(monkeypatch, tmp_path, text="[spoolman]\nserver = http://x:7912\n")
|
||||
config_loader.save_spool_map({0: 7, 2: 9}, "1")
|
||||
assert config_loader.list_spool_map("1") == {0: 7, 2: 9}
|
||||
|
||||
|
||||
def test_empty_map_clears_the_key(tmp_path, monkeypatch):
|
||||
_use_ini(monkeypatch, tmp_path)
|
||||
config_loader.save_spool_map({0: 42}, "1")
|
||||
config_loader.save_spool_map({}, "1") # clear
|
||||
# per-printer key gone -> falls back to the legacy global map
|
||||
assert config_loader.list_spool_map("1") == {0: 1, 1: 2}
|
||||
|
||||
|
||||
def test_parse_ignores_malformed_and_nonpositive(tmp_path, monkeypatch):
|
||||
_use_ini(monkeypatch, tmp_path,
|
||||
text="[spoolman]\nslot_spools = 0:1, x:y, 2:0, 3:-4, 4:5, junk\n")
|
||||
assert config_loader.list_spool_map() == {0: 1, 4: 5}
|
||||
@@ -54,6 +54,13 @@ function _loadSpoolmanStatus(){
|
||||
_spoolmanStatus=d;
|
||||
_slotSpoolMap=d.slot_spools||{};
|
||||
_updateSpoolmanStatusDot();
|
||||
_buildSpoolmanSection();
|
||||
renderSpoolmanSlotCard();
|
||||
if(d.configured){
|
||||
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(sd){
|
||||
_spoolmanSpools=sd.spools||[];
|
||||
});
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
function _updateSpoolmanStatusDot(){
|
||||
@@ -78,12 +85,8 @@ function _buildSpoolmanSection(){
|
||||
if(loading)loading.style.display='';
|
||||
|
||||
var usedSlots={};
|
||||
document.querySelectorAll('#fd-slots select').forEach(function(sel){
|
||||
var idx=parseInt(sel.value);
|
||||
if(idx>=0){
|
||||
var slot=(_amsSlots||[]).find(function(s){return s.slot_index===idx;});
|
||||
if(slot&&!usedSlots[idx])usedSlots[idx]=slot;
|
||||
}
|
||||
(_amsSlots||[]).forEach(function(slot){
|
||||
usedSlots[slot.slot_index]=slot;
|
||||
});
|
||||
|
||||
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(d){
|
||||
@@ -97,7 +100,7 @@ function _buildSpoolmanSection(){
|
||||
var currentSpool=_slotSpoolMap[String(idx)]||'';
|
||||
var opts='<option value="">–</option>'+_spoolmanSpools.map(function(sp){
|
||||
var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':'';
|
||||
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor+' ':'';
|
||||
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
|
||||
var name=sp.filament&&sp.filament.name?sp.filament.name:'Spool';
|
||||
return '<option value="'+sp.id+'"'+(sp.id==currentSpool?' selected':'')+'>'+
|
||||
escHtml('#'+sp.id+' '+vendor+name+rem)+'</option>';
|
||||
@@ -289,7 +292,7 @@ function renderPrinterDropdown(){
|
||||
menu.innerHTML=_printers.map(function(p){
|
||||
var active=_activePrinter&&String(p.id)===String(_activePrinter.id);
|
||||
var num=p.id;
|
||||
return '<a href="/printer'+num+'" style="display:block;padding:10px 14px;color:'+(active?'var(--accent)':'var(--txt)')+';text-decoration:none;font-size:13px;border-bottom:1px solid var(--border)" '+(active?'style="font-weight:600"':'')+'>'+
|
||||
return '<a href="'+((p.bridge_url||'').replace(/\/+$/,''))+'/printer'+num+'" style="display:block;padding:10px 14px;color:'+(active?'var(--accent)':'var(--txt)')+';text-decoration:none;font-size:13px;border-bottom:1px solid var(--border)" '+(active?'style="font-weight:600"':'')+'>'+
|
||||
(active?'▶ ':'')+p.name+'</a>';
|
||||
}).join('');
|
||||
}
|
||||
@@ -376,12 +379,10 @@ function applyLang(){
|
||||
setText('d-chart-label',T.panel_temps_chart);
|
||||
// Axis labels
|
||||
setText('ptitle-motion-xy',T.panel_motion_xy);
|
||||
setText('ptitle-motion-z',T.panel_motion_z);
|
||||
document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z);
|
||||
document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy);
|
||||
document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all);
|
||||
document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors);
|
||||
document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step);
|
||||
document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':','')));
|
||||
// Console
|
||||
setText('ptitle-console',T.panel_console_title);
|
||||
@@ -432,6 +433,7 @@ function applyLang(){
|
||||
setText('lbl-default-slot',T.settings_default_slot);
|
||||
setText('opt-slot-auto',T.settings_slot_auto);
|
||||
setText('lbl-auto-leveling',T.settings_auto_leveling);
|
||||
setText('lbl-vibration-compensation',T.settings_vibration_compensation);
|
||||
setText('lbl-file-ready-mode',T.settings_file_ready_mode);
|
||||
setText('opt-file-ready-dialog',T.settings_file_ready_dialog);
|
||||
setText('opt-file-ready-banner',T.settings_file_ready_banner);
|
||||
@@ -749,7 +751,7 @@ function applyState(){
|
||||
frb.style.display='none';
|
||||
if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
|
||||
_fdAutoOpenedFile=s.file_ready;
|
||||
startReadyFileWithSlots(s.file_ready,true);
|
||||
startReadyFileWithSlots(s.file_ready,true,s.filament_mismatch||null);
|
||||
}
|
||||
} else {
|
||||
frb.style.display='flex';
|
||||
@@ -962,6 +964,22 @@ function applyState(){
|
||||
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>';
|
||||
}
|
||||
var spoolSel='';
|
||||
if(_spoolmanStatus.configured&&!empty&&_spoolmanSpools.length){
|
||||
var curSpool=_slotSpoolMap[String(globalIdx)]||'';
|
||||
var spoolOpts='<option value="">–</option>'+_spoolmanSpools.map(function(sp){
|
||||
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
|
||||
var name=sp.filament?sp.filament.name:'#'+sp.id;
|
||||
var rem=sp.remaining_weight!=null?' '+sp.remaining_weight.toFixed(0)+'g':'';
|
||||
return '<option value="'+sp.id+'"'+(String(sp.id)==String(curSpool)?' selected':'')+'>'+
|
||||
escHtml(vendor+name+rem)+'</option>';
|
||||
}).join('');
|
||||
spoolSel='<div onclick="event.stopPropagation()" style="margin-top:6px;border-top:1px solid rgba(255,255,255,.07);padding-top:5px">'
|
||||
+'<div style="font-size:8px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px">🧵 Spoolman</div>'
|
||||
+'<select data-spool-slot="'+globalIdx+'" onchange="onAmsSpoolChange(this)" '
|
||||
+'style="width:100%;padding:3px 5px;font-size:10px;border-radius:5px;border:1px solid var(--border);background:var(--card);color:var(--txt);cursor:pointer">'+spoolOpts+'</select>'
|
||||
+'</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>'
|
||||
@@ -969,6 +987,7 @@ function applyState(){
|
||||
+vendorBadge
|
||||
+'<div class="slot-label">'+slotLabel+'</div>'
|
||||
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
|
||||
+spoolSel
|
||||
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
|
||||
+'</div>';
|
||||
});
|
||||
@@ -979,7 +998,10 @@ function applyState(){
|
||||
}
|
||||
html+='</div></div>';
|
||||
});
|
||||
document.getElementById('ams-slots').innerHTML=html;
|
||||
// Nicht rendern wenn ein Spool-Dropdown gerade offen ist (verhindert Schließen beim Poll)
|
||||
var activeEl=document.activeElement;
|
||||
var spoolOpen=activeEl&&activeEl.tagName==='SELECT'&&activeEl.dataset.spoolSlot!=null;
|
||||
if(!spoolOpen) document.getElementById('ams-slots').innerHTML=html;
|
||||
}
|
||||
|
||||
// camera overlay
|
||||
@@ -1065,6 +1087,7 @@ function openSettings(){
|
||||
document.getElementById('s-mode-id').value=d.mode_id||'';
|
||||
document.getElementById('s-default-slot').value=d.default_ams_slot||'auto';
|
||||
document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling);
|
||||
var vc=document.getElementById('s-vibration-compensation');if(vc)vc.checked=!!d.vibration_compensation;
|
||||
var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print;
|
||||
var frm=document.getElementById('s-file-ready-mode');if(frm)frm.value=(d.print_start_dialog===undefined?'1':String(d.print_start_dialog?1:0));
|
||||
var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning);
|
||||
@@ -1075,6 +1098,7 @@ function openSettings(){
|
||||
pi.value=sec;
|
||||
}
|
||||
renderFilamentMapping(d.filament_profiles||{});
|
||||
renderSpoolmanSlotCard();
|
||||
// Spoolman
|
||||
var su=document.getElementById('s-spoolman-url');if(su)su.value=d.spoolman_server||'';
|
||||
var sr=document.getElementById('s-spoolman-sync-rate');if(sr)sr.value=(d.spoolman_sync_rate!==undefined?d.spoolman_sync_rate:30);
|
||||
@@ -1222,6 +1246,64 @@ function _vendorCheck(cb){
|
||||
var v=cb.getAttribute('data-vendor');
|
||||
if(cb.checked)_vendorChecklistSel[v]=true; else delete _vendorChecklistSel[v];
|
||||
}
|
||||
function renderSpoolmanSlotCard(){
|
||||
var card=document.getElementById('spoolman-slot-card');
|
||||
var rows=document.getElementById('spoolman-slot-rows');
|
||||
if(!card||!rows)return;
|
||||
if(!_spoolmanStatus.configured){card.style.display='none';return;}
|
||||
card.style.display='';
|
||||
Promise.all([
|
||||
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}),
|
||||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();})
|
||||
]).then(function(res){
|
||||
var spools=res[0].spools||[];
|
||||
var slots=(res[1].result||[]).sort(function(a,b){return a.slot_index-b.slot_index;});
|
||||
if(!slots.length){rows.innerHTML='<span style="font-size:11px;color:var(--txt2)">Keine AMS-Slots bekannt.</span>';return;}
|
||||
rows.innerHTML=slots.map(function(slot){
|
||||
var idx=parseInt(slot.slot_index);
|
||||
var col=slot.color_hex||'#888';
|
||||
var mat=slot.material||'';
|
||||
var current=_slotSpoolMap[String(idx)]||'';
|
||||
var opts='<option value="">–</option>'+spools.map(function(sp){
|
||||
var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':'';
|
||||
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
|
||||
var name=sp.filament?sp.filament.name:'Spool #'+sp.id;
|
||||
var mat2=sp.filament&&sp.filament.material?' · '+sp.filament.material:'';
|
||||
return '<option value="'+sp.id+'"'+(String(sp.id)==String(current)?' selected':'')+'>'+
|
||||
escHtml('#'+sp.id+' '+vendor+name+mat2+rem)+'</option>';
|
||||
}).join('');
|
||||
return '<div style="display:flex;align-items:center;gap:8px;font-size:12px">'+
|
||||
'<span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:'+col+';border:1px solid var(--border);flex-shrink:0"></span>'+
|
||||
'<span style="color:var(--txt2);min-width:60px">Slot '+(idx+1)+' <span style="color:var(--txt2);font-size:10px">'+escHtml(mat)+'</span></span>'+
|
||||
'<select data-spool-slot="'+idx+'" style="flex:1;padding:3px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+opts+'</select></div>';
|
||||
}).join('');
|
||||
}).catch(function(){rows.innerHTML='<span style="font-size:11px;color:var(--err)">Spoolman nicht erreichbar</span>';});
|
||||
}
|
||||
|
||||
function onAmsSpoolChange(sel){
|
||||
var idx=sel.getAttribute('data-spool-slot');
|
||||
var val=sel.value;
|
||||
if(val) _slotSpoolMap[String(idx)]=parseInt(val);
|
||||
else delete _slotSpoolMap[String(idx)];
|
||||
var mapping={};
|
||||
Object.keys(_slotSpoolMap).forEach(function(k){mapping[k]=_slotSpoolMap[k];});
|
||||
fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',
|
||||
headers:{'Content-Type':'application/json'},body:JSON.stringify({slot_spools:mapping})});
|
||||
}
|
||||
|
||||
function saveSpoolmanSlots(){
|
||||
var mapping={};
|
||||
document.querySelectorAll('#spoolman-slot-rows select[data-spool-slot]').forEach(function(sel){
|
||||
var idx=sel.getAttribute('data-spool-slot');
|
||||
var val=sel.value;
|
||||
if(val)mapping[idx]=parseInt(val);
|
||||
});
|
||||
fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({slot_spools:mapping})}).then(function(r){return r.json();}).then(function(d){
|
||||
_slotSpoolMap=d.slot_spools||{};
|
||||
});
|
||||
}
|
||||
|
||||
function saveVisibleVendors(){
|
||||
var vendors=Object.keys(_vendorChecklistSel);
|
||||
fetch(_apiUrl('/kx/filament/visible_vendors'),{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
@@ -1333,7 +1415,14 @@ function doProfileImportUpload(files){
|
||||
// ── AMS Slot Edit ──
|
||||
var _slotEditIndex=-1;
|
||||
var _slotEditLoaded=false;
|
||||
var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS'];
|
||||
var _MAT_PRESETS=['PLA','PLA+','PLA SILK','PLA MATTE','PETG','ABS','ASA','TPU','PA','PC','HIPS'];
|
||||
function _normalizeMat(m){
|
||||
var s=m.toUpperCase().trim().replace(/-/g,' ').replace(/_/g,' ');
|
||||
var aliases={'PLAPLUS':'PLA+','PLA PLUS':'PLA+','SILK PLA':'PLA SILK','PLASILK':'PLA SILK',
|
||||
'PLA MATTE':'PLA','PLA MARBLE':'PLA','PLA WOOD':'PLA','TPE':'TPU',
|
||||
'PETG PLUS':'PETG+','PA6':'PA','PA12':'PA','PA66':'PA'};
|
||||
return aliases[s]||s;
|
||||
}
|
||||
var _BASE_MATERIAL_TYPES=['PLA','PETG','ABS','ASA','TPU','TPE','PA','PC','HIPS','PEI','PEEK'];
|
||||
function updateSlotEditFeedButton(){
|
||||
var btn=document.getElementById('btn-slot-edit-feed');
|
||||
@@ -1374,10 +1463,21 @@ function _fillSlotProfileDropdown(material, currentVendor, currentName){
|
||||
_loadOrcaFilaments(function(profiles){
|
||||
// Type-Filter: nur Profile vom passenden material zeigen (z.B. PLA → alle PLA-Varianten)
|
||||
var matU=(material||'').toUpperCase().trim();
|
||||
// PLA-Varianten: Drucker meldet "PLA SILK"/"PLA+"/"PLA MATTE", OrcaSlicer
|
||||
// speichert alle unter type=PLA — Namens-Keyword als Zusatzfilter.
|
||||
var _PLA_VARIANT_KW={'PLA SILK':'silk','PLA+':'pla+','PLA MATTE':'matte',
|
||||
'PLA MARBLE':'marble','PLA WOOD':'wood'};
|
||||
var variantKw=_PLA_VARIANT_KW[matU]||null;
|
||||
var baseMat=variantKw?'PLA':matU;
|
||||
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+' ');
|
||||
var nameL=(p.name||'').toLowerCase();
|
||||
// Basis-Typ muss passen
|
||||
var typeOk=baseMat===''||pt===baseMat||pt.startsWith(baseMat+'-')||pt.startsWith(baseMat+' ');
|
||||
if(!typeOk) return false;
|
||||
// Bei Variante: Name muss Keyword enthalten (z.B. "silk", "matte", "pla+")
|
||||
if(variantKw) return nameL.indexOf(variantKw)!==-1;
|
||||
return true;
|
||||
});
|
||||
sel.innerHTML='<option value="">'+tr('slot_edit_profile_default')+'</option>';
|
||||
// User-Profile (is_user) zuerst — eigene Optgroup '★ Eigene' an erster Stelle.
|
||||
@@ -1419,6 +1519,110 @@ function _fillSlotProfileDropdown(material, currentVendor, currentName){
|
||||
});
|
||||
});
|
||||
}
|
||||
// ── Pickr color picker ──────────────────────────────────────────────────────
|
||||
var _pickr=null;
|
||||
|
||||
function _initPickr(hex){
|
||||
// destroy previous instance if exists
|
||||
if(_pickr){ try{ _pickr.destroyAndRemove(); }catch(e){} _pickr=null; }
|
||||
var anchor=document.getElementById('slot-pickr-anchor');
|
||||
if(!anchor||typeof Pickr==='undefined') return;
|
||||
// fresh button element so Pickr can mount
|
||||
anchor.innerHTML='<div id="slot-pickr-btn"></div>';
|
||||
_pickr=Pickr.create({
|
||||
el:'#slot-pickr-btn',
|
||||
theme:'nano',
|
||||
default: hex||'#808080',
|
||||
inline: true,
|
||||
showAlways: true,
|
||||
components:{
|
||||
preview:true, opacity:false, hue:true,
|
||||
interaction:{ hex:true, rgba:false, input:true, save:false, clear:false }
|
||||
}
|
||||
});
|
||||
_pickr.on('change',function(color){
|
||||
var h=color.toHEXA().toString().slice(0,7);
|
||||
document.getElementById('slot-edit-color').value=h;
|
||||
document.getElementById('slot-edit-preview').style.background=h;
|
||||
});
|
||||
// Theme anpassen: Pickr benutzt eigene CSS-Variablen, wir überschreiben via style
|
||||
requestAnimationFrame(function(){
|
||||
var el=anchor.querySelector('.pickr');
|
||||
if(el) el.style.cssText='width:100%';
|
||||
var app=anchor.querySelector('.pcr-app');
|
||||
if(app){
|
||||
app.style.cssText='position:relative;width:100%;box-shadow:none;background:transparent';
|
||||
var btn=app.querySelector('.pcr-result');
|
||||
if(btn) btn.style.cssText='background:var(--raised);border:1px solid var(--border);color:var(--txt);border-radius:6px;font-size:12px';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Color swatches (localStorage, max 16) ──────────────────────────────────
|
||||
var _SWATCH_KEY='kxb_color_swatches';
|
||||
var _SWATCH_MAX=16;
|
||||
|
||||
function _loadSwatches(){
|
||||
try{ return JSON.parse(localStorage.getItem(_SWATCH_KEY)||'[]'); }catch(e){ return []; }
|
||||
}
|
||||
function _saveSwatches(arr){ try{ localStorage.setItem(_SWATCH_KEY, JSON.stringify(arr)); }catch(e){} }
|
||||
|
||||
function _addSwatch(hex){
|
||||
var arr=_loadSwatches().filter(function(c){ return c.toLowerCase()!==hex.toLowerCase(); });
|
||||
arr.unshift(hex);
|
||||
if(arr.length>_SWATCH_MAX) arr=arr.slice(0,_SWATCH_MAX);
|
||||
_saveSwatches(arr);
|
||||
}
|
||||
|
||||
function _renderSwatches(){
|
||||
var el=document.getElementById('slot-color-swatches');
|
||||
if(!el) return;
|
||||
var arr=_loadSwatches();
|
||||
if(!arr.length){ el.style.display='none'; return; }
|
||||
el.style.display='flex';
|
||||
el.innerHTML=arr.map(function(c){
|
||||
return '<div title="'+c+'" onclick="slotPickSwatch(\''+c+'\')" style="width:22px;height:22px;border-radius:4px;background:'+c+
|
||||
';border:2px solid rgba(255,255,255,.2);cursor:pointer;flex-shrink:0"></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function slotPickSwatch(hex){
|
||||
if(_pickr){ _pickr.setColor(hex); }
|
||||
var ci=document.getElementById('slot-edit-color');
|
||||
if(ci) ci.value=hex;
|
||||
document.getElementById('slot-edit-preview').style.background=hex;
|
||||
}
|
||||
|
||||
// ── Copy color from other slot ──────────────────────────────────────────────
|
||||
function _renderCopyFromSlot(currentGlobalIdx){
|
||||
var slots=(window._amsSlots||[]).filter(function(s){
|
||||
return s.global_index!==currentGlobalIdx && s.status==5 && Array.isArray(s.color);
|
||||
});
|
||||
var row=document.getElementById('slot-copy-row');
|
||||
var sel=document.getElementById('slot-copy-select');
|
||||
if(!row||!sel) return;
|
||||
if(!slots.length){ row.style.display='none'; return; }
|
||||
row.style.display='';
|
||||
var ph=document.getElementById('lbl-slot-copy-from');
|
||||
var phTxt=ph?ph.textContent:(T.slot_copy_from||'Copy color from slot…');
|
||||
sel.innerHTML='<option value="">'+phTxt+'</option>'+slots.map(function(s){
|
||||
var rgb=s.color;
|
||||
var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join('');
|
||||
return '<option value="'+hex+'">Slot '+(s.global_index+1)+' — '+(s.type||'?')+' '+hex+'</option>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function slotCopyColor(sel){
|
||||
if(!sel.value) return;
|
||||
var ci=document.getElementById('slot-edit-color');
|
||||
if(!ci) return;
|
||||
ci.value=sel.value;
|
||||
document.getElementById('slot-edit-preview').style.background=sel.value;
|
||||
sel.selectedIndex=0;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function openSlotEdit(i){
|
||||
var slot=(window._amsSlots||[])[i]||{};
|
||||
var globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i);
|
||||
@@ -1430,13 +1634,19 @@ function openSlotEdit(i){
|
||||
var ci=document.getElementById('slot-edit-color');
|
||||
ci.value=hex;
|
||||
document.getElementById('slot-edit-preview').style.background=hex;
|
||||
_initPickr(hex);
|
||||
_renderSwatches();
|
||||
_renderCopyFromSlot(globalIdx);
|
||||
var mat=(slot.type||'PLA').toUpperCase();
|
||||
document.getElementById('slot-edit-mat').value=mat;
|
||||
// Normalisieren für Button-Highlighting: PLA-Varianten auf nächsten Preset mappen
|
||||
var matNorm=_normalizeMat(mat);
|
||||
var btns=document.getElementById('slot-mat-btns');
|
||||
btns.innerHTML=_MAT_PRESETS.map(function(m){
|
||||
var active=m===mat||m===matNorm;
|
||||
return '<button class="mat-preset-btn" data-mat="'+m+'" onclick="selectMatPreset(\''+m+'\')" '
|
||||
+'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>';
|
||||
+(active?'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).
|
||||
@@ -1554,6 +1764,7 @@ function hexToRgb(hex){
|
||||
}
|
||||
function saveSlotEdit(){
|
||||
var hex=document.getElementById('slot-edit-color').value;
|
||||
_addSwatch(hex);
|
||||
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
|
||||
var color=hexToRgb(hex);
|
||||
var slotIdx=_slotEditIndex;
|
||||
@@ -1642,8 +1853,9 @@ function saveSettings(){
|
||||
device_id: document.getElementById('s-device-id').value,
|
||||
mode_id: document.getElementById('s-mode-id').value,
|
||||
default_ams_slot: document.getElementById('s-default-slot').value,
|
||||
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
|
||||
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
|
||||
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
|
||||
vibration_compensation: (document.getElementById('s-vibration-compensation')||{}).checked?1:0,
|
||||
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
|
||||
print_start_dialog: parseInt((document.getElementById('s-file-ready-mode')||{}).value||'1',10),
|
||||
web_upload_warning:webUploadWarning,
|
||||
poll_interval: Math.min(60,Math.max(1,parseInt((document.getElementById('s-poll-interval')||{}).value,10)||3)),
|
||||
@@ -1791,7 +2003,15 @@ function setStep(btn,v){
|
||||
currentStep=v;
|
||||
document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById('step-display').textContent=v;
|
||||
var ci=document.getElementById('step-custom');
|
||||
if(ci)ci.value='';
|
||||
}
|
||||
function setStepCustom(inp){
|
||||
var v=parseFloat(inp.value);
|
||||
if(!isNaN(v)&&v>0){
|
||||
currentStep=v;
|
||||
document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active'));
|
||||
}
|
||||
}
|
||||
function move(axis,dir,dist){
|
||||
// axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z
|
||||
@@ -2486,7 +2706,20 @@ function confirmStoreWebVerify(){
|
||||
});
|
||||
}
|
||||
|
||||
function startReadyFileWithSlots(filename,_autoOpen){
|
||||
function _showMismatchWarn(mismatches){
|
||||
var el=document.getElementById('fd-mismatch-warn');
|
||||
if(!el)return;
|
||||
if(!mismatches||!mismatches.length){el.style.display='none';el.innerHTML='';return;}
|
||||
var lines=mismatches.map(function(m){
|
||||
var slot='Slot '+(m.slot_index+1);
|
||||
if(m.reason==='empty')
|
||||
return '⚠ '+slot+': GCode needs '+m.gcode_material+' — slot is empty';
|
||||
return '⚠ '+slot+': GCode needs '+m.gcode_material+', loaded: '+(m.ams_material||'?');
|
||||
});
|
||||
el.innerHTML='<strong>Filament mismatch detected</strong><br>'+lines.join('<br>');
|
||||
el.style.display='';
|
||||
}
|
||||
function startReadyFileWithSlots(filename,_autoOpen,_mismatch){
|
||||
if(!_autoOpen) _fdAutoOpenedFile=null; // manueller Aufruf → Auto-Open-Sperre aufheben
|
||||
var fn=filename||S.file_ready;
|
||||
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
|
||||
@@ -2506,9 +2739,11 @@ function startReadyFileWithSlots(filename,_autoOpen){
|
||||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
|
||||
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
|
||||
openFilamentDialog(d.result||[]);
|
||||
_showMismatchWarn(_mismatch||null);
|
||||
}).catch(function(){
|
||||
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
|
||||
openFilamentDialog([]);
|
||||
_showMismatchWarn(_mismatch||null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2744,6 +2979,7 @@ function openFilamentDialog(slots){
|
||||
function closeFilamentDialog(){
|
||||
var dlg=document.getElementById('filament-dialog');
|
||||
if(dlg)dlg.classList.remove('open');
|
||||
_showMismatchWarn(null);
|
||||
_fdDialogOpen=false;
|
||||
if(_fdAutoOpenedFile){
|
||||
_fdUserCancelled=true;
|
||||
@@ -3142,7 +3378,7 @@ function loadPrinterTab(){
|
||||
'<div style="font-size:12px;color:var(--txt2);display:flex;gap:12px">'+
|
||||
'<span>🌡 '+nt+'°C</span><span>🛏 '+bt+'°C</span>'+
|
||||
'</div>'+
|
||||
(!isActive?'<a href="/printer'+printerNum+'" style="display:block;text-align:center;padding:7px;background:var(--accent);color:#fff;border-radius:7px;font-size:13px;font-weight:600;text-decoration:none;margin-top:4px">'+T.printers_switch+'</a>':'<div style="text-align:center;padding:7px;font-size:12px;color:var(--txt2)">'+T.printers_current+'</div>')+
|
||||
(!isActive?'<a href="'+url+'/printer'+printerNum+'" style="display:block;text-align:center;padding:7px;background:var(--accent);color:#fff;border-radius:7px;font-size:13px;font-weight:600;text-decoration:none;margin-top:4px">'+T.printers_switch+'</a>':'<div style="text-align:center;padding:7px;font-size:12px;color:var(--txt2)">'+T.printers_current+'</div>')+
|
||||
'</div>';
|
||||
}).join('');
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>KX-Bridge</title>
|
||||
<link rel="stylesheet" href="/kx/ui/style.css">
|
||||
<link rel="stylesheet" href="/kx/ui/lib/pickr-nano.min.css">
|
||||
<script src="/kx/ui/lib/pickr.min.js"></script>
|
||||
<body>
|
||||
|
||||
<div id="conn-error-banner" style="display:none;background:#c0392b;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:999;"></div>
|
||||
@@ -32,17 +34,6 @@
|
||||
<span id="h-version" style="font-size:11px;opacity:.5;margin-left:6px"></span>
|
||||
<div class="hbadge" id="h-badge"><span class="dot"></span><span id="h-state">Standby</span></div>
|
||||
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<span aria-hidden="true" style="font-size:15px;line-height:1;opacity:.85">🌐</span>
|
||||
<select class="theme-btn" id="lang-select" onchange="setLanguageFromSelect()" style="padding:6px 10px">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Espanol</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="theme-btn" onclick="showPanel('settings')" id="settings-btn" title="Einstellungen">⚙</button>
|
||||
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
|
||||
</header>
|
||||
@@ -57,15 +48,26 @@
|
||||
<span class="modal-title" id="slot-edit-title"></span>
|
||||
<button onclick="closeSlotEdit()" style="background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer;line-height:1">✕</button>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
|
||||
<div id="slot-edit-preview" style="width:56px;height:56px;border-radius:50%;border:3px solid rgba(255,255,255,.2);flex-shrink:0"></div>
|
||||
<div style="flex:1">
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:4px" id="lbl-slot-color"></div>
|
||||
<input type="color" id="slot-edit-color"
|
||||
oninput="document.getElementById('slot-edit-preview').style.background=this.value"
|
||||
style="width:100%;height:36px;border:1px solid var(--border);border-radius:6px;background:var(--raised);cursor:pointer;padding:2px">
|
||||
<div style="display:flex;align-items:flex-start;gap:16px;margin-bottom:12px">
|
||||
<div id="slot-edit-preview" style="width:56px;height:56px;border-radius:50%;border:3px solid rgba(255,255,255,.2);flex-shrink:0;margin-top:4px"></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-color"></div>
|
||||
<!-- Pickr anchor — JS mounts the picker here -->
|
||||
<div id="slot-pickr-anchor"></div>
|
||||
<!-- hidden input keeps the hex value for saveSlotEdit() -->
|
||||
<input type="hidden" id="slot-edit-color">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Recent color swatches (max 16, localStorage) -->
|
||||
<div id="slot-color-swatches" style="display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px"></div>
|
||||
<!-- Copy from slot -->
|
||||
<div id="slot-copy-row" style="display:none;margin-bottom:16px">
|
||||
<select id="slot-copy-select"
|
||||
style="width:100%;padding:5px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;box-sizing:border-box"
|
||||
onchange="slotCopyColor(this)">
|
||||
<option value="" id="lbl-slot-copy-from">Copy color from slot…</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-material"></div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px" id="slot-mat-btns">
|
||||
@@ -252,39 +254,51 @@
|
||||
|
||||
<!-- Achsensteuerung -->
|
||||
<div class="card">
|
||||
<div class="card-title"><span>✛</span> <span id="ptitle-motion-xy">XY-Achsen</span></div>
|
||||
<div class="joypad">
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,1,getStep())" title="Y+">▲</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(0,-1,getStep())" title="X−">◀</button>
|
||||
<button class="joy home" onclick="homeAll()" title="Home All">⌂</button>
|
||||
<button class="joy" onclick="move(0,1,getStep())" title="X+">▶</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,-1,getStep())" title="Y−">▼</button>
|
||||
<div></div>
|
||||
<div class="card-title"><span>✛</span> <span id="ptitle-motion-xy">Achsensteuerung</span></div>
|
||||
<div style="display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap">
|
||||
<!-- XY -->
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
|
||||
<div class="joypad">
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,1,getStep())" title="Y+">▲</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(0,-1,getStep())" title="X−">◀</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(0,1,getStep())" title="X+">▶</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,-1,getStep())" title="Y−">▼</button>
|
||||
<div></div>
|
||||
</div>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt);width:100%" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
|
||||
</div>
|
||||
<!-- Z -->
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
|
||||
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
|
||||
<button class="joy" onclick="move(2,1,getStep())" title="Z+">▲</button>
|
||||
<button class="joy" onclick="move(2,-1,getStep())" title="Z−">▼</button>
|
||||
</div>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt);width:100%" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-btns">
|
||||
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
|
||||
<button class="step-btn active" onclick="setStep(this,1)">1</button>
|
||||
<button class="step-btn" onclick="setStep(this,5)">5</button>
|
||||
<button class="step-btn" onclick="setStep(this,10)">10 mm</button>
|
||||
<!-- Einheitliche Step-Size -->
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-top:10px;flex-wrap:wrap">
|
||||
<div class="step-btns" style="margin-top:0">
|
||||
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
|
||||
<button class="step-btn active" onclick="setStep(this,1)">1</button>
|
||||
<button class="step-btn" onclick="setStep(this,5)">5</button>
|
||||
<button class="step-btn" onclick="setStep(this,10)">10</button>
|
||||
</div>
|
||||
<input id="step-custom" type="number" min="0.1" max="260" step="0.1"
|
||||
placeholder="mm"
|
||||
style="width:60px;padding:4px 6px;font-size:12px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);text-align:center"
|
||||
oninput="setStepCustom(this)">
|
||||
</div>
|
||||
<div class="home-btns">
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
|
||||
<!-- Globale Befehle zentriert -->
|
||||
<div style="display:flex;justify-content:center;gap:8px;margin-top:10px">
|
||||
<button class="btn btn-sm btn-accent" onclick="homeAll()"><span class="lbl-home-all">Home All</span></button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="disableMotors()"><span class="lbl-disable-motors">Motors Off</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span>↕</span> <span id="ptitle-motion-z">Z-Achse</span></div>
|
||||
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
|
||||
<button class="joy" onclick="move(2,1,getStep())" title="Z+">▲</button>
|
||||
<button class="joy" onclick="move(2,-1,getStep())" title="Z−">▼</button>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)"><span class="lbl-step">Schrittweite:</span> <span id="step-display">1</span> mm</div>
|
||||
</div>
|
||||
|
||||
<!-- Print Speed -->
|
||||
<div class="card">
|
||||
@@ -495,6 +509,10 @@
|
||||
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
|
||||
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-vibration-compensation" style="width:auto;margin:0">
|
||||
<label id="lbl-vibration-compensation" style="margin:0;cursor:pointer" for="s-vibration-compensation">Resonance compensation before print</label>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-file-ready-mode">Nach Upload: Druckstart-Verhalten</label>
|
||||
<select id="s-file-ready-mode">
|
||||
@@ -570,6 +588,12 @@
|
||||
<div id="visible-vendors-list" style="max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveVisibleVendors()"><span id="lbl-visible-vendors-save">Auswahl speichern</span></button>
|
||||
</div>
|
||||
<div class="card" id="spoolman-slot-card" style="display:none">
|
||||
<div class="card-title"><span>🧵</span> <span id="lbl-spoolman-slot-assign">Spoolman — Slot-Zuordnung</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:10px" id="lbl-spoolman-slot-hint">Spoolman-Spool pro AMS-Slot zuweisen. Der Verbrauch wird beim Druck automatisch gemeldet.</div>
|
||||
<div id="spoolman-slot-rows" style="display:flex;flex-direction:column;gap:8px"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:10px" onclick="saveSpoolmanSlots()"><span id="lbl-spoolman-slot-save">Zuordnung speichern</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integrationen -->
|
||||
@@ -582,7 +606,7 @@
|
||||
<input type="text" id="s-spoolman-url" placeholder="http://spoolman:7912" style="width:200px">
|
||||
</div>
|
||||
<div class="set-row">
|
||||
<label id="lbl-spoolman-sync-rate">Sync-Rate (s, 0=aus)</label>
|
||||
<label id="lbl-spoolman-sync-rate">Sync-Rate (s, 0=Druckende)</label>
|
||||
<input type="number" id="s-spoolman-sync-rate" min="0" max="3600" value="30" style="width:80px">
|
||||
</div>
|
||||
<div id="spoolman-status-row" style="margin-top:6px;font-size:12px;color:var(--txt2)">
|
||||
@@ -655,6 +679,7 @@
|
||||
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
|
||||
<div id="fd-mismatch-warn" style="display:none;margin-bottom:10px;padding:8px 10px;background:rgba(255,160,0,.12);border:1px solid rgba(255,160,0,.4);border-radius:8px;font-size:11px;color:#ffa000"></div>
|
||||
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
|
||||
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
|
||||
<button type="button" id="fd-objects-toggle" onclick="toggleFdObjects()"
|
||||
|
||||
2
web/themes/default/lib/pickr-nano.min.css
vendored
Normal file
2
web/themes/default/lib/pickr-nano.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
web/themes/default/lib/pickr.min.js
vendored
Normal file
3
web/themes/default/lib/pickr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -15,10 +15,46 @@
|
||||
body{background:var(--bg);color:var(--txt);font-family:var(--font);font-size:14px;min-height:100vh;display:flex;flex-direction:column}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
/* select/option-Farben explizit setzen — OrcaSlicers Device-Tab-Webview erbt
|
||||
sie sonst nicht und rendert weiße Schrift auf weißem Grund (Issue #29). */
|
||||
select{background:var(--raised)!important;color:var(--txt)!important}
|
||||
sie sonst nicht und rendert weiße Schrift auf weißem Grund (Issue #29).
|
||||
Einheitliches Styling für alle Dropdowns im gesamten UI. */
|
||||
select{
|
||||
background:var(--raised)!important;
|
||||
color:var(--txt)!important;
|
||||
border:1px solid var(--border)!important;
|
||||
border-radius:8px!important;
|
||||
padding:6px 10px!important;
|
||||
font-size:13px!important;
|
||||
appearance:none!important;
|
||||
-webkit-appearance:none!important;
|
||||
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23888' d='M6 8L0 0h12z'/%3E%3C/svg%3E")!important;
|
||||
background-repeat:no-repeat!important;
|
||||
background-position:right 10px center!important;
|
||||
padding-right:28px!important;
|
||||
cursor:pointer!important;
|
||||
outline:none!important;
|
||||
box-sizing:border-box!important;
|
||||
}
|
||||
select:focus{border-color:var(--accent)!important;box-shadow:0 0 0 2px rgba(0,200,255,0.18)!important}
|
||||
select option{background:var(--card)!important;color:var(--txt)!important}
|
||||
|
||||
/* Einheitliches Styling für Text/Number-Inputs */
|
||||
input[type=text],input[type=number],input[type=url],input[type=password],input[type=email],input[type=search]{
|
||||
background:var(--raised);
|
||||
color:var(--txt);
|
||||
border:1px solid var(--border);
|
||||
border-radius:8px;
|
||||
padding:6px 10px;
|
||||
font-size:13px;
|
||||
outline:none;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
input[type=text]:focus,input[type=number]:focus,input[type=url]:focus,
|
||||
input[type=password]:focus,input[type=email]:focus,input[type=search]:focus{
|
||||
border-color:var(--accent);
|
||||
box-shadow:0 0 0 2px rgba(0,200,255,0.18);
|
||||
}
|
||||
input::placeholder{color:var(--txt2);opacity:1}
|
||||
|
||||
/* ── HEADER ── */
|
||||
header{background:var(--card);border-bottom:1px solid var(--border);
|
||||
display:flex;align-items:center;gap:12px;padding:0 20px;height:52px;
|
||||
@@ -265,6 +301,8 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
|
||||
.modal-field input{background:var(--raised);border:1px solid var(--border);
|
||||
border-radius:7px;color:var(--txt);padding:7px 10px;font-size:13px;width:100%}
|
||||
.modal-field input:focus{outline:none;border-color:var(--accent)}
|
||||
.set-row{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}
|
||||
.set-row label{font-size:12px;color:var(--txt2)}
|
||||
.poll-btns{display:flex;gap:8px}
|
||||
.poll-btn{flex:1;padding:7px;background:var(--raised);border:1px solid var(--border);
|
||||
border-radius:7px;color:var(--txt2);cursor:pointer;font-size:13px;transition:all .15s}
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"lbl_light": "💡 Licht",
|
||||
"lbl_remaining": "Restzeit:",
|
||||
"lbl_slicer_time": "Slicer-Schätzung:",
|
||||
"lbl_spoolman_sync_rate": "Sync-Rate (s, 0=aus)",
|
||||
"lbl_spoolman_sync_rate": "Sync-Rate (s, 0=Druckende)",
|
||||
"lbl_spoolman_url": "Server-URL",
|
||||
"lbl_unload": "Ausziehen",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Kamera",
|
||||
"panel_extras_fan": "Lüfter",
|
||||
"panel_extras_light": "Licht",
|
||||
"panel_motion_xy": "XY-Achsen",
|
||||
"panel_motion_z": "Z-Achse",
|
||||
"panel_motion_xy": "Achsensteuerung",
|
||||
"panel_print_btn_cancel": "✕ Abbrechen",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Fortsetzen",
|
||||
@@ -213,6 +212,7 @@
|
||||
"progress_action_slots": "Slots zuordnen",
|
||||
"settings_auto_leveling": "Auto-Leveling vor Druck",
|
||||
"settings_auto_leveling_label": "Auto-Leveling vor dem Druck",
|
||||
"settings_vibration_compensation": "Resonanzkompensation",
|
||||
"settings_btn_tooltip": "Einstellungen",
|
||||
"settings_camera_on_print": "Kamera bei Druckstart einschalten",
|
||||
"settings_cat_connection": "Verbindung",
|
||||
@@ -325,5 +325,6 @@
|
||||
"update_docker_copied": "Kopiert! Ausführen: docker compose pull && docker compose up -d",
|
||||
"update_error": "Fehler",
|
||||
"update_none": "Bereits aktuell",
|
||||
"update_restarting": "Starte neu..."
|
||||
}
|
||||
"update_restarting": "Starte neu...",
|
||||
"slot_copy_from": "Farbe von Slot kopieren…"
|
||||
}
|
||||
@@ -123,7 +123,7 @@
|
||||
"lbl_light": "💡 Light",
|
||||
"lbl_remaining": "Remaining:",
|
||||
"lbl_slicer_time": "Slicer estimate:",
|
||||
"lbl_spoolman_sync_rate": "Sync rate (s, 0=off)",
|
||||
"lbl_spoolman_sync_rate": "Sync rate (s, 0=end of print)",
|
||||
"lbl_spoolman_url": "Server URL",
|
||||
"lbl_unload": "Unload",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Camera",
|
||||
"panel_extras_fan": "Fan",
|
||||
"panel_extras_light": "Light",
|
||||
"panel_motion_xy": "XY Axes",
|
||||
"panel_motion_z": "Z Axis",
|
||||
"panel_motion_xy": "Axes Control",
|
||||
"panel_print_btn_cancel": "✕ Cancel",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Resume",
|
||||
@@ -213,6 +212,7 @@
|
||||
"progress_action_slots": "Map slots",
|
||||
"settings_auto_leveling": "Auto-Leveling Default",
|
||||
"settings_auto_leveling_label": "Auto-Leveling before print",
|
||||
"settings_vibration_compensation": "Resonance Compensation",
|
||||
"settings_btn_tooltip": "Settings",
|
||||
"settings_camera_on_print": "Turn camera on at print start",
|
||||
"settings_cat_connection": "Connection",
|
||||
@@ -325,5 +325,6 @@
|
||||
"update_docker_copied": "Copied! Run: docker compose pull && docker compose up -d",
|
||||
"update_error": "Error",
|
||||
"update_none": "Already up to date",
|
||||
"update_restarting": "Restarting..."
|
||||
}
|
||||
"update_restarting": "Restarting...",
|
||||
"slot_copy_from": "Copy color from slot…"
|
||||
}
|
||||
@@ -123,7 +123,7 @@
|
||||
"lbl_light": "💡 Luz",
|
||||
"lbl_remaining": "Restante:",
|
||||
"lbl_slicer_time": "Estimación del slicer:",
|
||||
"lbl_spoolman_sync_rate": "Tasa de sincronización (s, 0=desact.)",
|
||||
"lbl_spoolman_sync_rate": "Tasa de sincronización (s, 0=fin impresión)",
|
||||
"lbl_spoolman_url": "URL del servidor",
|
||||
"lbl_unload": "Descargar",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Cámara",
|
||||
"panel_extras_fan": "Ventilador",
|
||||
"panel_extras_light": "Luz",
|
||||
"panel_motion_xy": "Ejes XY",
|
||||
"panel_motion_z": "Eje Z",
|
||||
"panel_motion_xy": "Control de Ejes",
|
||||
"panel_print_btn_cancel": "✕ Cancelar",
|
||||
"panel_print_btn_pause": "⏸ Pausa",
|
||||
"panel_print_btn_resume": "▶ Reanudar",
|
||||
@@ -213,6 +212,7 @@
|
||||
"progress_action_slots": "Asignar ranuras",
|
||||
"settings_auto_leveling": "Autonivelado antes de imprimir",
|
||||
"settings_auto_leveling_label": "Autonivelado antes de imprimir",
|
||||
"settings_vibration_compensation": "Compensación de resonancia",
|
||||
"settings_btn_tooltip": "Ajustes",
|
||||
"settings_camera_on_print": "Encender cámara al iniciar impresión",
|
||||
"settings_cat_connection": "Conexión",
|
||||
@@ -325,5 +325,6 @@
|
||||
"update_docker_copied": "Copiado. Ejecutar: docker compose pull && docker compose up -d",
|
||||
"update_error": "Error",
|
||||
"update_none": "Ya actualizado",
|
||||
"update_restarting": "Reiniciando..."
|
||||
}
|
||||
"update_restarting": "Reiniciando...",
|
||||
"slot_copy_from": "Copiar color del slot…"
|
||||
}
|
||||
@@ -123,7 +123,7 @@
|
||||
"lbl_light": "💡 Lumière",
|
||||
"lbl_remaining": "Restant :",
|
||||
"lbl_slicer_time": "Estimation slicer :",
|
||||
"lbl_spoolman_sync_rate": "Taux de sync. (s, 0=désact.)",
|
||||
"lbl_spoolman_sync_rate": "Taux de sync. (s, 0=fin impression)",
|
||||
"lbl_spoolman_url": "URL du serveur",
|
||||
"lbl_unload": "Décharger",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Caméra",
|
||||
"panel_extras_fan": "Ventilateur",
|
||||
"panel_extras_light": "Lumière",
|
||||
"panel_motion_xy": "Axes XY",
|
||||
"panel_motion_z": "Axe Z",
|
||||
"panel_motion_xy": "Contrôle des Axes",
|
||||
"panel_print_btn_cancel": "✕ Annuler",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Reprendre",
|
||||
@@ -325,5 +324,6 @@
|
||||
"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…"
|
||||
}
|
||||
"update_restarting": "Redémarrage…",
|
||||
"slot_copy_from": "Copier la couleur du slot…"
|
||||
}
|
||||
@@ -123,7 +123,7 @@
|
||||
"lbl_light": "💡 Luce",
|
||||
"lbl_remaining": "Rimanente:",
|
||||
"lbl_slicer_time": "Stima slicer:",
|
||||
"lbl_spoolman_sync_rate": "Frequenza sync (s, 0=disatt.)",
|
||||
"lbl_spoolman_sync_rate": "Frequenza sync (s, 0=fine stampa)",
|
||||
"lbl_spoolman_url": "URL server",
|
||||
"lbl_unload": "Rimuovi",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Camera",
|
||||
"panel_extras_fan": "Ventola",
|
||||
"panel_extras_light": "Luce",
|
||||
"panel_motion_xy": "Assi XY",
|
||||
"panel_motion_z": "Asse Z",
|
||||
"panel_motion_xy": "Controllo Assi",
|
||||
"panel_print_btn_cancel": "✕ Annulla",
|
||||
"panel_print_btn_pause": "⏸ Pausa",
|
||||
"panel_print_btn_resume": "▶ Riprendi",
|
||||
@@ -325,5 +324,6 @@
|
||||
"update_docker_copied": "Copiato! Eseguire: docker compose pull && docker compose up -d",
|
||||
"update_error": "Errore",
|
||||
"update_none": "Già aggiornato",
|
||||
"update_restarting": "Riavvio in corso..."
|
||||
}
|
||||
"update_restarting": "Riavvio in corso...",
|
||||
"slot_copy_from": "Copia colore dallo slot…"
|
||||
}
|
||||
@@ -123,7 +123,7 @@
|
||||
"lbl_light": "💡 灯光",
|
||||
"lbl_remaining": "剩余时间:",
|
||||
"lbl_slicer_time": "切片预估:",
|
||||
"lbl_spoolman_sync_rate": "同步频率(秒,0=关闭)",
|
||||
"lbl_spoolman_sync_rate": "同步频率(秒,0=打印结束)",
|
||||
"lbl_spoolman_url": "服务器地址",
|
||||
"lbl_unload": "退料",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "相机",
|
||||
"panel_extras_fan": "风扇",
|
||||
"panel_extras_light": "灯光",
|
||||
"panel_motion_xy": "XY 轴",
|
||||
"panel_motion_z": "Z 轴",
|
||||
"panel_motion_xy": "轴控制",
|
||||
"panel_print_btn_cancel": "✕ 取消",
|
||||
"panel_print_btn_pause": "⏸ 暂停",
|
||||
"panel_print_btn_resume": "▶ 继续",
|
||||
@@ -213,6 +212,7 @@
|
||||
"progress_action_slots": "分配槽位",
|
||||
"settings_auto_leveling": "打印前自动调平",
|
||||
"settings_auto_leveling_label": "打印前自动调平",
|
||||
"settings_vibration_compensation": "打印前共振补偿",
|
||||
"settings_btn_tooltip": "设置",
|
||||
"settings_camera_on_print": "打印开始时开启相机",
|
||||
"settings_cat_connection": "连接",
|
||||
@@ -325,5 +325,6 @@
|
||||
"update_docker_copied": "已复制!执行:docker compose pull && docker compose up -d",
|
||||
"update_error": "错误",
|
||||
"update_none": "已是最新版本",
|
||||
"update_restarting": "重启中..."
|
||||
}
|
||||
"update_restarting": "重启中...",
|
||||
"slot_copy_from": "从插槽复制颜色…"
|
||||
}
|
||||
Reference in New Issue
Block a user