4. Klipper之开发篇:API服务器Moonraker的源码分析
Moonraker是Klipper API的实现工具(服务),要想使用显示屏或者网页端(Fluidd或Mainsail)访问Klipper,都需要启动Moonraker服务,因为网页或者显示屏访问Klipper,实际就是将它们的HTTP访问请求变成访问Klipper的Unix Socket,需要Moonraker在中间转换。
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的工具。
更多推荐
所有评论(0)