- 物联网
- 后端
【免费下载链接】OctoPrint
OctoPrint is the snappy web interface for your 3D printer!
本文以 OctoPrint 内置的 Virtual Printer 插件为核心,系统讲解如何在不连接真实硬件的前提下调试 OctoPrint 的串口通信:从启用方式、config.yaml全套配置项(含默认值与源码级行为解析),到终端里可直接触发的调试命令,再到插件源码内部的队列/线程模型与工厂钩子实现。读完本文,你将掌握用虚拟打印机复现固件怪癖、通信错误、SD 打印等边界场景的完整套路,可直接用于插件开发与通信层调试。
为什么需要一台"虚拟打印机"
OctoPrint 的通信层(octoprint.comm)负责与真实打印机进行 G 代码收发、校验和与行号管理、重发(resend)协商、温度轮询等一系列复杂交互。在开发插件或排查通信问题时,反复开关真实打印机既不现实也很危险——尤其是测试 M112 急停、断连、固件无响应这类破坏性场景。
OctoPrint 从 2013 年的早期版本起就一直内置虚拟打印机能力,自 OctoPrint 1.4.1 起被完整抽取为独立的 bundled 插件。根据 docs/bundledplugins/virtual_printer.rst 的描述,它能够:
- 模拟多种固件怪癖(Repetier、Smoothie、Klipper 风格的温度/应答行为);
- 模拟通信问题(校验和错误、行号错乱、超时无响应);
- 通过
config.yaml高度定制行为,用于测试固件识别、重发逻辑、SD 打印等路径。
该插件的实际定位在其元数据中写得很清楚:"Provides a virtual printer via a virtual serial port for development and testing purposes"(见 src/octoprint/plugins/virtual_printer/init.py)。注意:虚拟打印机虽然能模拟温度变化、移动耗时等物理行为,但它的目的是调试 OctoPrint 的串口通信逻辑,而不是替代真实打印机的机械运动验证。
启用虚拟打印机
启用方式有两种,任选其一:
方式一:界面设置。在 OctoPrint 的 Settings(设置)→ 插件面板中打开 Virtual Printer 的配置页。该页面只有一个开关"Enable the virtual printer",对应settings.plugins.virtual_printer.enabled键,其模板源码位于 src/octoprint/plugins/virtual_printer/templates/virtual_printer_settings.jinja2,帮助文本明确说明:启用后会出现一个额外的串口VIRTUAL,由假打印机实现支撑,适用于开发调试。
方式二:config.yaml 配置。在config.yaml中写入plugins.virtual_printer.enabled: true(详见下文完整配置示例)。注意历史版本中该配置位于devel.virtualPrinter,插件提供了从旧位置到plugins.virtual_printer的自动迁移逻辑(见 src/octoprint/plugins/virtual_printer/init.py)。
启用后,在 Connection(连接)面板的串口下拉列表中会出现名为VIRTUAL的额外端口。这是通过插件钩子octoprint.comm.transport.serial.additional_port_names注入的(见 src/octoprint/plugins/virtual_printer/init.py);只有当enabled为 true 时该端口才会出现在列表中。在 tests/playwright/specs/connect.spec.js 中,OctoPrint 的端到端测试正是通过selectOption("VIRTUAL")完成"连接/断开虚拟打印机"的用例。
虚拟打印机的底层实现:端口、队列与线程
在深入配置项之前,先理解虚拟打印机的实现模型,这有助于理解每个配置项的作用。插件的串口工厂钩子octoprint.comm.transport.serial.factory在端口名为VIRTUAL且插件已启用时,会构造一个VirtualPrinter实例并交给 OctoPrint 的通信层(见 src/octoprint/plugins/virtual_printer/init.py)。也就是说,对 OctoPrint 而言它就是一个普通的串口对象,只是背后是纯软件模拟。
核心类VirtualPrinter定义在 src/octoprint/plugins/virtual_printer/virtual.py,它对外暴露了与 pyserial 一致的最小接口(write、readline、close、timeout属性等),内部则是一个精巧的模拟器:
incoming(RX 缓冲区):一个容量为rxBuffer字节的有界队列,对应固件的串口接收缓冲区。当它被写满时,write()会抛出SerialTimeoutException,模拟真实串口的阻塞(见 virtual.py);buffered(命令缓冲区):容量为commandBuffer条的队列,用于模拟 Marlin 式的运动命令缓冲;SD 打印线程把 G0/G1/G2/G3 送入该队列,由_processBuffer线程按耗时模拟执行(见 virtual.py 与 virtual.py);outgoing(输出队列):readline()从中取行返回给 OctoPrint,模拟固件回复;- 两条后台线程:
wait_thread(_processIncoming,解析收到的命令并生成回复)与buffer_thread(_processBuffer,消费运动缓冲),在构造函数中启动(见 virtual.py); - 温度模拟:每个处理周期调用
_simulateTemps(),让实际温度按剩余温差比例向目标/环境温度收敛(见 virtual.py)。
理解了这套模型,下面配置项的作用就一目了然了。
config.yaml 完整配置参考
以下配置位于config.yaml的plugins.virtual_printer键下。所有默认值以插件get_settings_defaults()的返回为准(见 src/octoprint/plugins/virtual_printer/init.py),并与官方文档 docs/development/virtual_printer.rst 交叉核对:
plugins: # Settings for the virtual printer virtual_printer: # 是否启用虚拟打印机并将其加入可用串口列表。默认 false enabled: true # 是否在 resend 请求之后额外发送一个 "ok"(模仿 Repetier)。默认 false okAfterResend: false # 是否强制通信必须携带校验和与行号(模仿 Repetier)。 # 为 true 时,没有行号/校验和的命令会被拒绝并报错。默认 false forceChecksum: false # 是否在 "ok" 响应中附带被确认的行号。默认 false okWithLinenumber: false # 模拟的挤出机数量。默认 1 numExtruders: 1 # 将指定热端钉死在固定温度,形如 {0: 200.0, 1: 210.0}。默认 null pinnedExtruders: null # M105 输出中是否额外包含当前工具温度段 T(独立于 T0/T1...)。 # true: > M105 # < ok T:23.5/0.0 T0:34.3/0.0 T1:23.5/0.0 B:43.2/0.0 # false: > M105 # < ok T0:34.3/0.0 T1:23.5/0.0 B:43.2/0.0 includeCurrentToolInTemps: true # M23 打开文件响应中是否包含文件名。 # true: > M23 filename.gcode # < File opened: filename.gcode Size: 27 # false: > M23 filename.gcode # < File opened includeFilenameInOpened: true # 是否模拟热床。默认 true hasBed: true # 是否模拟加热舱室。默认 false hasChamber: false # 是否以独立消息上报目标温度(Repetier 风格)。 # true: > M109 S220.0 # < TargetExtr0:220.0 # < ok # > M105 # < ok T0:34.3 T1:23.5 B:43.2 # false: > M109 S220.0 # < ok # > M105 # < ok T0:34.3/220.0 T1:23.5/0.0 B:43.2/0.0 repetierStyleTargetTemperature: false # 是否采用 Repetier 风格的重发(对同一行多次发送 resend)。默认 false repetierStyleResends: false # 是否在命令输出之前发送 ok。 # true: > M20 # < ok # < Begin file list # < End file list # false: > M20 # < Begin file list # < End file list # < ok okBeforeCommandOutput: false # M105 响应中第一个挤出机是否以 T 而非 T0 上报(Smoothie 风格)。默认 false smoothieTemperatureReporting: false # Klipper 风格温度上报:单挤出机时以 T0 而非 T 上报。默认 false(源码新增项) klipperTemperatureReporting: false # 是否启用 reprapfw 风格 M114 坐标响应。默认 false(源码新增项) reprapfwM114: false # SD 文件列表(M20)输出相关 sdFiles: # M20 响应是否包含文件大小。默认 true size: true # M20 响应是否包含时间戳(仅当 size=true 时生效)。默认 false timestamp: false # M20 响应是否包含长文件名(仅当 size=true 时生效)。默认 false longname: false # 长文件名是否加引号输出。默认 true longname_quoted: true # 是否以大写 DOS 文件名输出。默认 false upper_case: false # 从输出缓冲区取回数据的强制暂停间隔(秒)。默认 0.01 throttle: 0.01 # 当串口 RX 缓冲区为空时,是否每隔 waitInterval 秒发送 "wait"。默认 false # (注意:源码默认值为 true,见 __init__.py get_settings_defaults) sendWait: false # 发送 "wait" 行的间隔(秒)。默认 1 waitInterval: 1 # 模拟 RX 缓冲区大小(字节)。写满后 OctoPrint 侧的发送将阻塞。默认 64 rxBuffer: 64 # 模拟命令缓冲区容量(条数)。满时缓冲的命令将阻塞直到有空闲槽位。默认 4 commandBuffer: 4 # 是否支持 M112(模拟 kill)。默认 true supportM112: true # 是否把通过 M117 收到的消息以 "echo:" 行回显。默认 true echoOnM117: true # 是否模拟 M29 的残缺行为(响应后缺失 ok)。默认 true brokenM29: true # 是否模拟残缺的 resend 行为(源码新增项)。默认 false brokenResend: false # F 是否作为独立命令被支持。默认 false supportF: false # 上报的固件名称(用于测试固件识别)。默认 "Virtual Marlin 1.0" firmwareName: Virtual Marlin 1.0 # 是否模拟共享喷嘴(多个挤出机共享同一温度传感器)。默认 false sharedNozzle: false # 忙处理时是否发送 "busy" 消息。默认 false sendBusy: false # 发送 busy 消息的间隔(秒)。默认 2.0 busyInterval: 2.0 # 是否在连接时模拟一次复位。默认 true simulateReset: true # 模拟复位时发送的行 resetLines: - start - "Marlin: Virtual Marlin!" - "SD card ok" # 源码默认还包含一个二进制字符 "\x80", # 可用于测试通信层对非常规字节的处理 # 预置的 ok 响应池,用于模拟发错的 ok;也可在运行时通过 !!DEBUG:prepare_ok 填充。默认 [] preparedOks: [] # ok 响应的格式串。占位符: # lastN : 最后确认的行号 # buffer: 内部命令缓冲区空余槽位数 # 示例(扩展 ok 格式): ok N{lastN} P{buffer} okFormatString: ok # M115 输出格式串。占位符: # firmware_name: firmwareName 定义的固件名 m115FormatString: "FIRMWARE_NAME: {firmware_name} PROTOCOL_VERSION:1.0" # M115 输出是否包含能力报告。默认 true m115ReportCapabilities: true # 能力报告内容(enabled 时生效) capabilities: AUTOREPORT_TEMP: true AUTOREPORT_SD_STATUS: true AUTOREPORT_POS: false BUSY_PROTOCOL: false CHAMBER_TEMPERATURE: false EMERGENCY_PARSER: true EXTENDED_M20: false LFN_WRITE: false # M115 输出是否包含打印区域几何报告(对应 Marlin 的 M115_GEOMETRY_REPORT)。默认 false m115ReportArea: false # M114 坐标输出格式串(源码新增项) m114FormatString: "X:{x} Y:{y} Z:{z} E:{e[current]} Count: A:{a} B:{b} C:{c}" # 模拟环境温度(°C)。默认 21.3 ambientTemperature: 21.3 # 存在目标温度时 M105 的响应格式。占位符: # heater: 加热器 id(如 T0、T1、B) # actual: 加热器实际温度 # target: 加热器目标温度 m105TargetFormatString: "{heater}:{actual:.2f}/ {target:.2f}" # 无目标温度时 M105 的响应格式。占位符同上(无 target) m105NoTargetFormatString: "{heater}:{actual:.2f}" # M123 风扇 RPM 响应格式。占位符: # fan: 风扇 id(如 E0) # rpm: 风扇转速 m123RPMFormatString: "{fan}:{rpm} RPM" # M123 风扇功率响应格式。占位符: # fan: 风扇 id # power: 风扇功率等级 m123PowerFormatString: "{fan}@:{power}" # 虚拟风扇的最高转速(RPM)。默认 4560 fanMaxSpeed: 4560 # 是否启用虚拟 EEPROM。启用后在插件数据目录生成 eeprom.json, # 使设置跨连接持久化,并支持 M500/M501/M502/M504 等设置命令, # 响应风格参照 Marlin 2.0。默认 true enable_eeprom: true # 是否支持 M503。默认 true support_m503: true # 模拟线路噪声的重发比例(百分比)。默认 0 resend_ratio: 0 # 在指定行号上模拟通信错误,每项格式为 "<行号>:<错误类型>": # 100:resend 在第 100 行请求一次简单重发 # 105:resend_with_timeout 在第 105 行请求重发并模拟超时无响应 # 110:missing_lineno 在第 110 行模拟缺失行号 # 115:checksum_mismatch 在第 115 行模拟校验和不匹配 simulated_errors: - 100:resend - 105:resend_with_timeout - 110:missing_lineno - 115:checksum_mismatch配置项的源码级行为解读
以下几个配置项的行为值得结合源码细看,因为它们直接影响通信层的判定路径:
forceChecksum/ 校验和与行号解析。在_processIncoming中,收到含*的行会先剥离校验和并比对,不匹配则直接触发 resend;当行以N开头时按lastN + 1校验行号;而如果行既不带校验和、forceChecksum又为 true,则发送Error: Missing checksum并丢弃该行(见 virtual.py)。这可以精确测试 OctoPrint 通信层在强制校验模式下的行为。
simulated_errors的实现与上述解析深度耦合:只有携带行号(N开头)且行号恰好命中配置值的命令才会触发对应错误动作,且每个行号只触发一次(_already_simulated_errors去重),M110 重置行号时会清空已触发记录(见 virtual.py)。四种错误类型的含义与官方文档 docs/development/virtual_printer.rst 中给出的示例完全一致。
resend_ratio:内部换算为_resend_every_n = 100 // resend_ratio,即每收到n行触发一次带校验和错误的 resend,用于模拟线路噪声导致的行丢失(见 virtual.py 与 virtual.py)。
okFormatString与preparedOks:_ok()每次回复时优先从preparedOks弹出预置的"错误 ok",否则用okFormatString格式化,占位符lastN为最后确认行号、buffer为命令缓冲区空余槽位(见 virtual.py)。用ok N{lastN} P{buffer}即可模拟"扩展 ok"格式,测试 OctoPrint 对这类固件的兼容性。
pinnedExtruders与ambientTemperature:_simulateTemps()在每轮模拟中,若热端 id 命中pinnedExtruders则直接固定为该温度;否则按与目标/环境温度的差值比例逼近(见 virtual.py)。注意目标为 0 时温度会回落向ambientTemperature,因此该参数决定了"加热关闭后冷却"的基准值。
enable_eeprom/ 虚拟 EEPROM:启用后会在插件数据目录(get_plugin_data_folder())创建eeprom.json,首次启动写入默认设置,后续启动读取,从而跨连接持久化 M500 系列写入的值。EEPROM 默认设置模拟 Marlin 2.0 风格(steps、feedrate、max_accel 等),定义在VirtualEEPROM.get_default_settings()(见 virtual.py 及后续行)。
两点官方文档与源码默认值的差异(以源码为准):文档示例中sendWait: false,而源码默认值为true;文档的capabilities示例重复列出了两次AUTOREPORT_TEMP,实际源码能力集还包含BUSY_PROTOCOL、CHAMBER_TEMPERATURE;文档中m123RPMFormatString出现两次,第二个实际是m123PowerFormatString。配置时建议以本文表格(源码核对版)为准。
日志文件
启用后,虚拟打印机会把所有串口通信写入plugin_virtual_printer_serial.log,位于 OctoPrint 的日志文件夹(logs 目录)中。该日志由CleaningTimedRotatingFileHandler按天轮转、保留 3 份备份,格式为时间戳 + 消息(见 src/octoprint/plugins/virtual_printer/init.py)。
日志中每行通过<<<标记 OctoPrint 发送给打印机的数据(write()方向)、通过>>>标记打印机回复的数据(readline()方向),是排查"OctoPrint 到底发了什么、固件回了什么"的第一手证据。配合 OctoPrint 的日志下载功能,可以把该文件一并打包用于问题复现。
终端调试命令:!!DEBUG:
虚拟打印机最强的调试手段是通过 OctoPrint 的 Terminal(终端)标签页直接触发各种边界条件。所有命令以!!DEBUG:开头,例如发送!!DEBUG:action_disconnect会立即触发// action:disconnect让打印机断开。只发送!!DEBUG(不带命令)会回显完整的帮助信息。
以下是帮助文本中列出的全部命令(与 src/octoprint/plugins/virtual_printer/virtual.py 中的_debugTrigger帮助文本一致,并补充了帮助文本未完整列出但源码已实现的部分):
OctoPrint Virtual Printer debug commands help ? | This help. # Action Triggers(动作触发) action_pause | 向主机发送 "// action:pause" 动作触发。 action_resume | 向主机发送 "// action:resume" 动作触发。 action_disconnect | 向主机发送 "// action:disconnect" 动作触发。 action_custom <action>[ <parameters>] | 向主机发送自定义 "// action:<action> <parameters>" 动作触发。 # Communication Errors(通信错误模拟) dont_answer | 不确认下一条命令。 go_awol | 完全停止回复(对应源码 _debug_awol,write/readline 直接静默)。 trigger_resend_lineno | 触发一次行号不匹配的 resend 错误。 trigger_resend_checksum | 触发一次校验和不匹配的 resend 错误。 trigger_missing_checksum | 触发一次缺失校验和的 resend 错误。 trigger_missing_lineno | 触发一次"带校验和却无行号"的错误(不请求 resend)。 trigger_fatal_error_marlin | 触发一次 Marlin 风格的致命错误/模拟加热失败。 trigger_fatal_error_repetier | 触发一次 Repetier 风格的致命错误/模拟加热失败。 drop_connection | 断开串口连接(后续 write/readline 抛 SerialTimeoutException)。 prepare_ok <broken ok> | 将 <broken ok> 入队,后续用它替代真正的 "ok"。 rerequest_last | 对最后一行 +1 无限请求重发。 resend_ratio <int:percentage> | 将重发比例设为给定百分比(0-100),模拟线路噪声;设为 0 关闭。 toggle_klipper_connection | 切换 Klipper 连接状态;关闭后对所有命令回复 "!! Lost communication with MCU 'mcu'"。 # Reply Timing / Sleeping(回复时机与睡眠) sleep <int:seconds> | 睡眠 <seconds> 秒。 sleep_after <str:command> <int:seconds> | 每次执行 <command> 后睡眠 <seconds> 秒。 sleep_after_next <str:command> <int:seconds> | 下一次执行 <command> 后睡眠 <seconds> 秒。 # SD printing(SD 打印) start_sd <str:file> | 从 SD 中选择并开始打印文件 <file>。 select_sd <str:file> | 从 SD 中选择文件 <file>,暂不开始打印;用 start_sd 开始。 cancel_sd | 取消正在进行的 SD 打印。 # Misc(其他) send <str:message> | 向主机回发 <message>。 reset | 模拟复位,内部状态将丢失(对应 _reset(),会清空队列与调试标志)。 unbusy | 退出 busy 循环。 set_ambient <温度> | 动态设置环境温度(源码实现,帮助文本未列出)。 mintemp_error | 发送 MINTEMP 错误(源码实现,帮助文本未列出)。 maxtemp_error | 发送 MAXTEMP 错误(源码实现,帮助文本未列出)。调试命令的典型使用场景
- 测试超时/无响应路径:
go_awol或dont_answer可用于验证 OctoPrint 的 read timeout 处理、连接监控和"打印机无响应"告警逻辑。源码中_debug_awol会让write()静默吞掉数据、readline()睡眠一个 read timeout 后返回空行(见 virtual.py),与真实固件挂死行为一致。 - 测试重发协商:
trigger_resend_lineno、trigger_resend_checksum、rerequest_last、resend_ratio分别覆盖行号错乱、校验和错误、无限重发、随机噪声四种重发场景,是验证 OctoPrint 通信层重发逻辑是否健壮的标准手段。 - 测试动作钩子:
action_pause/action_resume/action_disconnect/action_custom触发// action:前缀的动作,可验证 OctoPrint 的动作命令处理与依赖它的插件行为。 - 测试 SD 打印链路:
start_sd/select_sd/cancel_sd配合虚拟 SD 卡文件夹(virtualSd基础目录),可以不走 OctoPrint 的上传/打印路径而直接从"固件侧"发起 SD 打印,测试 SD 状态轮询(M27)、暂停/恢复(M25/M24)等交互。 - 测试温度报告:
set_ambient、mintemp_error、maxtemp_error可用于验证温度告警与错误恢复逻辑。
这些命令的解析入口在_processIncoming中:凡是以!!DEBUG:开头(或恰好为!!DEBUG)的行都会被路由到_debugTrigger(),不会进入正常的命令处理(见 virtual.py)。参数型命令(如sleep、action_custom、prepare_ok、send、set_ambient、start_sd、select_sd、resend_ratio)通过类级预编译正则匹配,定义在 virtual.py。
从源码看插件的可扩展性
虚拟打印机插件本身还预留了扩展点:它注册了钩子octoprint.plugin.virtual_printer.custom_action,允许其他插件自定义!!DEBUG:action_custom的动作行为(见 virtual.py)。这意味着你可以在自己的插件中实现octoprint.plugin.virtual_printer.custom_action钩子,为调试注入完全自定义的固件行为。
小结与上手路线
虚拟打印机是 OctoPrint 通信层开发调试的"沙盒"。推荐的上手路线:
- 在设置面板勾选启用,或写入
plugins.virtual_printer.enabled: true; - 在连接面板选择
VIRTUAL端口连接,观察 Terminal 中的握手与温度轮询; - 打开
plugin_virtual_printer_serial.log,建立<<</>>>双向通信日志的阅读习惯; - 按需在
config.yaml中调整simulated_errors、resend_ratio、forceChecksum等参数复现目标 bug; - 在 Terminal 中使用
!!DEBUG:命令动态注入故障,配合!!DEBUG查看完整命令帮助; - 结合 tests/playwright/specs/connect.spec.js 的端到端用例思路,把关键场景固化为自动化测试。
进一步阅读:插件的用户视角说明见 docs/bundledplugins/virtual_printer.rst,完整源码位于 src/octoprint/plugins/virtual_printer/,配置项默认值可直接查阅 src/octoprint/plugins/virtual_printer/init.py。
- 物联网
- 后端
【免费下载链接】OctoPrint
OctoPrint is the snappy web interface for your 3D printer!
相关推荐
OctoPrint虚拟打印机配置与调试指南
OctoPrint虚拟打印机配置与调试指南 虚拟打印机简介 OctoPrint内置了一个强大的虚拟打印机插件,这个工具对于开发者调试串口通信功能特别有用。它能够
物联网后端Virtual ZPL Printer虚拟标签打印机完全使用指南
Virtual ZPL Printer是一款基于以太网的虚拟斑马标签打印机,专为测试条形码标签应用程序而设计。它利用Labelary服务,让您无需物理打印机就能
开发工具后端Fixed-Data-Table-2:如何用React构建处理百万级数据的高性能表格组件
Fixed Data Table 2:如何用React构建处理百万级数据的高性能表格组件 在当今数据驱动的应用开发中,处理大规模数据集是每个前端开发者面临的共同
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考