


























原创文章,转载引用务必注明链接,水平有限,如有疏漏,欢迎指正交流。
文章如有更新请访问 DFRobot 社区及 cnblogs 博客园,前者内容较全,后者排版及阅读体验更佳。
一晃才发现小半年没有发表新文章了,仿佛还在上个月。
这是一篇借助豆包,基于我的大纲与测试结果自动生成并经过我修改的文章,不知道效果如何。其实我本地很多存货,苦于没有时间精力整理成文,借助 AI 可能会是一个很好的方法。同时部分程序代码使用 AI 进行辅助,至于感受会在文末总结。
在 3D 打印的领域里,Klipper 固件凭借出色的性能,深受广大爱好者青睐。早期基于电视盒子打造的 Klipper 上位机,常见配置是 1G 内存+ 8G eMMC 存储。随着模型复杂度增加,上传的模型切片文件越来越大、数量越来越多,再加上延时摄影临时抓取的大量图片以及渲染生成的视频,这点存储空间就显得捉襟见肘了。
之前我写过文章,包括跟着思兼学习Klipper(10)使能Klipper从U盘读取Gcode并打印 以及 跟着思兼学习Klipper(23) 玩一玩必趣 BTT Pi 上位机,分别介绍热插拔自动挂载 U 盘或 SD 卡实现脱机打印,以及利用Alist+Rclone+网盘给上位机扩容。今天我们要更进一步,探索新的更完善的扩容方案。
软件环境:
硬件环境:
本文涉及的内容:


KBOX_EXT_SD/KBOX_EXT_U1这次的方案既支持脱机打印,又支持自动扩容。相比直接用 udev + mount 或者 fstab 挂载,优势明显:
规避系统启动风险:fstab 挂载一旦配置有误,比如设备路径写错、文件系统类型指定错误,极有可能导致系统启动失败。而 mergerfs 挂载能有效避免这类问题,确保系统顺利启动。
灵活存储运用:既能实现移动存储设备即插即打印,方便快捷地开启打印任务;又能常规插在上位机上作为扩容存储设备,不管是存放更大的 swapfile 来优化系统运行,还是存储延时摄影素材、大量的 gcode 文件,都不在话下。
解决目录显示问题:普通 mount 操作遇到同名目录时,会隐藏原目录,导致里面的文件无法访问。mergerfs 则不会出现这种状况,所有文件和目录都能正常显示,方便用户管理文件。
便捷切片软件上传:以往切片软件上传文件,常挂载到 gcodes 子目录,之后还得手动移动文件到合适位置。现在新方案支持切片软件直接上传到用于扩容的移动存储目录,大大简化了流程,提高了工作效率。
降低板载存储损耗:板载 eMMC 存储更换不便,且频繁读写易磨损。新方案把数据存储任务转移到可热插拔的移动存储设备,如 sd 卡。sd 卡用完可轻松更换,有效减少 eMMC 的磨损,延长其使用寿命。
接下来,我们来看看具体实现步骤。

