Moonraker是Klipper API的实现工具(服务),要想使用显示屏或者网页端(Fluidd或Mainsail)访问Klipper,必须启动Moonraker服务,因为网页或者显示屏访问Klipper,实际就是将它们的HTTP访问请求变成访问Klipper的Unix Socket,需要Moonraker在中间转换。

1. moonraker服务启动过程

        1.1 安装好moonraker后,通常在/etc/systemd/system下有moonraker服务文件moonraker.service或者它的链接文件,该服务实际上是执行moonraker/moonraker/moonraker.py文件,moonraker.py内容如下:

if __name__ == "__main__":
    import sys
    import importlib
    import pathlib
    pkg_parent = pathlib.Path(__file__).parent.parent
    sys.path.pop(0)
    sys.path.insert(0, str(pkg_parent))
    svr = importlib.import_module(".server", "moonraker")
    svr.main(False)  # type: ignore

        它重新设定了sys.path,并且跳到本目录下server.py的main函数开始执行。

        1.2 server.py的main函数解析了命令行传进来的参数,并赋值给app_args变量,如果命令行有相应的选项,则设定指定项参数为命令行参数,否则采用默认值。最后执行launch_server函数。

        1.3 launch_server创建Server类对象server,它加载相关模块server.load_component()后,进行初始化server.server_init(),然后执行直到退出server.run_until_exit()。 

2. 整体框架

        整个moonraker服务的运行核心流程就是:

        a. server.py中Server的创建(__init__)

        b. 加载模块(server.load_components)

        c .初始化(server.server_init)

        d. 运行(server.run_until_exit)

        2.1 加载模块

        大部分模块在moonraker/components目录下,比如Announcements类(模块),在moonraker/components/announcements.py文件底下有个函数:

def load_component(config: ConfigHelper) -> Announcements:
    return Announcements(config)

        该函数构造Announcements对象并返回该对象,当server.load_components加载模块时(实际后面调用server.load_component),调用上面的函数,并将返回对象保存在server.components中。通过server.lookup_component函数,可以找到保存的模块对象,进而实现模块间互相通信和调用资源的目的。

        2.2 服务运行机制

        所有模块以及Server并行运行,并不阻塞,都是依托Python协程功能,将要执行的事件,加载到event_loop中去执行,退出moonraker,相当于结束asyncio的事件循环。

3.通信

        在server.server_init调用各模块的函数component_init后,就会启动服务server.start_server(),该函数将启动moonraker的三大通信接口。

        3.1 Unix Socket服务端

        start_server函数先是启动在扩展模块extensions.py中的Unix Socket服务端:

async def start_unix_server(self) -> None:
        sockfile: str = self.server.get_app_args()["unix_socket_path"]
        sock_path = pathlib.Path(sockfile).expanduser().resolve()
        logging.info(f"Creating Unix Domain Socket at '{sock_path}'")
        try:
            self.uds_server = await asyncio.start_unix_server(
                self.on_unix_socket_connected, sock_path, limit=UNIX_BUFFER_LIMIT
            )
        except asyncio.CancelledError:
            raise
        except Exception:
            logging.exception(f"Failed to create Unix Domain Socket: {sock_path}")
            self.uds_server = None

        其调用asyncio.start_unix_server创建一个Unix套接字,用于进程间通信,它被定位在~/printer_data/comms/moonraker.sock,这里同时指定了一个被客户端连接后的回调函数:on_unix_socket_connected,这里可以通过这个socket套接字与moonraker通信。

        注意:当前用到的Klipper项目中,Moonraker仅仅只启动这个服务端,KlipperScreen和网页端都没有连接这个socket

        3.2 HTTP服务器

        start_server函数启动HTTP服务器,在application.py中启动并运行的。在server.py的函数start_server中,调用moonraker_app.listen,在application.py进而调用_create_http_server函数,该函数里面通过HTTPServer类构造了http_server对象,然后http_server开始监听设定的端口和地址。

