Pull to refresh

Приручаем динозавров, или как я писал свой собственный host controller для лаборатории 3D-печати

Reading time15 min
Views20K


В этой статье я хочу рассказать о своем опыте разработки свободного ПО для управления 3D принтерами на Qt5, проблемах и особенностях общения с RepRap'ами и прочим радостям.

Результатом этого опыта стал RepRaptor — минималистичный свободный host-controller для 3D принтеров.



Всех интересующихся приглашаю под кат.

Немного предыстории


За последний год я сильно заинтересовался 3D печатью, и не просто 3D печатью, а ее свободной составляющей — проектом RepRap. Руководство ВУЗа поддержало начинания, и в течении этого года моими и единомышленников усилиями в МГТУ МИРЭА открылась лаборатория 3D печати.
Очень скоро возникла проблема — существующее в экосистеме ПО имело недостатки, которые сильно мешали работе. Поэтому было решено эти проблемы ликвидировать, создав свое ПО.

Чтобы не обидеть ненароком авторов имеющегося ПО, не буду тыкать пальцем, тем более проблемы у целого ряда свободного ПО для 3D печати общие:
  • Они все написаны на интерпретируемых языках, открытие и разбор файлов занимают на слабом железе иногда и по 10 минут;
  • Те из них, что написаны на Mono еще и периодически падают, и обрекают кучу пластика на безвременную кончину (Виновато тут, скорее всего, не само Mono, а минимальное внимание к сборкам на ней от авторов);
  • В интерфейс вынесено мало кодов, зато встречается совсем уж избыточный функционал (прошу прощение за свой минимализм);

Почему Qt?




Как это все исправить? Довольно просто — написать хост на языке, который будет работать быстрее, и сделать его максимально простым. Сказано — сделано. Для разработки был выбран Qt, и не случайно. Этот замечательный фреймворк не только распространяется свободно, но и позволяет писать кроссплатформеные приложения без сопровождающей мультиплатформу в C++ боли, а так же не так давно периодически используемый мной QSerialPort стал официальной частью фреймворка.

Кроме того, я не знаю ни одной IDE, такой же быстрой и удобной для меня, как QtCreator.

Так как все окна, да и сам Ui в целом делался с помощью .ui файлов в WSIWYG редакторе Qt — я опущу все, связанное непосредственно с интерфейсом. Смотреть будем «под капот».

С чего начать, или как разговаривают принтеры


Общаться необходимо работать с целым зоопарком разнообразных плат (Melzi, RAMPS 1.4, Teensylu, Gen7 1.5.1 и т.д.), благо сообщество проекта RepRap уже давно определилось с протоколом и списком команд. Для общения с любой платой используется серийный порт и протокол G-code.

В своей лаборатории 3D печати мы с товарищами используем прошивку Repetier, которая, на ряду с прошивками Marlin и Teacup поддерживает большинство стандартных кодов.

Первое разочарование — протокол


Спецификация серийных портов RS-232 обширна и интересна, но, к сожалению редко где используется на полную. При эмуляции серийного порта по USB, очень часто к микроконтроллеру подключаются только линии TX и RX, отвечающие за передачу данных. Линии, такие как DTR (сброс при подключении), обычно не используются, и это печально.

Вместо аппаратного контроля за передачей данных RepRap-совместимые принтеры используют очень простой протокол, организованный по принципу PingPong:
  1. Принтер сообщает о простое строчкой wait;
  2. Хост отправляет команду G-Code, кончающуюся переносом строки;
  3. Принтер подтверждает прием команды строчкой Ok <номер строки>.


Выглядит это примерно так:
wait
G1 X10 Y10 Z10
ok 0
G1 X20 Y5 Z3
ok 0

Обратили внимание на 0 после «ok»? Это — номер строчки. При желании можно использовать контрольную сумму и номер строки при
отправке команд на принтер:
N1 G1 X10 Y10 Z10 *cs
ok 1
N2 G1 X20 Y5 Z3 *cs
ok 2

Где cs — вычисленная контрольная для строки.

Как правило кодогенераторы, преобразующие 3D модели в G-Code для печати контрольных сумм не генерируют, а оставляют это на усмотрение хоста.

У принтера есть буффер, размер которого в большинстве случаев составляет 16 команд.

Реализация протокола


Qt имеет отличную функцию для облегчения жизни разработчику — сигналы и слоты.
Сигнал — это новое для C++ понятие. Сигнал объявляется в классе подобно слоту:

