@@ -26,6 +26,8 @@ import sys
import tempfile
import tempfile
import time
import time
import threading
import threading
import io
import zipfile
# Bei PyInstaller-Binary liegt alles neben sys.executable, sonst neben __file__
# Bei PyInstaller-Binary liegt alles neben sys.executable, sonst neben __file__
_BASE = os . path . dirname ( sys . executable ) if getattr ( sys , " frozen " , False ) else os . path . dirname ( os . path . abspath ( __file__ ) )
_BASE = os . path . dirname ( sys . executable ) if getattr ( sys , " frozen " , False ) else os . path . dirname ( os . path . abspath ( __file__ ) )
@@ -128,10 +130,139 @@ def _parse_gcode_estimated_time(data: bytes) -> int:
elif unit == " m " : secs + = int ( val ) * 60
elif unit == " m " : secs + = int ( val ) * 60
elif unit == " s " : secs + = int ( val )
elif unit == " s " : secs + = int ( val )
if secs :
if secs :
log . info ( f " Slicer-Schätzzeit : { secs } s ( { m . group ( 1 ) . strip ( ) } ) " )
log . info ( f " Slicer estimate : { secs } s ( { m . group ( 1 ) . strip ( ) } ) " )
return secs
return secs
_FILAMENT_COLOR_KEYS = (
" filament_colour " , " filament_color " , " filament_colours " , " filament_colors " ,
" extruder_colour " , " extruder_color " ,
)
_FILAMENT_MATERIAL_KEYS = (
" filament_type " , " filament_types " , " filament_settings_id " , " filament_preset " ,
)
_KNOWN_MATERIALS = (
" PLA-CF " , " PETG-CF " , " PA-CF " , " PLA SILK " , " PETG " , " PLA " , " ABS " , " ASA " ,
" TPU " , " PA " , " PC " , " HIPS " , " PVA " ,
)
def _normalize_material ( value ) - > str :
text = str ( value or " " ) . upper ( ) . replace ( " _ " , " " ) . replace ( " - " , " - " ) . strip ( )
for material in _KNOWN_MATERIALS :
if re . search ( rf " (^|[^A-Z0-9]) { re . escape ( material ) } ([^A-Z0-9]|$) " , text ) :
return material
return text . split ( ) [ 0 ] if text else " PLA "
def _color_to_rgb ( value , default = None ) :
if default is None :
default = [ 255 , 255 , 255 ]
if isinstance ( value , list ) and len ( value ) > = 3 :
try :
return [ max ( 0 , min ( 255 , int ( value [ 0 ] ) ) ) ,
max ( 0 , min ( 255 , int ( value [ 1 ] ) ) ) ,
max ( 0 , min ( 255 , int ( value [ 2 ] ) ) ) ]
except Exception :
return default
if isinstance ( value , str ) :
m = re . search ( r " #?([0-9a-fA-F] {6} )(?:[0-9a-fA-F] {2} )? " , value )
if m :
raw = m . group ( 1 )
return [ int ( raw [ 0 : 2 ] , 16 ) , int ( raw [ 2 : 4 ] , 16 ) , int ( raw [ 4 : 6 ] , 16 ) ]
return default
def _rgba ( color ) - > list [ int ] :
rgb = _color_to_rgb ( color )
return [ rgb [ 0 ] , rgb [ 1 ] , rgb [ 2 ] , 255 ]
def _rgb_distance ( a , b ) - > int :
ar , ag , ab = _color_to_rgb ( a )
br , bg , bb = _color_to_rgb ( b )
return ( ar - br ) * * 2 + ( ag - bg ) * * 2 + ( ab - bb ) * * 2
def _parse_colors_from_text ( text : str ) - > list [ list [ int ] ] :
colors = [ ]
for match in re . finditer ( r " #?([0-9a-fA-F] {6} )(?:[0-9a-fA-F] {2} )? " , text or " " ) :
raw = match . group ( 1 )
colors . append ( [ int ( raw [ 0 : 2 ] , 16 ) , int ( raw [ 2 : 4 ] , 16 ) , int ( raw [ 4 : 6 ] , 16 ) ] )
return colors
def _split_config_values ( value : str ) - > list [ str ] :
value = ( value or " " ) . strip ( ) . strip ( " [] " )
parts = re . split ( r " [;,] " , value )
return [ p . strip ( ) . strip ( " \" ' " ) for p in parts if p . strip ( ) . strip ( " \" ' " ) ]
def _extract_config_value ( line : str ) - > str :
attr = re . search ( r " value=[ \" ' ]([^ \" ' ]+)[ \" ' ] " , line )
if attr :
return attr . group ( 1 )
if " = " in line :
return line . split ( " = " , 1 ) [ 1 ] . strip ( )
if " : " in line :
return line . split ( " : " , 1 ) [ 1 ] . strip ( )
return line
def _parse_filament_metadata_text ( text : str ) - > dict :
colors : list [ list [ int ] ] = [ ]
materials : list [ str ] = [ ]
for line in ( text or " " ) . splitlines ( ) :
low = line . lower ( )
if any ( k in low for k in _FILAMENT_COLOR_KEYS ) :
value = _extract_config_value ( line )
for color in _parse_colors_from_text ( value ) :
if color not in colors :
colors . append ( color )
if any ( k in low for k in _FILAMENT_MATERIAL_KEYS ) :
value = _extract_config_value ( line )
for part in _split_config_values ( value ) :
mat = _normalize_material ( part )
if mat and mat not in materials :
materials . append ( mat )
return { " colors " : colors , " materials " : materials }
def _merge_filament_metadata ( target : dict , source : dict ) :
for key in ( " colors " , " materials " ) :
for value in source . get ( key , [ ] ) :
if value not in target [ key ] :
target [ key ] . append ( value )
def _parse_uploaded_filament_metadata ( data : bytes , filename : str ) - > dict :
""" Best-effort extraction of slicer filament colors/materials from G-code or 3MF. """
meta = { " colors " : [ ] , " materials " : [ ] , " source " : " " }
suffix = pathlib . Path ( filename or " " ) . suffix . lower ( )
if suffix == " .3mf " or data [ : 4 ] == b " PK \x03 \x04 " :
try :
with zipfile . ZipFile ( io . BytesIO ( data ) ) as zf :
for name in zf . namelist ( ) :
lname = name . lower ( )
if not lname . endswith ( ( " .config " , " .json " , " .model " , " .xml " , " .gcode " ) ) :
continue
if zf . getinfo ( name ) . file_size > 2_000_000 :
continue
text = zf . read ( name ) . decode ( " utf-8 " , errors = " ignore " )
before = len ( meta [ " colors " ] ) + len ( meta [ " materials " ] )
_merge_filament_metadata ( meta , _parse_filament_metadata_text ( text ) )
if len ( meta [ " colors " ] ) + len ( meta [ " materials " ] ) > before and not meta [ " source " ] :
meta [ " source " ] = name
except Exception as e :
log . warning ( f " 3MF metadata could not be parsed: { e } " )
else :
search = ( data [ : 131072 ] + data [ - 262144 : ] ) . decode ( " utf-8 " , errors = " ignore " )
_merge_filament_metadata ( meta , _parse_filament_metadata_text ( search ) )
if meta [ " colors " ] or meta [ " materials " ] :
meta [ " source " ] = " gcode "
return meta
class KobraXBridge :
class KobraXBridge :
def __init__ ( self , client : KobraXClient , args = None ) :
def __init__ ( self , client : KobraXClient , args = None ) :
self . client = client
self . client = client
@@ -167,6 +298,7 @@ class KobraXBridge:
self . _ams_slots : list [ dict ] = [ ]
self . _ams_slots : list [ dict ] = [ ]
self . _ams_loaded_slot : int = - 1
self . _ams_loaded_slot : int = - 1
self . _last_uploaded_file : str = " "
self . _last_uploaded_file : str = " "
self . _uploaded_filament_metadata : dict [ str , dict ] = { }
self . _serve_dir = tempfile . TemporaryDirectory ( prefix = " kobrax_serve_ " )
self . _serve_dir = tempfile . TemporaryDirectory ( prefix = " kobrax_serve_ " )
self . _serve_dir_path : str = self . _serve_dir . name
self . _serve_dir_path : str = self . _serve_dir . name
@@ -200,6 +332,11 @@ class KobraXBridge:
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 " ] = " "
self . _state [ " file_ready " ] = " "
self . _state [ " print_duration " ] = 0
self . _state [ " remain_time " ] = 0
self . _state [ " slicer_time " ] = 0
self . _thumbnail_b64 = " "
self . _state [ " filename " ] = d . get ( " filename " , self . _state [ " filename " ] )
self . _state [ " filename " ] = d . get ( " filename " , self . _state [ " filename " ] )
if " progress " in d :
if " progress " in d :
self . _state [ " progress " ] = float ( d [ " progress " ] ) / 100.0
self . _state [ " progress " ] = float ( d [ " progress " ] ) / 100.0
@@ -251,7 +388,7 @@ class KobraXBridge:
thumb = details . get ( " thumbnail " ) or details . get ( " png_image " ) or " "
thumb = details . get ( " thumbnail " ) or details . get ( " png_image " ) or " "
if thumb :
if thumb :
self . _thumbnail_b64 = thumb
self . _thumbnail_b64 = thumb
log . info ( f " Vorschaubild empfangen : { len ( thumb ) } Zeichen base64 " )
log . info ( f " Thumbnail received : { len ( thumb ) } base64 chars " )
self . _push_status_update ( )
self . _push_status_update ( )
def _on_multicolor_box ( self , payload : dict ) :
def _on_multicolor_box ( self , payload : dict ) :
@@ -281,7 +418,7 @@ class KobraXBridge:
threading . Thread ( target = _tip_form , daemon = True ) . start ( )
threading . Thread ( target = _tip_form , daemon = True ) . start ( )
if slots :
if slots :
self . _ams_slots = slots
self . _ams_slots = slots
log . info ( f " AMS-S lots empfangen : { len ( slots ) } , loaded_slot= { self . _ams_loaded_slot } " )
log . info ( f " AMS s lots received : { len ( slots ) } , loaded_slot= { self . _ams_loaded_slot } " )
self . _push_status_update ( )
self . _push_status_update ( )
# OrcaSlicer filament preset IDs (MoonrakerPrinterAgent.cpp mapping)
# OrcaSlicer filament preset IDs (MoonrakerPrinterAgent.cpp mapping)
@@ -294,7 +431,7 @@ class KobraXBridge:
}
}
def _build_lane_data ( self ) - > dict :
def _build_lane_data ( self ) - > dict :
""" Baut BBL-AMS- JSON fü r OrcaSlicer DevFilaSystemParser::ParseV1_0. """
""" Build BBL-AMS JSON fo r OrcaSlicer DevFilaSystemParser::ParseV1_0. """
slots = self . _ams_slots
slots = self . _ams_slots
total = len ( slots )
total = len ( slots )
if total == 0 :
if total == 0 :
@@ -325,7 +462,7 @@ class KobraXBridge:
color_hex = color_raw [ : 6 ] . upper ( ) + " FF "
color_hex = color_raw [ : 6 ] . upper ( ) + " FF "
else :
else :
color_hex = " FFFFFFFF "
color_hex = " FFFFFFFF "
material = slot . get ( " type " , " PLA " ) . upper ( )
material = _normalize_material ( slot. get ( " type " , " PLA " ) )
tray_info_idx = self . _TRAY_INFO_IDX . get ( material , " OGFL99 " )
tray_info_idx = self . _TRAY_INFO_IDX . get ( material , " OGFL99 " )
tray_array . append ( {
tray_array . append ( {
" id " : str ( slot_id ) ,
" id " : str ( slot_id ) ,
@@ -352,6 +489,118 @@ class KobraXBridge:
" tray_exist_bits " : format ( tray_exist_bits , " X " ) ,
" tray_exist_bits " : format ( tray_exist_bits , " X " ) ,
}
}
def _uploaded_metadata_for ( self , filename : str ) - > dict :
if not filename :
return { " colors " : [ ] , " materials " : [ ] , " source " : " " }
return ( self . _uploaded_filament_metadata . get ( filename )
or self . _uploaded_filament_metadata . get ( os . path . basename ( filename ) )
or { " colors " : [ ] , " materials " : [ ] , " source " : " " } )
def _loaded_ams_slots ( self ) - > list [ tuple [ int , dict ] ] :
return [ ( i , s ) for i , s in enumerate ( self . _ams_slots ) if s . get ( " status " ) == 5 ]
def _filtered_loaded_ams_slots ( self ) - > list [ tuple [ int , dict ] ] :
loaded = self . _loaded_ams_slots ( )
default_slot = getattr ( self . _args , " default_ams_slot " , " auto " )
if default_slot == " auto " :
return loaded
try :
slot_idx = int ( default_slot )
except ValueError :
return loaded
selected = [ ( i , s ) for i , s in loaded if i == slot_idx ]
if selected :
return selected
log . warning ( f " Default slot { slot_idx } is empty - falling back to Auto " )
return loaded
def _build_ams_assignments ( self , filename : str ) - > list [ dict ] :
loaded = self . _filtered_loaded_ams_slots ( )
if not loaded :
return [ ]
metadata = self . _uploaded_metadata_for ( filename )
colors = metadata . get ( " colors " ) or [ ]
materials = metadata . get ( " materials " ) or [ ]
target_count = max ( len ( colors ) , len ( materials ) )
# Without slicer metadata, preserve the previous behavior: advertise all
# occupied slots and let the printer/firmware use its normal fallback.
if target_count == 0 :
return [ {
" paint_index " : i ,
" slot_index " : i ,
" paint_color " : _rgba ( slot . get ( " color " , [ 255 , 255 , 255 ] ) ) ,
" ams_color " : _rgba ( slot . get ( " color " , [ 255 , 255 , 255 ] ) ) ,
" material_type " : _normalize_material ( slot . get ( " type " , " PLA " ) ) ,
" reason " : " loaded " ,
} for i , slot in loaded ]
assignments = [ ]
used_slots : set [ int ] = set ( )
for paint_index in range ( target_count ) :
target_color = colors [ paint_index ] if paint_index < len ( colors ) else None
target_material = ( _normalize_material ( materials [ paint_index ] )
if paint_index < len ( materials ) else " " )
candidates = loaded
if target_material :
material_matches = [
( i , s ) for i , s in candidates
if _normalize_material ( s . get ( " type " , " PLA " ) ) == target_material
]
if material_matches :
candidates = material_matches
unused = [ ( i , s ) for i , s in candidates if i not in used_slots ]
if unused :
candidates = unused
def _score ( item ) :
slot_index , slot = item
score = 0
if target_color is not None :
score + = _rgb_distance ( target_color , slot . get ( " color " , [ 255 , 255 , 255 ] ) )
if target_material and _normalize_material ( slot . get ( " type " , " PLA " ) ) != target_material :
score + = 200000
return score , slot_index
slot_index , slot = min ( candidates , key = _score )
used_slots . add ( slot_index )
assignments . append ( {
" paint_index " : paint_index ,
" slot_index " : slot_index ,
" paint_color " : _rgba ( target_color or slot . get ( " color " , [ 255 , 255 , 255 ] ) ) ,
" ams_color " : _rgba ( slot . get ( " color " , [ 255 , 255 , 255 ] ) ) ,
" material_type " : _normalize_material ( slot . get ( " type " , target_material or " PLA " ) ) ,
" target_material " : target_material ,
" reason " : " metadata " ,
} )
summary = [
f " T { a [ ' paint_index ' ] } →S { a [ ' slot_index ' ] } { a [ ' material_type ' ] } "
for a in assignments
]
log . info ( f " AMS metadata mapping for { filename } : { ' , ' . join ( summary ) } " )
return assignments
def _build_anycubic_ams_mapping ( self , filename : str ) - > list [ dict ] :
return [ {
" paint_index " : a [ " paint_index " ] ,
" ams_index " : a [ " slot_index " ] ,
" paint_color " : a [ " paint_color " ] ,
" ams_color " : a [ " ams_color " ] ,
" material_type " : a [ " material_type " ] ,
} for a in self . _build_ams_assignments ( filename ) ]
def _build_simple_ams_mapping ( self , filename : str ) - > list [ dict ] :
return [ {
" slot_index " : a [ " slot_index " ] ,
" material_type " : a [ " material_type " ] ,
" color " : a [ " ams_color " ] [ : 3 ] ,
" paint_index " : a [ " paint_index " ] ,
" paint_color " : a [ " paint_color " ] [ : 3 ] ,
} for a in self . _build_ams_assignments ( filename ) ]
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
# WebSocket push
# WebSocket push
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
@@ -540,6 +789,16 @@ class KobraXBridge:
# Slicer-Zeitschätzung aus GCode-Header auslesen
# Slicer-Zeitschätzung aus GCode-Header auslesen
self . _state [ " slicer_time " ] = _parse_gcode_estimated_time ( file_data )
self . _state [ " slicer_time " ] = _parse_gcode_estimated_time ( file_data )
filament_meta = _parse_uploaded_filament_metadata ( file_data , remote_filename )
self . _uploaded_filament_metadata [ remote_filename ] = filament_meta
self . _uploaded_filament_metadata [ os . path . basename ( remote_filename ) ] = filament_meta
if filament_meta . get ( " colors " ) or filament_meta . get ( " materials " ) :
log . info (
" Upload filament metadata: "
f " colors= { filament_meta . get ( ' colors ' , [ ] ) } "
f " materials= { filament_meta . get ( ' materials ' , [ ] ) } "
f " source= { filament_meta . get ( ' source ' , ' ' ) } "
)
# Datei auf Disk ablegen (temp-Verzeichnis) damit Drucker sie per HTTP abrufen kann
# Datei auf Disk ablegen (temp-Verzeichnis) damit Drucker sie per HTTP abrufen kann
safe_name = os . path . basename ( remote_filename ) # keine Pfad-Traversal
safe_name = os . path . basename ( remote_filename ) # keine Pfad-Traversal
@@ -549,7 +808,7 @@ class KobraXBridge:
del file_data # RAM freigeben
del file_data # RAM freigeben
self . _last_uploaded_file = remote_filename
self . _last_uploaded_file = remote_filename
log . info ( f " Upload: { remote_filename } ( { file_size } bytes) md5= { file_md5 } → Druck er " )
log . info ( f " Upload: { remote_filename } ( { file_size } bytes) md5= { file_md5 } -> print er " )
# Datei per HTTP auf den Drucker hochladen (serve_path liegt bereits auf Disk)
# Datei per HTTP auf den Drucker hochladen (serve_path liegt bereits auf Disk)
upload_url = self . _state . get ( " upload_url " ) or None
upload_url = self . _state . get ( " upload_url " ) or None
@@ -559,10 +818,10 @@ class KobraXBridge:
None , self . client . upload_gcode , serve_path , remote_filename , upload_url
None , self . client . upload_gcode , serve_path , remote_filename , upload_url
)
)
except Exception as e :
except Exception as e :
log . error ( f " Upload fehlgeschlagen : { e } " )
log . error ( f " Upload failed : { e } " )
return web . json_response ( { " error " : str ( e ) } , status = 500 )
return web . json_response ( { " error " : str ( e ) } , status = 500 )
log . info ( f " Upload erfolgreich : { result } " )
log . info ( f " Upload succeeded : { result } " )
# Druck starten mit vollständigem Payload (inkl. serve-URL + md5 + size)
# Druck starten mit vollständigem Payload (inkl. serve-URL + md5 + size)
serve_url = f " http:// { request . host } /serve/ { remote_filename } "
serve_url = f " http:// { request . host } /serve/ { remote_filename } "
@@ -582,7 +841,7 @@ class KobraXBridge:
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 ) )
else :
else :
log . info ( f " Nur hochgeladen (print=false): { remote_filename } " )
log . info ( f " Uploaded only (print=false): { remote_filename } " )
self . _state [ " file_ready " ] = remote_filename
self . _state [ " file_ready " ] = remote_filename
# OctoPrint-kompatibler Response (OrcaSlicer wertet refs aus)
# OctoPrint-kompatibler Response (OrcaSlicer wertet refs aus)
@@ -607,31 +866,12 @@ class KobraXBridge:
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 ) :
self . _state [ " file_ready " ] = " "
self . _state [ " file_ready " ] = " "
default_slot = getattr ( self . _args , " default_ams_slot " , " auto " )
ams_box_mapping = self . _build_anycubic_ams_mapping ( filename )
all_loaded = [ ( i , s ) for i , s in enumerate ( self . _ams_slots ) if s . get ( " status " ) == 5 ]
use_ams = len ( ams_box_mapping ) > 0
if default_slot != " auto " :
log . info (
try :
f " AMS-Slots: { len ( self . _loaded_ams_slots ( ) ) } / { len ( self . _ams_slots ) } belegt "
slot_idx = int ( default_slot )
f " → { [ m [ ' ams_index ' ] for m in ams_box_mapping ] } "
loaded = [ ( i , s ) for i , s in all_loaded if i == slot_idx ]
)
if not loaded :
log . warning ( f " Standard-Slot { slot_idx } ist leer – fallback auf Auto " )
loaded = all_loaded
except ValueError :
loaded = all_loaded
else :
loaded = all_loaded
use_ams = len ( loaded ) > 0
ams_box_mapping = [
{
" paint_index " : i ,
" ams_index " : i ,
" paint_color " : [ 255 , 255 , 255 , 255 ] ,
" ams_color " : [ 255 , 255 , 255 , 255 ] ,
" material_type " : s . get ( " type " , " PLA " ) ,
}
for i , s in loaded
]
log . info ( f " AMS-Slots: { len ( loaded ) } / { len ( self . _ams_slots ) } belegt → { [ 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 " ,
@@ -660,9 +900,9 @@ class KobraXBridge:
log . info ( f " print/start → { filename } url= { url } ams= { len ( self . _ams_slots ) } slots " )
log . info ( f " print/start → { filename } url= { url } ams= { len ( self . _ams_slots ) } slots " )
result = self . client . publish ( " print " , " start " , payload , timeout = 15.0 )
result = self . client . publish ( " print " , " start " , payload , timeout = 15.0 )
if result :
if result :
log . info ( f " Druckstart bestätigt : state={ result . get ( ' state ' ) } " )
log . info ( f " Print start confirmed : state={ result . get ( ' state ' ) } " )
else :
else :
log . warning ( " Druck start: keine Antwort vom Druck er" )
log . warning ( " Print start: no response from print er" )
async def handle_print_start ( self , request ) :
async def handle_print_start ( self , request ) :
try :
try :
@@ -675,38 +915,9 @@ class KobraXBridge:
if not filename :
if not filename :
return web . json_response ( { " error " : " no filename " } , status = 400 )
return web . json_response ( { " error " : " no filename " } , status = 400 )
log . info ( f " Druck starten : { filename } " )
log . info ( f " Starting print : { filename } " )
# AMS-Mapping aus gecachtem State — leere Slots (status != 5) überspringen
default_slot = getattr ( self . _args , " default_ams_slot " , " auto " )
ams_box_mapping = [ ]
for i , slot in enumerate ( self . _ams_slots ) :
if slot . get ( " status " ) != 5 :
log . info ( f " AMS-Slot { i } leer (status= { slot . get ( ' status ' ) } ) – übersprungen " )
continue
if default_slot != " auto " :
try :
if i != int ( default_slot ) :
continue
except ValueError :
pass
ams_box_mapping . append ( {
" slot_index " : i ,
" material_type " : slot . get ( " type " , " PLA " ) ,
" color " : slot . get ( " color " , [ 255 , 255 , 255 ] ) ,
} )
# Fallback auf alle belegten Slots wenn gewählter Slot leer war
if default_slot != " auto " and not ams_box_mapping :
log . warning ( f " Standard-Slot { default_slot } leer – fallback auf alle belegten Slots " )
for i , slot in enumerate ( self . _ams_slots ) :
if slot . get ( " status " ) != 5 :
continue
ams_box_mapping . append ( {
" slot_index " : i ,
" material_type " : slot . get ( " type " , " PLA " ) ,
" color " : slot . get ( " color " , [ 255 , 255 , 255 ] ) ,
} )
ams_box_mapping = self . _build_simple_ams_mapping ( filename )
use_ams = len ( ams_box_mapping ) > 0
use_ams = len ( ams_box_mapping ) > 0
payload = {
payload = {
@@ -722,7 +933,7 @@ class KobraXBridge:
None , lambda : self . client . publish ( " print " , " start " , payload , timeout = 15.0 )
None , lambda : self . client . publish ( " print " , " start " , payload , timeout = 15.0 )
)
)
if result is None :
if result is None :
return web . json_response ( { " error " : " Keine Antwort vom Druck er" } , status = 504 )
return web . json_response ( { " error " : " No response from print er" } , status = 504 )
return web . json_response ( { " result " : " ok " } )
return web . json_response ( { " result " : " ok " } )
@@ -1353,7 +1564,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<span class= " slider-val " id= " d-fan-val " >0</span>
<span class= " slider-val " id= " d-fan-val " >0</span>
</div>
</div>
<div style= " margin-top:12px;display:flex;gap:8px;flex-wrap:wrap " >
<div style= " margin-top:12px;display:flex;gap:8px;flex-wrap:wrap " >
<button class= " btn btn-sm " style= " background:var(--raised);color:var(--txt) " onclick= " quickFan(0) " >Aus </button>
<button class= " btn btn-sm " style= " background:var(--raised);color:var(--txt) " onclick= " quickFan(0) " ><span class= " lbl-off " >Aus</span> </button>
<button class= " btn btn-sm " style= " background:var(--raised);color:var(--txt) " onclick= " quickFan(25) " >25 % </button>
<button class= " btn btn-sm " style= " background:var(--raised);color:var(--txt) " onclick= " quickFan(25) " >25 % </button>
<button class= " btn btn-sm " style= " background:var(--raised);color:var(--txt) " onclick= " quickFan(50) " >50 % </button>
<button class= " btn btn-sm " style= " background:var(--raised);color:var(--txt) " onclick= " quickFan(50) " >50 % </button>
<button class= " btn btn-sm " style= " background:var(--raised);color:var(--txt) " onclick= " quickFan(75) " >75 % </button>
<button class= " btn btn-sm " style= " background:var(--raised);color:var(--txt) " onclick= " quickFan(75) " >75 % </button>
@@ -1390,23 +1601,23 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<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= " kx-bridge.log "
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 " >⬇ <span id= " lbl-log-download " > Download</span></ 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 " >
<input id= " log-filter " type= " text " placeholder= " Filter… "
<input id= " log-filter " type= " text " placeholder= " Filter… "
oninput= " renderLog() "
oninput= " renderLog() "
style= " flex:1;min-width:120px;padding:5px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;font-family:var(--mono) " >
style= " flex:1;min-width:120px;padding:5px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;font-family:var(--mono) " >
<button id= " btn-autoscroll " onclick= " toggleAutoScroll() "
<button id= " btn-autoscroll " onclick= " toggleAutoScroll() "
style= " font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap " >⬇ Auto </button>
style= " font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap " >⬇ <span id= " lbl-log-auto " >Auto</span> </button>
<button onclick= " consoleLogs=[];renderLog() "
<button onclick= " consoleLogs=[];renderLog() "
style= " font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer " >✕ Clear </button>
style= " font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer " >✕ <span id= " lbl-log-clear " >Clear</span> </button>
</div>
</div>
<div style= " display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap " >
<div style= " display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap " >
<span style= " font-size:11px;color:var(--txt2);align-self:center;margin-right:2px " >Dir:</span>
<span id= " lbl-log-dir " style= " font-size:11px;color:var(--txt2);align-self:center;margin-right:2px " >Dir:</span>
<button class= " log-dir-btn active " id= " logdir-all " onclick= " setLogDir( ' all ' ) " style= " font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer " ></button>
<button class= " log-dir-btn 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 " >Topic:</span>
<span id= " lbl-log-topic " 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>
<button class= " log-topic-btn " data-topic= " info " onclick= " setLogTopic( ' info ' ) " style= " font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer " >info</button>
<button class= " log-topic-btn " data-topic= " info " onclick= " setLogTopic( ' info ' ) " style= " font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer " >info</button>
@@ -1464,16 +1675,20 @@ var LANG_DE={
confirm_cancel: ' Druck wirklich abbrechen? ' ,
confirm_cancel: ' Druck wirklich abbrechen? ' ,
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_ip: ' Drucker-IP ' ,settings_mqtt_port: ' MQTT-Port ' ,
settings_save: ' Speichern & Neustart ' ,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 ' ,settings_device_placeholder: ' 32 Hex-Zeichen ' , 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 ' ,
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 ' ,
lbl_conn_error: ' Verbindungsfehler: ' ,
lbl_conn_error: ' Verbindungsfehler: ' ,
settings_button_title: ' Einstellungen ' ,
slot_edit_title: ' Slot bearbeiten ' ,slot_edit_color: ' Farbe ' ,slot_edit_material: ' Material ' ,
slot_edit_title: ' Slot bearbeiten ' ,slot_edit_color: ' Farbe ' ,slot_edit_material: ' Material ' ,
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: ' Al le ' ,
ams_slot_select: ' Slot auswäh len ' ,
log_dir_all: ' Alle ' ,log_download: ' Download ' ,log_auto: ' Auto ' ,log_clear: ' Leeren ' ,log_dir: ' Richtung: ' ,log_topic: ' Topic: ' ,
log_settings_error: ' Settings-Fehler: ' ,log_axis_error: ' Achsen-Fehler: ' ,log_home_error: ' Home-Fehler: ' ,log_motors_error: ' Motoren-Fehler: ' ,log_temp_error: ' Temp-Fehler: ' ,log_light_error: ' Licht-Fehler: ' ,log_speed_error: ' Speed-Fehler: ' ,log_fan_error: ' Lüfter-Fehler: ' ,log_ams_error: ' AMS-Fehler: ' ,log_stream_unavailable: ' Stream nicht verfügbar ' ,
print_action_pause: ' Pause ' ,print_action_resume: ' Fortsetzen ' ,print_action_cancel: ' Abbrechen ' ,
file_ready_btn: ' ▶ Druck starten ' ,
file_ready_btn: ' ▶ Druck starten ' ,
file_cancel_btn: ' ✕ Abbrechen '
file_cancel_btn: ' ✕ Abbrechen '
};
};
@@ -1498,16 +1713,20 @@ var LANG_EN={
confirm_cancel: ' Really cancel the print? ' ,
confirm_cancel: ' Really cancel the print? ' ,
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_ip: ' Printer IP ' ,settings_mqtt_port: ' MQTT Port ' ,
settings_save: ' Save & Restart ' ,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 ' ,settings_device_placeholder: ' 32 hex characters ' , 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 ' ,
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 ' ,
lbl_conn_error: ' Connection error: ' ,
lbl_conn_error: ' Connection error: ' ,
settings_button_title: ' Settings ' ,
slot_edit_title: ' Edit Slot ' ,slot_edit_color: ' Color ' ,slot_edit_material: ' Material ' ,
slot_edit_title: ' Edit Slot ' ,slot_edit_color: ' Color ' ,slot_edit_material: ' Material ' ,
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 ' ,
ams_slot_select: ' Select slot ' ,
log_dir_all: ' All ' ,log_download: ' Download ' ,log_auto: ' Auto ' ,log_clear: ' Clear ' ,log_dir: ' Dir: ' ,log_topic: ' Topic: ' ,
log_settings_error: ' Settings error: ' ,log_axis_error: ' Axis error: ' ,log_home_error: ' Home error: ' ,log_motors_error: ' Motor error: ' ,log_temp_error: ' Temperature error: ' ,log_light_error: ' Light error: ' ,log_speed_error: ' Speed error: ' ,log_fan_error: ' Fan error: ' ,log_ams_error: ' AMS error: ' ,log_stream_unavailable: ' Stream unavailable ' ,
print_action_pause: ' Pause ' ,print_action_resume: ' Resume ' ,print_action_cancel: ' Cancel ' ,
file_ready_btn: ' ▶ Start Print ' ,
file_ready_btn: ' ▶ Start Print ' ,
file_cancel_btn: ' ✕ Cancel '
file_cancel_btn: ' ✕ Cancel '
};
};
@@ -1575,12 +1794,14 @@ function applyLang(){
setText( ' lbl-password ' ,T.settings_password);
setText( ' lbl-password ' ,T.settings_password);
setText( ' lbl-device-id ' ,T.settings_device_id);
setText( ' lbl-device-id ' ,T.settings_device_id);
setText( ' lbl-mode-id ' ,T.settings_mode_id);
setText( ' lbl-mode-id ' ,T.settings_mode_id);
var did=document.getElementById( ' s-device-id ' );if(did)did.setAttribute( ' placeholder ' ,T.settings_device_placeholder);
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-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);
var sb=document.getElementById( ' settings-btn ' );if(sb)sb.setAttribute( ' title ' ,T.settings_button_title);
// Speed buttons
// Speed buttons
setText( ' d-spd-lbl-1 ' ,T.speed_silent.replace(/^ \ S+ \ s/, ' ' ));
setText( ' d-spd-lbl-1 ' ,T.speed_silent.replace(/^ \ S+ \ s/, ' ' ));
setText( ' d-spd-lbl-2 ' ,T.speed_normal.replace(/^ \ S+ \ s/, ' ' ));
setText( ' d-spd-lbl-2 ' ,T.speed_normal.replace(/^ \ S+ \ s/, ' ' ));
@@ -1588,6 +1809,8 @@ function applyLang(){
// AMS feed/unload
// AMS feed/unload
document.querySelectorAll( ' .lbl-feed ' ).forEach(e=>e.textContent=T.lbl_feed);
document.querySelectorAll( ' .lbl-feed ' ).forEach(e=>e.textContent=T.lbl_feed);
document.querySelectorAll( ' .lbl-unload ' ).forEach(e=>e.textContent=T.lbl_unload);
document.querySelectorAll( ' .lbl-unload ' ).forEach(e=>e.textContent=T.lbl_unload);
setText( ' ams-no-data ' ,T.ams_no_data);
setText( ' ams-slot-lbl ' ,T.ams_slot_select);
// conn-btn text (nur wenn nicht im Übergangszustand)
// conn-btn text (nur wenn nicht im Übergangszustand)
updateConnBtn();
updateConnBtn();
// Slot-Edit-Dialog
// Slot-Edit-Dialog
@@ -1596,6 +1819,11 @@ function applyLang(){
setText( ' btn-slot-edit-save ' ,T.slot_edit_save);
setText( ' btn-slot-edit-save ' ,T.slot_edit_save);
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( ' lbl-log-download ' ,T.log_download);
setText( ' lbl-log-auto ' ,T.log_auto);
setText( ' lbl-log-clear ' ,T.log_clear);
setText( ' lbl-log-dir ' ,T.log_dir);
setText( ' lbl-log-topic ' ,T.log_topic);
setText( ' file-ready-btn ' ,T.file_ready_btn);
setText( ' file-ready-btn ' ,T.file_ready_btn);
setText( ' file-cancel-btn ' ,T.file_cancel_btn);
setText( ' file-cancel-btn ' ,T.file_cancel_btn);
}
}
@@ -1634,7 +1862,7 @@ var logTopicFilter=''; // '' = no topic filter
function clog(msg,cls) {
function clog(msg,cls) {
cls=cls|| ' msg-info ' ;
cls=cls|| ' msg-info ' ;
var ts=new Date().toLocaleTimeString(' de ' , { hour: ' 2-digit ' ,minute: ' 2-digit ' ,second: ' 2-digit ' });
var ts=new Date().toLocaleTimeString(currentLang=== ' de ' ? ' de ' : ' en ' , { hour: ' 2-digit ' ,minute: ' 2-digit ' ,second: ' 2-digit ' });
_appendLog( { ts:ts,lvl: ' ' ,name: ' ui ' ,msg:msg},cls);
_appendLog( { ts:ts,lvl: ' ' ,name: ' ui ' ,msg:msg},cls);
}
}
function _lvlCls(lvl) {
function _lvlCls(lvl) {
@@ -1846,10 +2074,10 @@ function updateConnBtn(){
var offline=S.kobra_state=== ' offline ' ;
var offline=S.kobra_state=== ' offline ' ;
if(offline) {
if(offline) {
btn.className= ' conn-btn disconnected ' ;
btn.className= ' conn-btn disconnected ' ;
btn.textContent=T.btn_connect|| ' ⚡ Verbinden ' ;
btn.textContent=T.btn_connect|| ' ⚡ Connect ' ;
} else {
} else {
btn.className= ' conn-btn connected ' ;
btn.className= ' conn-btn connected ' ;
btn.textContent=T.btn_disconnect|| ' ✕ Tre nnen ' ;
btn.textContent=T.btn_disconnect|| ' ✕ Disco nnect ' ;
}
}
}
}
@@ -1991,7 +2219,7 @@ function saveSlotEdit(){
closeSlotEdit();
closeSlotEdit();
clog((T.slot_edit_ok|| ' AMS Slot ' )+ ' ' +(_slotEditIndex+1)+ ' : ' +mat+ ' ' +hex, ' msg-ok ' );
clog((T.slot_edit_ok|| ' AMS Slot ' )+ ' ' +(_slotEditIndex+1)+ ' : ' +mat+ ' ' +hex, ' msg-ok ' );
})
})
.catch(function(e) { clog( ' Fehler: ' +e, ' msg-err ' );});
.catch(function(e) { clog((T.log_error|| ' Error: ' )+ ' ' +e, ' msg-err ' );});
}
}
document.addEventListener( ' DOMContentLoaded ' ,function() {
document.addEventListener( ' DOMContentLoaded ' ,function() {
document.getElementById( ' s-printer-ip ' ).addEventListener( ' input ' ,function() {
document.getElementById( ' s-printer-ip ' ).addEventListener( ' input ' ,function() {
@@ -2030,7 +2258,7 @@ function saveSettings(){
},4000);
},4000);
}).catch(function(e) {
}).catch(function(e) {
btn.disabled=false;setText( ' btn-save-settings ' ,T.settings_save);
btn.disabled=false;setText( ' btn-save-settings ' ,T.settings_save);
clog( ' Settings-Fehler: ' +e, ' msg-err ' );
clog((T.log_settings_error|| ' Settings error: ' )+ ' ' +e, ' msg-err ' );
});
});
}
}
function checkUpdate() {
function checkUpdate() {
@@ -2077,7 +2305,7 @@ async function poll(){
Object.assign(S,d);
Object.assign(S,d);
applyState();
applyState();
updateHistory();
updateHistory();
}catch(e) { clog( ' Poll-Fehler: ' +e, ' msg-err ' )}
}catch(e) { clog((T.log_poll_error|| ' Poll error: ' )+ ' ' +e, ' msg-err ' )}
}
}
var pollTimer;
var pollTimer;
(function() {
(function() {
@@ -2087,10 +2315,10 @@ var pollTimer;
// ── Print actions ──
// ── Print actions ──
function printAction(a) {
function printAction(a) {
post( ' /printer/print/ ' +a, {} ).then(function() { clog(' Druck: ' +a , ' msg-ok ' );poll()})
post( ' /printer/print/ ' +a, {} ).then(function() { clog((T.nav_print|| ' Print ' )+ ' : ' +(T[ ' print_action_ ' +a]||a) , ' msg-ok ' );poll()})
.catch(function(e) { clog( ' Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_error|| ' Error: ' )+ ' ' +e, ' msg-err ' )});
}
}
function confirmCancel() { if(confirm(' Druck wirklich abbrechen ? ' ))printAction( ' cancel ' )}
function confirmCancel() { if(confirm(T.confirm_cancel|| ' Really cancel the print ? ' ))printAction( ' cancel ' )}
// ── Axis motion ──
// ── Axis motion ──
// axis codes: 0=X, 1=Y, 2=Z
// axis codes: 0=X, 1=Y, 2=Z
@@ -2106,28 +2334,28 @@ function move(axis,dir,dist){
// axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z
// axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z
var axisMap= { 0:1,1:2,2:3};
var axisMap= { 0:1,1:2,2:3};
post( ' /api/axis ' , { axis:axisMap[axis],move_type:1,distance:dir*dist})
post( ' /api/axis ' , { axis:axisMap[axis],move_type:1,distance:dir*dist})
.then(function() { clog( ' Achse ' +(axis===0? ' X ' :axis===1? ' Y ' : ' Z ' )+ ' ' +(dir>0? ' + ' : ' ' )+dir*dist+ ' mm ' , ' msg-ok ' )})
.then(function() { clog((T.log_axis|| ' Axis ' )+ ' ' +(axis===0? ' X ' :axis===1? ' Y ' : ' Z ' )+ ' ' +(dir>0? ' + ' : ' ' )+dir*dist+ ' mm ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Achse-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_axis_error|| ' Axis error: ' )+ ' ' +e, ' msg-err ' )});
}
}
function homeAll() {
function homeAll() {
post( ' /api/axis ' , { axis:5,move_type:2,distance:0})
post( ' /api/axis ' , { axis:5,move_type:2,distance:0})
.then(function() { clog( ' Home All ' , ' msg-ok ' )})
.then(function() { clog( ' Home All ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Home-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_home_error|| ' Home error: ' )+ ' ' +e, ' msg-err ' )});
}
}
function homeXY() {
function homeXY() {
post( ' /api/axis ' , { axis:4,move_type:2,distance:0})
post( ' /api/axis ' , { axis:4,move_type:2,distance:0})
.then(function() { clog( ' Home XY ' , ' msg-ok ' )})
.then(function() { clog( ' Home XY ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Home-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_home_error|| ' Home error: ' )+ ' ' +e, ' msg-err ' )});
}
}
function homeZ() {
function homeZ() {
post( ' /api/axis ' , { axis:3,move_type:2,distance:0})
post( ' /api/axis ' , { axis:3,move_type:2,distance:0})
.then(function() { clog( ' Home Z ' , ' msg-ok ' )})
.then(function() { clog( ' Home Z ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Home-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_home_error|| ' Home error: ' )+ ' ' +e, ' msg-err ' )});
}
}
function disableMotors() {
function disableMotors() {
post( ' /api/axis ' , { action: ' turnOff ' })
post( ' /api/axis ' , { action: ' turnOff ' })
.then(function() { clog( ' Motors Off ' , ' msg-ok ' )})
.then(function() { clog( ' Motors Off ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Motors-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_motors_error|| ' Motor error: ' )+ ' ' +e, ' msg-err ' )});
}
}
// ── Temperature ──
// ── Temperature ──
@@ -2135,21 +2363,21 @@ function setNozzle(){
var v=parseFloat(document.getElementById( ' p-nozzle-inp ' ).value||0);
var v=parseFloat(document.getElementById( ' p-nozzle-inp ' ).value||0);
post( ' /api/temperature ' , { nozzle:v,bed:S.bed_target})
post( ' /api/temperature ' , { nozzle:v,bed:S.bed_target})
.then(function() { clog( ' Nozzle → ' +v+ ' °C ' , ' msg-ok ' )})
.then(function() { clog( ' Nozzle → ' +v+ ' °C ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Temp-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_temp_error|| ' Temperature error: ' )+ ' ' +e, ' msg-err ' )});
}
}
function setBed() {
function setBed() {
var v=parseFloat(document.getElementById( ' p-bed-inp ' ).value||0);
var v=parseFloat(document.getElementById( ' p-bed-inp ' ).value||0);
post( ' /api/temperature ' , { nozzle:S.nozzle_target,bed:v})
post( ' /api/temperature ' , { nozzle:S.nozzle_target,bed:v})
.then(function() { clog(T.label_bed+ ' → ' +v+ ' °C ' , ' msg-ok ' )})
.then(function() { clog(T.label_bed+ ' → ' +v+ ' °C ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Temp-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_temp_error|| ' Temperature error: ' )+ ' ' +e, ' msg-err ' )});
}
}
// ── Light ──
// ── Light ──
function setLight() {
function setLight() {
var on=document.getElementById( ' d-light-toggle ' ).checked;
var on=document.getElementById( ' d-light-toggle ' ).checked;
post( ' /api/light ' , { on:on,brightness:80})
post( ' /api/light ' , { on:on,brightness:80})
.then(function() { clog( ' Lic ht ' +(on? ' an, ' +br+ ' % ' : ' aus ' ), ' msg-ok ' )})
.then(function() { clog(on?(T.log_light_on|| ' Lig ht on ' ):(T.log_light_off|| ' Light off ' ), ' msg-ok ' )})
.catch(function(e) { clog( ' Lic ht-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_light_error|| ' Lig ht error: ' )+ ' ' +e, ' msg-err ' )});
}
}
// ── Print Speed ──
// ── Print Speed ──
@@ -2160,7 +2388,7 @@ function setSpeed(mode){
if(b) b.classList.toggle( ' spd-active ' ,m===mode);
if(b) b.classList.toggle( ' spd-active ' ,m===mode);
});
});
post( ' /api/speed ' , { mode:mode})
post( ' /api/speed ' , { mode:mode})
.catch(function(e) { clog( ' Speed-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_speed_error|| ' Speed error: ' )+ ' ' +e, ' msg-err ' )});
}
}
// ── Fan ──
// ── Fan ──
@@ -2168,15 +2396,15 @@ function setFan(){
var v=parseInt(document.getElementById( ' d-fan ' ).value);
var v=parseInt(document.getElementById( ' d-fan ' ).value);
document.getElementById( ' d-fan-val ' ).textContent=v;
document.getElementById( ' d-fan-val ' ).textContent=v;
post( ' /api/fan ' , { speed:v})
post( ' /api/fan ' , { speed:v})
.then(function() { clog( ' Lüfter → ' +v+ ' % ' , ' msg-ok ' )})
.then(function() { clog((T.log_fan|| ' Fan → ' )+ ' ' +v+ ' % ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Lüfter-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_fan_error|| ' Fan error: ' )+ ' ' +e, ' msg-err ' )});
}
}
function quickFan(v) {
function quickFan(v) {
document.getElementById( ' d-fan ' ).value=v;
document.getElementById( ' d-fan ' ).value=v;
document.getElementById( ' d-fan-val ' ).textContent=v;
document.getElementById( ' d-fan-val ' ).textContent=v;
post( ' /api/fan ' , { speed:v})
post( ' /api/fan ' , { speed:v})
.then(function() { clog( ' Lüfter → ' +v+ ' % ' , ' msg-ok ' )})
.then(function() { clog((T.log_fan|| ' Fan → ' )+ ' ' +v+ ' % ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Lüfter-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_fan_error|| ' Fan error: ' )+ ' ' +e, ' msg-err ' )});
}
}
// ── AMS ──
// ── AMS ──
@@ -2184,7 +2412,7 @@ function amsFeed(type){
var slot=parseInt(document.getElementById( ' ams-slot-sel ' ).value);
var slot=parseInt(document.getElementById( ' ams-slot-sel ' ).value);
post( ' /api/ams/feed ' , { slot_index:slot,type:type})
post( ' /api/ams/feed ' , { slot_index:slot,type:type})
.then(function() { clog((type===1?T.lbl_feed:T.lbl_unload)+ ' Slot ' +(slot+1), ' msg-ok ' )})
.then(function() { clog((type===1?T.lbl_feed:T.lbl_unload)+ ' Slot ' +(slot+1), ' msg-ok ' )})
.catch(function(e) { clog( ' AMS-Fehler: ' +e, ' msg-err ' )});
.catch(function(e) { clog((T.log_ams_error|| ' AMS error: ' )+ ' ' +e, ' msg-err ' )});
}
}
// ── Camera ──
// ── Camera ──
@@ -2201,13 +2429,13 @@ function camStart(){
img.style.display= ' none ' ;
img.style.display= ' none ' ;
ph.style.display= ' flex ' ;
ph.style.display= ' flex ' ;
camOn=false;
camOn=false;
document.getElementById( ' cam-toggle-btn ' ).textContent=T.btn_cam_start|| ' ▶ K amera ' ;
document.getElementById( ' cam-toggle-btn ' ).textContent=T.btn_cam_start|| ' ▶ C amera ' ;
clog((T.log_error|| ' Fehle r:' )+ ' Stream nicht verfügbar ' , ' msg-err ' );
clog((T.log_error|| ' Erro r:' )+ ' ' +(T.log_stream_unavailable|| ' Stream unavailable ' ) ,' msg-err ' );
};
};
img.src= ' /api/camera/stream?t= ' +Date.now();
img.src= ' /api/camera/stream?t= ' +Date.now();
camOn=true;
camOn=true;
document.getElementById( ' cam-toggle-btn ' ).textContent=T.btn_cam_stop|| ' ◼ K amera ' ;
document.getElementById( ' cam-toggle-btn ' ).textContent=T.btn_cam_stop|| ' ◼ C amera ' ;
clog((T.log_cam_start|| ' K amera ge startet ' ), ' msg-ok ' );
clog((T.log_cam_start|| ' C amera started ' ), ' msg-ok ' );
// MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden
// MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden
setTimeout(function() {
setTimeout(function() {
sp.style.display= ' none ' ;
sp.style.display= ' none ' ;
@@ -2216,7 +2444,7 @@ function camStart(){
}).catch(function(e) {
}).catch(function(e) {
sp.style.display= ' none ' ;
sp.style.display= ' none ' ;
ph.style.display= ' flex ' ;
ph.style.display= ' flex ' ;
clog((T.log_error|| ' Fehle r:' )+ ' ' +e, ' msg-err ' );
clog((T.log_error|| ' Erro r:' )+ ' ' +e, ' msg-err ' );
});
});
}
}
function camStop() {
function camStop() {
@@ -2227,9 +2455,9 @@ function camStop(){
document.getElementById( ' cam-spinner ' ).style.display= ' none ' ;
document.getElementById( ' cam-spinner ' ).style.display= ' none ' ;
document.getElementById( ' cam-placeholder ' ).style.display= ' flex ' ;
document.getElementById( ' cam-placeholder ' ).style.display= ' flex ' ;
camOn=false;
camOn=false;
document.getElementById( ' cam-toggle-btn ' ).textContent=T.btn_cam_start|| ' ▶ K amera ' ;
document.getElementById( ' cam-toggle-btn ' ).textContent=T.btn_cam_start|| ' ▶ C amera ' ;
clog(T.log_cam_stop|| ' K amera ge stoppt ' , ' msg-ok ' );
clog(T.log_cam_stop|| ' C amera stopped ' , ' msg-ok ' );
}).catch(function(e) { clog((T.log_error|| ' Fehle r:' )+ ' ' +e, ' msg-err ' )});
}).catch(function(e) { clog((T.log_error|| ' Erro r:' )+ ' ' +e, ' msg-err ' )});
}
}
function toggleCam() { if(camOn)camStop();else camStart()}
function toggleCam() { if(camOn)camStop();else camStart()}
</script>
</script>
@@ -2292,7 +2520,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
pass
pass
self . _state [ " print_state " ] = " error "
self . _state [ " print_state " ] = " error "
self . _state [ " kobra_state " ] = " offline "
self . _state [ " kobra_state " ] = " offline "
log . info ( " M anue ll getrennt " )
log . info ( " Disconnected m anua lly " )
return web . json_response ( { " result " : " disconnected " } )
return web . json_response ( { " result " : " disconnected " } )
async def handle_api_speed ( self , request ) :
async def handle_api_speed ( self , request ) :
@@ -2427,7 +2655,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
" video " , " startCapture " , None , timeout = 8.0
" video " , " startCapture " , None , timeout = 8.0
) )
) )
state = ( result or { } ) . get ( " state " , " " )
state = ( result or { } ) . get ( " state " , " " )
log . info ( f " K amera startCapture: state={ state } " )
log . info ( f " C amera startCapture: state={ state } " )
return web . json_response ( { " result " : " ok " , " state " : state } )
return web . json_response ( { " result " : " ok " , " state " : state } )
async def handle_api_camera_stop ( self , request ) :
async def handle_api_camera_stop ( self , request ) :
@@ -2441,7 +2669,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
""" Einzelner JPEG-Frame aus dem Kamera-Stream – für Obico und andere Snapshot-Clients. """
""" Einzelner JPEG-Frame aus dem Kamera-Stream – für Obico und andere Snapshot-Clients. """
url = self . _state . get ( " camera_url " , " " )
url = self . _state . get ( " camera_url " , " " )
if not url :
if not url :
return web . Response ( status = 503 , text = " Keine K amera- URL bekannt " )
return web . Response ( status = 503 , text = " No c amera URL known " )
is_rtsp = url . lower ( ) . startswith ( " rtsp:// " )
is_rtsp = url . lower ( ) . startswith ( " rtsp:// " )
input_args = [ " -fflags " , " nobuffer " , " -flags " , " low_delay " ]
input_args = [ " -fflags " , " nobuffer " , " -flags " , " low_delay " ]
if is_rtsp :
if is_rtsp :
@@ -2463,7 +2691,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
except Exception as e :
except Exception as e :
return web . Response ( status = 503 , text = str ( e ) )
return web . Response ( status = 503 , text = str ( e ) )
if not jpeg :
if not jpeg :
return web . Response ( status = 503 , text = " Kein Frame empfangen " )
return web . Response ( status = 503 , text = " No frame received " )
return web . Response ( body = jpeg , content_type = " image/jpeg " ,
return web . Response ( body = jpeg , content_type = " image/jpeg " ,
headers = { " Cache-Control " : " no-cache " } )
headers = { " Cache-Control " : " no-cache " } )
@@ -2471,7 +2699,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
""" MJPEG proxy: FLV → MJPEG via ffmpeg, served as multipart/x-mixed-replace. """
""" MJPEG proxy: FLV → MJPEG via ffmpeg, served as multipart/x-mixed-replace. """
url = self . _state . get ( " camera_url " , " " )
url = self . _state . get ( " camera_url " , " " )
if not url :
if not url :
return web . Response ( status = 503 , text = " Keine K amera- URL bekannt " )
return web . Response ( status = 503 , text = " No c amera URL known " )
is_rtsp = url . lower ( ) . startswith ( " rtsp:// " )
is_rtsp = url . lower ( ) . startswith ( " rtsp:// " )
ffmpeg_input_args = [
ffmpeg_input_args = [
@@ -2500,10 +2728,10 @@ function toggleCam(){if(camOn)camStop();else camStart()}
stderr = asyncio . subprocess . DEVNULL ,
stderr = asyncio . subprocess . DEVNULL ,
)
)
except ( FileNotFoundError , OSError ) as e :
except ( FileNotFoundError , OSError ) as e :
log . warning ( " K amera: ffmpeg nicht gefunden – K amerastream nicht verfügbar " )
log . warning ( " C amera: ffmpeg not found - c amera stream unavailable " )
return web . Response ( status = 503 , text = " ffmpeg not found " )
return web . Response ( status = 503 , text = " ffmpeg not found " )
except Exception as e :
except Exception as e :
log . warning ( f " K amera: ffmpeg konnte nicht gestartet werden : { e } " )
log . warning ( f " C amera: ffmpeg could not be started : { e } " )
return web . Response ( status = 503 , text = str ( e ) )
return web . Response ( status = 503 , text = str ( e ) )
boundary = " kobraxframe "
boundary = " kobraxframe "
@@ -2543,7 +2771,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
except Exception :
except Exception :
return resp
return resp
except Exception as e :
except Exception as e :
log . warning ( f " K amera-S tream u nterbrochen : { e } " )
log . warning ( f " C amera s tream i nterrupted : { e } " )
finally :
finally :
try :
try :
proc . kill ( )
proc . kill ( )
@@ -2559,7 +2787,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
if not os . path . isfile ( serve_path ) :
if not os . path . isfile ( serve_path ) :
return web . Response ( status = 404 , text = " not found " )
return web . Response ( status = 404 , text = " not found " )
size = os . path . getsize ( serve_path )
size = os . path . getsize ( serve_path )
log . info ( f " Drucker lädt Datei ab : { filename } ( { size } bytes) " )
log . info ( f " Printer downloading file : { filename } ( { size } bytes) " )
return web . FileResponse ( serve_path , headers = {
return web . FileResponse ( serve_path , headers = {
" Content-Disposition " : f ' attachment; filename= " { filename } " '
" Content-Disposition " : f ' attachment; filename= " { filename } " '
} )
} )
@@ -2592,6 +2820,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
" thumbnail " : self . _thumbnail_b64 ,
" thumbnail " : self . _thumbnail_b64 ,
" connection_error " : s [ " connection_error " ] ,
" connection_error " : s [ " connection_error " ] ,
" file_ready " : s [ " file_ready " ] ,
" file_ready " : s [ " file_ready " ] ,
" uploaded_filaments " : self . _uploaded_metadata_for ( s [ " file_ready " ] or self . _last_uploaded_file ) ,
" version " : self . _read_version ( ) ,
" version " : self . _read_version ( ) ,
} )
} )
@@ -2603,7 +2832,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
if namespace == " lane_data " :
if namespace == " lane_data " :
await asyncio . get_event_loop ( ) . run_in_executor ( None , self . _get_ams_slots_fresh )
await asyncio . get_event_loop ( ) . run_in_executor ( None , self . _get_ams_slots_fresh )
lanes = self . _build_lane_data ( )
lanes = self . _build_lane_data ( )
log . info ( f " AMS-S ync: { len ( lanes ) } L anes an OrcaSlicer " )
log . info ( f " AMS s ync: { len ( lanes ) } l anes sent to OrcaSlicer " )
return web . json_response ( {
return web . json_response ( {
" result " : {
" result " : {
" namespace " : " lane_data " ,
" namespace " : " lane_data " ,
@@ -2701,7 +2930,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
return response
return response
def _restart_bridge ( self ) :
def _restart_bridge ( self ) :
log . info ( " Bridge wird neu gestartet … " )
log . info ( " Restarting bridge ... " )
exe = sys . executable
exe = sys . executable
# PyInstaller frozen binary: sys.argv[0] == sys.executable → nicht doppelt übergeben
# PyInstaller frozen binary: sys.argv[0] == sys.executable → nicht doppelt übergeben
if getattr ( sys , " frozen " , False ) :
if getattr ( sys , " frozen " , False ) :
@@ -2792,12 +3021,12 @@ function toggleCam(){if(camOn)camStop();else camStart()}
return web . json_response ( { " error " : f " Gitea HTTP { resp . status } " } , status = 502 )
return web . json_response ( { " error " : f " Gitea HTTP { resp . status } " } , status = 502 )
releases = await resp . json ( content_type = None )
releases = await resp . json ( content_type = None )
if not releases :
if not releases :
return web . json_response ( { " error " : " Keine R eleases ge funden " } , status = 404 )
return web . json_response ( { " error " : " No r eleases fo und" } , status = 404 )
# Dev: neuestes Release mit "-dev+" im Tag suchen
# Dev: neuestes Release mit "-dev+" im Tag suchen
if is_dev :
if is_dev :
dev_releases = [ r for r in releases if " -dev+ " in r . get ( " tag_name " , " " ) ]
dev_releases = [ r for r in releases if " -dev+ " in r . get ( " tag_name " , " " ) ]
if not dev_releases :
if not dev_releases :
return web . json_response ( { " error " : " Keine Dev-R eleases ge funden " } , status = 404 )
return web . json_response ( { " error " : " No dev r eleases fo und" } , status = 404 )
data = dev_releases [ 0 ]
data = dev_releases [ 0 ]
else :
else :
data = releases [ 0 ]
data = releases [ 0 ]
@@ -2890,7 +3119,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
break
break
self . ws_clients . discard ( ws )
self . ws_clients . discard ( ws )
log . info ( f " WS client getrennt ( { len ( self . ws_clients ) } verbleibend ) " )
log . info ( f " WS client disconnected ( { len ( self . ws_clients ) } remaining ) " )
return ws
return ws
async def _handle_ws_rpc ( self , ws : web . WebSocketResponse , raw : str ) :
async def _handle_ws_rpc ( self , ws : web . WebSocketResponse , raw : str ) :
@@ -2950,11 +3179,8 @@ function toggleCam(){if(camOn)camStop();else camStart()}
elif method == " printer.print.start " :
elif method == " printer.print.start " :
filename = params . get ( " filename " , self . _last_uploaded_file )
filename = params . get ( " filename " , self . _last_uploaded_file )
loop = asyncio . get_event_loop ( )
loop = asyncio . get_event_loop ( )
resp = await loop . run_in_executor (
await loop . run_in_executor ( None , lambda : self . _start_print ( filename ) )
None , lambda : self . client . publish ( " print " , " start " ,
result = " ok "
{ " filename " : filename , " use_ams " : False } , timeout = 15.0 )
)
result = " ok " if resp else " timeout "
elif method == " printer.print.pause " :
elif method == " printer.print.pause " :
loop = asyncio . get_event_loop ( )
loop = asyncio . get_event_loop ( )
await loop . run_in_executor ( None , self . client . pause_print )
await loop . run_in_executor ( None , self . client . pause_print )
@@ -2975,7 +3201,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
log . debug ( f " Unbekannte RPC-Methode: { method } " )
log . debug ( f " Unbekannte RPC-Methode: { method } " )
result = { }
result = { }
except Exception as e :
except Exception as e :
log . error ( f " RPC-Fehle r fü r { method } : { e } " )
log . error ( f " RPC erro r fo r { method } : { e } " )
error = { " code " : - 32603 , " message " : str ( e ) }
error = { " code " : - 32603 , " message " : str ( e ) }
if rpc_id is not None :
if rpc_id is not None :
@@ -3009,7 +3235,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
# ── Offline-Modus: warten bis Drucker wieder erreichbar ──────────
# ── Offline-Modus: warten bis Drucker wieder erreichbar ──────────
if _offline :
if _offline :
if self . _printer_reachable ( ) :
if self . _printer_reachable ( ) :
log . info ( " Druck er er rei chbar – stelle MQTT-Verbindung her … " )
log . info ( " Print er rea chable - connecting MQTT ... " )
try :
try :
self . client . connect ( )
self . client . connect ( )
_offline = False
_offline = False
@@ -3020,7 +3246,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
except Exception as e :
except Exception as e :
err = _mqtt_error_msg ( e )
err = _mqtt_error_msg ( e )
self . _state [ " connection_error " ] = err
self . _state [ " connection_error " ] = err
log . warning ( f " Verbindungsaufbau fehlgeschlagen : { err } " )
log . warning ( f " Connection attempt failed : { err } " )
stop_event . wait ( _probe_interval )
stop_event . wait ( _probe_interval )
continue
continue
else :
else :
@@ -3045,10 +3271,10 @@ function toggleCam(){if(camOn)camStop();else camStart()}
if slots :
if slots :
self . _ams_slots = slots
self . _ams_slots = slots
except Exception as e :
except Exception as e :
log . warning ( f " Poll-Fehle r: { e } " )
log . warning ( f " Poll erro r: { e } " )
# Prüfen ob Drucker wirklich weg ist
# Prüfen ob Drucker wirklich weg ist
if not self . _printer_reachable ( ) :
if not self . _printer_reachable ( ) :
log . info ( " Drucker nicht er rei chbar – wechsle in Offline-Modus " )
log . info ( " Printer un rea chable - switching to offline mode " )
self . _state [ " print_state " ] = " error "
self . _state [ " print_state " ] = " error "
self . _state [ " kobra_state " ] = " offline "
self . _state [ " kobra_state " ] = " offline "
self . _state [ " connection_error " ] = f " Printer unreachable ( { self . _args . printer_ip } ) "
self . _state [ " connection_error " ] = f " Printer unreachable ( { self . _args . printer_ip } ) "
@@ -3152,13 +3378,13 @@ async def run_bridge(args):
# Verbindungsversuch beim Start – bei Fehler im Offline-Modus weiterlaufen
# Verbindungsversuch beim Start – bei Fehler im Offline-Modus weiterlaufen
loop = asyncio . get_event_loop ( )
loop = asyncio . get_event_loop ( )
log . info ( f " Verbinde mit Druck er { args . printer_ip } : { args . mqtt_port } … " )
log . info ( f " Connecting to print er { args . printer_ip } : { args . mqtt_port } ... " )
try :
try :
await loop . run_in_executor ( None , client . connect )
await loop . run_in_executor ( None , client . connect )
log . info ( " MQTT verbunden " )
log . info ( " MQTT verbunden " )
except Exception as e :
except Exception as e :
err = _mqtt_error_msg ( e )
err = _mqtt_error_msg ( e )
log . warning ( f " Verbindung fehlgeschlagen : { err } – starte im Offline-Modus " )
log . warning ( f " Connection failed : { err } - starting in offline mode " )
bridge . _state [ " print_state " ] = " error "
bridge . _state [ " print_state " ] = " error "
bridge . _state [ " kobra_state " ] = " offline "
bridge . _state [ " kobra_state " ] = " offline "
bridge . _state [ " connection_error " ] = err
bridge . _state [ " connection_error " ] = err
@@ -3182,7 +3408,7 @@ async def run_bridge(args):
_local_ip = _s . getsockname ( ) [ 0 ]
_local_ip = _s . getsockname ( ) [ 0 ]
except Exception :
except Exception :
_local_ip = args . host
_local_ip = args . host
log . info ( f " Bridge läuft auf http:// { _local_ip } : { args . port } " )
log . info ( f " Bridge running at http:// { _local_ip } : { args . port } " )
log . info ( f " OrcaSlicer → Klipper → Host: { _local_ip } Port: { args . port } " )
log . info ( f " OrcaSlicer → Klipper → Host: { _local_ip } Port: { args . port } " )
log . info ( " Ctrl-C zum Beenden " )
log . info ( " Ctrl-C zum Beenden " )
@@ -3196,7 +3422,7 @@ async def run_bridge(args):
stop_event . set ( )
stop_event . set ( )
await runner . cleanup ( )
await runner . cleanup ( )
client . disconnect ( )
client . disconnect ( )
log . info ( " Bridge beendet " )
log . info ( " Bridge stopped " )
def main ( ) :
def main ( ) :