diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -8,6 +8,7 @@ set(AtCoreLib_SRCS atcore.cpp + copythread.cpp seriallayer.cpp gcodecommands.cpp ifirmware.cpp diff --git a/src/core/atcore.h b/src/core/atcore.h --- a/src/core/atcore.h +++ b/src/core/atcore.h @@ -303,6 +303,12 @@ */ void sdCardFileListChanged(const QStringList &fileList); + /** + * @brief WriteToSD precentage changed. + * @param newProgress. + */ + void sdWriteProgressChanged(float newProgress); + public slots: /** @@ -473,6 +479,13 @@ */ void sdCardPrintStatus(); + /** + * @brief Write a file to the machines SdCard + * @param fileName: Local file to send to machine + * @return True if successful + */ + bool writeFiletoSd(const QString &fileName); + private slots: /** * @brief processQueue send commands from the queue. diff --git a/src/core/atcore.cpp b/src/core/atcore.cpp --- a/src/core/atcore.cpp +++ b/src/core/atcore.cpp @@ -36,6 +36,7 @@ #include "atcore.h" #include "atcore_version.h" +#include "copythread.h" #include "seriallayer.h" #include "gcodecommands.h" #include "printthread.h" @@ -836,3 +837,56 @@ qCDebug(ATCORE_CORE) << "SerialError:" << errorString; emit atcoreMessage(QStringLiteral("SerialError: %1").arg(errorString)); } + +bool AtCore::writeFiletoSd(const QString &fileName) +{ + //Do a few sanity checks before attempting to write to SD. + + //Can't write an empty fileName. + if (fileName.isEmpty()) { + qCDebug(ATCORE_CORE) << "Sd Write Failed: Empty fileName."; + return false; + } + //Can not write if plugin doesn't have Sd Support. + if (!d->firmwarePlugin->isSdSupported()) { + qCDebug(ATCORE_CORE) << "Sd Write Failed: Firmware plugin does not support Sd functions."; + return false; + } + + //If the card is not mounted try to read the file list and mount it. + if (!d->sdCardMounted) { + getSDFileList(); + if (!d->sdCardMounted) { + qCDebug(ATCORE_CORE) << "Sd Write Failed unable to mount Sd Card"; + return false; + } + } + // Be sure the printer is IDLE + if (d->printerState != AtCore::IDLE) { + qCDebug(ATCORE_CORE) << "Sd Write Failed: Device is Busy"; + return false; + } + + const QString fname = fileName.mid(fileName.lastIndexOf(QStringLiteral("/")) + 1, fileName.length()); + + //Process the gcode with a copyThread. + QThread *thread = new QThread(); + CopyThread *copyThread = new CopyThread(this, fileName); + copyThread->moveToThread(thread); + + connect(copyThread, &CopyThread::copyProgressChanged, this, &AtCore::sdWriteProgressChanged, Qt::QueuedConnection); + connect(thread, &QThread::started, copyThread, &CopyThread::start); + connect(copyThread, &CopyThread::finished, thread, &QThread::quit); + connect(thread, &QThread::finished, copyThread, &CopyThread::deleteLater); + + connect(copyThread, &CopyThread::nextCommand, this, [this, ©Thread](const QString & nextLine) { + serial()->pushCommand(nextLine.toLocal8Bit()); + copyThread->processJob(); + }); + + if (!thread->isRunning()) { + thread->start(); + } + + return true; +} diff --git a/src/core/copythread.h b/src/core/copythread.h new file mode 100644 --- /dev/null +++ b/src/core/copythread.h @@ -0,0 +1,110 @@ +/* AtCore + Copyright (C) <2017> + + Authors: + Chris Rizzitello + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ +#pragma once + +#include +#include + +#include "atcore.h" + +/** + * @brief The CopyThread class + * A Thread for running a copy to sd job + * + * see AtCore::writeToSD for example of how to create a copy thread. + * + */ +class ATCORE_EXPORT CopyThread : public QObject +{ + Q_OBJECT +public: + /** + * @brief Create a new Copy Thread + * @param parent: Parent of the tread + * @param fileName: gcode File to copy + */ + CopyThread(AtCore *parent, const QString &fileName); +signals: + /** + * @brief Print job has finished + */ + void finished(); + + /** + * @brief A command has caused an error + * @param err: the offending command + */ + void error(QString err); + + /** + * @brief The copy job's progress has changed + */ + void copyProgressChanged(float); + + /** + * @brief the next command of the job + * @param comm: Command to be sent next + */ + void nextCommand(const QString &comm); + + /** + * @brief Printer state was changed + * @param state: new state + */ + void stateChanged(AtCore::STATES state); + +public slots: + /** + * @brief start the copy thread + */ + void start(); + + /** + * @brief process the current job + */ + void processJob(); +private slots: + + /** + * @brief Set printer state + * @param state: the new printer state + */ + void setState(AtCore::STATES state); + +private: + /** + * @brief parse the next line + */ + void nextLine(); + + /** + * @brief end the copy + */ + void endCopy(); + + /** + * @brief d: Private storage for the thread + */ + class CopyThreadPrivate; + CopyThreadPrivate *d; +}; diff --git a/src/core/copythread.cpp b/src/core/copythread.cpp new file mode 100644 --- /dev/null +++ b/src/core/copythread.cpp @@ -0,0 +1,147 @@ +/* AtCore + Copyright (C) <2018> + + Authors: + Chris Rizzitello + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ +#include +#include + +#include "copythread.h" + +Q_LOGGING_CATEGORY(COPY_THREAD, "org.kde.atelier.core.copyThread") +/** + * @brief The CopyThreadPrivate class + */ +class CopyThread::CopyThreadPrivate +{ +public: + AtCore *core = nullptr; //!<@param core: Pointer to AtCore + QTextStream *gcodestream = nullptr; //!<@param gcodestream: Steam the job is read from + float progress = 0; //!<@param printProgress: Progress of the print job + qint64 totalSize = 0; //!<@param totalSize: total file size + qint64 stillSize = 0; //!<@param stillSize: remaining file + QString cline; //!<@param cline: current line + AtCore::STATES state = AtCore::IDLE;//!<@param state: printer state + QFile *file = nullptr; //!<@param file: gcode File to stream from +}; + +CopyThread::CopyThread(AtCore *parent, const QString &fileName) : d(new CopyThreadPrivate) +{ + d->core = parent; + d->state = d->core->state(); + d->file = new QFile(fileName); + d->file->open(QFile::ReadOnly); + d->totalSize = d->file->bytesAvailable(); + d->stillSize = d->totalSize; + d->gcodestream = new QTextStream(d->file); +} + +void CopyThread::start() +{ + // we only want to do this when printing + connect(this, &CopyThread::stateChanged, d->core, &AtCore::setState, Qt::QueuedConnection); + connect(d->core, &AtCore::stateChanged, this, &CopyThread::setState, Qt::QueuedConnection); + connect(this, &CopyThread::finished, this, &CopyThread::deleteLater); + d->core->pushCommand(QStringLiteral("M28 %1").arg(d->file->fileName())); + + // force a command if the printer doesn't send "wait" when idle + processJob(); +} + +void CopyThread::processJob() +{ + if (d->gcodestream->atEnd()) { + endCopy(); + } + + switch (d->state) { + case AtCore::STARTPRINT: + case AtCore::IDLE: + case AtCore::BUSY: + setState(AtCore::BUSY); + nextLine(); + while (!d->gcodestream->atEnd()) { + nextLine(); + emit nextCommand(d->cline); + } + break; + + case AtCore::ERRORSTATE: + qCDebug(COPY_THREAD) << "Error State"; + break; + + case AtCore::STOP: { + endCopy(); + break; + } + + case AtCore::PAUSE: + break; + + default: + qCDebug(COPY_THREAD) << "Unknown State"; + break; + } +} + +void CopyThread::endCopy() +{ + emit copyProgressChanged(100); + qCDebug(COPY_THREAD) << "atEnd"; + d->core->pushCommand(QStringLiteral("M29 %1").arg(d->file->fileName())); + disconnect(d->core, &AtCore::stateChanged, this, &CopyThread::setState); + emit stateChanged(AtCore::IDLE); + disconnect(this, &CopyThread::stateChanged, d->core, &AtCore::setState); + emit finished(); +} + +void CopyThread::nextLine() +{ + d->cline = d->gcodestream->readLine(); + qCDebug(COPY_THREAD) << "Nextline:" << d->cline; + d->stillSize -= d->cline.size() + 1; //remove read chars + d->progress = float(d->totalSize - d->stillSize) * 100.0 / float(d->totalSize); + qCDebug(COPY_THREAD) << "progress:" << QString::number(d->progress); + if (int(d->progress) % 1) { + emit copyProgressChanged(d->progress); + } +} + +void CopyThread::setState(AtCore::STATES newState) +{ + if (d->state == AtCore::STATES::DISCONNECTED && + ( + newState == AtCore::STATES::PAUSE || + newState == AtCore::STATES::STOP + ) + ) { + qCDebug(COPY_THREAD) << "Serial not connected !"; + return; + } + if (newState != d->state) { + qCDebug(COPY_THREAD) << QStringLiteral("State changed from [%1] to [%2]") + .arg(QVariant::fromValue(d->state).value(), + QVariant::fromValue(newState).value()); + disconnect(d->core, &AtCore::stateChanged, this, &CopyThread::setState); + d->state = newState; + emit stateChanged(d->state); + connect(d->core, &AtCore::stateChanged, this, &CopyThread::setState, Qt::QueuedConnection); + } +} diff --git a/src/plugins/marlinplugin.h b/src/plugins/marlinplugin.h --- a/src/plugins/marlinplugin.h +++ b/src/plugins/marlinplugin.h @@ -55,9 +55,26 @@ */ QString name() const override; + /** + * @brief Translate common commands to firmware specific command. + * @param command: command to translate + * @return firmware specific translated command + */ + QByteArray translate(const QString &command) override; + /** * @brief validateCommand to filter commands from messages * @param lastMessage: last Message from printer */ void validateCommand(const QString &lastMessage) override; + +private: + /** + * @brief Convert a filename to 8.3 Because Marlin will only accept those, Generates a 8.3 like Filename. + * This is Close to but not a 8.3 filename for instance we do not check the Filesystem so ~# will not be added if files with similar names are on the sd card + * Marlin will overwrite files with the samename. + * @param fileName: to convert to 8.3 format. + * @return Return a 8.3 Format name + */ + QString to83(const QString &fileName); }; diff --git a/src/plugins/marlinplugin.cpp b/src/plugins/marlinplugin.cpp --- a/src/plugins/marlinplugin.cpp +++ b/src/plugins/marlinplugin.cpp @@ -23,6 +23,7 @@ License along with this library. If not, see . */ #include +#include #include #include "marlinplugin.h" @@ -45,6 +46,33 @@ qCDebug(MARLIN_PLUGIN) << name() << " plugin loaded!"; } +QByteArray MarlinPlugin::translate(const QString &command) +{ + //Capture M28 + static const auto marlin_M28RegEx = QRegularExpression(QStringLiteral("M28 \\S+")); + //Capture both M29 and M29 String but not M290 + static const auto marlin_M29RegEx = QRegularExpression(QStringLiteral("M29( \\S*)?[^0]")); + + QString temp = command; + if (marlin_M28RegEx.match(command).hasMatch()) { + QString filename = temp.mid(4, temp.length()); + filename = to83(filename); + temp = QStringLiteral("M28 %1").arg(filename); + + } else if (marlin_M29RegEx.match(command).hasMatch()) { + if (temp.length() > 3) { + //command contains file name + QString filename = temp.mid(4, temp.length()); + filename = to83(filename); + temp = QStringLiteral("M29 %1").arg(filename); + } else { + temp = QStringLiteral("M29"); + } + } + + return temp.toLocal8Bit(); +} + void MarlinPlugin::validateCommand(const QString &lastMessage) { if (lastMessage.contains(QStringLiteral("End file list"))) { @@ -96,3 +124,19 @@ } } } + +QString MarlinPlugin::to83(const QString &fileName) +{ + //Generate an 8.3 Like FileName. + QString newName; + if (fileName.lastIndexOf(QStringLiteral(".") < 9)) { + newName = fileName.mid(0, fileName.lastIndexOf(QStringLiteral("."))); + } else { + newName = fileName.mid(0, 8); + } + if (newName.length() > 8) { + newName.resize(8); + } + newName = newName.append(fileName.mid(fileName.lastIndexOf(QStringLiteral(".")), 4)); + return newName.toUpper(); +} diff --git a/src/widgets/sdwidget.h b/src/widgets/sdwidget.h --- a/src/widgets/sdwidget.h +++ b/src/widgets/sdwidget.h @@ -58,6 +58,11 @@ */ void deleteSdFile(const QString &fileName); + /** + * @brief User has selected to write a file for the sd card. + */ + void writeSdFile(); + private: QListWidget *listSdFiles = nullptr; }; diff --git a/src/widgets/sdwidget.cpp b/src/widgets/sdwidget.cpp --- a/src/widgets/sdwidget.cpp +++ b/src/widgets/sdwidget.cpp @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +#include #include #include #include @@ -42,6 +43,12 @@ } }); + newButton = new QPushButton(tr("Send File")); + hBoxLayout->addWidget(newButton); + connect(newButton, &QPushButton::clicked, this, [this] { + emit writeSdFile(); + }); + newButton = new QPushButton(tr("Delete Selected")); hBoxLayout->addWidget(newButton); connect(newButton, &QPushButton::clicked, this, [this] { diff --git a/testclient/mainwindow.h b/testclient/mainwindow.h --- a/testclient/mainwindow.h +++ b/testclient/mainwindow.h @@ -136,6 +136,11 @@ */ void initWidgets(); + /** + * @brief Handle WriteToSd + */ + void writeToSd(); + //Private GUI Items //menuView is global to allow for docks to be added / removed. QMenu *menuView = nullptr; diff --git a/testclient/mainwindow.cpp b/testclient/mainwindow.cpp --- a/testclient/mainwindow.cpp +++ b/testclient/mainwindow.cpp @@ -54,6 +54,7 @@ connect(core, &AtCore::stateChanged, this, &MainWindow::printerStateChanged); connect(core, &AtCore::portsChanged, this, &MainWindow::locateSerialPort); connect(core, &AtCore::sdCardFileListChanged, sdWidget, &SdWidget::updateFilelist); + connect(sdWidget, &SdWidget::writeSdFile, this, &MainWindow::writeToSd); } void MainWindow::initMenu() @@ -584,3 +585,31 @@ statusWidget->showPrintArea(false); } } + +void MainWindow::writeToSd() +{ + static QString filter = tr("GCode Files(*.gco *.gcode)"); + QString fileName = QFileDialog::getOpenFileName( + this + , tr("Write file to SD") + , QDir::homePath() + , tr("All Files(*.*);;GCode Files(*.gco *.gcode)") + , &filter + ); + + if (!fileName.isEmpty()) { + QProgressDialog *dialog = new QProgressDialog(); + dialog->setLabelText(tr("Writing to SD...")); + dialog->setModal(true); + connect(core, &AtCore::sdWriteProgressChanged, dialog, &QProgressDialog::setValue); + connect(dialog, &QProgressDialog::canceled, this, [this] { + core->setState(AtCore::STOP); + }); + connect(core, &AtCore::writeFiletoSd, dialog, &QProgressDialog::exec); + if (!core->writeFiletoSd(fileName)) { + QMessageBox::warning(this, tr("Write to Sd"), tr("Writing FAILED")); + dialog->close(); + core->setState(AtCore::IDLE); + } + } +}