class Example : public QObject
{
    Q_OBJECT
    public:
        Example();

    signals:
         void exampleSignal(int);
} 

Сигнал не может ничего вернуть, поэтому всегда имеет тип void, а вместо аргумента указывается тип передаваемой сигналом переменной. Инициализировать его не требуется.

Вызывается сигнал из любого метода родительского класса, и очень просто:

emit exampleSignal(100);

После такого вызова все слоты, присоединённые к этому сигналу будут вызваны при первой возможности в порядке присоединения, и им передастся значение 100.
Что такое слот? Коротко — это самый обычный метод, объявленный слотом

class Example2 : public QObject
{
    Q_OBJECT
    public:
        Example();

    public slots:
         void exampleSlot(int);
} 

Соединяются сигнал со слотом тоже очень просто:

Example e1;
Example2 e2;
connect(&e1, SIGNAL(exampleSignal(int)), &e2, SLOT(exampleSlot(int)));

Есть и ограничение — у сигнала и слота обязательно должен совпадать передаваемый тип.

Учитывая сигналы и слоты можно очень легко написать асинхронный приемник\передатчик для связи по серийному порту.

Самая простая часть — чтение.

Для реализации связи по серийному порту используется упомянутый выше QSerialPort (экземпляр которого назовем «printer»), имеющий сигнал readyRead(), вызывающийся каждый раз, когда на порт приходит информация. Все что от нас требуется — это создать в своем классе слот, который мы и будем вызывать при появлении этого сигнала, соединить их, и ждать. Что же мы будем читать? Как уже описано выше, в первую очередь нас интересуют ответы ok и wait. Так как наш код исполняется асинхронно, а у принтера есть буфер — нам надо где-то сохранить колличество строчек ок, которые мы приняли, чтобы при отправлении знать, сколько мы можем отправить. Хранить их будет в переменной «readyRecieve».

Слот:

void MainWindow::readSerial() 
{
    if(printer.canReadLine()) //На всякий случай проверяем, есть ли линия в буффере
    {
        QByteArray data = printer.readLine(); //Читаем пришедшую строку

        if(data.startsWith("ok")) readyRecieve++;  //Можно отправить еще одну строчку
        else if(data.startsWith("wait")) readyRecieve = 1; //А вот это значит, что принтер в простое - отправляем только 1 команду

        printMsg(QString(data)); //Покажем пользователю, что мы получили от принтера
    }
}

Отлично, с приемом разобрались. А что у нас с отправкой? Вот тут и видим дефект протокола. Никакой аппаратной команды для обозначения готовности к приему данных у нас нет, а значит нету и соответствующего сигнала у нашего QSerialPort. Значит отправлять будем по таймеру. Таймер в Qt работает до безобразия просто и удобно — мы создаем экземпляр класса QTimer, соединяем его сигнал timeout() с нашим слотом, который будет выполнятся по этому таймеру, а после этого запускаем его — timer.start(ms). В последствии было выяснено, что в зависимости от производительности ПК оптимальный промежуток находится от 1 до 5 мс. Кстати, если указать таймеру промежуток 0, то выполнятся он будет как только у Qt появится свободная минутка.

Еще стоит упомянуть, что отправляем мы либо команду от пользователя, либо файл. Так как разбор файла сводится к заурядной загрузки файла в массив, а так же фильтровании линий целиком из комментариев (чтобы не отправлять лишнее на принтер и не захламлять память), я решил опустить эту часть. Массив строк из файла называется gcode.

Реализация отправки:

void MainWindow::sendNext()
{
    if(injectingCommand && printer.isWritable() && readyRecieve > 0) //Пытаемся вклинить команду от пользователя
    {
        sendLine(userCommand); //отправляем в порт (userCommand и флаг injectingCommand служат для вставки команды от пользователя)
        readyRecieve--; //уменьшаем колличество строчек, которые принтер готов принять
        injectingCommand=false; //уже вклинили команду, так что флаг стоит опустить
        return; //обрываем наш отправщик
    }
    else if(sending && !paused && readyRecieve > 0 && !sdprinting && printer.isWritable()) //Проверяем, отправляем ли мы сейчас файл
    {
        if(currentLine >= gcode.size()) //Не кончился ли файл
        {
            sending = false; //Ну рас уж кончился - то мы его уже не отправляем
            currentLine = 0; //Обнуляем счетчик отправленых из файла линий
            ui->sendBtn->setText("Send"); //обновляем интерфейс
            ui->pauseBtn->setDisabled("true"); //обновляем интерфейс
            ui->filelines->setText(QString::number(gcode.size()) //обновляем интерфейс
                                   + QString("/")
                                   + QString::number(currentLine)
                                   + QString(" Lines"));
            return; //обрываем функцию отправки
        }
        sendLine(gcode.at(currentLine)); //если файл не кончился - отправляем следующую линию
        currentLine++; //Увеличиваем счетчик отправленых из файла линий
        readyRecieve--; //Уменьшаем счетчик команд

        ui->filelines->setText(QString::number(gcode.size()) //Обновляем счетчик линий в интерфейсе
                               + QString("/")
                               + QString::number(currentLine)
                               + QString(" Lines"));
        ui->progressBar->setValue(((float)currentLine/gcode.size()) * 100); //обновляем полоску 
    }
}

