FirmwareUpdater: MMU 2.0 / Caterina flashing

This commit is contained in:
Vojtech Kral 2018-07-24 17:42:12 +02:00 committed by bubnikv
parent a7eaf38853
commit a32bd17b75
8 changed files with 496 additions and 109 deletions

View file

@ -1,12 +1,23 @@
#include "FirmwareDialog.hpp"
#include <numeric>
#include <algorithm>
#include <thread>
#include <stdexcept>
#include <boost/format.hpp>
#include <boost/asio.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/log/trivial.hpp>
#include "libslic3r/Utils.hpp"
#include "avrdude/avrdude-slic3r.hpp"
#include "GUI.hpp"
#include "MsgDialog.hpp"
#include "../Utils/HexFile.hpp"
#include "../Utils/Serial.hpp"
// wx includes need to come after asio because of the WinSock.h problem
#include "FirmwareDialog.hpp"
#include <wx/app.h>
#include <wx/event.h>
#include <wx/sizer.h>
@ -22,16 +33,18 @@
#include <wx/collpane.h>
#include <wx/msgdlg.h>
#include "libslic3r/Utils.hpp"
#include "avrdude/avrdude-slic3r.hpp"
#include "GUI.hpp"
#include "../Utils/Serial.hpp"
namespace fs = boost::filesystem;
namespace asio = boost::asio;
using boost::system::error_code;
namespace Slic3r {
using Utils::HexFile;
using Utils::SerialPortInfo;
using Utils::Serial;
// This enum discriminates the kind of information in EVT_AVRDUDE,
// it's stored in the ExtraLong field of wxCommandEvent.
@ -40,6 +53,7 @@ enum AvrdudeEvent
AE_MESSAGE,
AE_PROGRESS,
AE_EXIT,
AE_ERROR,
};
wxDECLARE_EVENT(EVT_AVRDUDE, wxCommandEvent);
@ -52,7 +66,6 @@ struct FirmwareDialog::priv
{
enum AvrDudeComplete
{
AC_NONE,
AC_SUCCESS,
AC_FAILURE,
AC_CANCEL,
@ -61,7 +74,7 @@ struct FirmwareDialog::priv
FirmwareDialog *q; // PIMPL back pointer ("Q-Pointer")
wxComboBox *port_picker;
std::vector<Utils::SerialPortInfo> ports;
std::vector<SerialPortInfo> ports;
wxFilePickerCtrl *hex_picker;
wxStaticText *txt_status;
wxGauge *progressbar;
@ -80,7 +93,9 @@ struct FirmwareDialog::priv
AvrDude::Ptr avrdude;
std::string avrdude_config;
unsigned progress_tasks_done;
unsigned progress_tasks_bar;
bool cancelled;
const bool extra_verbose; // For debugging
priv(FirmwareDialog *q) :
q(q),
@ -89,16 +104,25 @@ struct FirmwareDialog::priv
timer_pulse(q),
avrdude_config((fs::path(::Slic3r::resources_dir()) / "avrdude" / "avrdude.conf").string()),
progress_tasks_done(0),
cancelled(false)
progress_tasks_bar(0),
cancelled(false),
extra_verbose(false)
{}
void find_serial_ports();
void flashing_start(bool flashing_l10n);
void flashing_start(unsigned tasks);
void flashing_done(AvrDudeComplete complete);
size_t hex_num_sections(const wxString &path);
void check_model_id(const HexFile &metadata, const SerialPortInfo &port);
void prepare_common(AvrDude &, const SerialPortInfo &port);
void prepare_mk2(AvrDude &, const SerialPortInfo &port);
void prepare_mk3(AvrDude &, const SerialPortInfo &port);
void prepare_mm_control(AvrDude &, const SerialPortInfo &port);
void perform_upload();
void cancel();
void on_avrdude(const wxCommandEvent &evt);
void ensure_joined();
};
void FirmwareDialog::priv::find_serial_ports()
@ -122,7 +146,7 @@ void FirmwareDialog::priv::find_serial_ports()
}
}
void FirmwareDialog::priv::flashing_start(bool flashing_l10n)
void FirmwareDialog::priv::flashing_start(unsigned tasks)
{
txt_stdout->Clear();
txt_status->SetLabel(_(L("Flashing in progress. Please do not disconnect the printer!")));
@ -132,9 +156,10 @@ void FirmwareDialog::priv::flashing_start(bool flashing_l10n)
hex_picker->Disable();
btn_close->Disable();
btn_flash->SetLabel(btn_flash_label_flashing);
progressbar->SetRange(flashing_l10n ? 500 : 200); // See progress callback below
progressbar->SetRange(200 * tasks); // See progress callback below
progressbar->SetValue(0);
progress_tasks_done = 0;
progress_tasks_bar = 0;
cancelled = false;
timer_pulse.Start(50);
}
@ -158,56 +183,51 @@ void FirmwareDialog::priv::flashing_done(AvrDudeComplete complete)
}
}
size_t FirmwareDialog::priv::hex_num_sections(const wxString &path)
void FirmwareDialog::priv::check_model_id(const HexFile &metadata, const SerialPortInfo &port)
{
fs::ifstream file(fs::path(path.wx_str()));
if (! file.good()) {
return 0;
if (metadata.model_id.empty()) {
// No data to check against
return;
}
asio::io_service io;
Serial serial(io, port.port, 115200);
serial.printer_setup();
enum {
TIMEOUT = 1000,
RETREIES = 3,
};
if (! serial.printer_ready_wait(RETREIES, TIMEOUT)) {
throw wxString::Format(_(L("Could not connect to the printer at %s")), port.port);
}
static const char *hex_terminator = ":00000001FF\r";
size_t res = 0;
std::string line;
while (getline(file, line, '\n').good()) {
// Account for LF vs CRLF
if (!line.empty() && line.back() != '\r') {
line.push_back('\r');
error_code ec;
serial.printer_write_line("PRUSA Rev");
while (serial.read_line(TIMEOUT, line, ec)) {
if (ec) { throw wxString::Format(_(L("Could not connect to the printer at %s")), port.port); }
if (line == "ok") { continue; }
if (line == metadata.model_id) {
return;
} else {
throw wxString::Format(_(L(
"The firmware hex file does not match the printer model.\n"
"The hex file is intended for:\n %s\n"
"Printer reports:\n %s"
)), metadata.model_id, line);
}
if (line == hex_terminator) {
res++;
}
line.clear();
}
return res;
}
void FirmwareDialog::priv::perform_upload()
void FirmwareDialog::priv::prepare_common(AvrDude &avrdude, const SerialPortInfo &port)
{
auto filename = hex_picker->GetPath();
std::string port = port_picker->GetValue().ToStdString();
int selection = port_picker->GetSelection();
if (selection != -1) {
// Verify whether the combo box list selection equals to the combo box edit value.
if (this->ports[selection].friendly_name == port)
port = this->ports[selection].port;
}
if (filename.IsEmpty() || port.empty()) { return; }
const bool extra_verbose = false; // For debugging
const auto num_secions = hex_num_sections(filename);
const auto filename_utf8 = filename.utf8_str();
flashing_start(num_secions > 1);
// It is ok here to use the q-pointer to the FirmwareDialog
// because the dialog ensures it doesn't exit before the background thread is done.
auto q = this->q;
// Init the avrdude object
AvrDude avrdude(avrdude_config);
// Build argument list(s)
std::vector<std::string> args {{
extra_verbose ? "-vvvvv" : "-v",
"-p", "atmega2560",
@ -215,11 +235,10 @@ void FirmwareDialog::priv::perform_upload()
// The Prusa's avrdude is patched to never send semicolons inside the data packets, as the USB to serial chip
// is flashed with a buggy firmware.
"-c", "wiring",
"-P", port,
"-P", port.port,
"-b", "115200", // TODO: Allow other rates? Ditto below.
"-D",
// XXX: Safe mode?
"-U", (boost::format("flash:w:0:%1%:i") % filename_utf8.data()).str(),
"-U", (boost::format("flash:w:0:%1%:i") % filename.utf8_str().data()).str(),
}};
BOOST_LOG_TRIVIAL(info) << "Invoking avrdude, arguments: "
@ -228,32 +247,172 @@ void FirmwareDialog::priv::perform_upload()
});
avrdude.push_args(std::move(args));
if (num_secions > 1) {
// The hex file also contains another section with l10n data to be flashed into the external flash on MK3 (Einsy)
// This is done via another avrdude invocation, here we build arg list for that:
std::vector<std::string> args_l10n {{
extra_verbose ? "-vvvvv" : "-v",
"-p", "atmega2560",
// Using the "Arduino" mode to program Einsy's external flash with languages, using the STK500 protocol (not the STK500v2).
// The Prusa's avrdude is patched again to never send semicolons inside the data packets.
"-c", "arduino",
"-P", port,
"-b", "115200",
"-D",
"-u", // disable safe mode
"-U", (boost::format("flash:w:1:%1%:i") % filename_utf8.data()).str(),
}};
}
BOOST_LOG_TRIVIAL(info) << "Invoking avrdude for external flash flashing, arguments: "
<< std::accumulate(std::next(args_l10n.begin()), args_l10n.end(), args_l10n[0], [](std::string a, const std::string &b) {
return a + ' ' + b;
});
void FirmwareDialog::priv::prepare_mk2(AvrDude &avrdude, const SerialPortInfo &port)
{
flashing_start(1);
prepare_common(avrdude, port);
}
avrdude.push_args(std::move(args_l10n));
void FirmwareDialog::priv::prepare_mk3(AvrDude &avrdude, const SerialPortInfo &port)
{
flashing_start(2);
prepare_common(avrdude, port);
auto filename = hex_picker->GetPath();
// The hex file also contains another section with l10n data to be flashed into the external flash on MK3 (Einsy)
// This is done via another avrdude invocation, here we build arg list for that:
std::vector<std::string> args_l10n {{
extra_verbose ? "-vvvvv" : "-v",
"-p", "atmega2560",
// Using the "Arduino" mode to program Einsy's external flash with languages, using the STK500 protocol (not the STK500v2).
// The Prusa's avrdude is patched again to never send semicolons inside the data packets.
"-c", "arduino",
"-P", port.port,
"-b", "115200",
"-D",
"-u", // disable safe mode
"-U", (boost::format("flash:w:1:%1%:i") % filename.utf8_str().data()).str(),
}};
BOOST_LOG_TRIVIAL(info) << "Invoking avrdude for external flash flashing, arguments: "
<< std::accumulate(std::next(args_l10n.begin()), args_l10n.end(), args_l10n[0], [](std::string a, const std::string &b) {
return a + ' ' + b;
});
avrdude.push_args(std::move(args_l10n));
}
void FirmwareDialog::priv::prepare_mm_control(AvrDude &avrdude, const SerialPortInfo &port_in)
{
// Check if the port has the PID/VID of 0x2c99/3
// If not, check if it is the MMU (0x2c99/4) and reboot the by opening @ 1200 bauds
BOOST_LOG_TRIVIAL(info) << "Flashing MMU 2.0, looking for VID/PID 0x2c99/3 or 0x2c99/4 ...";
SerialPortInfo port = port_in;
if (! port.id_match(0x2c99, 3)) {
if (! port.id_match(0x2c99, 4)) {
// This is not a Prusa MMU 2.0 device
BOOST_LOG_TRIVIAL(error) << boost::format("Not a Prusa MMU 2.0 device: `%1%`") % port.port;
throw wxString::Format(_(L("The device at `%s` is not am Original Prusa i3 MMU 2.0 device")), port.port);
}
BOOST_LOG_TRIVIAL(info) << boost::format("Found VID/PID 0x2c99/4 at `%1%`, rebooting the device ...") % port.port;
{
asio::io_service io;
Serial serial(io, port.port, 1200);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// Wait for the bootloader to show up
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
// Look for the rebooted device
BOOST_LOG_TRIVIAL(info) << "... looking for VID/PID 0x2c99/3 ...";
auto new_ports = Utils::scan_serial_ports_extended();
unsigned hits = 0;
for (auto &&new_port : new_ports) {
if (new_port.id_match(0x2c99, 3)) {
hits++;
port = std::move(new_port);
}
}
if (hits == 0) {
BOOST_LOG_TRIVIAL(error) << "No VID/PID 0x2c99/3 device found after rebooting the MMU 2.0";
throw wxString::Format(_(L("Failed to reboot the device at `%s` for programming")), port.port);
} else if (hits > 1) {
// We found multiple 0x2c99/3 devices, this is bad, because there's no way to find out
// which one is the one user wants to flash.
BOOST_LOG_TRIVIAL(error) << "Several VID/PID 0x2c99/3 devices found after rebooting the MMU 2.0";
throw wxString::Format(_(L("Multiple Original Prusa i3 MMU 2.0 devices found. Please only connect one at a time for flashing.")), port.port);
}
}
BOOST_LOG_TRIVIAL(info) << boost::format("Found VID/PID 0x2c99/3 at `%1%`, flashing ...") % port.port;
auto filename = hex_picker->GetPath();
std::vector<std::string> args {{
extra_verbose ? "-vvvvv" : "-v",
"-p", "atmega32u4",
"-c", "avr109",
"-P", port.port,
"-b", "57600",
"-D",
"-U", (boost::format("flash:w:0:%1%:i") % filename.utf8_str().data()).str(),
}};
BOOST_LOG_TRIVIAL(info) << "Invoking avrdude, arguments: "
<< std::accumulate(std::next(args.begin()), args.end(), args[0], [](std::string a, const std::string &b) {
return a + ' ' + b;
});
avrdude.push_args(std::move(args));
}
void FirmwareDialog::priv::perform_upload()
{
auto filename = hex_picker->GetPath();
if (filename.IsEmpty()) { return; }
int selection = port_picker->GetSelection();
if (selection == wxNOT_FOUND) { return; }
std::string port_selected = port_picker->GetValue().ToStdString();
const SerialPortInfo &port = this->ports[selection];
// Verify whether the combo box list selection equals to the combo box edit value.
if (this->ports[selection].friendly_name != port_selected) { return; }
const bool extra_verbose = false; // For debugging
HexFile metadata(filename.wx_str());
// const auto filename_utf8 = filename.utf8_str();
flashing_start(metadata.device == HexFile::DEV_MK3 ? 2 : 1);
// Init the avrdude object
AvrDude avrdude(avrdude_config);
// It is ok here to use the q-pointer to the FirmwareDialog
// because the dialog ensures it doesn't exit before the background thread is done.
auto q = this->q;
this->avrdude = avrdude
.on_run([this, metadata, port](AvrDude &avrdude) {
auto queue_error = [&](wxString message) {
avrdude.cancel();
auto evt = new wxCommandEvent(EVT_AVRDUDE, this->q->GetId());
evt->SetExtraLong(AE_ERROR);
evt->SetString(std::move(message));
wxQueueEvent(this->q, evt);
};
try {
switch (metadata.device) {
case HexFile::DEV_MK3:
this->check_model_id(metadata, port);
this->prepare_mk3(avrdude, port);
break;
case HexFile::DEV_MM_CONTROL:
this->check_model_id(metadata, port);
this->prepare_mm_control(avrdude, port);
break;
default:
this->prepare_mk2(avrdude, port);
break;
}
} catch (const wxString &message) {
queue_error(message);
} catch (const std::exception &ex) {
queue_error(wxString::Format(_(L("Error accessing port at %s: %s")), port.port, ex.what()));
}
})
.on_message(std::move([q, extra_verbose](const char *msg, unsigned /* size */) {
if (extra_verbose) {
BOOST_LOG_TRIVIAL(debug) << "avrdude: " << msg;
@ -306,12 +465,15 @@ void FirmwareDialog::priv::on_avrdude(const wxCommandEvent &evt)
// and then display overall progress during the latter tasks.
if (progress_tasks_done > 0) {
progressbar->SetValue(progress_tasks_done - 100 + evt.GetInt());
progressbar->SetValue(progress_tasks_bar + evt.GetInt());
}
if (evt.GetInt() == 100) {
timer_pulse.Stop();
progress_tasks_done += 100;
if (progress_tasks_done % 3 != 0) {
progress_tasks_bar += 100;
}
progress_tasks_done++;
}
break;
@ -321,11 +483,17 @@ void FirmwareDialog::priv::on_avrdude(const wxCommandEvent &evt)
complete_kind = cancelled ? AC_CANCEL : (evt.GetInt() == 0 ? AC_SUCCESS : AC_FAILURE);
flashing_done(complete_kind);
ensure_joined();
break;
// Make sure the background thread is collected and the AvrDude object reset
if (avrdude) { avrdude->join(); }
avrdude.reset();
case AE_ERROR:
txt_stdout->AppendText(evt.GetString());
flashing_done(AC_FAILURE);
ensure_joined();
{
GUI::ErrorDialog dlg(this->q, evt.GetString());
dlg.ShowModal();
}
break;
default:
@ -333,6 +501,13 @@ void FirmwareDialog::priv::on_avrdude(const wxCommandEvent &evt)
}
}
void FirmwareDialog::priv::ensure_joined()
{
// Make sure the background thread is collected and the AvrDude object reset
if (avrdude) { avrdude->join(); }
avrdude.reset();
}
// Public