基于前述 OpenPnP 架构和 Qt/C++ 框架,以下是对半导体贴片机控制软件的详细模块实现,重点扩展 G-code 控制模块 和 多镜头支持,并提供所需的硬件清单及其具体实现
基于前述 OpenPnP 架构和 Qt/C++ 框架,以下是对半导体贴片机控制软件的详细模块实现,重点扩展 G-code 控制模块 和 多镜头支持,并提供所需的硬件清单及其具体实现。代码将延续之前的框架,确保可运行且与 OpenPnP 的功能(如视觉处理、配置切换)对齐。每个模块包括功能描述、代码实现、硬件需求和实际应用中的注意事项。实现基于 Qt 6、OpenCV 和 QSerialPort,适配 DIY 和小批量 SMT 生产。
一、所需模块与硬件需求
1. 模块清单
以下是贴片机控制软件所需的完整模块,扩展了 G-code 控制和多镜头支持:
-
用户界面模块(UI Module):提供交互界面,显示状态、摄像头视图和控制面板。
-
核心控制模块(Core Control Module):协调任务,管理硬件、视觉和数据。
-
硬件抽象模块(Hardware Abstraction Module):封装运动控制、吸嘴和摄像头操作。
-
G-code 控制模块(G-code Control Module):发送和解析 G-code,控制步进电机和供料器。
-
视觉处理模块(Vision Processing Module):支持多镜头,处理元件定位和旋转检测。
-
数据管理模块(Data Management Module):管理元件配置、贴片坐标和日志。
-
路径优化模块(Path Optimization Module):优化贴片路径,减少移动时间。
2. 硬件需求
基于 OpenPnP 的推荐()和实际 SMT 需求,以下是基础硬件清单:
-
运动控制:
-
步进电机:3 轴(X/Y/Z)步进电机(如 NEMA 17,推荐 RobotDigg)。
-
电机驱动器:支持 G-code 的控制器(如 Arduino + CNC Shield 或 TinyG,)。
-
限位开关:用于 Z 轴校准()。
-
-
吸嘴与供料器:
-
真空吸嘴:多尺寸吸嘴(如 0.5mm、1.0mm),适配 0603、0805 等芯片。
-
供料器:带式供料器(Tape Feeder)或盘式供料器(Tray Feeder),支持 G-code 控制()。
-
真空泵:提供吸力,推荐小型电磁阀控制。
-
-
摄像头:
-
顶部摄像头(Top Vision):高分辨率 USB 摄像头(如 Logitech C920),用于元件定位。
-
底部摄像头(Bottom Vision):用于吸嘴校准和贴装验证。
-
光源:环形 LED 灯,确保均匀光照。
-
-
控制器:
-
单板计算机或 PC:运行 Qt 软件,推荐 Raspberry Pi 4 或工业 PC()。
-
串口通信:USB 转串口模块(如 FT232R),用于 G-code 传输。
-
-
其他:
-
PCB 夹具:固定电路板,确保贴装精度。
-
电源:12V/24V 电源,适配电机和真空泵。
-
二、具体实现
以下是详细的模块代码,扩展 G-code 控制和多镜头支持,包含完整项目结构和运行示例。
1. 项目结构
PickAndPlace/
├── main.cpp
├── PickAndPlaceMachine.h/cpp // 核心控制
├── HardwareInterface.h/cpp // 硬件抽象
├── GcodeController.h/cpp // G-code 控制
├── VisionProcessor.h/cpp // 视觉处理(多镜头)
├── DataManager.h/cpp // 数据管理
├── PathOptimizer.h/cpp // 路径优化
├── main.qml // 用户界面
├── qml.qrc
├── components.json // 元件配置
├── components.csv // 贴片坐标
└── log.txt // 日志文件
2. Qt 项目文件(PickAndPlace.pro)
pro
QT += quick serialport
CONFIG += c++11
SOURCES += \
main.cpp \
PickAndPlaceMachine.cpp \
HardwareInterface.cpp \
GcodeController.cpp \
VisionProcessor.cpp \
DataManager.cpp \
PathOptimizer.cpp
HEADERS += \
PickAndPlaceMachine.h \
HardwareInterface.h \
GcodeController.h \
VisionProcessor.h \
DataManager.h \
PathOptimizer.h
RESOURCES += qml.qrc
INCLUDEPATH += /path/to/opencv/include
LIBS += -L/path/to/opencv/lib -lopencv_core -lopencv_imgproc -lopencv_imgcodecs -lopencv_highgui
3. 用户界面模块(main.qml)
功能:支持多镜头切换、G-code 控制、元件类型选择和状态显示。
qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
ApplicationWindow {
visible: true
width: 800
height: 600
title: "Pick and Place Machine"
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
Text {
id: statusText
text: machine.status
font.pixelSize: 16
}
RowLayout {
ComboBox {
id: componentType
model: ["0603", "0805"]
onCurrentTextChanged: machine.setComponentType(currentText)
}
ComboBox {
id: cameraSelect
model: ["Top Camera", "Bottom Camera"]
onCurrentTextChanged: machine.setCamera(currentText)
}
Button {
text: "Start Job"
onClicked: machine.startJob("components.csv")
}
Button {
text: "Stop Job"
onClicked: machine.stopJob()
}
}
RowLayout {
TextField {
id: xInput
placeholderText: "X"
validator: DoubleValidator {}
}
TextField {
id: yInput
placeholderText: "Y"
validator: DoubleValidator {}
}
TextField {
id: zInput
placeholderText: "Z"
validator: DoubleValidator {}
}
Button {
text: "Move"
onClicked: machine.manualMove(xInput.text, yInput.text, zInput.text)
}
}
RowLayout {
Button {
text: "Detect Component"
onClicked: {
var pos = machine.detectComponent()
console.log("Component at: " + pos.x + ", " + pos.y)
}
}
Button {
text: "Detect Rotation"
onClicked: {
var angle = machine.detectRotation()
console.log("Rotation: " + angle + " degrees")
}
}
Button {
text: "Calibrate Nozzle"
onClicked: machine.calibrateNozzle()
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "lightgray"
Text {
anchors.centerIn: parent
text: "Camera View (Placeholder)"
}
}
}
}
说明:
-
添加相机选择(Top Camera、Bottom Camera),支持多镜头切换。
-
扩展控制按钮,包括吸嘴校准(Calibrate Nozzle)。
4. 核心控制模块(PickAndPlaceMachine.h/cpp)
功能:协调各模块,管理贴片任务、G-code 控制和多镜头操作。
PickAndPlaceMachine.h:
cpp
#ifndef PICKANDPLACEMACHINE_H
#define PICKANDPLACEMACHINE_H
#include <QObject>
#include "HardwareInterface.h"
#include "GcodeController.h"
#include "VisionProcessor.h"
#include "DataManager.h"
#include "PathOptimizer.h"
class PickAndPlaceMachine : public QObject {
Q_OBJECT
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
public:
explicit PickAndPlaceMachine(QObject *parent = nullptr);
QString status() const { return m_status; }
public slots:
void startJob(const QString &filePath);
void stopJob();
void manualMove(double x, double y, double z);
void setComponentType(const QString &type);
void setCamera(const QString &camera);
QPointF detectComponent();
double detectRotation();
void calibrateNozzle();
signals:
void statusChanged(const QString &status);
private:
HardwareInterface *hardware;
GcodeController *gcode;
VisionProcessor *vision;
DataManager *dataManager;
PathOptimizer *pathOptimizer;
QString m_status;
bool isRunning;
QString currentComponentType;
QString currentCamera;
};
#endif
PickAndPlaceMachine.cpp:
cpp
#include "PickAndPlaceMachine.h"
#include <QDebug>
PickAndPlaceMachine::PickAndPlaceMachine(QObject *parent) : QObject(parent), isRunning(false) {
hardware = new HardwareInterface(this);
gcode = new GcodeController(this);
vision = new VisionProcessor(this);
dataManager = new DataManager(this);
pathOptimizer = new PathOptimizer(this);
connect(hardware, &HardwareInterface::statusUpdated, this, &PickAndPlaceMachine::statusChanged);
connect(gcode, &GcodeController::statusUpdated, this, &PickAndPlaceMachine::statusChanged);
connect(vision, &VisionProcessor::statusUpdated, this, &PickAndPlaceMachine::statusChanged);
m_status = "Idle";
currentCamera = "Top Camera";
dataManager->loadComponentConfig("components.json");
}
void PickAndPlaceMachine::startJob(const QString &filePath) {
if (isRunning) return;
isRunning = true;
m_status = "Running";
emit statusChanged(m_status);
QList<QPointF> points = dataManager->loadJobData(filePath);
points = pathOptimizer->optimizePath(points);
for (const QPointF &point : points) {
if (!isRunning) break;
QString type = dataManager->getComponentType(point);
setComponentType(type);
setCamera("Top Camera");
QPointF pos = vision->detectComponent(dataManager->getTemplatePath(type));
double angle = vision->detectRotation(dataManager->getTemplatePath(type));
double z = dataManager->getZHeight(type);
gcode->moveTo(pos.x(), pos.y(), z);
gcode->pick();
gcode->moveTo(point.x(), point.y(), z + 10);
setCamera("Bottom Camera");
vision->detectComponent(dataManager->getFiducialTemplate()); // 贴装验证
gcode->place();
dataManager->log(QString("Placed %1 at X:%2 Y:%3").arg(type).arg(point.x()).arg(point.y()));
}
isRunning = false;
m_status = "Idle";
emit statusChanged(m_status);
}
void PickAndPlaceMachine::stopJob() {
isRunning = false;
m_status = "Stopped";
emit statusChanged(m_status);
}
void PickAndPlaceMachine::manualMove(double x, double y, double z) {
gcode->moveTo(x, y, z);
}
void PickAndPlaceMachine::setComponentType(const QString &type) {
currentComponentType = type;
m_status = QString("Component type set to %1").arg(type);
emit statusChanged(m_status);
}
void PickAndPlaceMachine::setCamera(const QString &camera) {
currentCamera = camera;
vision->setCamera(camera);
m_status = QString("Camera set to %1").arg(camera);
emit statusChanged(m_status);
}
QPointF PickAndPlaceMachine::detectComponent() {
return vision->detectComponent(dataManager->getTemplatePath(currentComponentType));
}
double PickAndPlaceMachine::detectRotation() {
return vision->detectRotation(dataManager->getTemplatePath(currentComponentType));
}
void PickAndPlaceMachine::calibrateNozzle() {
setCamera("Bottom Camera");
QPointF pos = vision->detectComponent(dataManager->getFiducialTemplate());
gcode->moveTo(pos.x(), pos.y(), 0);
m_status = QString("Nozzle calibrated at X:%1 Y:%2").arg(pos.x()).arg(pos.y());
emit statusChanged(m_status);
}
说明:
-
集成 G-code 控制(GcodeController)和多镜头支持(setCamera)。
-
startJob 使用顶部摄像头定位元件,底部摄像头验证贴装。
-
calibrateNozzle 使用底部摄像头校准吸嘴偏移。
5. G-code 控制模块(GcodeController.h/cpp)
功能:发送和解析 G-code,控制步进电机和供料器()。
GcodeController.h:
cpp
#ifndef GCODECONTROLLER_H
#define GCODECONTROLLER_H
#include <QObject>
#include <QSerialPort>
class GcodeController : public QObject {
Q_OBJECT
public:
explicit GcodeController(QObject *parent = nullptr);
~GcodeController();
public slots:
void moveTo(double x, double y, double z);
void pick();
void place();
void advanceFeeder();
signals:
void statusUpdated(const QString &status);
private:
QSerialPort *serialPort;
void sendGcode(const QString &command);
};
#endif
GcodeController.cpp:
cpp
#include "GcodeController.h"
#include <QDebug>
GcodeController::GcodeController(QObject *parent) : QObject(parent) {
serialPort = new QSerialPort(this);
serialPort->setPortName("COM1"); // 替换为实际串口
serialPort->setBaudRate(QSerialPort::Baud9600);
if (!serialPort->open(QIODevice::ReadWrite)) {
emit statusUpdated("Error: Cannot open serial port");
}
}
GcodeController::~GcodeController() {
if (serialPort->isOpen()) serialPort->close();
}
void GcodeController::sendGcode(const QString &command) {
if (serialPort->isOpen()) {
serialPort->write(command.toUtf8() + "\n");
serialPort->waitForBytesWritten(1000);
emit statusUpdated("Sent G-code: " + command);
} else {
emit statusUpdated("Error: Serial port not open");
}
}
void GcodeController::moveTo(double x, double y, double z) {
QString command = QString("G1 X%1 Y%2 Z%3").arg(x).arg(y).arg(z);
sendGcode(command);
}
void GcodeController::pick() {
sendGcode("M3"); // 开启真空泵
}
void GcodeController::place() {
sendGcode("M5"); // 关闭真空泵
}
void GcodeController::advanceFeeder() {
sendGcode("M600"); // 供料器前进
}
说明:
-
使用 QSerialPort 发送 G-code(如 G1 移动、M3/M5 控制真空泵)。
-
支持供料器控制(M600),可根据硬件调整命令。
-
实际中需配置正确串口(COM1)和波特率。
6. 视觉处理模块(VisionProcessor.h/cpp)
功能:支持多镜头(顶部和底部),实现元件定位、旋转检测和吸嘴校准()。
VisionProcessor.h:
cpp
#ifndef VISIONPROCESSOR_H
#define VISIONPROCESSOR_H
#include <QObject>
#include <opencv2/opencv.hpp>
class VisionProcessor : public QObject {
Q_OBJECT
public:
explicit VisionProcessor(QObject *parent = nullptr);
public slots:
void setCamera(const QString &camera);
QPointF detectComponent(const QString &templatePath);
double detectRotation(const QString &templatePath);
signals:
void statusUpdated(const QString &status);
private:
cv::Mat captureFrame(const QString &camera);
QString currentCamera;
};
#endif
VisionProcessor.cpp:
cpp
#include "VisionProcessor.h"
#include <QDebug>
#include <opencv2/imgproc.hpp>
VisionProcessor::VisionProcessor(QObject *parent) : QObject(parent), currentCamera("Top Camera") {}
void VisionProcessor::setCamera(const QString &camera) {
currentCamera = camera;
emit statusUpdated("Using camera: " + camera);
}
cv::Mat VisionProcessor::captureFrame(const QString &camera) {
// 模拟摄像头,实际中使用 QCamera 或 VideoCapture
QString imagePath = (camera == "Top Camera") ? "component.png" : "fiducial.png";
cv::Mat frame = cv::imread(imagePath.toStdString());
return frame.empty() ? cv::Mat() : frame;
}
QPointF VisionProcessor::detectComponent(const QString &templatePath) {
cv::Mat frame = captureFrame(currentCamera);
if (frame.empty()) {
emit statusUpdated("Error: No image captured");
return QPointF(-1, -1);
}
cv::Mat gray;
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
cv::Mat templ = cv::imread(templatePath.toStdString(), cv::IMREAD_GRAYSCALE);
if (templ.empty()) {
emit statusUpdated("Error: No template found");
return QPointF(-1, -1);
}
cv::Mat result;
cv::matchTemplate(gray, templ, result, cv::TM_CCOEFF_NORMED);
double minVal, maxVal;
cv::Point minLoc, maxLoc;
cv::minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc);
emit statusUpdated(QString("Component detected at X:%1 Y:%2").arg(maxLoc.x).arg(maxLoc.y));
return QPointF(maxLoc.x, maxLoc.y);
}
double VisionProcessor::detectRotation(const QString &templatePath) {
cv::Mat frame = captureFrame(currentCamera);
if (frame.empty()) {
emit statusUpdated("Error: No image captured");
return 0.0;
}
cv::Mat gray;
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
cv::Mat edges;
cv::Canny(gray, edges, 50, 150);
std::vector<cv::Vec2f> lines;
cv::HoughLines(edges, lines, 1, CV_PI / 180, 100);
if (!lines.empty()) {
float theta = lines[0][1];
double angle = theta * 180 / CV_PI;
emit statusUpdated(QString("Rotation detected: %1 degrees").arg(angle));
return angle;
}
return 0.0;
}
说明:
-
支持多镜头(Top Camera、Bottom Camera),通过 setCamera 切换。
-
顶部摄像头用于元件定位,底部摄像头用于吸嘴校准(Fiducial 标记)。
-
实际中可使用 QCamera 或 OpenCV 的 VideoCapture 替换 captureFrame。
7. 数据管理模块(DataManager.h/cpp)
功能:管理元件配置、贴片坐标和日志。
DataManager.h:
cpp
#ifndef DATAMANAGER_H
#define DATAMANAGER_H
#include <QObject>
#include <QJsonDocument>
class DataManager : public QObject {
Q_OBJECT
public:
explicit DataManager(QObject *parent = nullptr);
void loadComponentConfig(const QString &filePath);
QList<QPointF> loadJobData(const QString &filePath);
QString getComponentType(const QPointF &point);
QString getTemplatePath(const QString &type);
QString getFiducialTemplate();
double getZHeight(const QString &type);
void log(const QString &message);
private:
QJsonDocument componentConfig;
QMap<QPointF, QString> jobData;
};
#endif
DataManager.cpp:
cpp
#include "DataManager.h"
#include <QFile>
#include <QJsonArray>
#include <QTextStream>
#include <QDebug>
DataManager::DataManager(QObject *parent) : QObject(parent) {}
void DataManager::loadComponentConfig(const QString &filePath) {
QFile file(filePath);
if (file.open(QIODevice::ReadOnly)) {
componentConfig = QJsonDocument::fromJson(file.readAll());
file.close();
}
}
QList<QPointF> DataManager::loadJobData(const QString &filePath) {
QList<QPointF> points;
jobData.clear();
QFile file(filePath);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine();
QStringList parts = line.split(",");
if (parts.size() >= 4) {
QPointF point(parts[0].toDouble(), parts[1].toDouble());
points.append(point);
jobData[point] = parts[3];
}
}
file.close();
}
return points;
}
QString DataManager::getComponentType(const QPointF &point) {
return jobData.value(point, "0603");
}
QString DataManager::getTemplatePath(const QString &type) {
QJsonArray components = componentConfig.array();
for (const QJsonValue &value : components) {
QJsonObject obj = value.toObject();
if (obj["id"].toString() == type) {
return obj["template"].toString();
}
}
return "";
}
QString DataManager::getFiducialTemplate() {
return "fiducial_template.png"; // Fiducial 标记模板
}
double DataManager::getZHeight(const QString &type) {
QJsonArray components = componentConfig.array();
for (const QJsonValue &value : components) {
QJsonObject obj = value.toObject();
if (obj["id"].toString() == type) {
return obj["z_height"].toDouble();
}
}
return 0.5;
}
void DataManager::log(const QString &message) {
QFile logFile("log.txt");
if (logFile.open(QIODevice::Append | QIODevice::Text)) {
QTextStream out(&logFile);
out << QDateTime::currentDateTime().toString() << ": " << message << "\n";
logFile.close();
}
}
components.json:
json
[
{
"id": "0603",
"width": 1.6,
"height": 0.8,
"template": "0603_template.png",
"z_height": 0.5
},
{
"id": "0805",
"width": 2.0,
"height": 1.25,
"template": "0805_template.png",
"z_height": 0.6
}
]
components.csv:
csv
10.0,20.0,5.0,0603
15.0,25.0,5.0,0805
8. 路径优化模块(PathOptimizer.h/cpp)
功能:优化贴片路径,减少移动时间。
PathOptimizer.h:
cpp
#ifndef PATHOPTIMIZER_H
#define PATHOPTIMIZER_H
#include <QObject>
#include <QPointF>
class PathOptimizer : public QObject {
Q_OBJECT
public:
explicit PathOptimizer(QObject *parent = nullptr);
QList<QPointF> optimizePath(const QList<QPointF> &points);
};
#endif
PathOptimizer.cpp:
cpp
#include "PathOptimizer.h"
#include <QDebug>
PathOptimizer::PathOptimizer(QObject *parent) : QObject(parent) {}
QList<QPointF> PathOptimizer::optimizePath(const QList<QPointF> &points) {
QList<QPointF> sorted = points;
std::sort(sorted.begin(), sorted.end(), [](const QPointF &a, const QPointF &b) {
return a.x() < b.x(); // 简单 X 轴排序
});
qDebug() << "Optimized path:" << sorted;
return sorted;
}
9. 主程序(main.cpp)
cpp
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "PickAndPlaceMachine.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QQmlApplicationEngine engine;
PickAndPlaceMachine machine;
engine.rootContext()->setContextProperty("machine", &machine);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
三、实际应用中的注意事项
-
硬件配置:
-
步进电机:确保电机驱动器支持 G-code(如 GRBL 固件的 Arduino,)。校准步进距离(steps/mm)。
-
吸嘴:使用多尺寸吸嘴,定期校准真空压力()。
-
摄像头:顶部摄像头需要高分辨率(1080p),底部摄像头用于 Fiducial 定位()。校准畸变(OpenCV 的 calibrateCamera)。
-
供料器:带式供料器需支持 G-code 控制(如 M600,)。
-
-
G-code 控制:
-
协议兼容:验证控制器支持标准 G-code(如 G1、M3、M5)。TinyG 板支持扩展命令(如 M114,)。
-
串口通信:确保 QSerialPort 的波特率和端口正确,测试通信延迟。
-
错误处理:添加 G-code 响应解析:
cpp
void GcodeController::sendGcode(const QString &command) { if (serialPort->isOpen()) { serialPort->write(command.toUtf8() + "\n"); serialPort->waitForBytesWritten(1000); if (serialPort->waitForReadyRead(1000)) { QString response = serialPort->readAll(); emit statusUpdated("G-code response: " + response); } } }
-
-
多镜头支持:
-
切换逻辑:确保 VisionProcessor 正确切换摄像头(实际中需配置多个 VideoCapture 实例)。
-
校准:使用 Fiducial 标记校准底部摄像头()。
-
性能:多镜头处理耗时,使用 QtConcurrent 优化:
cpp
#include <QtConcurrent> QPointF VisionProcessor::detectComponent(const QString &templatePath) { QFuture<QPointF> future = QtConcurrent::run([this, templatePath]() { // 模板匹配代码 return QPointF(maxLoc.x, maxLoc.y); }); return future.result(); }
-
-
调试与测试:
-
日志:检查 log.txt 验证任务执行。
-
视觉测试:准备 component.png 和 fiducial_template.png,测试定位精度。
-
硬件校准:运行 calibrateNozzle 确保吸嘴位置正确。
-
四、具体使用案例
-
DIY PCB 贴装:
-
场景:贴装 10 个 0603 电阻和 5 个 0805 电容。
-
步骤:
-
配置 components.json 和 components.csv。
-
使用顶部摄像头定位元件,底部摄像头校准吸嘴。
-
启动任务,检查 log.txt。
-
-
硬件:Arduino + CNC Shield、Logitech C920 摄像头、带式供料器。
-
-
小批量生产:
-
场景:生产 50 块 IoT 模块,每块 30 个元件。
-
步骤:
-
配置多吸嘴和供料器,发送 M600 控制供料。
-
优化路径,减少移动时间。
-
使用多线程视觉处理,提高效率。
-
-
硬件:TinyG 控制器、双 USB 摄像头、真空泵。
-
五、与 Qt 学习计划的结合
结合《C++ GUI Programming with Qt 4》:
-
第1-5天:学习 QML、信号与槽,搭建 main.qml。
-
第6-15天:实现 HardwareInterface、GcodeController、VisionProcessor,集成 OpenCV 和 QSerialPort。
-
第16-25天:添加多镜头支持、配置切换和路径优化。
-
第26-30天:优化性能,部署到 Raspberry Pi 或 PC。
六、补充资源
-
OpenPnP:https://openpnp.org, https://github.com/openpnp/openpnp/wiki[](https://openpnp.org/)
-
Qt:Qt 6.9
-
OpenCV:OpenCV: OpenCV modules
-
硬件:RobotDigg (https://www.robotdigg.com), TinyG ().
七、总结
该框架实现了 OpenPnP 的核心功能,扩展了 G-code 控制和多镜头支持,覆盖所有必要模块。硬件需求包括步进电机、吸嘴、摄像头和控制器,代码可运行于 DIY 和小批量生产。如果需要更详细的代码(如 Gerber 文件解析、复杂路径优化)或硬件调试指导,请告诉我!
更多推荐


所有评论(0)