Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40a27a47fc | |||
| 7815c66a82 |
@@ -1,5 +1,51 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.9.16] – 2026-05-22
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- **Kamera bei Druckstart automatisch einschalten:** neue Einstellung „Kamera bei
|
||||||
|
Druckstart einschalten" — die Bridge startet den Kamera-Stream automatisch, wenn
|
||||||
|
ein Druck beginnt (für OrcaSlicer und die Bridge-UI).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Einfarbiger Druck durch leeren AMS-Slot blockiert:** OrcaSlicer schreibt alle
|
||||||
|
konfigurierten Filamente in den GCode-Header, auch wenn das Modell nur eines
|
||||||
|
nutzt — die Bridge meldete dem Drucker dadurch alle Farben als nötig, und ein
|
||||||
|
leerer ungenutzter Slot brach den Druck ab. Die Bridge mappt jetzt nur die im
|
||||||
|
GCode tatsächlich genutzten Filamente.
|
||||||
|
- **Filament-Sync jetzt positionstreu:** Bei einem leeren Slot in der Mitte
|
||||||
|
(z.B. Slot 1 gelb, 2 leer, 3 rot, 4 weiß) zeigte OrcaSlicer die Farben auf den
|
||||||
|
falschen Slots. Behoben — leere Slots behalten ihre Position, und das
|
||||||
|
Sync-Farbformat folgt der Happy-Hare-Konvention (RRGGBB ohne `#`).
|
||||||
|
- **Slicer-Zeit + Thumbnail fehlten nach Browser-Reload** (oder bei Druckstart
|
||||||
|
direkt aus OrcaSlicer): beide werden jetzt aus dem GCode-Store anhand des
|
||||||
|
Dateinamens wiederhergestellt statt aus flüchtigem State.
|
||||||
|
- **Deutsche Übersetzungslücken** im ACE-Trockner-Dialog behoben.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Wiederholte Log-Zeilen werden als Zähler („×N") zusammengefasst statt zu spammen;
|
||||||
|
Status-Poll-Verkehr wird nicht mehr auf INFO geloggt.
|
||||||
|
- Neuer Level-Filter (Alle / Fehler / Warnungen), Toast bei neuen Fehlern, volle
|
||||||
|
Tracebacks im Browser-Log und ein Download-Dateiname mit Zeitstempel.
|
||||||
|
|
||||||
|
## [0.9.15] – 2026-05-21
|
||||||
|
|
||||||
|
### Fixes (Issue #29)
|
||||||
|
- **UI im OrcaSlicer-Device-Tab kaputt:** OrcaSlicers eingebetteter Webview lädt
|
||||||
|
nur das nackte HTML und ignoriert externe `<script>`/`<link>`-Tags — nach der
|
||||||
|
v0.9.14-Theme-Auslagerung funktionierte dort kein Button mehr. Die Bridge
|
||||||
|
bettet CSS + JS jetzt inline in die Seite ein — funktioniert in Browser UND
|
||||||
|
OrcaSlicer-Webview.
|
||||||
|
- **Dropdowns unlesbar (weiß auf weiß) im OrcaSlicer-Webview:** `color-scheme` +
|
||||||
|
explizite `select`/`option`-Farben ergänzt, damit die nativen Dropdowns in
|
||||||
|
Hell- und Dunkel-Theme korrekt dargestellt werden.
|
||||||
|
- **„Select slots"-Button tat direkt nach Upload nichts:** eine fehlende
|
||||||
|
Variablen-Deklaration (`storeFiles`) warf einen `ReferenceError`, wenn vor dem
|
||||||
|
Laden des Browser-Tabs geklickt wurde. Behoben.
|
||||||
|
- **Upload-Banner kam nach abgeschlossenem Druck zurück:** der „file ready"-Status
|
||||||
|
wurde nur bei Stop/Abbruch geleert, nicht bei `finished`. Jetzt auch nach
|
||||||
|
erfolgreichem Druckende geleert.
|
||||||
|
|
||||||
## [0.9.14] – 2026-05-21
|
## [0.9.14] – 2026-05-21
|
||||||
|
|
||||||
### Neu
|
### Neu
|
||||||
|
|||||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -1,5 +1,50 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.9.16] – 2026-05-22
|
||||||
|
|
||||||
|
### New
|
||||||
|
- **Auto-start camera on print:** new setting "Turn camera on at print start" —
|
||||||
|
when enabled, the bridge starts the camera stream automatically when a print
|
||||||
|
begins (works for both OrcaSlicer and the Bridge UI).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Single-color print blocked by an empty AMS slot:** OrcaSlicer writes all
|
||||||
|
configured filaments into the GCode header even when the model uses only one,
|
||||||
|
so the bridge told the printer it needed every color — and an empty unused slot
|
||||||
|
aborted the print. The bridge now maps only the filaments actually used by the
|
||||||
|
GCode.
|
||||||
|
- **Filament sync now position-accurate:** with an empty slot in the middle
|
||||||
|
(e.g. slot 1 yellow, 2 empty, 3 red, 4 white) OrcaSlicer showed the colors
|
||||||
|
shifted onto the wrong slots. Fixed — empty slots keep their position, and the
|
||||||
|
sync color format follows the Happy Hare convention (RRGGBB without `#`).
|
||||||
|
- **Slicer time + thumbnail missing after a browser reload** (or when a print was
|
||||||
|
started directly from OrcaSlicer): both are now restored from the GCode store
|
||||||
|
by filename instead of relying on volatile state.
|
||||||
|
- **German translation gaps** in the ACE dryer dialog fixed.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Repeated log lines are collapsed into a counter ("×N") instead of spamming the
|
||||||
|
console; status-poll traffic is no longer logged at INFO.
|
||||||
|
- New log level filter (All / Errors / Warnings), a toast on new errors, full
|
||||||
|
tracebacks forwarded to the browser log, and a timestamped download filename.
|
||||||
|
|
||||||
|
## [0.9.15] – 2026-05-21
|
||||||
|
|
||||||
|
### Fixes (Issue #29)
|
||||||
|
- **UI in the OrcaSlicer device tab was broken:** OrcaSlicer's embedded webview
|
||||||
|
only loads the bare HTML and ignores external `<script>`/`<link>` tags, so after
|
||||||
|
the v0.9.14 theme split none of the buttons worked in the device tab. The
|
||||||
|
bridge now inlines CSS + JS into the page — works in both the browser and the
|
||||||
|
OrcaSlicer webview.
|
||||||
|
- **Dropdowns unreadable (white-on-white) in the OrcaSlicer webview:** added
|
||||||
|
`color-scheme` + explicit `select`/`option` colors so the native dropdowns
|
||||||
|
render correctly in dark and light theme.
|
||||||
|
- **"Select slots" button did nothing right after an upload:** a missing variable
|
||||||
|
declaration (`storeFiles`) threw a `ReferenceError` when clicked before the
|
||||||
|
Browser tab had loaded. Fixed.
|
||||||
|
- **Upload banner came back after a finished print:** the "file ready" state was
|
||||||
|
only cleared on stop/cancel, not on `finished`. Now cleared on completion too.
|
||||||
|
|
||||||
## [0.9.14] – 2026-05-21
|
## [0.9.14] – 2026-05-21
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ def _load_config_file(path: pathlib.Path):
|
|||||||
"DEVICE_ID": (CONFIG_SECTION_CONNECTION, "device_id"),
|
"DEVICE_ID": (CONFIG_SECTION_CONNECTION, "device_id"),
|
||||||
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
||||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||||
|
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||||
}
|
}
|
||||||
for env_key, (section, option) in mapping.items():
|
for env_key, (section, option) in mapping.items():
|
||||||
@@ -95,6 +96,7 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
|||||||
cfg[CONFIG_SECTION_PRINT] = {
|
cfg[CONFIG_SECTION_PRINT] = {
|
||||||
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
||||||
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
||||||
|
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
||||||
}
|
}
|
||||||
cfg[CONFIG_SECTION_BRIDGE] = {
|
cfg[CONFIG_SECTION_BRIDGE] = {
|
||||||
"poll_interval": "3",
|
"poll_interval": "3",
|
||||||
@@ -175,3 +177,4 @@ MODE_ID = get("MODE_ID", "")
|
|||||||
DEVICE_ID = get("DEVICE_ID", "")
|
DEVICE_ID = get("DEVICE_ID", "")
|
||||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||||
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
||||||
|
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
||||||
|
|||||||
@@ -371,9 +371,12 @@ class KobraXClient:
|
|||||||
report_registered = True
|
report_registered = True
|
||||||
|
|
||||||
topic = self._pub_topic(msg_type)
|
topic = self._pub_topic(msg_type)
|
||||||
log.info("TX %-25s action=%-12s data=%s",
|
# Status-Poll-TX (query/getInfo) ist reines Rauschen (alle paar Sekunden) →
|
||||||
f"{msg_type}/request", action,
|
# auf DEBUG. Aktions-TX (start/set/control/move/…) bleibt INFO sichtbar.
|
||||||
json.dumps(data, ensure_ascii=False) if data else "null")
|
_tx_level = logging.DEBUG if action in ("query", "getInfo") else logging.INFO
|
||||||
|
log.log(_tx_level, "TX %-25s action=%-12s data=%s",
|
||||||
|
f"{msg_type}/request", action,
|
||||||
|
json.dumps(data, ensure_ascii=False) if data else "null")
|
||||||
try:
|
try:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._sock.sendall(_build_publish(topic, payload))
|
self._sock.sendall(_build_publish(topic, payload))
|
||||||
|
|||||||
@@ -151,11 +151,19 @@ class _BrowserLogHandler(logging.Handler):
|
|||||||
_fmt = logging.Formatter(datefmt="%H:%M:%S")
|
_fmt = logging.Formatter(datefmt="%H:%M:%S")
|
||||||
|
|
||||||
def emit(self, record: logging.LogRecord):
|
def emit(self, record: logging.LogRecord):
|
||||||
|
msg = record.getMessage()
|
||||||
|
# Exceptions mit Traceback in den Browser durchreichen (sonst sieht der
|
||||||
|
# Nutzer nur "Fehler: X" ohne Kontext).
|
||||||
|
if record.exc_info:
|
||||||
|
try:
|
||||||
|
msg += "\n" + self._fmt.formatException(record.exc_info)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
entry = {
|
entry = {
|
||||||
"ts": self._fmt.formatTime(record, "%H:%M:%S"),
|
"ts": self._fmt.formatTime(record, "%H:%M:%S"),
|
||||||
"lvl": record.levelname,
|
"lvl": record.levelname,
|
||||||
"name": record.name,
|
"name": record.name,
|
||||||
"msg": record.getMessage(),
|
"msg": msg,
|
||||||
}
|
}
|
||||||
_log_buffer.append(entry)
|
_log_buffer.append(entry)
|
||||||
for q in list(_log_sse_queues):
|
for q in list(_log_sse_queues):
|
||||||
@@ -554,6 +562,7 @@ class KobraXBridge:
|
|||||||
self._store = store if store is not None else GCodeStore(args.data_dir)
|
self._store = store if store is not None else GCodeStore(args.data_dir)
|
||||||
self._serve_dir_path: str = self._store._gcode_dir
|
self._serve_dir_path: str = self._store._gcode_dir
|
||||||
self._current_job_id: str = ""
|
self._current_job_id: str = ""
|
||||||
|
self._camera_autostarted: bool = False
|
||||||
|
|
||||||
self._thumbnail_b64: str = ""
|
self._thumbnail_b64: str = ""
|
||||||
self._ace_dry_presets: dict[str, dict] = self._load_ace_dry_presets_config()
|
self._ace_dry_presets: dict[str, dict] = self._load_ace_dry_presets_config()
|
||||||
@@ -665,6 +674,20 @@ class KobraXBridge:
|
|||||||
if kobra_state:
|
if kobra_state:
|
||||||
self._state["kobra_state"] = kobra_state
|
self._state["kobra_state"] = kobra_state
|
||||||
|
|
||||||
|
# Kamera bei Druckstart automatisch einschalten (Settings-Option).
|
||||||
|
# Zentral hier, damit es alle Druck-Startwege abdeckt (OrcaSlicer + UI).
|
||||||
|
# _camera_autostarted verhindert Mehrfach-Trigger pro Druck.
|
||||||
|
if kobra_state == "printing":
|
||||||
|
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
|
||||||
|
self._camera_autostarted = True
|
||||||
|
try:
|
||||||
|
self.client.start_camera()
|
||||||
|
log.info("Kamera bei Druckstart automatisch eingeschaltet")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||||
|
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||||
|
self._camera_autostarted = False
|
||||||
|
|
||||||
# Job-History: Druckstart erkennen
|
# Job-History: Druckstart erkennen
|
||||||
if kobra_state == "printing" and not self._current_job_id:
|
if kobra_state == "printing" and not self._current_job_id:
|
||||||
filename = d.get("filename", self._state.get("filename", ""))
|
filename = d.get("filename", self._state.get("filename", ""))
|
||||||
@@ -687,6 +710,11 @@ class KobraXBridge:
|
|||||||
log.info(f"Job abgebrochen: {self._current_job_id}")
|
log.info(f"Job abgebrochen: {self._current_job_id}")
|
||||||
self._current_job_id = ""
|
self._current_job_id = ""
|
||||||
|
|
||||||
|
# Nach Druckende das Upload-Banner verschwinden lassen (Issue #29): der
|
||||||
|
# Drucker meldet "finished" nach erfolgreichem Druck — file_ready wurde
|
||||||
|
# bisher nur bei stoped/canceled geleert, dadurch kam das Banner zurück.
|
||||||
|
if kobra_state == "finished":
|
||||||
|
self._state["file_ready"] = ""
|
||||||
if kobra_state in ("stoped", "canceled"):
|
if kobra_state in ("stoped", "canceled"):
|
||||||
self._state["progress"] = 0.0
|
self._state["progress"] = 0.0
|
||||||
self._state["filename"] = ""
|
self._state["filename"] = ""
|
||||||
@@ -728,6 +756,22 @@ class KobraXBridge:
|
|||||||
if kobra_state:
|
if kobra_state:
|
||||||
self._state["print_state"] = KOBRA_TO_KLIPPER_STATE.get(kobra_state, "standby")
|
self._state["print_state"] = KOBRA_TO_KLIPPER_STATE.get(kobra_state, "standby")
|
||||||
self._state["kobra_state"] = kobra_state
|
self._state["kobra_state"] = kobra_state
|
||||||
|
# Upload-Banner nach Druckende ausblenden (Issue #29) – der State kommt
|
||||||
|
# je nach Drucker auch über info/report (project.state), nicht nur print/report.
|
||||||
|
if kobra_state in ("finished", "stoped", "canceled"):
|
||||||
|
self._state["file_ready"] = ""
|
||||||
|
# Kamera-Autostart auch hier (OrcaSlicer meldet Start oft via info/report).
|
||||||
|
# _camera_autostarted-Guard verhindert Doppel-Start mit _on_print.
|
||||||
|
if kobra_state == "printing":
|
||||||
|
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
|
||||||
|
self._camera_autostarted = True
|
||||||
|
try:
|
||||||
|
self.client.start_camera()
|
||||||
|
log.info("Kamera bei Druckstart automatisch eingeschaltet")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||||
|
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||||
|
self._camera_autostarted = False
|
||||||
if project:
|
if project:
|
||||||
if "filename" in project:
|
if "filename" in project:
|
||||||
self._state["filename"] = project["filename"]
|
self._state["filename"] = project["filename"]
|
||||||
@@ -1220,49 +1264,61 @@ class KobraXBridge:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _build_lane_data(self) -> dict:
|
def _build_lane_data(self) -> dict:
|
||||||
"""Build lane_data for filament sync from loaded, printable slots only.
|
"""Baut BBL-AMS-JSON für OrcaSlicer DevFilaSystemParser::ParseV1_0.
|
||||||
|
|
||||||
Slots are compacted in sync order (installed slots first) so Orca/Happy
|
POSITIONSTREU: jeder physische Slot behält seine Position (tray id =
|
||||||
Hare does not infer empty gaps between tray ids.
|
Slot-Position). Leere Slots werden als Platzhalter-Tray gemeldet, NICHT
|
||||||
|
weggefiltert/komprimiert — sonst rutschen die Farben auf falsche Positionen
|
||||||
|
(z.B. Slot 1=gelb, 2=leer, 3=rot → rot dürfte nicht auf Position 2 landen).
|
||||||
"""
|
"""
|
||||||
loaded_slots = self._loaded_slots_for_print()
|
slots = self._ams_slots
|
||||||
if not loaded_slots:
|
total = len(slots)
|
||||||
|
if total == 0:
|
||||||
return {"ams": [], "ams_exist_bits": "0", "tray_exist_bits": "0"}
|
return {"ams": [], "ams_exist_bits": "0", "tray_exist_bits": "0"}
|
||||||
|
|
||||||
ams_buckets: dict[int, list[tuple[int, dict]]] = {}
|
ams_count = (total + 3) // 4
|
||||||
tray_exist_bits = 0
|
|
||||||
|
|
||||||
for sync_index, (_global_index, slot) in enumerate(sorted(loaded_slots, key=lambda item: item[0])):
|
|
||||||
ams_id = sync_index // 4
|
|
||||||
slot_id = sync_index % 4
|
|
||||||
tray_exist_bits |= (1 << sync_index)
|
|
||||||
ams_buckets.setdefault(ams_id, []).append((slot_id, slot))
|
|
||||||
|
|
||||||
ams_exist_bits = 0
|
ams_exist_bits = 0
|
||||||
|
tray_exist_bits = 0
|
||||||
ams_array = []
|
ams_array = []
|
||||||
for ams_id in sorted(ams_buckets.keys()):
|
|
||||||
|
for ams_id in range(ams_count):
|
||||||
ams_exist_bits |= (1 << ams_id)
|
ams_exist_bits |= (1 << ams_id)
|
||||||
tray_array = []
|
tray_array = []
|
||||||
for slot_id, slot in sorted(ams_buckets[ams_id], key=lambda item: item[0]):
|
max_slot = min(3, total - ams_id * 4 - 1)
|
||||||
color_raw = slot.get("color", [255, 255, 255])
|
for slot_id in range(max_slot + 1):
|
||||||
if isinstance(color_raw, list) and len(color_raw) >= 3:
|
slot_index = ams_id * 4 + slot_id
|
||||||
color_hex = "{:02X}{:02X}{:02X}FF".format(
|
slot = slots[slot_index] if slot_index < total else {}
|
||||||
int(color_raw[0]), int(color_raw[1]), int(color_raw[2])
|
occupied = slot.get("status") == 5
|
||||||
)
|
|
||||||
elif isinstance(color_raw, str) and len(color_raw) >= 6:
|
|
||||||
color_hex = color_raw[:6].upper() + "FF"
|
|
||||||
else:
|
|
||||||
color_hex = "FFFFFFFF"
|
|
||||||
|
|
||||||
material = slot.get("type", "PLA").upper()
|
if occupied:
|
||||||
tray_info_idx = self._TRAY_INFO_IDX.get(material, "OGFL99")
|
tray_exist_bits |= (1 << slot_index)
|
||||||
tray_array.append({
|
color_raw = slot.get("color", [255, 255, 255])
|
||||||
"id": str(slot_id),
|
if isinstance(color_raw, list) and len(color_raw) >= 3:
|
||||||
"tag_uid": "0000000000000000",
|
color_hex = "{:02X}{:02X}{:02X}FF".format(
|
||||||
"tray_info_idx": tray_info_idx,
|
int(color_raw[0]), int(color_raw[1]), int(color_raw[2])
|
||||||
"tray_type": material,
|
)
|
||||||
"tray_color": color_hex,
|
elif isinstance(color_raw, str) and len(color_raw) >= 6:
|
||||||
})
|
color_hex = color_raw[:6].upper() + "FF"
|
||||||
|
else:
|
||||||
|
color_hex = "FFFFFFFF"
|
||||||
|
material = slot.get("type", "PLA").upper()
|
||||||
|
tray_info_idx = self._TRAY_INFO_IDX.get(material, "OGFL99")
|
||||||
|
tray_array.append({
|
||||||
|
"id": str(slot_id),
|
||||||
|
"tag_uid": "0000000000000000",
|
||||||
|
"tray_info_idx": tray_info_idx,
|
||||||
|
"tray_type": material,
|
||||||
|
"tray_color": color_hex,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
tray_array.append({
|
||||||
|
"id": str(slot_id),
|
||||||
|
"tag_uid": "0000000000000000",
|
||||||
|
"tray_info_idx": "",
|
||||||
|
"tray_type": "",
|
||||||
|
"tray_color": "00000000",
|
||||||
|
"tray_slot_placeholder": "1",
|
||||||
|
})
|
||||||
|
|
||||||
ams_array.append({"id": str(ams_id), "info": "0002", "tray": tray_array})
|
ams_array.append({"id": str(ams_id), "info": "0002", "tray": tray_array})
|
||||||
|
|
||||||
@@ -1294,24 +1350,34 @@ class KobraXBridge:
|
|||||||
self.ws_clients -= dead
|
self.ws_clients -= dead
|
||||||
|
|
||||||
def _build_mmu_object(self) -> dict:
|
def _build_mmu_object(self) -> dict:
|
||||||
loaded_slots = sorted(self._loaded_slots_for_print(), key=lambda item: item[0])
|
# POSITIONSTREU: ein Gate je physischem Slot, in Reihenfolge. Leere Slots
|
||||||
if not loaded_slots:
|
# bekommen gate_status=0 (statt weggelassen zu werden) – sonst rutschen die
|
||||||
|
# Farben in OrcaSlicer auf falsche Gates (Slot 1=gelb, 2=leer, 3=rot →
|
||||||
|
# rot darf nicht auf Gate 1 landen). gate_status 0=leer, 1=verfügbar.
|
||||||
|
slots = sorted(
|
||||||
|
((int(s.get("global_index", i)), s) for i, s in enumerate(self._ams_slots)),
|
||||||
|
key=lambda item: item[0],
|
||||||
|
)
|
||||||
|
if not slots:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
_TEMP = {"PLA": 210, "PETG": 230, "ABS": 240, "ASA": 250,
|
_TEMP = {"PLA": 210, "PETG": 230, "ABS": 240, "ASA": 250,
|
||||||
"TPU": 220, "PA": 260, "PC": 270, "HIPS": 220}
|
"TPU": 220, "PA": 260, "PC": 270, "HIPS": 220}
|
||||||
num_gates = len(loaded_slots)
|
num_gates = len(slots)
|
||||||
gate_status, gate_material, gate_color, gate_temperature, gate_color_rgb = [], [], [], [], []
|
gate_status, gate_material, gate_color, gate_temperature, gate_color_rgb = [], [], [], [], []
|
||||||
for _global_index, slot in loaded_slots:
|
for _global_index, slot in slots:
|
||||||
gate_status.append(1)
|
occupied = slot.get("status") == 5
|
||||||
material = slot.get("type", "PLA").upper()
|
gate_status.append(1 if occupied else 0)
|
||||||
|
material = (slot.get("type") or "PLA").upper() if occupied else ""
|
||||||
gate_material.append(material)
|
gate_material.append(material)
|
||||||
c = slot.get("color", [0, 0, 0])
|
c = slot.get("color", [0, 0, 0]) if occupied else [0, 0, 0]
|
||||||
gate_color.append("#{:02X}{:02X}{:02X}".format(*c[:3]))
|
# Happy Hare erwartet gate_color als RRGGBB OHNE '#' (Klipper-Limitation).
|
||||||
gate_color_rgb.append([round(c[0]/255, 3), round(c[1]/255, 3), round(c[2]/255, 3)])
|
# Leerer Gate: leerer String + RGB [0,0,0].
|
||||||
gate_temperature.append(_TEMP.get(material, 210))
|
gate_color.append("{:02X}{:02X}{:02X}".format(*c[:3]) if occupied else "")
|
||||||
|
gate_color_rgb.append([round(c[0]/255, 3), round(c[1]/255, 3), round(c[2]/255, 3)] if occupied else [0.0, 0.0, 0.0])
|
||||||
|
gate_temperature.append(_TEMP.get(material, 210) if occupied else 0)
|
||||||
|
|
||||||
loaded_index_map = {global_index: idx for idx, (global_index, _) in enumerate(loaded_slots)}
|
loaded_index_map = {global_index: idx for idx, (global_index, _) in enumerate(slots)}
|
||||||
active_gate = loaded_index_map.get(int(self._ams_loaded_slot), -1)
|
active_gate = loaded_index_map.get(int(self._ams_loaded_slot), -1)
|
||||||
return {
|
return {
|
||||||
"num_gates": num_gates,
|
"num_gates": num_gates,
|
||||||
@@ -1824,7 +1890,7 @@ class KobraXBridge:
|
|||||||
log.info(f"Upload+Print (print=true): {remote_filename}")
|
log.info(f"Upload+Print (print=true): {remote_filename}")
|
||||||
self._state["file_ready"] = ""
|
self._state["file_ready"] = ""
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.run_in_executor(None, lambda: self._start_print(remote_filename, serve_url, file_md5, file_size))
|
loop.run_in_executor(None, lambda: self._start_print(remote_filename, serve_url, file_md5, file_size, gcode_filaments=gcode_filaments))
|
||||||
else:
|
else:
|
||||||
log.info(f"Nur hochgeladen (print=false): {remote_filename}")
|
log.info(f"Nur hochgeladen (print=false): {remote_filename}")
|
||||||
self._state["file_ready"] = remote_filename
|
self._state["file_ready"] = remote_filename
|
||||||
@@ -1849,12 +1915,32 @@ class KobraXBridge:
|
|||||||
}
|
}
|
||||||
}, status=201)
|
}, status=201)
|
||||||
|
|
||||||
def _start_print(self, filename: str, url: str = "", md5: str = "", filesize: int = 0):
|
def _start_print(self, filename: str, url: str = "", md5: str = "", filesize: int = 0,
|
||||||
|
gcode_filaments: list | None = None):
|
||||||
self._state["file_ready"] = ""
|
self._state["file_ready"] = ""
|
||||||
loaded = self._select_loaded_slots_for_print(warn_on_empty_default=True)
|
loaded = self._select_loaded_slots_for_print(warn_on_empty_default=True)
|
||||||
|
|
||||||
|
# Nur die im GCode TATSÄCHLICH genutzten Paints auf Slots mappen. OrcaSlicer
|
||||||
|
# schreibt im Header alle konfigurierten Filamente (filament_colour=…;…;…;…),
|
||||||
|
# nutzt aber oft nur eines (z.B. einfarbig → nur T3). Würden wir alle
|
||||||
|
# belegten Slots mappen, erwartet der Drucker alle Farben und blockiert,
|
||||||
|
# wenn ein anderer (ungenutzter) Slot leer ist. Die genutzten Paint-Indizes
|
||||||
|
# liefert _extract_filament_info via is_used (echte T<n>-Tool-Changes).
|
||||||
|
used_paint_indices = None
|
||||||
|
if gcode_filaments:
|
||||||
|
used = [int(f["slot_index"]) for f in gcode_filaments
|
||||||
|
if f.get("is_used") and "slot_index" in f]
|
||||||
|
if used:
|
||||||
|
used_paint_indices = set(used)
|
||||||
|
|
||||||
|
if used_paint_indices is not None:
|
||||||
|
# GCode-Paint-Index N entspricht AMS-Slot N (global_index). Nur belegte
|
||||||
|
# genutzte Slots mappen; nicht-belegte genutzte → später Warnung möglich.
|
||||||
|
loaded = [(gidx, s) for (gidx, s) in loaded if gidx in used_paint_indices]
|
||||||
|
|
||||||
use_ams = len(loaded) > 0
|
use_ams = len(loaded) > 0
|
||||||
ams_box_mapping = self._build_auto_ams_box_mapping(loaded_slots=loaded)
|
ams_box_mapping = self._build_auto_ams_box_mapping(loaded_slots=loaded)
|
||||||
log.debug(f"AMS-Slots: {len(loaded)}/{len(self._ams_slots)} belegt → {[i for i, _ in loaded]}")
|
log.debug(f"AMS-Slots: {len(loaded)} gemappt (genutzte Paints: {used_paint_indices}) → {[i for i, _ in loaded]}")
|
||||||
auto_leveling = getattr(self._args, "auto_leveling", 1)
|
auto_leveling = getattr(self._args, "auto_leveling", 1)
|
||||||
payload = {
|
payload = {
|
||||||
"taskid": "-1",
|
"taskid": "-1",
|
||||||
@@ -2049,9 +2135,30 @@ class KobraXBridge:
|
|||||||
status=500,
|
status=500,
|
||||||
content_type="text/html; charset=utf-8",
|
content_type="text/html; charset=utf-8",
|
||||||
)
|
)
|
||||||
html = tpl.replace("__UI_ASSETS_VER__", self._ui_asset_cache_buster())
|
page = tpl.replace("__UI_ASSETS_VER__", self._ui_asset_cache_buster())
|
||||||
|
|
||||||
return web.Response(text=html, content_type="text/html",
|
# CSS + JS INLINE einbetten statt nur zu verlinken. OrcaSlicers
|
||||||
|
# eingebetteter Device-Tab-Webview lädt externe <link>/<script src>
|
||||||
|
# NICHT (nur das nackte HTML) → ohne Inlining funktioniert dort kein
|
||||||
|
# einziger Button (Issue #29). Im normalen Browser ist es ebenso korrekt.
|
||||||
|
base = os.path.join(_WEB_BASE, "web", "themes", self._ui_theme)
|
||||||
|
try:
|
||||||
|
css = pathlib.Path(os.path.join(base, "style.css")).read_text(encoding="utf-8")
|
||||||
|
page = page.replace(
|
||||||
|
'<link rel="stylesheet" href="/kx/ui/style.css">',
|
||||||
|
"<style>\n" + css + "\n</style>")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
js = pathlib.Path(os.path.join(base, "app.js")).read_text(encoding="utf-8")
|
||||||
|
js = js.replace("'__VERSION__'", f"'{self._read_version()}'")
|
||||||
|
page = page.replace(
|
||||||
|
'<script src="/kx/ui/app.js"></script>',
|
||||||
|
"<script>\n" + js + "\n</script>")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return web.Response(text=page, content_type="text/html",
|
||||||
headers={"Cache-Control": "no-store, no-cache, must-revalidate"})
|
headers={"Cache-Control": "no-store, no-cache, must-revalidate"})
|
||||||
|
|
||||||
async def handle_api_light(self, request):
|
async def handle_api_light(self, request):
|
||||||
@@ -2508,6 +2615,23 @@ class KobraXBridge:
|
|||||||
|
|
||||||
async def handle_api_state(self, request):
|
async def handle_api_state(self, request):
|
||||||
s = self._state
|
s = self._state
|
||||||
|
# Slicer-Zeit + Thumbnail sind nur flüchtig im State (werden beim Upload
|
||||||
|
# gesetzt). Nach Browser-Reload oder bei OrcaSlicer-Direktdruck (Datei kam
|
||||||
|
# nicht über den UI-Upload) fehlen sie → aus dem GCode-Store anhand des
|
||||||
|
# laufenden Dateinamens nachladen.
|
||||||
|
slicer_time = s["slicer_time"]
|
||||||
|
thumbnail = self._thumbnail_b64
|
||||||
|
fname = s.get("filename", "")
|
||||||
|
if fname and (not slicer_time or not thumbnail):
|
||||||
|
try:
|
||||||
|
gf = self._store.get_file_by_name(fname)
|
||||||
|
if gf:
|
||||||
|
if not slicer_time and gf.get("est_print_time_sec"):
|
||||||
|
slicer_time = int(gf["est_print_time_sec"])
|
||||||
|
if not thumbnail and gf.get("thumbnail_b64"):
|
||||||
|
thumbnail = gf["thumbnail_b64"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"printer_name": s["printer_name"],
|
"printer_name": s["printer_name"],
|
||||||
"firmware_version": s["firmware_version"],
|
"firmware_version": s["firmware_version"],
|
||||||
@@ -2523,7 +2647,7 @@ class KobraXBridge:
|
|||||||
"curr_layer": s["curr_layer"],
|
"curr_layer": s["curr_layer"],
|
||||||
"total_layers": s["total_layers"],
|
"total_layers": s["total_layers"],
|
||||||
"filename": s["filename"],
|
"filename": s["filename"],
|
||||||
"slicer_time": s["slicer_time"],
|
"slicer_time": slicer_time,
|
||||||
"camera_url": s["camera_url"],
|
"camera_url": s["camera_url"],
|
||||||
"fan_speed": s["fan_speed"],
|
"fan_speed": s["fan_speed"],
|
||||||
"print_speed_mode": s["print_speed_mode"],
|
"print_speed_mode": s["print_speed_mode"],
|
||||||
@@ -2536,7 +2660,7 @@ class KobraXBridge:
|
|||||||
"ace_units": list(self._ace_box_ids),
|
"ace_units": list(self._ace_box_ids),
|
||||||
"ace_auto_feed": dict(self._ace_auto_feed),
|
"ace_auto_feed": dict(self._ace_auto_feed),
|
||||||
"ace_dry_presets": self._ace_dry_presets,
|
"ace_dry_presets": self._ace_dry_presets,
|
||||||
"thumbnail": self._thumbnail_b64,
|
"thumbnail": thumbnail,
|
||||||
"connection_error": s["connection_error"],
|
"connection_error": s["connection_error"],
|
||||||
"file_ready": s["file_ready"],
|
"file_ready": s["file_ready"],
|
||||||
"version": self._read_version(),
|
"version": self._read_version(),
|
||||||
@@ -2618,6 +2742,7 @@ class KobraXBridge:
|
|||||||
"device_id": self._args.device_id,
|
"device_id": self._args.device_id,
|
||||||
"default_ams_slot": getattr(self._args, "default_ams_slot", "auto"),
|
"default_ams_slot": getattr(self._args, "default_ams_slot", "auto"),
|
||||||
"auto_leveling": getattr(self._args, "auto_leveling", 1),
|
"auto_leveling": getattr(self._args, "auto_leveling", 1),
|
||||||
|
"camera_on_print": getattr(self._args, "camera_on_print", 0),
|
||||||
"ace_dry_presets": self._ace_dry_presets,
|
"ace_dry_presets": self._ace_dry_presets,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2646,6 +2771,7 @@ class KobraXBridge:
|
|||||||
cfg.set("connection", "device_id", str(data.get("device_id", self._args.device_id or "")))
|
cfg.set("connection", "device_id", str(data.get("device_id", self._args.device_id or "")))
|
||||||
cfg.set("print", "default_ams_slot", str(data.get("default_ams_slot", getattr(self._args, "default_ams_slot", "auto"))))
|
cfg.set("print", "default_ams_slot", str(data.get("default_ams_slot", getattr(self._args, "default_ams_slot", "auto"))))
|
||||||
cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1))))
|
cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1))))
|
||||||
|
cfg.set("print", "camera_on_print", str(int(bool(data.get("camera_on_print", getattr(self._args, "camera_on_print", 0))))))
|
||||||
if not cfg.has_option("bridge", "poll_interval"):
|
if not cfg.has_option("bridge", "poll_interval"):
|
||||||
cfg.set("bridge", "poll_interval", "3")
|
cfg.set("bridge", "poll_interval", "3")
|
||||||
printer_name = str(data.get("printer_name", "")).strip()
|
printer_name = str(data.get("printer_name", "")).strip()
|
||||||
@@ -2910,12 +3036,15 @@ class KobraXBridge:
|
|||||||
|
|
||||||
async def handle_api_log_download(self, request):
|
async def handle_api_log_download(self, request):
|
||||||
"""Gibt alle gepufferten Log-Einträge als Plaintext zum Download."""
|
"""Gibt alle gepufferten Log-Einträge als Plaintext zum Download."""
|
||||||
lines = [f"[{e['ts']}] {e['lvl']:<5} {e['name']}: {e['msg']}" for e in _log_buffer]
|
header = (f"# KX-Bridge Log | Version {self._read_version()} | "
|
||||||
text = "\n".join(lines)
|
f"{time.strftime('%Y-%m-%d %H:%M:%S')} | {len(_log_buffer)} Einträge\n")
|
||||||
|
lines = [f"[{e['ts']}] {e['lvl']:<7} {e['name']}: {e['msg']}" for e in _log_buffer]
|
||||||
|
text = header + "\n".join(lines) + "\n"
|
||||||
|
fname = f"kx-bridge-log_{time.strftime('%Y%m%d-%H%M%S')}.txt"
|
||||||
return web.Response(
|
return web.Response(
|
||||||
body=text.encode("utf-8"),
|
body=text.encode("utf-8"),
|
||||||
content_type="text/plain",
|
content_type="text/plain",
|
||||||
headers={"Content-Disposition": 'attachment; filename="kx-bridge.log"'},
|
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_api_update_check(self, request):
|
async def handle_api_update_check(self, request):
|
||||||
@@ -3487,6 +3616,7 @@ def main():
|
|||||||
parser.add_argument("--device-id", default=env_loader.DEVICE_ID)
|
parser.add_argument("--device-id", default=env_loader.DEVICE_ID)
|
||||||
parser.add_argument("--default-ams-slot",default=env_loader.DEFAULT_AMS_SLOT)
|
parser.add_argument("--default-ams-slot",default=env_loader.DEFAULT_AMS_SLOT)
|
||||||
parser.add_argument("--auto-leveling", type=int, default=env_loader.AUTO_LEVELING)
|
parser.add_argument("--auto-leveling", type=int, default=env_loader.AUTO_LEVELING)
|
||||||
|
parser.add_argument("--camera-on-print", type=int, default=env_loader.CAMERA_ON_PRINT)
|
||||||
|
|
||||||
parser.add_argument("--host", default="0.0.0.0",
|
parser.add_argument("--host", default="0.0.0.0",
|
||||||
help="Bind-Adresse für den Bridge-Server")
|
help="Bind-Adresse für den Bridge-Server")
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ Referenzliste für JavaScript-/DOM-Hooks.
|
|||||||
| `#logdir-all` | Hook / Selektor |
|
| `#logdir-all` | Hook / Selektor |
|
||||||
| `#logdir-rx` | Hook / Selektor |
|
| `#logdir-rx` | Hook / Selektor |
|
||||||
| `#logdir-tx` | Hook / Selektor |
|
| `#logdir-tx` | Hook / Selektor |
|
||||||
|
| `#log-lbl-level` | i18n-Label "Level:" |
|
||||||
|
| `#loglvl-all` | onclick `setLogLevel('all')` |
|
||||||
|
| `#loglvl-err` | onclick `setLogLevel('err')` — nur Fehler |
|
||||||
|
| `#loglvl-warn` | onclick `setLogLevel('warn')` — Fehler + Warnungen |
|
||||||
| `#nb-console` | Hook / Selektor |
|
| `#nb-console` | Hook / Selektor |
|
||||||
| `#nb-dashboard` | Hook / Selektor |
|
| `#nb-dashboard` | Hook / Selektor |
|
||||||
| `#nb-printers` | Hook / Selektor |
|
| `#nb-printers` | Hook / Selektor |
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ var LANG_DE={
|
|||||||
card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',lbl_elapsed:'Verstrichen:',lbl_remaining:'Restzeit:',lbl_slicer_time:'Slicer-Schätzung:',lbl_layers:'Layer',
|
card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',lbl_elapsed:'Verstrichen:',lbl_remaining:'Restzeit:',lbl_slicer_time:'Slicer-Schätzung:',lbl_layers:'Layer',
|
||||||
speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
||||||
lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen',
|
lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen',
|
||||||
card_ace_dry:'ACE Trocknung',ace_dry_dryer:'Trockner',ace_dry_status_off:'Status: Aus',ace_dry_status_on:'Status: Aktiv',ace_dry_status_remaining:'Rest',ace_dry_humidity:'Luftfeuchte',ace_dry_current_temp:'Temperatur',ace_dry_chart:'Verlauf (Temp/Feuchte)',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Trocknungstemperatur',ace_dry_time_line:'Trocknungszeit',ace_dry_ui_pending:'(nur UI, Backend folgt)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Speichern & Neustart',ace_dry_dialog_custom_name:'Custom Name',
|
card_ace_dry:'ACE Trocknung',ace_dry_dryer:'Trockner',ace_dry_status_off:'Status: Aus',ace_dry_status_on:'Status: Aktiv',ace_dry_status_remaining:'Rest',ace_dry_humidity:'Luftfeuchte',ace_dry_current_temp:'Temperatur',ace_dry_chart:'Verlauf (Temp/Feuchte)',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto-Nachschub',ace_dry_enable:'Trocknung aktivieren',ace_dry_temp_line:'Trocknungstemperatur',ace_dry_time_line:'Trocknungszeit',ace_dry_ui_pending:'(nur UI, Backend folgt)',ace_dry_dialog_title:'Trockner Temp/Zeit-Einstellungen',ace_dry_dialog_temp:'Temperatur (30-80°C)',ace_dry_dialog_time:'Restzeit (h:m:s)',ace_dry_dialog_confirm:'Bestätigen',ace_dry_dialog_cancel:'Abbrechen',ace_dry_dialog_save_restart:'Speichern & Neustart',ace_dry_dialog_custom_name:'Eigener Name',ace_dry_dialog_reset_default:'Auf Standard zurücksetzen',
|
||||||
cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera',
|
cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera',
|
||||||
btn_pause:'⏸ Pause',btn_resume:'▶ Weiter',btn_cancel:'✕ Stopp',
|
btn_pause:'⏸ Pause',btn_resume:'▶ Weiter',btn_cancel:'✕ Stopp',
|
||||||
label_nozzle:'Nozzle',label_bed:'Bett',label_fan:'🌀 Lüfter',label_light:'💡 Licht',label_on_off:'Ein / Aus',label_speed:'Geschwindigkeit',
|
label_nozzle:'Nozzle',label_bed:'Bett',label_fan:'🌀 Lüfter',label_light:'💡 Licht',label_on_off:'Ein / Aus',label_speed:'Geschwindigkeit',
|
||||||
@@ -112,7 +112,7 @@ var LANG_DE={
|
|||||||
settings_title:'Einstellungen',settings_connection:'Verbindung',settings_print:'Druckeinstellungen',settings_poll:'Poll-Intervall',settings_version:'Version',
|
settings_title:'Einstellungen',settings_connection:'Verbindung',settings_print:'Druckeinstellungen',settings_poll:'Poll-Intervall',settings_version:'Version',
|
||||||
settings_save:'Speichern & Neustart',settings_printer_name:'Drucker-Name',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port',
|
settings_save:'Speichern & Neustart',settings_printer_name:'Drucker-Name',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port',
|
||||||
settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID',hint_ip_no_port:'Nur IP-Adresse, kein Port (z.B. 192.168.1.102)',
|
settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID',hint_ip_no_port:'Nur IP-Adresse, kein Port (z.B. 192.168.1.102)',
|
||||||
settings_default_slot:'Standard-Slot (Einfarbdruck)',settings_slot_auto:'Auto (alle belegten Slots)',settings_auto_leveling:'Auto-Leveling vor Druck',
|
settings_default_slot:'Standard-Slot (Einfarbdruck)',settings_slot_auto:'Auto (alle belegten Slots)',settings_auto_leveling:'Auto-Leveling vor Druck',settings_camera_on_print:'Kamera bei Druckstart einschalten',
|
||||||
update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell',
|
update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell',
|
||||||
update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler',
|
update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler',
|
||||||
btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen',
|
btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen',
|
||||||
@@ -121,7 +121,7 @@ var LANG_DE={
|
|||||||
slot_edit_load:'⬇ Einziehen',slot_edit_unload:'⬆ Ausziehen',
|
slot_edit_load:'⬇ Einziehen',slot_edit_unload:'⬆ Ausziehen',
|
||||||
slot_edit_save:'💾 Speichern',slot_edit_custom:'z.B. PLA, PETG, ABS…',
|
slot_edit_save:'💾 Speichern',slot_edit_custom:'z.B. PLA, PETG, ABS…',
|
||||||
slot_edit_ok:'AMS Slot',
|
slot_edit_ok:'AMS Slot',
|
||||||
log_dir_all:'Alle',
|
log_dir_all:'Alle',log_lvl_label:'Level:',
|
||||||
file_ready_btn:'▶ Druck starten',
|
file_ready_btn:'▶ Druck starten',
|
||||||
file_slots_btn:'🎨 Slots wählen',
|
file_slots_btn:'🎨 Slots wählen',
|
||||||
file_cancel_btn:'✕ Abbrechen',
|
file_cancel_btn:'✕ Abbrechen',
|
||||||
@@ -159,7 +159,7 @@ var LANG_EN={
|
|||||||
card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed:',lbl_remaining:'Remaining:',lbl_slicer_time:'Slicer estimate:',lbl_layers:'Layer',
|
card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed:',lbl_remaining:'Remaining:',lbl_slicer_time:'Slicer estimate:',lbl_layers:'Layer',
|
||||||
speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
||||||
lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload',
|
lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload',
|
||||||
card_ace_dry:'ACE Drying',ace_dry_dryer:'Dryer',ace_dry_status_off:'Status: Off',ace_dry_status_on:'Status: Active',ace_dry_status_remaining:'Remaining',ace_dry_humidity:'Humidity',ace_dry_current_temp:'Temperature',ace_dry_chart:'History (Temp/Humidity)',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Drying Temperature',ace_dry_time_line:'Drying Time',ace_dry_ui_pending:'(UI only, backend next)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Save & Restart',ace_dry_dialog_custom_name:'Custom Name',
|
card_ace_dry:'ACE Drying',ace_dry_dryer:'Dryer',ace_dry_status_off:'Status: Off',ace_dry_status_on:'Status: Active',ace_dry_status_remaining:'Remaining',ace_dry_humidity:'Humidity',ace_dry_current_temp:'Temperature',ace_dry_chart:'History (Temp/Humidity)',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Drying Temperature',ace_dry_time_line:'Drying Time',ace_dry_ui_pending:'(UI only, backend next)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Save & Restart',ace_dry_dialog_custom_name:'Custom Name',ace_dry_dialog_reset_default:'Reset to Default',
|
||||||
cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera',
|
cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera',
|
||||||
btn_pause:'⏸ Pause',btn_resume:'▶ Resume',btn_cancel:'✕ Stop',
|
btn_pause:'⏸ Pause',btn_resume:'▶ Resume',btn_cancel:'✕ Stop',
|
||||||
label_nozzle:'Nozzle',label_bed:'Bed',label_fan:'🌀 Fan',label_light:'💡 Light',label_on_off:'On / Off',label_speed:'Speed',
|
label_nozzle:'Nozzle',label_bed:'Bed',label_fan:'🌀 Fan',label_light:'💡 Light',label_on_off:'On / Off',label_speed:'Speed',
|
||||||
@@ -175,7 +175,7 @@ var LANG_EN={
|
|||||||
settings_title:'Settings',settings_connection:'Connection',settings_print:'Print Settings',settings_poll:'Poll Interval',settings_version:'Version',
|
settings_title:'Settings',settings_connection:'Connection',settings_print:'Print Settings',settings_poll:'Poll Interval',settings_version:'Version',
|
||||||
settings_save:'Save & Restart',settings_printer_name:'Printer Name',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port',
|
settings_save:'Save & Restart',settings_printer_name:'Printer Name',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port',
|
||||||
settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID',hint_ip_no_port:'IP address only, no port (e.g. 192.168.1.102)',
|
settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID',hint_ip_no_port:'IP address only, no port (e.g. 192.168.1.102)',
|
||||||
settings_default_slot:'Default Slot (single color)',settings_slot_auto:'Auto (all loaded slots)',settings_auto_leveling:'Auto-Leveling before print',
|
settings_default_slot:'Default Slot (single color)',settings_slot_auto:'Auto (all loaded slots)',settings_auto_leveling:'Auto-Leveling before print',settings_camera_on_print:'Turn camera on at print start',
|
||||||
update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date',
|
update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date',
|
||||||
update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error',
|
update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error',
|
||||||
btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect',
|
btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect',
|
||||||
@@ -184,7 +184,7 @@ var LANG_EN={
|
|||||||
slot_edit_load:'⬇ Load',slot_edit_unload:'⬆ Unload',
|
slot_edit_load:'⬇ Load',slot_edit_unload:'⬆ Unload',
|
||||||
slot_edit_save:'💾 Save',slot_edit_custom:'e.g. PLA, PETG, ABS…',
|
slot_edit_save:'💾 Save',slot_edit_custom:'e.g. PLA, PETG, ABS…',
|
||||||
slot_edit_ok:'AMS Slot',
|
slot_edit_ok:'AMS Slot',
|
||||||
log_dir_all:'All',
|
log_dir_all:'All',log_lvl_label:'Level:',
|
||||||
file_ready_btn:'▶ Start Print',
|
file_ready_btn:'▶ Start Print',
|
||||||
file_slots_btn:'🎨 Select Slots',
|
file_slots_btn:'🎨 Select Slots',
|
||||||
file_cancel_btn:'✕ Cancel',
|
file_cancel_btn:'✕ Cancel',
|
||||||
@@ -362,6 +362,7 @@ function applyLang(){
|
|||||||
setText('lbl-default-slot',T.settings_default_slot);
|
setText('lbl-default-slot',T.settings_default_slot);
|
||||||
setText('opt-slot-auto',T.settings_slot_auto);
|
setText('opt-slot-auto',T.settings_slot_auto);
|
||||||
setText('lbl-auto-leveling',T.settings_auto_leveling);
|
setText('lbl-auto-leveling',T.settings_auto_leveling);
|
||||||
|
setText('lbl-camera-on-print',T.settings_camera_on_print);
|
||||||
|
|
||||||
setText('lbl-update-check',T.update_check);
|
setText('lbl-update-check',T.update_check);
|
||||||
setText('lbl-update-apply',T.update_apply);
|
setText('lbl-update-apply',T.update_apply);
|
||||||
@@ -402,6 +403,8 @@ function applyLang(){
|
|||||||
updateSlotEditFeedButton();
|
updateSlotEditFeedButton();
|
||||||
var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom);
|
var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom);
|
||||||
setText('logdir-all',T.log_dir_all);
|
setText('logdir-all',T.log_dir_all);
|
||||||
|
setText('loglvl-all',T.log_dir_all);
|
||||||
|
setText('log-lbl-level',T.log_lvl_label);
|
||||||
setText('file-ready-btn',T.file_ready_btn);
|
setText('file-ready-btn',T.file_ready_btn);
|
||||||
setText('file-slots-btn',T.file_slots_btn);
|
setText('file-slots-btn',T.file_slots_btn);
|
||||||
setText('file-cancel-btn',T.file_cancel_btn);
|
setText('file-cancel-btn',T.file_cancel_btn);
|
||||||
@@ -480,6 +483,7 @@ var consoleLogs=[];
|
|||||||
var logAutoScroll=true;
|
var logAutoScroll=true;
|
||||||
var logBadgeCount=0;
|
var logBadgeCount=0;
|
||||||
var logDirFilter='all'; // 'all'|'rx'|'tx'
|
var logDirFilter='all'; // 'all'|'rx'|'tx'
|
||||||
|
var logLevelFilter='all'; // 'all'|'err'|'warn'
|
||||||
var logTopicFilter=''; // '' = no topic filter
|
var logTopicFilter=''; // '' = no topic filter
|
||||||
|
|
||||||
function clog(msg,cls){
|
function clog(msg,cls){
|
||||||
@@ -496,16 +500,41 @@ function _lvlCls(lvl){
|
|||||||
function _appendLog(entry,forceCls){
|
function _appendLog(entry,forceCls){
|
||||||
var cls=forceCls||_lvlCls(entry.lvl);
|
var cls=forceCls||_lvlCls(entry.lvl);
|
||||||
var label=entry.name?'['+entry.name+'] ':'';
|
var label=entry.name?'['+entry.name+'] ':'';
|
||||||
consoleLogs.push({ts:entry.ts,msg:label+entry.msg,cls:cls});
|
var fullMsg=label+entry.msg;
|
||||||
|
// Wiederholungen als Zähler zusammenfassen (×N) statt N identische Zeilen.
|
||||||
|
var last=consoleLogs[consoleLogs.length-1];
|
||||||
|
if(last&&last.msg===fullMsg&&last.cls===cls){
|
||||||
|
last.count=(last.count||1)+1;
|
||||||
|
last.ts=entry.ts; // letzte Sichtung
|
||||||
|
renderLog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
consoleLogs.push({ts:entry.ts,msg:fullMsg,cls:cls,count:1});
|
||||||
if(consoleLogs.length>500)consoleLogs.shift();
|
if(consoleLogs.length>500)consoleLogs.shift();
|
||||||
// Badge wenn Tab nicht aktiv und Fehler/Warnungen
|
// Badge + Toast wenn Tab nicht aktiv und Fehler/Warnungen
|
||||||
if(currentPanel!=='console'&&(cls==='msg-err'||cls==='msg-warn')){
|
if(currentPanel!=='console'&&(cls==='msg-err'||cls==='msg-warn')){
|
||||||
logBadgeCount++;
|
logBadgeCount++;
|
||||||
var bc=logBadgeCount>99?'99+':logBadgeCount;
|
var bc=logBadgeCount>99?'99+':logBadgeCount;
|
||||||
['log-badge','log-badge-bot'].forEach(function(id){var b=document.getElementById(id);if(b){b.style.display='inline';b.textContent=bc;}});
|
['log-badge','log-badge-bot'].forEach(function(id){var b=document.getElementById(id);if(b){b.style.display='inline';b.textContent=bc;}});
|
||||||
}
|
}
|
||||||
|
if(cls==='msg-err')showToast(entry.msg.split('\n')[0]);
|
||||||
renderLog();
|
renderLog();
|
||||||
}
|
}
|
||||||
|
// Kurze rote Snackbar bei Fehlern (auch wenn Konsole-Tab nicht offen).
|
||||||
|
var _toastTimer=null;
|
||||||
|
function showToast(msg){
|
||||||
|
var t=document.getElementById('kx-toast');
|
||||||
|
if(!t){
|
||||||
|
t=document.createElement('div'); t.id='kx-toast';
|
||||||
|
t.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--err);color:#fff;padding:10px 18px;border-radius:8px;font-size:13px;z-index:9999;max-width:90vw;box-shadow:0 4px 16px rgba(0,0,0,.4);cursor:pointer';
|
||||||
|
t.onclick=function(){showPanel('console');t.style.display='none';};
|
||||||
|
document.body.appendChild(t);
|
||||||
|
}
|
||||||
|
t.textContent='⚠ '+msg;
|
||||||
|
t.style.display='block';
|
||||||
|
clearTimeout(_toastTimer);
|
||||||
|
_toastTimer=setTimeout(function(){t.style.display='none';},6000);
|
||||||
|
}
|
||||||
function setLogDir(dir){
|
function setLogDir(dir){
|
||||||
logDirFilter=dir;
|
logDirFilter=dir;
|
||||||
document.querySelectorAll('.log-dir-btn').forEach(function(b){
|
document.querySelectorAll('.log-dir-btn').forEach(function(b){
|
||||||
@@ -514,6 +543,14 @@ function setLogDir(dir){
|
|||||||
});
|
});
|
||||||
renderLog();
|
renderLog();
|
||||||
}
|
}
|
||||||
|
function setLogLevel(lvl){
|
||||||
|
logLevelFilter=lvl;
|
||||||
|
document.querySelectorAll('.log-lvl-btn').forEach(function(b){
|
||||||
|
b.style.background=b.id==='loglvl-'+lvl?'var(--accent)':'var(--raised)';
|
||||||
|
b.style.color=b.id==='loglvl-'+lvl?'#fff':'var(--txt2)';
|
||||||
|
});
|
||||||
|
renderLog();
|
||||||
|
}
|
||||||
function setLogTopic(topic){
|
function setLogTopic(topic){
|
||||||
var inp=document.getElementById('log-filter');
|
var inp=document.getElementById('log-filter');
|
||||||
var active=inp.value===topic;
|
var active=inp.value===topic;
|
||||||
@@ -534,11 +571,16 @@ function renderLog(){
|
|||||||
var m=l.msg;
|
var m=l.msg;
|
||||||
if(logDirFilter==='rx'&&!/ RX[ (]/.test(m))return false;
|
if(logDirFilter==='rx'&&!/ RX[ (]/.test(m))return false;
|
||||||
if(logDirFilter==='tx'&&!/ TX[ (]/.test(m))return false;
|
if(logDirFilter==='tx'&&!/ TX[ (]/.test(m))return false;
|
||||||
|
if(logLevelFilter==='err'&&l.cls!=='msg-err')return false;
|
||||||
|
if(logLevelFilter==='warn'&&l.cls!=='msg-err'&&l.cls!=='msg-warn')return false;
|
||||||
if(fl&&!m.toLowerCase().includes(fl))return false;
|
if(fl&&!m.toLowerCase().includes(fl))return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
var savedScroll=logAutoScroll?null:el.scrollTop;
|
var savedScroll=logAutoScroll?null:el.scrollTop;
|
||||||
el.innerHTML=rows.map(l=>`<div><span class="ts">${l.ts}</span><span class="${l.cls}">${escHtml(l.msg)}</span></div>`).join('');
|
el.innerHTML=rows.map(function(l){
|
||||||
|
var cnt=(l.count&&l.count>1)?' <span style="opacity:.7">(×'+l.count+')</span>':'';
|
||||||
|
return '<div><span class="ts">'+l.ts+'</span><span class="'+l.cls+'">'+escHtml(l.msg)+'</span>'+cnt+'</div>';
|
||||||
|
}).join('');
|
||||||
if(logAutoScroll)el.scrollTop=el.scrollHeight;
|
if(logAutoScroll)el.scrollTop=el.scrollHeight;
|
||||||
else if(savedScroll!==null)el.scrollTop=savedScroll;
|
else if(savedScroll!==null)el.scrollTop=savedScroll;
|
||||||
}
|
}
|
||||||
@@ -867,6 +909,7 @@ function openSettings(){
|
|||||||
document.getElementById('s-mode-id').value=d.mode_id||'';
|
document.getElementById('s-mode-id').value=d.mode_id||'';
|
||||||
document.getElementById('s-default-slot').value=d.default_ams_slot||'auto';
|
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);
|
document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling);
|
||||||
|
var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print;
|
||||||
});
|
});
|
||||||
var v=localStorage.getItem('pollInterval')||'2000';
|
var v=localStorage.getItem('pollInterval')||'2000';
|
||||||
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
|
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
|
||||||
@@ -1007,6 +1050,7 @@ function saveSettings(){
|
|||||||
mode_id: document.getElementById('s-mode-id').value,
|
mode_id: document.getElementById('s-mode-id').value,
|
||||||
default_ams_slot: document.getElementById('s-default-slot').value,
|
default_ams_slot: document.getElementById('s-default-slot').value,
|
||||||
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
|
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
|
||||||
|
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
|
||||||
}).then(function(){
|
}).then(function(){
|
||||||
btn.textContent=T.update_restarting;
|
btn.textContent=T.update_restarting;
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
@@ -1577,6 +1621,10 @@ function formatDur(sec){
|
|||||||
var _storeFileId=null;
|
var _storeFileId=null;
|
||||||
var _storeFilename=null;
|
var _storeFilename=null;
|
||||||
var _filamentDialogMode='store'; // 'store' oder 'banner'
|
var _filamentDialogMode='store'; // 'store' oder 'banner'
|
||||||
|
// GCode-Store-Dateiliste. MUSS deklariert sein – sonst ReferenceError, wenn
|
||||||
|
// "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen
|
||||||
|
// wurde (Issue #29 / Theme-Auslagerung PR #27).
|
||||||
|
var storeFiles=[];
|
||||||
|
|
||||||
var _gcodeFilaments=[];
|
var _gcodeFilaments=[];
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,10 @@
|
|||||||
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
|
<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>
|
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||||
|
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
||||||
|
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -406,7 +410,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
|
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
|
||||||
<span><span>≡</span> <span id="ptitle-console">Ereignis-Log</span></span>
|
<span><span>≡</span> <span id="ptitle-console">Ereignis-Log</span></span>
|
||||||
<a id="btn-log-dl" href="/api/log/download" download="kx-bridge.log"
|
<a id="btn-log-dl" href="/api/log/download" download
|
||||||
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none">⬇ Download</a>
|
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none">⬇ Download</a>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;align-items:center">
|
<div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;align-items:center">
|
||||||
@@ -423,6 +427,10 @@
|
|||||||
<button class="log-dir-btn active" id="logdir-all" onclick="setLogDir('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
<button class="log-dir-btn active" id="logdir-all" onclick="setLogDir('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||||
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">RX</button>
|
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">RX</button>
|
||||||
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">TX</button>
|
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">TX</button>
|
||||||
|
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level">Level:</span>
|
||||||
|
<button class="log-lvl-btn active" id="loglvl-all" onclick="setLogLevel('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||||
|
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⛔ Errors</button>
|
||||||
|
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⚠ Warn</button>
|
||||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px">Topic:</span>
|
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px">Topic:</span>
|
||||||
<button class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">AMS</button>
|
<button class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">AMS</button>
|
||||||
<button class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">print</button>
|
<button class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">print</button>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
:root{
|
:root{
|
||||||
|
color-scheme:dark; /* native Form-Controls (select) im Webview dunkel rendern */
|
||||||
--bg:#1a1a1f;--card:#24242c;--raised:#2e2e3a;--border:#3a3a4a;
|
--bg:#1a1a1f;--card:#24242c;--raised:#2e2e3a;--border:#3a3a4a;
|
||||||
--txt:#f0f0f5;--txt2:#8888aa;--accent:#00c8ff;--accent2:#ff6b35;
|
--txt:#f0f0f5;--txt2:#8888aa;--accent:#00c8ff;--accent2:#ff6b35;
|
||||||
--ok:#4cde80;--err:#ff4d6d;--warn:#ffb020;
|
--ok:#4cde80;--err:#ff4d6d;--warn:#ffb020;
|
||||||
@@ -6,12 +7,17 @@
|
|||||||
--mono:"JetBrains Mono","Fira Code",monospace;
|
--mono:"JetBrains Mono","Fira Code",monospace;
|
||||||
}
|
}
|
||||||
[data-theme=light]{
|
[data-theme=light]{
|
||||||
|
color-scheme:light;
|
||||||
--bg:#f0f0f5;--card:#fff;--raised:#e8e8f0;--border:#d0d0e0;
|
--bg:#f0f0f5;--card:#fff;--raised:#e8e8f0;--border:#d0d0e0;
|
||||||
--txt:#1a1a2e;--txt2:#666680;
|
--txt:#1a1a2e;--txt2:#666680;
|
||||||
}
|
}
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
body{background:var(--bg);color:var(--txt);font-family:var(--font);font-size:14px;min-height:100vh;display:flex;flex-direction:column}
|
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}
|
a{color:var(--accent);text-decoration:none}
|
||||||
|
/* select/option-Farben explizit setzen — OrcaSlicers Device-Tab-Webview erbt
|
||||||
|
sie sonst nicht und rendert weiße Schrift auf weißem Grund (Issue #29). */
|
||||||
|
select{background:var(--raised)!important;color:var(--txt)!important}
|
||||||
|
select option{background:var(--card)!important;color:var(--txt)!important}
|
||||||
|
|
||||||
/* ── HEADER ── */
|
/* ── HEADER ── */
|
||||||
header{background:var(--card);border-bottom:1px solid var(--border);
|
header{background:var(--card);border-bottom:1px solid var(--border);
|
||||||
|
|||||||
Reference in New Issue
Block a user