主要目的是选择合适的文件系统,对扫描到的移动存储设备格式化,设置分区标签为 KBOX_EXT_SD/KBOX_EXT_U1 等,方便后续正确挂载,并忽视其他存储设备。
因为使用 microSD 或 U 盘这种闪存设备,且只存储简单文件无高级特性需求,考虑到 Windows/Linux 系统兼容性,存储寿命等因素,这里推荐如果需要 Windows 下也可以访问,推荐 exFAT 文件系统,否则可以尝试 f2fs 文件系统。其他如 ext4、btrfs 等也可以选择。
# 安装 exFAT 或 f2fs 文件系统支持包
sudo apt install exfatprogs f2fs-tools util-linux attr bc
使用步骤:
sudo bash prepare_storage.sh 对设备进行初始化,根据提示选择正确的设备,如果不确定,可以在插拔移动存储前后,在终端输入 lsblk,对比后确认。主要功能:
#!/bin/bash
# '''
# Author: sjqlwy sjqlwy@yeah.net
# Date: 2025年4月13日
# Description:初始化存储设备用于系统扩容以及脱机打印
# '''
# 获取根目录所在设备
root_device=$(df / | tail -1 | awk '{print $1}' | sed 's/[0-9]*$//')
# 扫描可用的 SD 卡和 U 盘
devices=()
sizes=()
for device in /sys/block/*; do
device_name=$(basename $device)
if [[ $device_name == "mmcblk"* || $device_name == "sd"* ]]; then
device_path="/dev/$device_name"
if [ "$device_path" != "$root_device" ]; then
devices+=("$device_path")
size=$(blockdev --getsize64 "$device_path")
# 将大小转换为GB并保留1位小数
size_gb=$(printf "%.1f" $(echo "scale=1; $size / 1024 / 1024 / 1024" | bc))
sizes+=("$size_gb GB")
fi
fi
done
# 显示可用设备供用户选择
if [ ${#devices[@]} -eq 0 ]; then
echo "未找到可用的 SD 卡或 U 盘。"
exit 1
fi
echo "可用的设备有:"
for i in "${!devices[@]}"; do
echo "$((i + 1)). ${devices[$i]} (${sizes[$i]})"
done
# 获取用户选择
read -p "请输入你要选择的设备编号 (1 - ${#devices[@]}): " choice
if ! [[ $choice =~ ^[0-9]+$ ]] || [ $choice -lt 1 ] || [ $choice -gt ${#devices[@]} ]; then
echo "无效的选择。"
exit 1
fi
selected_device=${devices[$((choice - 1))]}
# 卸载设备上的所有分区
for partition in $(lsblk -n -o NAME $selected_device | grep -E '^[^[:space:]]+[0-9]+$'); do
umount "/dev/$partition" 2>/dev/null
done
# 删除原有分区表
parted -s "$selected_device" mklabel gpt
if [ $? -ne 0 ]; then
echo "删除原有分区表失败,请检查设备状态。"
exit 1
fi
# 创建新分区
parted -s "$selected_device" mkpart primary 0% 100%
if [ $? -ne 0 ]; then
echo "创建新分区失败,请检查设备状态。"
exit 1
fi
# 等待内核识别新分区
sleep 2
# 通知内核重新读取分区表
partprobe "$selected_device"
# 获取新分区的设备路径
if [[ $selected_device == *"mmcblk"* ]]; then
new_partition="${selected_device}p1"
else
new_partition="${selected_device}1"
fi
# 检查新分区是否存在
if [ ! -b "$new_partition" ]; then
echo "新分区 $new_partition 未被识别,请检查设备状态。"
exit 1
fi
# 生成合适的 LABEL
if [[ $selected_device == *"mmcblk"* ]]; then
label="KBOX_EXT_SD"
else
existing_labels=$(lsblk -o LABEL | grep '^KBOX_EXT_U')
max_num=0
for existing_label in $existing_labels; do
num=$(echo $existing_label | grep -oE '[0-9]+$' | sed 's/^0*//')
if [ -n "$num" ] && [ $num -gt $max_num ]; then
max_num=$num
fi
done
new_num=$((max_num + 1))
label="KBOX_EXT_U${new_num}"
fi
# 让用户选择文件系统格式
echo "请选择文件系统格式:"
echo "1. exfat(双系统访问,推荐)"
echo "2. f2fs"
read -p "请输入对应的数字 (1 - 2): " fs_choice
if [ "$fs_choice" != "1" ] && [ "$fs_choice" != "2" ]; then
echo "无效的选择,请输入 1 或 2。"
exit 1
fi
if [ "$fs_choice" == "1" ]; then
fs_type="exfat"
mkfs.exfat -n "$label" "$new_partition"
# 给 exfat 分区添加 msftdata 标签
partition_number=$(echo "$new_partition" | grep -oE '[0-9]+$')
parted -s "$selected_device" set "$partition_number" msftdata on
else
fs_type="f2fs"
mkfs.f2fs -l "$label" "$new_partition"
fi
if [ $? -ne 0 ]; then
echo "格式化新分区为 $fs_type 格式失败,请检查设备状态。"
exit 1
fi
# 修改 mount_point 为和 label 相同的格式
mount_point="/mnt/$label"
mkdir -p "$mount_point"
mount "$new_partition" "$mount_point"
if [ $? -ne 0 ]; then
echo "挂载新分区 $new_partition 失败,请检查设备状态。"
exit 1
fi
# 获取当前 sudo 执行用户
user=$(logname)
# 创建目录
mkdir -p "$mount_point/gcodes"
mkdir -p "$mount_point/timelapse"
# 根据文件系统格式决定是否修改目录所有者
if [ "$fs_type" == "f2fs" ]; then
chown -R "$user" "$mount_point/gcodes" "$mount_point/timelapse"
fi
# 卸载新分区
umount "$mount_point"
if [ $? -ne 0 ]; then
echo "卸载新分区 $new_partition 失败,请手动卸载。"
fi
echo "设备 $selected_device 已成功删除原有分区,创建了 $fs_type 分区,标签为 $label,并创建了 gcodes 和 timelapse 目录。"
注意:
KBOX_EXT_Ux 而不是 KBOX_EXT_USBx至此我们获得了一个标签为 KBOX_EXT_* 的移动存储,并可以在 Windows 下访问 (exFAT)。
# 安装 mergerfs,此处使用了镜像源,且下载 Klipper Pro 系列上位机所使用的 x86_64(amd64) 架构软件包
wget https://cors.isteed.cc/https://github.com/trapexit/mergerfs/releases/download/2.40.2/mergerfs_2.40.2.debian-bookworm_amd64.deb
sudo dpkg -i mergerfs*.deb
# 创建目录 local_gcodes 用于存放从原 ~/printer_data/gcodes 目录迁移来的模型切片文件
cd && mkdir ~/printer_data/local_gcodes
mv ~/printer_data/gcodes/* ~/printer_data/local_gcodes/
注意:
~/printer_data/local_gcodes 目录中的文件不可见,所以我们提前移出既往使用直接将移动存储设备挂载到 ~/printer_data/local_gcodes 子目录下,存在诸多不便,所以本次使用 /mnt/目录下,然后使用 mergerfs 整合到虚拟目录。udev 规则如同智能管家,能识别标签为 KBOX_EXT_SD/USBx 设备插入,并按规则自动挂载到指定的 /mnt/[LABEL] 目录。在 /etc/udev/rules.d/ 目录创建新规则文件,文件名可自定义,建议遵循规范便于管理,如创建名为 99-kboxext-mount.rules 的文件(数字表示优先级,越小越高,这里用 99)。
mkdir -p $HOME/kbox_scripts/
# 创建一个脚本,查找根目录所在磁盘用于排除,且输出符合 udev 规则。key:vaule
tee $HOME/kbox_scripts/get_rootfs.sh > /dev/null << 'EOF'
#!/bin/bash
device=$(/bin/findmnt -n -o SOURCE /)
echo "root_device=$device"
EOF
# 创建自动挂载规则,我们仍然参考 homeassistant 的脚本
# 注意这里使用 pi 用户
sudo tee /etc/udev/rules.d/99-kboxext-mount.rules > /dev/null << 'EOF'
# Get the root device
IMPORT{program}="/bin/bash /home/pi/kbox_scripts/get_rootfs.sh"
ENV{root_device}="%E{root_device}"
# Import the partition info into the environment variables
IMPORT{program}="/usr/sbin/blkid -o udev -p %N"
# Exit if partition is not a filesystem
ENV{ID_FS_USAGE}!="filesystem", GOTO="abort_rule"
# Exit if the label doesn't start with KBOX_EXT_
ENV{ID_FS_LABEL}!="KBOX_EXT_*", GOTO="abort_rule"
# Exit if it's the root device
ENV{DEVNAME}=="%E{root_device}", GOTO="abort_rule"
# Determine the mount point using the label
ENV{mount_point}="/mnt/%E{ID_FS_LABEL}"
# General mount options
ACTION=="add", ENV{mount_options}="relatime,sync,uid=pi,gid=pi"
# Filesystem-specific mount options
# ACTION=="add", ENV{ID_FS_TYPE}=="vfat|ntfs", ENV{mount_options}="{mount_options},utf8,gid=100,umask=002"
ACTION=="add", ENV{ID_FS_TYPE}=="f2fs", ENV{mount_options}="$env{mount_options}"
ACTION=="add", ENV{ID_FS_TYPE}=="exfat", ENV{mount_options}="$env{mount_options},utf8"
# Create the mount point directory if it doesn't exist and mount the device
ACTION=="add", RUN{program}+="/usr/bin/mkdir -p %E{mount_point}", RUN{program}+="/usr/bin/systemd-mount --no-block --automount=no --collect -o %E{mount_options} $devnode %E{mount_point}"
# Umount the device on 'remove' action (a.k.a unplug or eject the USB drive)
ACTION=="remove", RUN{program}+="/usr/bin/systemd-umount %E{mount_point}"
# Exit
LABEL="abort_rule"
EOF
# 增加可执行权限,并设置生效
# sudo chmod +x /etc/udev/rules.d/99-kboxext-mount.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
# sudo systemctl restart udev
注意:
sudo udevadm test $(udevadm info -q path -n /dev/mmcblk0p1)是一个 很重要的故障排查命令至此,我们可以实现插入移动存储设备后,系统自动挂载到 /mnt/KBOX_EXT_* 目录下。

创建 mergerfs 服务系统来实现系统启动时自动创建虚拟目录,且在 moonraker 服务之前启动,以便能正确读取 gcodes 目录。
参考 mergerfs 官网并做了一些修改。
cat << _EOF_ > /tmp/mergerfs-klipper.service
[Unit]
Description = MergerFS Service for Klipper Hot-Swap Expansion
After = local-fs.target
Before = moonraker.service
[Service]
Type=simple
KillMode=none
ExecStart=$(which mergerfs) \
-f \
-o cache.files=auto-full \
-o category.create=epff \
-o func.getattr=newest \
-o dropcacheonclose=true \
/mnt/KBOX_EXT_SD/gcodes=RW,100M:/mnt/KBOX_EXT_U1/gcodes=RW,100M:$HOME/local_gcodes=RW,200M \
$HOME/printer_data/gcodes
ExecStop=$(which fusermount) -uz $HOME/printer_data/gcodes
Restart=on-failure
[Install]
WantedBy = multi-user.target
_EOF_
sudo mv /tmp/mergerfs-klipper.service /lib/systemd/system/mergerfs-klipper.service
sudo systemctl enable --now mergerfs-klipper.service
注意:
local-fs.target 的核心用途是确保在系统启动期间,所有本地文件系统(像根文件系统、/home、/var 等分区)都能正确挂载。在达到这个目标状态之后,系统才会继续启动其他依赖于本地文件系统的服务。-f 参数epff,即如果存在目标文件夹则优先写入,否则按由左至右首次发现的可用目录写入。存储设备写入策略复杂,优先写入哪个设备由策略、RW/RO/NC 挂载、保留的最少空间等因素决定。我们希望有 sd 卡插入时优先写入 sd 卡,但默认最小保留空间为 4G,若磁盘所在文件系统可用空间少于 4G,系统就不会往该磁盘写入数据。我测试时用的 4G sd 卡,格式化后可用空间不足 4G,导致系统一直不往 sd 卡写入,这个问题排查了很久。另外,写入策略还有先发现先写入的特点,即先挂载的节点优先写入。解决办法一是更换容量更大的 sd 卡,确保格式化后可用空间大于 4G;二是深入了解系统存储管理机制,根据实际情况调整写入策略相关设置。例如,可通过修改 mergerfs 的配置文件,调整最小保留空间等参数,但这需要一定的技术基础,操作前建议备份相关配置文件,以防设置错误导致系统异常。
mergerfs 的日志信息相当于没有,需要参考官方文档:https://trapexit.github.io/mergerfs/runtime_interfaces/ 使用运行时接口
sudo apt-get install attr
sudo mergerfs -f -o cache.files=off,func.getattr=newest,dropcacheonclose=false,category.create=all /mnt/KBOX_EXT/gcodes:/home/pi/local_gcodes=RO /home/pi/printer_data/gcodes
getfattr -n user.mergerfs.category.create /home/pi/printer_data/gcodes/.mergerfs
# 创建文件
sudo mergerfs -f -o cache.files=off,func.getattr=newest,dropcacheonclose=false,category.create=all /mnt/KBOX_EXT/gcodes=RW:/home/pi/local_gcodes=RO /home/pi/printer_data/gcodes
touch /home/pi/printer_data/gcodes/new-file
getfattr -n user.mergerfs.allpaths /home/pi/printer_data/gcodes/new-file
# 报错 read-only fs
# 显示所有值
getfattr -d /home/pi/printer_data/gcodes/.mergerfs
目前仅以 gcodes 目录演示,计划增加 timelaspe 目录,以及其他可能目录。
当 udev 规则未生效时,可以按照以下步骤进行排查:
/etc/udev/rules.d/ 目录下,且文件名以 .rules 结尾,通常文件名以数字开头(如 80-kboxext-mount.rules),数字表示规则执行的优先级。udev 规则文件使用特定的语法,任何语法错误都可能导致规则不生效。可以使用 udevadm test 命令来测试规则文件的语法。例如,对于规则文件 80-kboxext-mount.rules,可以使用以下命令:sudo udevadm test $(udevadm info -q path -n /dev/mmcblk0p1)
将 /dev/mmcblk0p1 替换为你要测试的实际设备。如果规则文件存在语法错误,该命令会输出相应的错误信息。
udev 可能没有及时加载新规则。可以使用以下命令重新加载规则:sudo udevadm control --reload-rules
udevadm trigger 命令触发设备的添加或移除事件。例如,对于设备 /dev/mmcblk0p1,可以使用以下命令触发添加事件:sudo udevadm trigger --verbose --action=add --sysname-match=/dev/mmcblk0p1
sudo udevadm info -q path -n /dev/mmcblk0p1
sudo udevadm info -q env -n /dev/mmcblk0p1
udevadm info 命令查看设备的属性,确保规则中使用的属性(如 ID_FS_LABEL、ID_FS_TYPE 等)与实际设备的属性一致。例如,查看 /dev/mmcblk0p1 的设备属性:udevadm info --attribute-walk --name=/dev/mmcblk0p1
该命令会输出设备的详细属性信息,检查规则中使用的属性是否正确。
KERNEL 或 ENV{ID_PATH} 进行过滤,需要检查设备的内核名称和路径是否满足条件。RUN 指令调用的脚本),确保脚本具有可执行权限。可以使用 chmod 命令添加执行权限,例如:chmod +x /path/to/your/script.sh
blkid、systemd-mount 等)已经正确安装,并且在 udev 运行环境中可用。udev 的相关信息通常会记录在系统日志中,可以使用 journalctl 命令查看系统日志,查找与 udev 相关的错误信息。例如:sudo journalctl -u systemd-udevd.service
RUN 指令将调试信息输出到日志文件中。例如,在规则中添加以下内容:ACTION=="add", RUN{program}+="/bin/echo 'Device added: %E{DEVNAME}' >> /var/log/udev_debug.log"
这样,当设备添加事件触发时,会将设备名称记录到 /var/log/udev_debug.log 文件中,方便查看调试信息。
通过以上步骤,应该能够逐步排查出 udev 规则未生效的原因。
经过一系列设置和优化,我们的方案基本达成预期目标。现在,既能从电脑拷贝切好的 gcode 文件,插卡就能直接打印,大大提高了打印效率;又实现了持久化扩容,有足够空间存放模型切片文件、延时摄影素材等。热插拔功能让使用存储设备更加便捷,有效解决了早期 Klipper 上位机存储不足的问题,为 3D 打印工作提供了更稳定、高效的存储解决方案。希望大家通过这篇文章,都能顺利实现 Klipper 上位机的热插拔自动扩容,享受更流畅的 3D 打印体验。如果在实践过程中遇到问题,欢迎随时交流探讨。
多了一些废话,还是需要人工参与大量校正
需要作者懂一些基础编程知识,否则生成的代码不可用都不知道如何 DEBUG
还需要我继续学习如何更好地使用 AI 工具。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。