From 34a4e984e70c155f2d17f47f334a4035c318c914 Mon Sep 17 00:00:00 2001 From: solutionphil Date: Sat, 5 Sep 2026 10:08:27 +0200 Subject: [PATCH] Bridge voronFDM to Moonraker only once Moonraker has identified Klipper Between "Websocket Opened" and the end of its Klippy handshake Moonraker has not registered the printer.* methods yet and answers them with "Method not found". That window is milliseconds on an idle host and seconds on a loaded one, and voronFDM, refused on its subscribe and its first gcode, answers with printer.firmware_restart. Measured 2026-09-05 on the dev printer during a boot with the post-update setup running in the background: Moonraker listening at 64 s, voronFDM's requests refused at 65-66 s, two FIRMWARE_RESTARTs at 66.7 s and 66.9 s (the second one an internal error inside the first), Klipper back at 82 s, the panel some 40 s later than it needed to be. No kit script and no KAOS macro sends that command; the request strings are in the display binary. The relay now polls /server/info and makes the bridge only once klippy_state is ready, error or shutdown -- an error is exactly what the panel must be allowed to show. Past UP_WAIT (now 120 s) it bridges regardless, so a Klipper that never comes up still cannot take the display with it. voronFDM is held, not refused, as before: it never retries a refused connection. Co-Authored-By: Claude Fable 5.1 --- scripts/wsrelay/relay.py | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/scripts/wsrelay/relay.py b/scripts/wsrelay/relay.py index 664a6b6..5734f67 100755 --- a/scripts/wsrelay/relay.py +++ b/scripts/wsrelay/relay.py @@ -13,8 +13,10 @@ # that read voronFDM's stdout and injected the print.start the display was not sending; it was retired # once the display's own print flow was repaired, and nothing has replaced it. import asyncio +import json import tornado.web, tornado.websocket, tornado.ioloop from tornado.websocket import websocket_connect +from tornado.httpclient import AsyncHTTPClient import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s wsrelay %(message)s") UP = "ws://127.0.0.1:7125/websocket" @@ -26,7 +28,39 @@ # cycle plus its ~15 s of panel initialisation. Turning it away once is therefore expensive, and holding # it for a few seconds costs nothing. This also removes the only reason the boot ordering had to be # exact, which is what made it safe to look at that at all. -UP_WAIT = 60 +# ... and wait for Moonraker to have IDENTIFIED KLIPPER, not merely to be listening. Between "Websocket +# Opened" and the end of its Klippy handshake Moonraker has not registered the printer.* methods yet, and +# answers them with "Method not found". That window is milliseconds on an idle host and seconds on a +# loaded one -- and voronFDM, told "Method not found" for its subscribe and its first gcode, answers with +# printer.firmware_restart. Measured 2026-09-05 on the dev printer, a boot with the post-update setup +# running in the background: Moonraker listening at 64 s, voronFDM's requests refused at 65-66 s, two +# FIRMWARE_RESTARTs at 66.7 s and 66.9 s, Klipper back at 82 s, the panel some 40 s later than it +# needed to be. So the bridge is only made once /server/info reports a klippy_state other than +# "startup" or "disconnected" -- ready, error and shutdown all count, because an error is exactly what +# the panel must be allowed to show. The cap is the same UP_WAIT: past it the bridge is made regardless, +# so a Klipper that never comes up still cannot take the display with it. +UP_WAIT = 120 +INFO = "http://127.0.0.1:7125/server/info" +SETTLED = ("ready", "error", "shutdown") + +async def klippy_settled(deadline): + """Wait until Moonraker reports Klipper identified (state in SETTLED) or the deadline passes. + Returns (state, last) -- state None means the deadline won.""" + loop = tornado.ioloop.IOLoop.current() + client = AsyncHTTPClient() + last = "?" + while True: + try: + resp = await client.fetch(INFO, request_timeout=3) + state = json.loads(resp.body)["result"].get("klippy_state", "?") + if state in SETTLED: + return state, last + last = "klippy_state=" + str(state) + except Exception as e: + last = str(e) + if loop.time() >= deadline: + return None, last + await asyncio.sleep(1) class Relay(tornado.websocket.WebSocketHandler): def check_origin(self, origin): return True @@ -34,6 +68,13 @@ async def open(self, *a): self.up = None; self.alive = True loop = tornado.ioloop.IOLoop.current() deadline = loop.time() + UP_WAIT + state, last = await klippy_settled(deadline) + if not self.alive: + return + if state is None: + logging.info("klippy not identified after %ss (last: %s) -- bridging anyway", UP_WAIT, last) + else: + logging.info("klippy %s -- bridging", state) tries = 0 last = "?" while self.alive: