release: v0.9.1-beta14

This commit is contained in:
2026-04-26 14:58:20 +02:00
parent 23756b82a9
commit fc681316fc
7 changed files with 208 additions and 131 deletions

View File

@@ -29,6 +29,19 @@ _BASE = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os
sys.path.insert(0, _BASE)
from kobrax_client import KobraXClient
try:
import imageio_ffmpeg
def _find_ffmpeg() -> str:
return imageio_ffmpeg.get_ffmpeg_exe()
except ImportError:
def _find_ffmpeg() -> str:
exe_name = "ffmpeg.exe" if sys.platform == "win32" else "ffmpeg"
local = os.path.join(_BASE, exe_name)
if os.path.isfile(local):
return local
return "ffmpeg"
try:
from aiohttp import web
import aiohttp
@@ -1031,17 +1044,17 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<button class="step-btn" onclick="setStep(this,10)">10 mm</button>
</div>
<div class="home-btns">
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeAxis('X')"><span class="lbl-home-x">Home X</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeAxis('Y')"><span class="lbl-home-y">Home Y</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeAxis('Z')"><span class="lbl-home-z">Home Z</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
<button class="btn btn-sm btn-accent" onclick="homeAll()"><span class="lbl-home-all">Home All</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="disableMotors()"><span class="lbl-disable-motors">Motors Off</span></button>
</div>
</div>
<div class="card">
<div class="card-title"><span>↕</span> <span id="ptitle-motion-z">Z-Achse</span></div>
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
<button class="joy" onclick="move(2,-1,getStep())" title="Z">▲</button>
<button class="joy" onclick="move(2,1,getStep())" title="Z+">▼</button>
<button class="joy" onclick="move(2,1,getStep())" title="Z+">▲</button>
<button class="joy" onclick="move(2,-1,getStep())" title="Z">▼</button>
</div>
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)"><span class="lbl-step">Schrittweite:</span> <span id="step-display">1</span> mm</div>
</div>
@@ -1155,7 +1168,7 @@ var LANG_DE={
panel_print_title:'Drucksteuerung',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Fortsetzen',panel_print_btn_cancel:'✕ Abbrechen',panel_print_temps_live:'Temperaturen (Live)',
label_set:'Setzen',label_off:'Aus',
panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heizbett',panel_temps_chart:'Verlauf (letzte 60 Messungen)',label_target_c:'Ziel:',
panel_motion_xy:'XY-Achsen',panel_motion_z:'Z-Achse',label_step:'Schrittweite:',btn_home_x:'Home X',btn_home_y:'Home Y',btn_home_z:'Home Z',btn_home_all:'Home All',
panel_motion_xy:'XY-Achsen',panel_motion_z:'Z-Achse',label_step:'Schrittweite:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motoren aus',
panel_ams_title:'AMS / Filamentbox',ams_no_data:'Keine AMS-Daten empfangen',label_slot:'Slot',
panel_extras_light:'Licht',panel_extras_fan:'Lüfter',panel_extras_camera:'Kamera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop',
panel_console_title:'Ereignis-Log',
@@ -1181,7 +1194,7 @@ var LANG_EN={
panel_print_title:'Print Control',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Resume',panel_print_btn_cancel:'✕ Cancel',panel_print_temps_live:'Temperatures (Live)',
label_set:'Set',label_off:'Off',
panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heated Bed',panel_temps_chart:'History (last 60 readings)',label_target_c:'Target:',
panel_motion_xy:'XY Axes',panel_motion_z:'Z Axis',label_step:'Step size:',btn_home_x:'Home X',btn_home_y:'Home Y',btn_home_z:'Home Z',btn_home_all:'Home All',
panel_motion_xy:'XY Axes',panel_motion_z:'Z Axis',label_step:'Step size:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motors Off',
panel_ams_title:'AMS / Filament Box',ams_no_data:'No AMS data received',label_slot:'Slot',
panel_extras_light:'Light',panel_extras_fan:'Fan',panel_extras_camera:'Camera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop',
panel_console_title:'Event Log',
@@ -1233,10 +1246,10 @@ function applyLang(){
// Axis labels
setText('ptitle-motion-xy',T.panel_motion_xy);
setText('ptitle-motion-z',T.panel_motion_z);
document.querySelectorAll('.lbl-home-x').forEach(e=>e.textContent=T.btn_home_x);
document.querySelectorAll('.lbl-home-y').forEach(e=>e.textContent=T.btn_home_y);
document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z);
document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy);
document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all);
document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors);
document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step);
document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':','')));
// Console
@@ -1591,16 +1604,25 @@ function move(axis,dir,dist){
.catch(function(e){clog('Achse-Fehler: '+e,'msg-err')});
}
function homeAll(){
post('/api/axis',{axis:4,move_type:2,distance:0})
post('/api/axis',{axis:5,move_type:2,distance:0})
.then(function(){clog('Home All','msg-ok')})
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
}
function homeAxis(ax){
var m={X:1,Y:2,Z:3};
post('/api/axis',{axis:m[ax],move_type:2,distance:0})
.then(function(){clog('Home '+ax,'msg-ok')})
function homeXY(){
post('/api/axis',{axis:4,move_type:2,distance:0})
.then(function(){clog('Home XY','msg-ok')})
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
}
function homeZ(){
post('/api/axis',{axis:3,move_type:2,distance:0})
.then(function(){clog('Home Z','msg-ok')})
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
}
function disableMotors(){
post('/api/axis',{action:'turnOff'})
.then(function(){clog('Motors Off','msg-ok')})
.catch(function(e){clog('Motors-Fehler: '+e,'msg-err')});
}
// ── Temperature ──
function setNozzle(){
@@ -1808,15 +1830,21 @@ function toggleCam(){if(camOn)camStop();else camStart()}
body = await request.json()
except Exception:
body = {}
axis = int(body.get("axis", 4))
move_type = int(body.get("move_type", 2))
distance = float(body.get("distance", 0))
action = body.get("action", "move")
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, lambda: self.client.publish(
"axis", "move",
{"axis": axis, "move_type": move_type, "distance": distance},
timeout=0
))
if action == "turnOff":
await loop.run_in_executor(None, lambda: self.client.publish(
"axis", "turnOff", None, timeout=0
))
else:
axis = int(body.get("axis", 4))
move_type = int(body.get("move_type", 2))
distance = float(body.get("distance", 0))
await loop.run_in_executor(None, lambda: self.client.publish(
"axis", "move",
{"axis": axis, "move_type": move_type, "distance": distance},
timeout=0
))
return web.json_response({"result": "ok"})
async def handle_api_temperature(self, request):
@@ -1880,14 +1908,6 @@ function toggleCam(){if(camOn)camStop();else camStart()}
if not url:
return web.Response(status=503, text="Keine Kamera-URL bekannt")
boundary = "kobraxframe"
resp = web.StreamResponse(headers={
"Content-Type": f"multipart/x-mixed-replace;boundary={boundary}",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
})
await resp.prepare(request)
is_rtsp = url.lower().startswith("rtsp://")
ffmpeg_input_args = [
"-fflags", "nobuffer",
@@ -1896,22 +1916,38 @@ function toggleCam(){if(camOn)camStop();else camStart()}
if is_rtsp:
ffmpeg_input_args += ["-probesize", "32", "-analyzeduration", "0", "-rtsp_transport", "tcp"]
else:
# HTTP-FLV/HLS: braucht mehr Probe-Puffer für Container-Erkennung
ffmpeg_input_args += ["-probesize", "1000000", "-analyzeduration", "1000000"]
proc = await asyncio.create_subprocess_exec(
"ffmpeg", "-loglevel", "quiet",
*ffmpeg_input_args,
"-i", url,
"-vf", "fps=15,scale=640:-1",
"-f", "image2pipe",
"-vcodec", "mjpeg",
"-q:v", "3",
"-flush_packets", "1",
"pipe:1",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
# ffmpeg erst starten BEVOR der StreamResponse geöffnet wird
# (damit wir bei Fehler noch eine normale HTTP-Response senden können)
try:
proc = await asyncio.create_subprocess_exec(
_find_ffmpeg(), "-loglevel", "quiet",
*ffmpeg_input_args,
"-i", url,
"-vf", "fps=15,scale=640:-1",
"-f", "image2pipe",
"-vcodec", "mjpeg",
"-q:v", "3",
"-flush_packets", "1",
"pipe:1",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
except (FileNotFoundError, OSError) as e:
log.warning("Kamera: ffmpeg nicht gefunden Kamerastream nicht verfügbar")
return web.Response(status=503, text="ffmpeg not found")
except Exception as e:
log.warning(f"Kamera: ffmpeg konnte nicht gestartet werden: {e}")
return web.Response(status=503, text=str(e))
boundary = "kobraxframe"
resp = web.StreamResponse(headers={
"Content-Type": f"multipart/x-mixed-replace;boundary={boundary}",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
})
await resp.prepare(request)
buf = b""
try:
@@ -2107,7 +2143,12 @@ function toggleCam(){if(camOn)camStop();else camStart()}
def _restart_bridge(self):
log.info("Bridge wird neu gestartet …")
os.execv(sys.executable, [sys.executable] + sys.argv)
exe = sys.executable
# PyInstaller frozen binary: sys.argv[0] == sys.executable → nicht doppelt übergeben
if getattr(sys, "frozen", False):
os.execv(exe, [exe])
else:
os.execv(exe, [exe] + sys.argv)
# ─── Update ──────────────────────────────────────────────────────────────
@@ -2544,6 +2585,10 @@ def main():
if args.printer_ip and ":" in args.printer_ip:
args.printer_ip = args.printer_ip.split(":")[0]
# Windows braucht ProactorEventLoop für asyncio.create_subprocess_exec
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
asyncio.run(run_bridge(args))