Ну вот, с серийным портом разобрались? Еще нет. Еще нам надо отловить ошибки. Для этого вновь прибегнем к сигналам и слотам, на этот раз будем слушать сигнал error(SerialPort::SerialPortError error) от нашего экземпляра QSerialPort:

void MainWindow::serialError(QSerialPort::SerialPortError error)
{
    if(error == QSerialPort::NoError) return; //Нет ошибки - нет проблем

    if(printer.isOpen()) printer.close(); //Ну а если сюда дело дошло - не плохо бы закрыть порт, если он еще открыт

    if(sending) paused = true; //Если что-то отправляем на принтер - поставим на паузу отправку

    ui->connectBtn->setText("Connect"); //Обновим интерфейс
    ui->sendBtn->setDisabled(true); //Обновим интерфейс
    ui->pauseBtn->setDisabled(true); //Обновим интерфейс
    ui->controlBox->setDisabled(true); //Обновим интерфейс
    ui->consoleGroup->setDisabled(true); //Обновим интерфейс

    qDebug() << error; //На всякий случай отправим ошибку в консоль

    QString errorMsg; //Переведем нашу ошибку на понятный язык
    switch(error)
    {
    case QSerialPort::DeviceNotFoundError:
        errorMsg = "Device not found";
        break;

    case QSerialPort::PermissionError:
        errorMsg = "Insufficient permissions\nAlready opened?";
        break;

    case QSerialPort::OpenError:
        errorMsg = "Cant open port\nAlready opened?";
        break;

    case QSerialPort::TimeoutError:
        errorMsg = "Serial connection timed out";
        break;

    case QSerialPort::WriteError:
    case QSerialPort::ReadError:
        errorMsg = "I/O Error";
        break;

    case QSerialPort::ResourceError:
        errorMsg = "Disconnected";
        break;

    default:
        errorMsg = "Unknown error\nSomething went wrong";
        break;
    }

    ErrorWindow errorwindow(this, errorMsg); //И покажем окошко с ошибкой
    errorwindow.exec();
}




Неаккуратное обращение с принтером расстраивает динозаврика, %username%.

Делаем наш хост умнее


Выносим кнопки команд


Мы научились отправлять файл в серийный порт и правильно держать протокол, но для полноценного хоста этого мало. Помимо возможности отправлять файл построчно, необходимо еще дать пользователю возможность отправлять команды самому. Как вбитые руками, так и выведенные на кнопки в интерфейсе.
Какие команды стоит вывести? Мнения авторов разных, уже имеющихся программ расходятся, но я постарался вывести максимум:



Иконок пока нет, но они обязательно появятся в последующих релизах. Реализация большинства этих кнопок весьма простая:

void MainWindow::homeall()
{
    injectCommand("G28");
}


Метод injectCommand, способ работы которого мы уже узнали ранее, в реализации отправщика кода тоже весьма прост:

void MainWindow::injectCommand(QString command)
{
    injectingCommand = true; //Поставим флаг вставки команды 
    userCommand = command; //Запомним команду
}

Получаем дополнительные данные


RepRap — это суровый DIY. Настолько суровый, что случается всякое:



Далеко не всегда во время настройки можно быть уверенным, что принтер поведет себя так, как надо. Одним из очень важных значений является температура, и следить за ней жизненно необходимо. У некоторых людей на принтере есть дисплеи, отображающие оперативную информацию, но далеко не у всех. К тому же, можно физически находится от принтера далеко, или под неверным углом, и читать дисплей уже не выйдет. Именно поэтому требуется отображать температуру, и чем оперативнее — тем лучше.


Под температуру определим отдельную группу элементов

