建议不是本行又感兴趣的小伙伴们先看下面两篇了解一下Marlin: 开源Marlin2.x源代码架构学习笔记 3D打印机marlin固件框架与GCode命令总结
YouTube上的老外通俗易懂的方式讲解Marlin2.0
然后再看下面的文章。
1、摘要
说到 RepRaptor,我们就有必要来了解下RepRap 3D打印机。RepRap是人类历史上第一部可以自我复制型的机器。RepRap是一部可以生成塑料实物的免费桌面型 3D 打印机。由于 RepRap 很多部件都是由塑料制成了,并且 RepRap 都可以进行打印,所以 RepRap 可以自我复制。也就是说,任何人都可以花一些时间收集够材料进行制作。这也意味着,如果您已经有一台 RepRap 了,您可以在打印很多有用的物件的同时,为朋友再打印出另一部 RepRap。RepRap 致力于自我复制型机器的制作,并无偿的提供给大家使用。我们用 3D 打印来实现这些,但如果您使用其它的技术也实现了自我复制并愿意无偿提供给大家使用。那么,这里也将非常欢迎您的加入。RepRap是第一款低成本 3D 打印机,并且 RepRap 还开创了开源 3D 打印机的革命。在手工制作类社区的所有成员中被广泛使用的一款 3D 打印机。
关键词:3D打印机;自我复制;RepRap;RepRaptor
2、RepRaptor简介
RepRaptor是一个可用于支持GCode指令3D打印上位机,它是用QT5来编写的。之所以使用QT5来编写,这是因为开发者希望它能够任何硬件上运行。因此,RepRaptor也可以用于控制RepRap 来实现3D模型的打印。它的界面是这样的:
我们能够看到的是,它已经具备了3D打印机上位机的基本雏形。目前3D打印机的主流的架构主要是这样的:
因此,RepRaptor相当于充当了上位机这个部分的工作:
只可惜,这个版本仅仅发布到了RepRaptor v0.3.8以后就没有再进行继续更新了。
但是没关系,我们可以基于这个雏形,做出属于我们自己的3D打印机上位机,然后我们就可以买一台支持联机打印的3D打印机,愉快的进行模型打印了!
3、RepRaptor源码架构导读
在开发属于自己的3D打印机上位机之前,我们必须获得它的源码:
代码语言:javascript复制git clone https://github.com/josefprusa/RepRapCalculator.git
然后使用QT Creator将其打开,接下来我们就可以看到下面的代码结构:
3.1、界面介绍
在这里,我们可以看到这是一个基于QT Designer做UI界面,类似于MFC一样拖控件,然后再使用C 写逻辑。因此,我们能够看到它是由6个UI界面来完成的,分别是:
(1) aboutwindow.ui
关于项目的介绍
(2)eepromwindow.ui
获取打印机EEPROM中的数据并展现到界面上来,此部分功能不全。
(3) errorwindow.ui
一些运行错误的提示窗口
(4)mainwindow.ui
主页面,用于控制打印机的常规操作、获取打印机反馈的信息,例如温度、速度:
(5)sdwindow.ui
使用SD卡进行打印锁需要的设置和文件读取等功能,此部分功能不全。
(6)settingswindow.ui
一些参数的设置,此部分功能不全
3.2、核心代码架构导读
4、打造属于我们自己的3D打印机上位机
4.1、成功打造属于我们自己的3D打印机上位机的前提
当然,想要学会打造自己的打印机的前提,你得具备以下基础知识:
- 掌握QT软件开发(如果你会C#或者其它当然也没问题)
- 掌握3D打印机GCode指令协议
- 其它必要的知识,如设计模式、数据结构等。
4.2、核心交互逻辑的实现
关于GCode的格式以及响应的通讯协议可以参考:
代码语言:javascript复制https://marlinfw.org/meta/gcode/
这上面列出了几乎所有Marlin支持的GCode协议指令。
从源码导读部分,我们最需要关心的是mainwindow.cpp、sender.cpp和parser.cpp这三个文件,因为它们是实现3D打印机上位机成功的基础,这里我们能够看到这三个线程之间的交集部分,也就是mainwindow.cpp里的这段代码:
代码语言:javascript复制//Parser thread signal-slots and init
parserWorker->moveToThread(parserThread);
connect(parserThread, &QThread::finished, parserWorker, &QObject::deleteLater);
connect(this, &MainWindow::startedReadingEEPROM, parserWorker, &Parser::setEEPROMReadingMode);
connect(parserWorker, &Parser::receivedTemperature, this, &MainWindow::updateTemperature);
connect(parserWorker, &Parser::receivedSDFilesList, this, &MainWindow::initSDprinting);
connect(parserWorker, &Parser::receivedEEPROMLine, this, &MainWindow::EEPROMSettingReceived);
connect(parserWorker, &Parser::recievingEEPROMDone, this, &MainWindow::openEEPROMeditor);
connect(parserWorker, &Parser::receivedError, this, &MainWindow::receivedError);
connect(parserWorker, &Parser::receivedSDDone, this, &MainWindow::receivedSDDone);
connect(parserWorker, &Parser::receivedSDUpdate, this, &MainWindow::updateSDStatus);connect(parserWorker, &Parser::receivedNotSDPrinting, this, &MainWindow::receivedNotSDPrinting);
parserThread->start();
parserThread->setPriority(QThread::HighestPriority);
//Sender thread signal-slots and init
senderWorker->moveToThread(senderThread);
connect(senderThread, &QThread::finished, senderWorker, &QObject::deleteLater);
connect(parserWorker, &Parser::receivedOkNum, senderWorker, &Sender::receivedOkNum);
connect(parserWorker, &Parser::receivedOkWait, senderWorker, &Sender::receivedOkWait);
connect(parserWorker, &Parser::receivedResend, senderWorker, &Sender::receivedResend);
connect(parserWorker, &Parser::receivedStart, senderWorker, &Sender::receivedStart);
connect(senderWorker, &Sender::errorReceived, this, &MainWindow::serialError);
connect(senderWorker, &Sender::dataReceived, parserWorker, &Parser::parse);
connect(senderWorker, &Sender::dataReceived, this, &MainWindow::readSerial);
connect(senderWorker, &Sender::reportProgress, this, &MainWindow::updateFileProgress);
connect(senderWorker, &Sender::baudrateSetFailed, this, &MainWindow::baudrateSetFailed);
connect(this, &MainWindow::setFile, senderWorker, &Sender::setFile);
connect(this, &MainWindow::startPrinting, senderWorker, &Sender::startPrinting);
connect(this, &MainWindow::stopPrinting, senderWorker, &Sender::stopPrinting);
connect(this, &MainWindow::pause, senderWorker, &Sender::pause);
connect(this, &MainWindow::setBaudrate, senderWorker, &Sender::setBaudrate);
connect(this, &MainWindow::openPort, senderWorker, &Sender::openPort);
connect(this, &MainWindow::closePort, senderWorker, &Sender::closePort);
connect(this, &MainWindow::injectCommand, senderWorker, &Sender::injectCommand);
connect(this, &MainWindow::flushInjectionBuffer, senderWorker, &Sender::flushInjectionBuffer);
senderThread->start();
senderThread->setPriority(QThread::TimeCriticalPriority);
(1)线程交互的设计
当我们看懂了这段代码以后就可以将它们抽象成我们自己的代码,即分解为:
以下是我基于上面的这个架构进行了简单的修改:
代码语言:javascript复制/*注册串口线程*/
void MainWindow::Register_Uart_thread()
{
uartThread = new QThread(this);
uartWorker = new SerialThread(Sensor_Uart,Sensor_Baud);
uartWorker->moveToThread(uartThread);
connect(uartThread, &QThread::finished, uartWorker, &QObject::deleteLater);
connect(uartWorker, &SerialThread::reportProgress, this, &MainWindow::updateFileProgress);
connect(uartWorker, &SerialThread::update_layer, this, &MainWindow::updateModelLayer);
connect(uartWorker, &SerialThread::send_debug_gcode_line, this, &MainWindow::GCode_Debug);
connect(this, &MainWindow::startPrinting, uartWorker, &SerialThread::startPrinting);
connect(this, &MainWindow::stopPrinting, uartWorker, &SerialThread::stopPrinting);
connect(this, &MainWindow::pausePrinting, uartWorker, &SerialThread::pausePrinting);
connect(this, &MainWindow::recovery_print, uartWorker, &SerialThread::recovery_print);
uartThread->start();
uartThread->setPriority(QThread::TimeCriticalPriority);
}
/*注册文件管理线程*/
void MainWindow::Register_FileManage_thread()
{
fileThread = new QThread(this);
fileWorker = new file_manage();
fileWorker->moveToThread(fileThread);
connect(fileThread, &QThread::finished, fileWorker, &QObject::deleteLater);
connect(this, &MainWindow::setGodeFile, fileWorker, &file_manage::parseFile);
connect(fileWorker, &file_manage::update_gcode_info_display, this, &MainWindow::GCode_File_Info);
connect(fileWorker, &file_manage::send_gcode_file, uartWorker, &SerialThread::setFile);
fileThread->start();
fileThread->setPriority(QThread::HighestPriority);
}
/*注册协议解析线程*/
void MainWindow::Register_Protocol_Parse_thread()
{
parserThread = new QThread(this);
parseWorker = new protocol_parse();
parseWorker->moveToThread(parserThread);
connect(parserThread, &QThread::finished, parseWorker, &QObject::deleteLater);
connect(parseWorker, &protocol_parse::hotbed_temp, this, &MainWindow::Display_HotBed_Temp);
connect(parseWorker, &protocol_parse::hotend_temp, this, &MainWindow::Display_Hotend_Temp);
connect(parseWorker, &protocol_parse::move_position, this, &MainWindow::Display_Move_Position);
connect(uartWorker, &SerialThread::Data_Process, parseWorker, &protocol_parse::Data_Process);
parserThread->start();
parserThread->setPriority(QThread::HighestPriority);
}
(2)打印GCode文件与用户发送GCode命令的核心实现
(3)协议解析的核心实现
关于协议解析部分,我依然采用的是正则表达式的方案来实现,例如对温度部分的处理:
代码语言:javascript复制typedef struct
{
QString raw;
double e_c, b_c;
double e_t, b_t;
} TemperatureReadings;
//正则表达式获取温度
void MainWindow::Get_Temp_Test()
{
int p = 0;
QStringList data_list ;
TemperatureReadings r;
/*对于单喷头打印机,它回复的数据格式是这样的*/
QByteArray data = "T:41.88 /0.00 B:56.02 /60.00 @:0 B@:0 W:?";
QRegExp temperatureRegxp("-?(([0-9]\d*\.\d*)|(0\.\d*[0-9]\d*)|([0-9]\d*))");
while ((p = temperatureRegxp.indexIn(data, p)) != -1)
{
data_list.append(temperatureRegxp.cap(1));
//上一个匹配的字符串的长度
p = temperatureRegxp.matchedLength();
}
r.raw = QString(data);
r.e_c = data_list[0].toDouble(); //喷头当前温度
r.e_t = data_list[1].toDouble(); //喷头目标温度
r.b_c = data_list[2].toDouble(); //热床当前温度
r.b_t = data_list[3].toDouble(); //热床目标温度
qDebug() << "解析源字符串:" << r.raw << " " << "解析长度:" << data_list.length();
qDebug() << r.e_c ;
qDebug() << r.e_t ;
qDebug() << r.b_c ;
qDebug() << r.b_t ;
}
例如对当前移动坐标的获取:
代码语言:javascript复制typedef struct
{
QString raw;
double x,y,z ;
} CoreXYZ ;
//正则表达式获取坐标
void MainWindow::Get_GCodeXYZ_Test()
{
int p = 0;
QStringList data_list ;
CoreXYZ r;
/*移动指令,例如解析下面这个指令*/
QByteArray data = "G1 F1200 X-9.914 Y-9.843 E3.36222";
QRegExp CoreXYZRegxp("-?(([XYZ]|[0-9]\d*\.\d*)|(0\.\d*[0-9]\d*)|([0-9]\d*))");
while ((p = CoreXYZRegxp.indexIn(data, p)) != -1)
{
data_list.append(CoreXYZRegxp.cap(1));
//上一个匹配的字符串的长度
p = CoreXYZRegxp.matchedLength();
}
r.raw = QString(data);
r.x = data_list[0].toDouble();
r.y = data_list[1].toDouble();
qDebug() << "解析源字符串:" << r.raw << " " << "解析长度:" << data_list.length();
qDebug() << data_list[0];
qDebug() << data_list[1];
}
(4)其它功能的设计
这部分就发挥大家自己的想象了,我先做了个测试版本,随便捣鼓一下,已经能够正常打印模型了:
目前这个项目还没有开源,我还在持续的完善中,希望最后能够助力一把 RepRapCalculator,希望加什么功能,大家可以在下方给我留言。
5、总结
要做属于自己的打印机,需要掌握以下技能:
- 掌握QT软件开发(如果你会C#或者其它当然也没问题)
- 掌握3D打印机GCode指令协议
- 其它必要的知识,如设计模式、数据结构等。
另外,让我们对3D打印开源的小伙伴们为3D打印开源发发力量!
6、参考文献 & 引用
[1] G-Code Index. (n.d.). Marlin Firmware. https://marlinfw.org/meta/gcode/
[2] Neo, T. (2017, May 27). G-Code Index. Github. https://github.com/NeoTheFox/RepRaptor
[3] Neo, T. (2017, May 27). A Qt RepRap Gcode Sender/Host Controller. https://opensourcelibs.com/lib/repraptor
[4]卢华东. (2015, December 26). RepRap 3D Printer 入门介绍. https://blog.csdn.net/lu_embedded/article/details/50409510