def _create_http_server(
        self, port: int, address: str, **kwargs
    ) -> Optional[HTTPServer]:
        args: Dict[str, Any] = dict(max_body_size=MAX_BODY_SIZE, xheaders=True)
        args.update(kwargs)
        svr = HTTPServer(self.mutable_router, **args)
        logging.info(f"YSZ-GOGO: create http server, port: {port}, host:{address}")
        try:
            svr.listen(port, address)
        except Exception as e:
            svr_type = "HTTPS" if "ssl_options" in args else "HTTP"
            self.server.add_warning(
                f"Failed to start {svr_type} server: {e}.  See moonraker.log "
                "for more details.", exc_info=e
            )
            return None
        return svr

        注意:这里监听端口是7125,地址是0.0.0.0,它表示该HTTP服务监听所有地址的7125端口。

        KlipperScreen,mainsail,fluidd与moonraker通信,实际是通过websockets协议通信,因为这个协议允许客户端与服务器全双工实时通信,主要由模块websockets.py实现,在构造websocket对象时,注册了两个路由/websocket和/klippysocket,它们仍然是要到HTTPServer的路由表中去,HTTP服务启动时,可以通过路由找到websocket通信回调对象。

        3.3 Unix Socket客户端

        start_server函数中self.klippy_connection.connect()创建Unix Socket客户端,连接到klippy的Unix Socket上,在模块klippy_connection的connect函数中,实际调用了asyncio.open_unix_connection函数来完成连接到klippy的Unix Socket套接字文件的,该文件定位在printer_data/comms/klippy.sock。

以下是部分关键代码:

try:
    reader, writer = await self.open_klippy_connection(True)
except asyncio.CancelledError:
    raise
logging.info("Klippy Connection Established")
...

async def open_klippy_connection(
        self, primary: bool = False
    ) -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]:
        if not primary and not self.is_connected():
            raise ServerError("Klippy Unix Connection Not Available", 503)
        return await asyncio.open_unix_connection(
            str(self.uds_address), limit=UNIX_BUFFER_LIMIT)

        moonraker就是通过Unix Socket和klipper进行通信的。

4. 联接

        moonraker将KlipperScreen/Mainsail/Fluidd等应用的通信指令转到Klipper的过程:

        应用 -> Http服务器 -> 解析打包 -> Unix Socket -> Klipper

        以一个开始打印文件命令为例

  • 应用连接:当浏览器或显示屏等连接moonraker时,会连接moonraker的HTTP服务器,这时,websocket.py模块中WebSocket类成员open会被回调,可以在moonraker.log中看到open函数打印的连接信息。
  • 应用发送(开始打印文件)指令:

{"jsonrpc":"2.0","method":"printer.print.start","params":{"filename":"test.gcode"},"id":12}

  • 在websocket.py模块WebSocket类成员on_message函数收到上面的指令
  • 执行_process_message函数(实体在common.py中定义)解析收到的指令,并查询注册(通过server.register_endpoint函数将指令端点与回调函数关联,保存在MoonrakerApp的json_rpc变量中)的端口集,查询指令的method是否有指令匹配,若有匹配,则调用其回调函数进行指令处理
  • 指令的method是printer.print.start,它对应端点/printer/print/start,它在klippy_apis.py中的KlippyAPI类构造函数中被注册,回调函数是_gcode_start_print
  • 调用回调函数,将指令中文件名重新打包成klippy能识别的指令,发送给klippy的Unix Socket:

{"id":547591357728,"method":"gcode/script","params":{"script":"SDCARD_PRINT_FILE FILENAME=\\"test.gcode\\""}}\x03

         注意,发送到Klipper的Unix Socket的指令,以0x03字符结尾。

5.为什么需要moonraker?

        应用为什么不直接发送指令给Klipper呢?

        理由很简单,klipper进程不必要去处理网页访问,以及去解析各种网络协议,解析外部各种工具的通信协议,它只需要一种协议即可,因此需要moonraker在中间作转换,以方便扩展可以访问klipper的工具。

        

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