Как же ее узнать? Во время нагрева, принтер перестает принимать команды, и вместо ok или wait начинает присылать температуру. Строка температуры выглядит так:
T:196.94 /210 B:23.19 /0 B@:0 @:4

Читаемо для человека, но не очень удобно для разбора. В первых версиях я разбирал эту строку прямо в слоте, отвечающем за прием информации, но тесты показали, что разбор строки прямо в этом слоте слишком сильно замедляет работу программы. До последнего я пытался избежать работы с потоками, так как одна из главных причин, по которой RepRaptor был написан — быстродействие. Многопоточность отлично ускоряет работу софта на многоядерных системах, но у нас с железом было все не так радужно. Однако выбора не осталось — надо было перенести в отдельный поток либо разбор, либо само соединение. Было решено пойти на компромисс — перенести разбор в отдельный поток, и позволить пользователю отключить проверку температуры.

Как реализована многопоточность в Qt? Очень удобно. Есть несколько способов создать отдельный поток. Полноценный способ — создать поток с помощью QThread, но нам не нужен полноценный поток для простого разбора строки, так что будем использовать другой способ — передадим нашу функцию разбора вместе с аргументам объекту QFuture, и будем за ним следить. Делается это так — для начала нам необходимо создать экземпляр QFutureWatcher, класса, который следит за QFuture, и сообщает нам о его состоянии. Затем надо написать нашу функцию разбора. Так как функция может вернуть только одну переменную, я решил создать специальный тип для передачи температуры:

typedef struct
{
    double e, b;
} TemperatureReadings;

И функция разбора:

TemperatureReadings MainWindow::parseStatus(QByteArray data)
{
    QString extmp = ""; // Подготовим строку для температуры экструдера
    QString btmp = ""; // Подготовим строку для температуры кровати

    for(int i = 2; data.at(i) != '/'; i++) //Считываем температуру экструдера
    {
        extmp+=data.at(i);
    }
    for(int i = data.indexOf("B:")+2; data.at(i) != '/'; i++) //Считываем температуру кровати
    {
        btmp+=data.at(i);
    }

    TemperatureReadings t;
    t.e = extmp.toDouble(); //Переводим строки в числа
    t.b = btmp.toDouble(); 

    return t; //Возвращаем температуру
}

Теперь осталось только отдать эту функцию потоку при удобном случае (вставляем дополнительную проверку в автомат функции приема):

//....
else if(checkingTemperature && data.startsWith("T:")) // Определяем, температура ли это
        {
            QFuture<TemperatureReadings> parseThread = QtConcurrent::run(this, &MainWindow::parseStatus, data); //Создаем объект QFuture (QtConcurrent::run создает экземпляр QFuture из метода parseStatus)
            statusWatcher.setFuture(parseThread); //Передаем наш объект QFuture наблюдателю
            ui->tempLine->setText(data); //Продублируем строку в интерфейсе
        }
//...

Осталось только создать слот, чтобы подключить к нему сигнал, передающий из QFutureWatcher результаты выполнения функции:

void MainWindow::updateStatus()
{
    TemperatureReadings r = statusWatcher.future().result(); //Получаем результат из потока
    ui->extruderlcd->display(r.e); //Обновляем интерфейс
    ui->bedlcd->display(r.b); 

    sinceLastTemp.restart(); //Перезапускам таймер последнего снятия температуры
}

Вот и все, теперь каждый раз, когда принтер сообщает нам о температуре — мы разбираем эту строку и красиво отображаем ее в интерфейсе.

Проблема в том, что сам принтер ее присылает только во время нагрева, но мы можем его попросить прислать нам ее в любое другое время, отправив команду для проверки температуры M105. Отправлять ее будем по таймеру, при выполнении нескольких условий. Так же, как и ранее для функции отправки создаем новый таймер, и новый слот для подключения к его сигналу. На этот раз таймеру ставим значение побольше, например 1500мс:

void MainWindow::checkStatus()
{
    if(checkingTemperature && //Если проверяем температуру....
            (sinceLastTemp.elapsed() > statusTimer.interval()) //И последний раз ее получили не раньше, чем задано
            && statusWatcher.isFinished()) injectCommand("M105"); //И если наш поток уже закончил, тогда отправляем команду M105
}

Кто-то возможно скажет, что эти проверки излишние, но когда ты управляешь принтером с Asus EEEPC 900AX, и хочешь одновременно читать хабр — это необходимость.

Печать с SD карты




