Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion scripts/wsrelay/relay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -26,14 +28,53 @@
# 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
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:
Expand Down