2.Klipper开发篇:Klipper上位机源码分析
从Klipper执行流程和指令传输路线理清Klipper上位机整个源码架构和运行逻辑,理解klipper是如何启动运行的?指令是从哪里获取,去哪里执行?
1.klipper服务启动
Klipper启动最核心的服务是klipper.service,安装好klipper后,在/etc/systemd/system下有该服务的文件,该服务文件的[Service]段内容是(注意,下面~代表linux用户目录,实际样式不是这样):
[Service]
Type=simple
User=tronxy
RemainAfterExit=yes
WorkingDirectory=~/klipper
EnvironmentFile=~/printer_data/systemd/klipper.env
ExecStart=~/klippy-env/bin/python $KLIPPER_ARGS
Restart=always
RestartSec=10
可见它实际上是执行~/printer_data/systemd/klipper.env中的命令,该命令是:
~/klipper/klippy/klippy.py /home/tronxy/printer_data/config/printer.cfg -I ~/printer_data/comms/klippy.serial -l ~/printer_data/logs/klippy.log -a ~/printer_data/comms/klippy.sock
命令涉及到klippy.py文件,它是上位机主进程文件,printer.cfg是打印配置文件
命令行直接跟在klippy.py后面的是配置文件printer.cfg;
-I(大写i)指定虚拟终端,在klippy.py文件中解析时,默认为/tmp/printer,这里指~/printer_data/comms/klippy.serial ,可以直接向它发命令:
echo "M114" >> ~/printer_data/comms/klippy.serial
但只能通过浏览器或者KlipperScreen的控制台看到回显
-l(小写L)指定日志文件klippy.log
-a 指定API服务器的socket文件,这里指向~/printer_data/comms/klippy.sock
它是一个Unix Socket用以和其它应用(如Moonraker)通信用的,可以编写一个Unix Socket客户端,向它向发送指令。
klippy.py文件中的main函数会解析上面的命令行,读取printer.cfg配置文件,完成打印机各模块实例化,然后启动下位机串口通讯,等待响应操作请求。
2. 整体框架
服务启动后,运行到klippy.py的main函数,执行流程如下:
main()解析命令行 -> 命令行参数打包到start_args中 -> 创建反应器reactor -> 构造Printer对象 -> 执行Printer.run进入无限循环,main函数核心代码如下:
while 1:...main_reactor = reactor.Reactor(gc_checking=True)printer = Printer(main_reactor, bglogger, start_args)res = printer.run()...
2.1 构造反应器reactor
reactor是驱动整个klipper上位机运行的核心对象,它在reactor.py中被构造,这里有两种可能的reactor,源代码如下:
# Use the poll based reactor if it is available
try:select.pollReactor = PollReactor
except:Reactor = SelectReactor
它可能是PollReactor类构造的,也可能是SelectReactor构造的,区别就是PollReactor采用poll库,SelectReactor采用select库,poll是select进化版。
无论是poll还是select,它们都监控sockets,open files, 或者 pipes(所有带fileno()方法的文件句柄)何时变成readable 和writeable, 或者通信错误,并返回文件句柄和事件(fd,event)。通常用它来监控多个sockets或pipes或文件是否有可读的信息然后处理这些信息,poll对IO文件不限制数量,select限制数量,较新版本的python都使用PollReactor。
2.2 反应器reactor的核心功能
printer对象运行run函数进入无限循环,最终是调用Reactor._dispatch_loop函数,也就是这个函数完成了整个klipper运行的循环。该函数的while循环体里,主要执行两大核心功能:
_check_timers:检查对应函数register_callback注册到reactor中的延时执行函数时间是否到了,哪个函数时间到了就执行它。相当于并行的执行注册进来的回调函数,主要负责klipper的extra下各模块的回调。
_poll.poll(waittime):通过poll库机制检查对应函数register_fd注册到reactor中的文件句柄是否可读或可写,然后调用对应的读取或写出回调函数。主要负责打印文件的读写,虚拟终端文件的读写,websocket的读写(与moonraker通信)等。
核心代码如下:
# Main loop 主循环def _dispatch_loop(self):...while self._process:timeout = self._check_timers(eventtime, busy)...res = self._poll.poll(int(math.ceil(timeout * 1000.)))...for fd, event in res:...if event & (select.POLLIN | select.POLLHUP):self._fds[fd].read_callback(eventtime)...if event & select.POLLOUT:self._fds[fd].write_callback(eventtime)...
2.3 klipper整体如何运行
klipper根据配置文件,启用指定的模块,大部分模块都是在extra文件夹下,如printer.cfg中如果配置了[verify_heater],则extra下verify_heater.py文件就被启用。
具体实现:一开始构造Printer对象时,注册了一个函数_connect到reactor中,待执行reactor核心循环后,回调_connect函数,该函数一开始就_read_config()->load_object()根据config.getsection加载对应的模块。
每个被启用的模块都会构造相应的对象,同时注册回调函数到reactor中,这样就可以在执行Printer.run后被调用,进而各个模块都被连起来执行了。
3. 指令源及指令执行流程
klipper有三大指令源,分别是虚拟终端,网络指令,打印文件。
3.1 虚拟终端
klipper默认虚拟终端(串口)是/tmp/printer,实际运行时,使用-I(大写的i)指定这个文件,klipper.service指定为~/printer_data/comms/klippy.serial。
该虚拟终端文件在klippy.py的main函数中被打开,文件句柄保存在start_args['gcode_fd']中,传给Printer对象。
Printer构造时,通过add_early_printer_objects函数加载了gcode.py中的GCodeDispatch类对象,保存在printer的objects中,名为gcode;同时也加载了GCodeIO类对象,保存在printer的objects中,名为gcode_io。
加载GCodeIO过程中注册'gcode_fd'文件句柄到反应器reactor,回调函数_process_data处理虚拟文件读取的数据,并解析出命令调用gcode._process_commands函数执行指令,核心代码如下:
# Support reading gcode from a pseudo-tty interface
class GCodeIO:def __init__(self, printer):self.printer = printer...self.fd = printer.get_start_args().get("gcode_fd")self.reactor = printer.get_reactor()...if self.is_fileinput and self.fd_handle is None:self.fd_handle = self.reactor.register_fd(self.fd,self._process_data)
3.2 网络指令
网络指令一般由显示屏/网页发出,指令传输流程如下:
网页/显示屏指令->moonraker->printer_data/comms/klippy.sock->klipper/webhooks.py。
webhooks.py中的WebHooks对象也是add_early_printer_objects加载到Printer中去的,加载过程中创建socket以及注册回调函数流程如下:
ServerSocket类 -> 创建klippy.sock(路径由命令行-a指定)-> 注册register_fd函数_handle_accept到反应器reactor -> 函数_handle_accept监听到连接 -> 创建ClientConnection对象。
ClientConnection类->注册socket文件句柄:process_received(接收)和_do_send(发送)到reactor中 -> process_received函数接收moonraker传进的指令 -> 解析指令,获取指令对应在WebHooks类中注册的回调函数 -> 执行函数 -> do_send反馈结果到moonraker。
注意:WebHooks类在很多模块中都有一些注册它的函数,因此moonraker会因启用的配置不一样而可以被传进来的指令也不一样
Gcode指令(method:gcode/script)->被GCodeHelper类注册到Webhooks中->执行_handle_script->调用gcode.py中GCodeDispatch类成员run_script->实际执行_process_commands函数。
class WebHooks:def __init__(self, printer):self.printer = printer...self.sconn = ServerSocket(self, printer)class ServerSocket:def __init__(self, webhooks, printer):self.printer = printerself.webhooks = webhooksself.reactor = printer.get_reactor()...#创建socketself.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)self.sock.setblocking(0)self.sock.bind(server_address)self.sock.listen(1)#注册到reactorself.fd_handle = self.reactor.register_fd(self.sock.fileno(), self._handle_accept)...
3.3 打印文件
printer.cfg配置[virtual_sdcard]启用模块klippy/extras/virtual_sdcard.py
开始打印一个文件的流程如下:
moonraker发送命令SDCARD_PRINT_FILE -> 执行virtual_sdcard.py的VirtualSD类成员cmd_SDCARD_PRINT_FILE -> 执行_load_file函数 -> 通过os.path定位到文件,io.open打开文件 -> 然后调用do_resume启动打印 -> 注册work_handler到reactor中 -> 不停的读取文件内容 -> 调用gcode.py中GCodeDispatch类成员run_script执行指令 -> 实际执行_process_commands函数。
4.指令执行过程
4.1 指令传递过程
GCodeDispatch类成员run_script -> 实际执行_process_commands函数 -> 解析命令(踢出;注释行,行号N,提取命令号gcode_handler,匹配已注册的命令,即通过函数register_command注册在成员变量gcode_handlers上的命令)-> 执行其回调函数。
handler = self.gcode_handlers.get(cmd, self.cmd_default)
handler(gcmd)
4.2 G1指令
G1是几乎所有移动指令(G2,G3孤形指令也会被转化为G1执行)的基本指令,打印文件中95%以上的指令都是G1指令,因此,了解G1指令如何执行,几乎可以全部了解整个运动指令的执行过程。
G1指令在gcode_move.py中被注册的回调函数为cmd_G1,该函数解析命令传入的参数,获取移动的终点和速度,然后将参数传给ToolHead.move(),该函数会根据参数创建一个Move对象,然后将它加到(add_move)移动队列中,等待被执行。后续怎么执行一条移动指令,在另一篇文章中分析。
到此,整个klipper上位机核心功能分析完了,klipper的其它模块功能(比如温度控制,调平,复位等)可以到extra目录下找到相应的py模块文件,分析它的实现过程。