Очень многие типовые платы для 3D принтеров имеют встроенный слот для SD карты, или способ такой слот подключить. Такой метод печати является предпочтительным, так как в любой момент можно отключится от принтера и уйти, однако каждый раз вытаскивать SD карту зачастую лень, особенно если печатаешь много мелких деталей. Конечно, можно передать файл через серийный порт, но передача файла таким способом занимает едва ли меньше времени, чем сама печать.

В любом случае, хост должен уметь работать с SD картой, а это значит:
  • Получать список файлов
  • Выбирать файл
  • Поставить файл на печать
  • Поставить печать на паузу
  • Сообщать о прогрессе

А это значит — больше парсинга! Но на этот раз, опрашивая список файлов мы можем не волноваться о производительности — в этот момент она не важна. Для отображения списка файлов нам понадобится диалог. В диалог мы будем передавать массив строк с файлами, а он нам вернет один файл, а заодно скажет нашей программе, что она в режиме печати с SD.

Каждый раз когда на хост будет приходить строка, с которой начинается список файлов на SD — будем открывать диалог со списком файлов. Для этого еще раз изменим наш приемник:

//...	
	if(readingFiles) //Если читаем файлы
        {
            if(!data.contains("End file list")) sdFiles.append(data); //Если список не кончился - добавляем строчку в список файлов
            else
            {
                readingFiles = false; //Если кончился список 
                emit sdReady(); //Отправим сигнал о готовности списка файлов
            }
        }
//...
	else if(data.startsWith("Done")) sdprinting = false; //Если получили Done - печать с SD окончена
	else if(data.startsWith("SD printing byte") && sdWatcher.isFinished()) //Разберем статус SD печати
	{
    	QFuture<double> parseSDThread = QtConcurrent::run(this, &MainWindow::parseSDStatus, data); //Скормим строку потоку с парсером
        sdWatcher.setFuture(parseSDThread); //Следим за потоком
    }
    else if(data.contains("Begin file list")) //Список файлов с SD карты
    {
         sdFiles.clear(); Очистим массив файлов
         readingFiles = true; //Поставим флаг, идет получение файлов
    }
//...

Флаг нужен, так как наш метод чтения вызывается для каждой строки.

За одно для открытия диалога будем посылать сигнал. Самому же себе. Да, соединять можно даже сигналы со слотами одного и того же объекта.

Слот, который мы подключаем к этому сигналу:

void MainWindow::initSDprinting()
{
    SDWindow sdwindow(sdFiles, this); 

    connect(&sdwindow, SIGNAL(fileSelected(QString)), this, SLOT(selectSDfile(QString)));

    sdwindow.exec();
}

После этого надо перевести интерфейс в режим печати. Я сделал это путем введения дополнительного флага sdprinting. Статус печати с SD карты проверяется в схожей манере с температурой.

Софт — в массы


Как бы сильно я не любил GitHub — далеко не все любят собирать софт сами. Не смотря на кроссплатформенность Qt — это фреймворк, а не среда для кросс-компиляции. А это значит, помимо бинариков под платформу Linux-amd64 нужны еще бинарики под Linux-i386, Windows 32 и OSX 64. Что ж, с первыми двумя все просто — достаточно добавить нужные наборы в QtCreator. А как быть с Windows 32 и OSX 64? С последней — никак. Я пытался, но я умываю руки. Единственный способ собрать что-то под OSX — делать это на OSX. К сожалению, легально это сделать не возможно.

Debian и его пакеты


Очень хотелось собрать пакет под любимый Linux Mint, на котором и ведется разработка. По началу я хотел собрать пакеты для PPA, но в итоге пришел к тому, что собирать надо со статическими библиотеками Qt, а потому пакеты на первое время собирать пришлось руками. Почему статическая линковка? Все очень просто — даже в Ubuntu 14.04, на которой основан Mint в источниках имеется только Qt 5.2.1. В ходе тестов первых двух релизов на разных системах выявились баги QSerialPort, исправленные в новых версиях, а потому было принято решение поставлять все с последними версиями. К тому же, вики Qt говорит, что статически линкованые библиотеки несколько быстрее работают.

Руками пакет собирается весьма просто — он должен содержать дерево каталогов файловой системы Debian, в котором размещены все файлы, которые мы собираемся установить. Написав .desktop файл для того, чтобы получить пункт в меню на конечной системе, я написал небольшой bash скрипт для сборки нужной мне версии пакетов:

#!/bin/bashcd 

# $1 - первый аргумент командной строки после скрипта

mkdir repraptor-$1-1-i386 
mkdir repraptor-$1-1-amd64

mkdir repraptor-$1-1-i386/DEBIAN/
mkdir repraptor-$1-1-i386/usr/
mkdir repraptor-$1-1-i386/usr/local/
mkdir repraptor-$1-1-i386/usr/local/bin
mkdir repraptor-$1-1-i386/usr/local/share

mkdir repraptor-$1-1-amd64/DEBIAN/
mkdir repraptor-$1-1-amd64/usr/
mkdir repraptor-$1-1-amd64/usr/local/
mkdir repraptor-$1-1-amd64/usr/local/bin

cp ../RepRaptor-linux32/RepRaptor repraptor-$1-1-i386/usr/local/bin/repraptor
cp ../RepRaptor-linux64/RepRaptor repraptor-$1-1-amd64/usr/local/bin/repraptor

cp share repraptor-$1-1-i386/usr/local/ -r
cp share repraptor-$1-1-amd64/usr/local/ -r

echo "Package: repraptor
Version: $1-1
Section: base
Priority: optional
Architecture: i386
Maintainer: [Hello Habr!]
Description: RepRaptor
 A Qt RepRap gcode sender/host controller" > repraptor-$1-1-i386/DEBIAN/control
echo "Package: repraptor
Version: $1-1
Section: base
Priority: optional
Architecture: amd64
Maintainer: [Hello Habr!]
Description: RepRaptor
 A Qt RepRap gcode sender/host controller" > repraptor-$1-1-amd64/DEBIAN/control

dpkg-deb --build repraptor-$1-1-i386
dpkg-deb --build repraptor-$1-1-amd64


Все, что он делает — это создает дерево каталогов, копирует иконки и .desktop файл, а после этого генерирует описание пакета для пакетного менеджера, вставляя нужную версию.

Windows и MXE


Одно дело — собирать пакеты под свою ОС, другое дело — под чужую. Тут необходим тулчеин — набор инструментов для сборки. Благо под Windows есть отличный MinGW, а для Linux есть не менее отличный MXE — менеджер окружения для кросс-компиляции с использованием свободных библиотек.

Установка до смешного простая — клонируем MXE и говорим ему, какие нам библиотеки нужны:

git clone https://github.com/mxe/mxe.git

cd mxe

make qtbase qtserialport

После этого можно смело идти пить кофе — сборка всего этого добра занимает приличное время.

После того, как сборка окончена — можно написать скрипт сборки проекта под Windows автоматически:

#!/bin/bash

#Добавим исполняемые файлы MXE в PATH
export PATH=/home/neothefox/src/mxe/usr/bin:$PATH 

#Удаляем старый каталог
rm -rf RepRaptor-win32

#Клонируем свежую версию и переходим в ее папку
git clone https://github.com/NeoTheFox/RepRaptor RepRaptor-win32
cd RepRaptor-win32

#Генерируем Makefile из проекта QtCreator и собираем проект
/home/neothefox/src/mxe/usr/bin/i686-w64-mingw32.static-qmake-qt5 RepRaptor.pro
make

#Собираем ZIP архив с лицензией и исполняемым файлом
cp LICENCE release/
cd release
zip RepRaptor-$1-win32.zip RepRaptor.exe LICENCE

#Переносим его в каталог с остальными сборками
mv RepRaptor-$1-win32.zip ../../RepRaptor-$1-win32.zip

Просто и сердито. Работоспособность легко проверяется в Wine:



Выводы


Когда я это начинал — я надеялся написать простую отправлялку G-code файлов, которая ничего больше бы не делала, а справится планировал за вечер. Но, как это часто бывает — проект вышел за рамки изначального плана, и теперь представляет из себя нечто большее. Впереди еще много чего предстоит сделать — хотя бы тот же графический редактор EEPROM, которого так не хватает, а так же поддержка контрольных сумм.

Однако уже сейчас RepRaptor позволяет мне спокойно использовать свой ASUS EEEPC как стабильный хост для принтера, чего не мог достичь ни один другой хост из тех, что я пробовал. Ну и знания Qt у меня определенно улучшились, и все равно еще есть место для оптимизаций.

Эта статья так же приурочена к выходу первой стабильной версии, которую я сам теперь использую каждый день — 0.2.

Всем спасибо за внимание! Надеюсь мой опыт был вам полезен.

Напоследок — вот армия объектов, напечатанных во время тестов:



Ссылки


Первая стабильная версия
RepRap wiki
GitHub
Tags:
Hubs:
+33
Comments26

Articles