From 3897e46970c6ba02cdd9e5605d546cffe00833ad Mon Sep 17 00:00:00 2001 From: Stefan Reinauer Date: Sat, 11 Jul 2026 15:44:00 -0700 Subject: [PATCH 1/2] core: fix POSIX serial and disk error handling Use macOS callout devices and initialize the full recovery buffer. Return failed track selections and correct IBM sector error accounting. Use bounded formatting for console and diagnostic messages. --- .../ArduinoFloppyReader/Main.cpp | 5 ++-- ArduinoFloppyReader/lib/ADFWriter.cpp | 27 +++++++++++-------- ArduinoFloppyReader/lib/ArduinoInterface.cpp | 6 ++--- ArduinoFloppyReader/lib/SerialIO.cpp | 5 +++- ArduinoFloppyReader/lib/ibm_sectors.cpp | 3 ++- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/ArduinoFloppyReader/ArduinoFloppyReader/Main.cpp b/ArduinoFloppyReader/ArduinoFloppyReader/Main.cpp index 07d76f4..cafb131 100644 --- a/ArduinoFloppyReader/ArduinoFloppyReader/Main.cpp +++ b/ArduinoFloppyReader/ArduinoFloppyReader/Main.cpp @@ -135,7 +135,7 @@ void listSettings(const std::wstring& port) { ArduinoFloppyReader::DiagnosticResponse resp = io.openPort(port); if (resp != ArduinoFloppyReader::DiagnosticResponse::drOK) { printf("Unable to connect to device: \n"); - printf(io.getLastErrorStr().c_str()); + printf("%s", io.getLastErrorStr().c_str()); return; } @@ -154,7 +154,7 @@ void programmeSetting(const std::wstring& port, const std::wstring& settingName, ArduinoFloppyReader::DiagnosticResponse resp = io.openPort(port); if (resp != ArduinoFloppyReader::DiagnosticResponse::drOK) { printf("Unable to connect to device: \n"); - printf(io.getLastErrorStr().c_str()); + printf("%s", io.getLastErrorStr().c_str()); return; } @@ -594,4 +594,3 @@ int main(int argc, char* argv[], char *envp[]) return 0; } - diff --git a/ArduinoFloppyReader/lib/ADFWriter.cpp b/ArduinoFloppyReader/lib/ADFWriter.cpp index 38b3cca..4fa11f3 100644 --- a/ArduinoFloppyReader/lib/ADFWriter.cpp +++ b/ArduinoFloppyReader/lib/ADFWriter.cpp @@ -287,11 +287,9 @@ bool attemptFixSector(const DecodedTrack& decodedTrack, DecodedSector& outputSec int ones = 0; } SectorCounter[8]; - SectorCounter* sectorSum = new SectorCounter[SECTOR_BYTES + SECTOR_BYTES]; + SectorCounter* sectorSum = new SectorCounter[SECTOR_BYTES + SECTOR_BYTES]{}; if (!sectorSum) return false; - memset(sectorSum, 0, sizeof(sectorSum)); - // Calculate the number of '1's and '0's in each block for (const DecodedSector& sec : decodedTrack.invalidSectors[sectorNumber]) for (int byteNumber = 0; byteNumber < SECTOR_BYTES + SECTOR_BYTES; byteNumber++) @@ -568,7 +566,7 @@ bool ADFWriter::runDiagnostics(const std::wstring& portName, std::function>> Latest version is V%i.%i.%i", major, minor, rev); #else - sprintf(latestFirmwareVersion, ">>> Latest version is V%i.%i.%i", major, minor, rev); + snprintf(latestFirmwareVersion, sizeof(latestFirmwareVersion), + ">>> Latest version is V%i.%i.%i", major, minor, rev); #endif messageOutput(false, latestFirmwareVersion); messageOutput(false, ""); @@ -751,7 +754,7 @@ bool ADFWriter::runDiagnostics(const std::wstring& portName, std::function 320)) { @@ -1603,7 +1606,8 @@ ADFResult ADFWriter::diskToIBMST(const std::wstring& outputFile, const bool inHD // First step is to ID the disk // Select the track we're working on if (m_device.selectTrack(0) != DiagnosticResponse::drOK) return ADFResult::adfrDriveError; - if (m_device.selectSurface(DiskSurface::dsLower) != DiagnosticResponse::drOK) ADFResult::adfrDriveError; + if (m_device.selectSurface(DiskSurface::dsLower) != DiagnosticResponse::drOK) + return ADFResult::adfrDriveError; // To hold a raw track RawTrackDataHD data; @@ -1658,8 +1662,10 @@ ADFResult ADFWriter::diskToIBMST(const std::wstring& outputFile, const bool inHD const DiskSurface surface = ((currentTrack % numHeads) == 1) ? DiskSurface::dsUpper : DiskSurface::dsLower; // Select the track we're working on - if (m_device.selectTrack(cylinder) != DiagnosticResponse::drOK) ADFResult::adfrCompletedWithErrors; - if (m_device.selectSurface(surface) != DiagnosticResponse::drOK) ADFResult::adfrCompletedWithErrors; + if (m_device.selectTrack(cylinder) != DiagnosticResponse::drOK) + return ADFResult::adfrDriveError; + if (m_device.selectSurface(surface) != DiagnosticResponse::drOK) + return ADFResult::adfrDriveError; // Reset (but keep the initial ones!) if (currentTrack > 0) decodedTrack.sectors.clear(); @@ -2926,4 +2932,3 @@ ADFResult ADFWriter::GuessDiskDensity(bool& isHD) { if (m_device.checkDiskCapacity(isHD) == DiagnosticResponse::drOK) return ADFResult::adfrComplete; else return ADFResult::adfrAborted; } - diff --git a/ArduinoFloppyReader/lib/ArduinoInterface.cpp b/ArduinoFloppyReader/lib/ArduinoInterface.cpp index 0f2315e..a3cac78 100644 --- a/ArduinoFloppyReader/lib/ArduinoInterface.cpp +++ b/ArduinoFloppyReader/lib/ArduinoInterface.cpp @@ -860,14 +860,15 @@ DiagnosticResponse ArduinoInterface::selectTrack(const unsigned char trackIndex, #ifdef _WIN32 sprintf_s(buf, "%c%02i%c", COMMAND_GOTOTRACK_REPORT, trackIndex, flags); #else - sprintf(buf, "%c%02i%c", COMMAND_GOTOTRACK_REPORT, trackIndex, flags); + snprintf(buf, sizeof(buf), "%c%02i%c", COMMAND_GOTOTRACK_REPORT, + trackIndex, flags); #endif } else { #ifdef _WIN32 sprintf_s(buf, "%c%02i", COMMAND_GOTOTRACK, trackIndex); #else - sprintf(buf, "%c%02i", COMMAND_GOTOTRACK, trackIndex); + snprintf(buf, sizeof(buf), "%c%02i", COMMAND_GOTOTRACK, trackIndex); #endif } @@ -2885,4 +2886,3 @@ DiagnosticResponse ArduinoInterface::eeprom_SetIndexAlignMode(bool enabled) { return m_lastError; } - diff --git a/ArduinoFloppyReader/lib/SerialIO.cpp b/ArduinoFloppyReader/lib/SerialIO.cpp index 5256420..e53d724 100644 --- a/ArduinoFloppyReader/lib/SerialIO.cpp +++ b/ArduinoFloppyReader/lib/SerialIO.cpp @@ -351,7 +351,10 @@ void SerialIO::enumSerialPorts(std::vector& serialPorts) while ((entry = readdir(dir))) { #ifdef __APPLE__ std::string tmp = entry->d_name; - if (tmp.substr(0,7) != "tty.usb") continue; + // Use callout devices for outgoing connections. Unlike tty devices, + // cu devices do not wait for modem carrier and are the conventional + // macOS endpoint for USB serial applications. + if (tmp.rfind("cu.usb", 0) != 0) continue; std::string name = "/dev/" + std::string(entry->d_name); SerialPortInformation prt; quicka2w(name, prt.portName); diff --git a/ArduinoFloppyReader/lib/ibm_sectors.cpp b/ArduinoFloppyReader/lib/ibm_sectors.cpp index 145ba5b..568065c 100644 --- a/ArduinoFloppyReader/lib/ibm_sectors.cpp +++ b/ArduinoFloppyReader/lib/ibm_sectors.cpp @@ -233,7 +233,8 @@ namespace IBM { // Standardize the sector DecodedSector sec; sec.data = sector.data.data; - sec.numErrors = sector.headerErrors + sector.dataValid ? 0 : 1; + sec.numErrors = sector.headerErrors + + (sector.dataValid ? 0 : 1); // See if this already exists auto it = decodedTrack.sectors.find(sector.header.sector - 1); From 99c0b29955a45adbd23bc51c9321cc28cf8fce29 Mon Sep 17 00:00:00 2001 From: Stefan Reinauer Date: Sat, 11 Jul 2026 15:44:11 -0700 Subject: [PATCH 2/2] qt: add cross-platform DrawBridge frontend Build a native Qt Widgets interface around the portable backend. Run device work on threads and marshal updates through the GUI thread. --- .../ArduinoFloppyReaderQt/CMakeLists.txt | 26 + .../ArduinoFloppyReaderQt/MainWindow.cpp | 812 ++++++++++++++++++ .../ArduinoFloppyReaderQt/MainWindow.h | 112 +++ .../ArduinoFloppyReaderQt/README.md | 30 + .../ArduinoFloppyReaderQt/main.cpp | 18 + .../ArduinoFloppyReaderQt/resources.qrc | 12 + ArduinoFloppyReader/CMakeLists.txt | 26 + 7 files changed, 1036 insertions(+) create mode 100644 ArduinoFloppyReader/ArduinoFloppyReaderQt/CMakeLists.txt create mode 100644 ArduinoFloppyReader/ArduinoFloppyReaderQt/MainWindow.cpp create mode 100644 ArduinoFloppyReader/ArduinoFloppyReaderQt/MainWindow.h create mode 100644 ArduinoFloppyReader/ArduinoFloppyReaderQt/README.md create mode 100644 ArduinoFloppyReader/ArduinoFloppyReaderQt/main.cpp create mode 100644 ArduinoFloppyReader/ArduinoFloppyReaderQt/resources.qrc create mode 100644 ArduinoFloppyReader/CMakeLists.txt diff --git a/ArduinoFloppyReader/ArduinoFloppyReaderQt/CMakeLists.txt b/ArduinoFloppyReader/ArduinoFloppyReaderQt/CMakeLists.txt new file mode 100644 index 0000000..2a590bb --- /dev/null +++ b/ArduinoFloppyReader/ArduinoFloppyReaderQt/CMakeLists.txt @@ -0,0 +1,26 @@ +qt_add_executable(DrawBridgeQt MACOSX_BUNDLE + main.cpp + MainWindow.cpp + MainWindow.h + resources.qrc +) + +target_link_libraries(DrawBridgeQt PRIVATE + drawbridge-core + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +set_target_properties(DrawBridgeQt PROPERTIES + OUTPUT_NAME "DrawBridge" + MACOSX_BUNDLE_BUNDLE_NAME "DrawBridge" + MACOSX_BUNDLE_GUI_IDENTIFIER "uk.co.robsmithdev.DrawBridge" + MACOSX_BUNDLE_SHORT_VERSION_STRING "2.8.8" + MACOSX_BUNDLE_BUNDLE_VERSION "2.8.8" +) + +install(TARGETS DrawBridgeQt + BUNDLE DESTINATION . + RUNTIME DESTINATION bin +) diff --git a/ArduinoFloppyReader/ArduinoFloppyReaderQt/MainWindow.cpp b/ArduinoFloppyReader/ArduinoFloppyReaderQt/MainWindow.cpp new file mode 100644 index 0000000..fba71e0 --- /dev/null +++ b/ArduinoFloppyReader/ArduinoFloppyReaderQt/MainWindow.cpp @@ -0,0 +1,812 @@ +#include "MainWindow.h" + +#include "ADFWriter.h" +#include "ArduinoInterface.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace ArduinoFloppyReader; + +namespace { +QString qstr(const std::string &value) +{ + return QString::fromUtf8(value.c_str()); +} + +std::wstring wstr(const QString &value) +{ + return value.toStdWString(); +} + +QString resultMessage(ADFResult result, const std::string &detail) +{ + switch (result) { + case ADFResult::adfrFileError: + return "Unable to open the selected image file."; + case ADFResult::adfrFileIOError: + return "An error occurred while accessing the image file."; + case ADFResult::adfrDiskWriteProtected: + return "The disk is write protected."; + case ADFResult::adfrFirmwareTooOld: + return "This operation requires newer DrawBridge firmware."; + case ADFResult::adfrMediaSizeMismatch: + return "The image and disk density do not match."; + case ADFResult::adfrExtendedADFNotSupported: + return "Extended ADF files are not supported."; + case ADFResult::adfrBadSCPFile: + return "The SCP file is unsupported or incompatible."; + case ADFResult::adfrIPFLibraryNotAvailable: + return "The CAPSImg library is unavailable or could not read the IPF file."; + case ADFResult::adfrDriveError: + return detail.empty() ? "A drive communication error occurred." + : "A drive communication error occurred:\n\n" + qstr(detail); + default: + return "The operation could not be completed."; + } +} + +QString operationText(CallbackOperation operation) +{ + switch (operation) { + case CallbackOperation::coStarting: return "Preparing interface..."; + case CallbackOperation::coReading: return "Reading cylinder..."; + case CallbackOperation::coWriting: return "Writing cylinder..."; + case CallbackOperation::coVerifying: return "Verifying cylinder..."; + case CallbackOperation::coRetryReading: return "Re-reading cylinder..."; + case CallbackOperation::coRetryWriting: return "Re-writing cylinder..."; + case CallbackOperation::coReVerifying: return "Re-verifying cylinder..."; + case CallbackOperation::coReadingFile: return "Reading file..."; + } + return {}; +} +} + +OperationDialog::OperationDialog(const QString &title, QWidget *parent) + : QDialog(parent), m_status(new QLabel), m_operation(new QLabel), + m_cylinder(new QLabel("0")), m_side(new QLabel), m_good(new QLabel("0")), + m_bad(new QLabel("0")), m_progress(new QProgressBar), + m_abort(new QPushButton("&Abort")) +{ + setWindowTitle(title); + setModal(true); + setMinimumWidth(520); + setWindowFlag(Qt::WindowContextHelpButtonHint, false); + + auto *top = new QHBoxLayout; + auto *messages = new QVBoxLayout; + messages->addWidget(m_status); + messages->addWidget(m_operation); + top->addLayout(messages, 1); + top->addWidget(m_abort); + + auto *fields = new QHBoxLayout; + fields->addWidget(new QLabel("Cylinder:")); + fields->addWidget(m_cylinder); + fields->addSpacing(12); + fields->addWidget(new QLabel("Current Side:")); + fields->addWidget(m_side); + fields->addStretch(); + fields->addWidget(new QLabel("Good Sectors:")); + fields->addWidget(m_good); + fields->addSpacing(12); + fields->addWidget(new QLabel("Partial Sectors:")); + fields->addWidget(m_bad); + + auto *layout = new QVBoxLayout(this); + layout->addLayout(top); + layout->addLayout(fields); + layout->addWidget(m_progress); + connect(m_abort, &QPushButton::clicked, this, &OperationDialog::reject); +} + +OperationDialog::~OperationDialog() +{ + m_aborted = true; + if (m_thread.joinable()) m_thread.join(); +} + +void OperationDialog::invoke(const std::function &function, bool blocking) +{ + if (QThread::currentThread() == thread()) { + function(); + return; + } + QMetaObject::invokeMethod(this, function, + blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection); +} + +void OperationDialog::start(std::function operation) +{ + m_thread = std::thread([this, operation = std::move(operation)] { + operation(*this); + invoke([this] { + m_finished = true; + m_abort->setText("Close"); + accept(); + }); + }); +} + +void OperationDialog::setRange(int maximum) +{ + invoke([this, maximum] { m_progress->setRange(0, maximum); }); +} + +void OperationDialog::setProgress(int value) +{ + invoke([this, value] { m_progress->setValue(value); }); +} + +void OperationDialog::setStatus(const QString &status) +{ + invoke([this, status] { m_status->setText(status); }); +} + +void OperationDialog::setOperation(CallbackOperation operation) +{ + const QString text = operationText(operation); + invoke([this, text] { m_operation->setText(text); }); +} + +void OperationDialog::setTrack(int track, DiskSurface surface, int good, + int bad, int total) +{ + invoke([this, track, surface, good, bad, total] { + m_cylinder->setText(QString::number(track)); + m_side->setText(surface == DiskSurface::dsLower ? "Lower" : "Upper"); + if (good >= 0) + m_good->setText(total >= 0 ? QString("%1/%2").arg(good).arg(total) + : QString::number(good)); + if (bad >= 0) m_bad->setText(QString::number(bad)); + }); +} + +int OperationDialog::askRetry(const QString &title, const QString &message) +{ + int answer = QMessageBox::Abort; + invoke([&] { + QMessageBox box(QMessageBox::Warning, title, message, + QMessageBox::Abort | QMessageBox::Retry | QMessageBox::Ignore, + this); + answer = box.exec(); + }, true); + return answer; +} + +void OperationDialog::showError(const QString &title, const QString &message) +{ + invoke([this, title, message] { QMessageBox::critical(this, title, message); }, true); +} + +void OperationDialog::showInformation(const QString &title, + const QString &message) +{ + invoke([this, title, message] { QMessageBox::information(this, title, message); }, true); +} + +void OperationDialog::showCompletion(const QString &asset, const QString &message) +{ + invoke([this, asset, message] { + QMessageBox box(this); + box.setWindowTitle("DrawBridge"); + const QPixmap image(asset); + if (!image.isNull()) box.setIconPixmap(image); + box.setText(message); + box.exec(); + }, true); +} + +void OperationDialog::reject() +{ + if (!m_finished) { + m_aborted = true; + m_abort->setEnabled(false); + m_abort->setText("Aborting..."); + return; + } + QDialog::reject(); +} + +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) +{ + buildInterface(); + restoreSettings(); + refreshPorts(); + + auto *portTimer = new QTimer(this); + portTimer->setInterval(2000); + connect(portTimer, &QTimer::timeout, this, &MainWindow::refreshPorts); + portTimer->start(); +} + +MainWindow::~MainWindow() = default; + +void MainWindow::buildInterface() +{ + setWindowTitle("DrawBridge aka Arduino Amiga Floppy Disk Reader and Writer"); + setMinimumSize(690, 420); + resize(720, 440); + + auto *central = new QWidget; + auto *root = new QVBoxLayout(central); + auto *header = new QHBoxLayout; + auto *connection = new QGroupBox("DrawBridge (Arduino) Connection"); + auto *grid = new QGridLayout(connection); + m_port = new QComboBox; + m_refresh = new QPushButton("Refresh"); + m_density = new QComboBox; + m_density->addItems({"Automatic", "DD", "HD"}); + auto *diagnostics = new QPushButton("&Diagnostics..."); + auto *configure = new QPushButton("DrawBridge &Config..."); + auto *clean = new QPushButton("&Clean Drive..."); + configure->setMinimumWidth(150); + clean->setMinimumWidth(120); + diagnostics->setMinimumWidth(120); + grid->addWidget(new QLabel("Port:"), 0, 0); + grid->addWidget(m_port, 0, 1, 1, 3); + grid->addWidget(m_refresh, 0, 4); + grid->addWidget(new QLabel("Disk Type:"), 1, 0); + grid->addWidget(m_density, 1, 1); + grid->addWidget(configure, 1, 2); + grid->addWidget(clean, 1, 3); + grid->addWidget(diagnostics, 1, 4); + header->addWidget(connection, 1); + auto *logo = new QLabel; + logo->setPixmap(QPixmap(":/drawbridge/floppy.bmp")); + logo->setAlignment(Qt::AlignCenter); + header->addWidget(logo); + root->addLayout(header); + + m_tabs = new QTabWidget; + m_tabs->addTab(buildReadPage(), "Read Disk"); + m_tabs->addTab(buildWritePage(), "Write Disk"); + root->addWidget(m_tabs, 1); + + auto *footer = new QLabel( + "DrawBridge by Robert Smith — " + "amiga.robsmithdev.co.uk"); + footer->setOpenExternalLinks(true); + footer->setAlignment(Qt::AlignCenter); + root->addWidget(footer); + setCentralWidget(central); + + auto *about = menuBar()->addMenu("&Help")->addAction("&About DrawBridge"); + connect(about, &QAction::triggered, this, &MainWindow::showAbout); + connect(m_refresh, &QPushButton::clicked, this, &MainWindow::refreshPorts); + connect(diagnostics, &QPushButton::clicked, this, &MainWindow::runDiagnostics); + connect(configure, &QPushButton::clicked, this, &MainWindow::configureDevice); + connect(clean, &QPushButton::clicked, this, &MainWindow::cleanDrive); +} + +QWidget *MainWindow::buildReadPage() +{ + auto *page = new QWidget; + auto *layout = new QVBoxLayout(page); + auto *top = new QGridLayout; + m_readFormat = new QComboBox; + m_readFormat->addItems({"ADF (Amiga)", "IMA (IBM PC)", "ST (Atari ST)", + "SCP (Any/Flux)"}); + m_readFile = new QLineEdit; + auto *browse = new QPushButton("..."); + auto *start = new QPushButton("Start..."); + top->addWidget(new QLabel("Save to:"), 0, 0); + top->addWidget(m_readFormat, 0, 1); + top->addWidget(new QLabel("Tracks:"), 0, 2); + m_tracks = new QButtonGroup(this); + auto *trackRow = new QHBoxLayout; + for (const auto &[id, text] : std::vector>{ + {80, "0–79"}, {82, "0–81"}, {84, "0–83"}}) { + auto *radio = new QRadioButton(text); + m_tracks->addButton(radio, id); + trackRow->addWidget(radio); + if (id == 80) radio->setChecked(true); + } + top->addLayout(trackRow, 0, 3, 1, 2); + top->addWidget(new QLabel("File name:"), 1, 0); + top->addWidget(m_readFile, 1, 1, 1, 3); + top->addWidget(browse, 1, 4); + layout->addLayout(top); + m_scpNote = new QLabel( + "DD SCP files have limited flux resolution. HD SCP files contain MFM " + "levels without preservation-quality timing data. This is a hardware " + "limitation, not an SCP format limitation."); + m_scpNote->setWordWrap(true); + m_flux = new QCheckBox("Experimental Accurate Flux Read (not recommended)"); + layout->addWidget(m_scpNote); + layout->addWidget(m_flux); + auto *buttons = new QHBoxLayout; + buttons->addStretch(); + buttons->addWidget(start); + layout->addLayout(buttons); + connect(browse, &QPushButton::clicked, this, &MainWindow::browseReadFile); + connect(start, &QPushButton::clicked, this, &MainWindow::readDisk); + connect(m_readFormat, &QComboBox::currentIndexChanged, + this, &MainWindow::updateReadOptions); + return page; +} + +QWidget *MainWindow::buildWritePage() +{ + auto *page = new QWidget; + auto *layout = new QVBoxLayout(page); + auto *file = new QHBoxLayout; + m_writeFile = new QLineEdit; + auto *browse = new QPushButton("..."); + file->addWidget(new QLabel("Disk Image File:")); + file->addWidget(m_writeFile, 1); + file->addWidget(browse); + layout->addLayout(file); + m_verify = new QCheckBox("Verify Write (ADF/IMA/ST only)"); + m_precomp = new QCheckBox( + "Use Write Precompensation (ADF/IMA/ST, DD only)"); + m_erase = new QCheckBox( + "Perform several track erases before write (helps with some old disks)"); + m_index = new QCheckBox( + "Align tracks to index (rather than random like the Amiga) — slower"); + layout->addWidget(m_verify); + layout->addWidget(m_precomp); + layout->addWidget(m_erase); + layout->addWidget(m_index); + auto *buttons = new QHBoxLayout; + auto *start = new QPushButton("Start..."); + buttons->addStretch(); + buttons->addWidget(start); + layout->addLayout(buttons); + connect(browse, &QPushButton::clicked, this, &MainWindow::browseWriteFile); + connect(start, &QPushButton::clicked, this, &MainWindow::writeDisk); + connect(m_writeFile, &QLineEdit::textChanged, + this, &MainWindow::updateWriteOptions); + return page; +} + +void MainWindow::restoreSettings() +{ + QSettings settings; + m_density->setCurrentIndex(settings.value("density", 0).toInt()); + m_tabs->setCurrentIndex(settings.value("mode", 0).toInt()); + m_readFormat->setCurrentIndex(settings.value("read/format", 0).toInt()); + if (auto *button = m_tracks->button(settings.value("read/tracks", 80).toInt())) + button->setChecked(true); + m_flux->setChecked(settings.value("read/accurateFlux", false).toBool()); + m_verify->setChecked(settings.value("write/verify", true).toBool()); + m_precomp->setChecked(settings.value("write/precomp", true).toBool()); + m_erase->setChecked(settings.value("write/erase", false).toBool()); + m_index->setChecked(settings.value("write/index", true).toBool()); + updateReadOptions(); + updateWriteOptions(); +} + +void MainWindow::saveSettings() +{ + QSettings settings; + settings.setValue("port", selectedPort()); + settings.setValue("density", m_density->currentIndex()); + settings.setValue("mode", m_tabs->currentIndex()); + settings.setValue("read/format", m_readFormat->currentIndex()); + settings.setValue("read/tracks", m_tracks->checkedId()); + settings.setValue("read/accurateFlux", m_flux->isChecked()); + settings.setValue("write/verify", m_verify->isChecked()); + settings.setValue("write/precomp", m_precomp->isChecked()); + settings.setValue("write/erase", m_erase->isChecked()); + settings.setValue("write/index", m_index->isChecked()); +} + +void MainWindow::refreshPorts() +{ + const QString previous = selectedPort().isEmpty() + ? QSettings().value("port").toString() : selectedPort(); + std::vector ports; + ArduinoInterface::enumeratePorts(ports); + QStringList names; + for (const auto &port : ports) names << QString::fromStdWString(port); + names.removeDuplicates(); + names.sort(Qt::CaseInsensitive); + QStringList existing; + for (int i = 0; i < m_port->count(); ++i) existing << m_port->itemText(i); + if (names == existing) return; + m_port->blockSignals(true); + m_port->clear(); + m_port->addItems(names); + const int selected = names.indexOf(previous); + if (selected >= 0) m_port->setCurrentIndex(selected); + m_port->blockSignals(false); +} + +QString MainWindow::selectedPort() const +{ + return m_port->currentText(); +} + +bool MainWindow::requirePort() +{ + if (!selectedPort().isEmpty()) return true; + QMessageBox::warning(this, "DrawBridge", "No compatible serial port was found."); + return false; +} + +void MainWindow::updateReadOptions() +{ + const bool scp = m_readFormat->currentIndex() == 3; + m_scpNote->setVisible(scp); + m_flux->setVisible(scp); + const QStringList extensions{"adf", "ima", "st", "scp"}; + const QString current = m_readFile->text(); + if (!current.isEmpty() && current.contains('.')) { + QString replacement = current.left(current.lastIndexOf('.') + 1); + replacement += extensions.at(m_readFormat->currentIndex()); + m_readFile->setText(replacement); + } +} + +MainWindow::ImageType MainWindow::writeImageType() const +{ + const QString suffix = m_writeFile->text().section('.', -1).toLower(); + if (suffix == "scp") return ImageType::Scp; + if (suffix == "ipf") return ImageType::Ipf; + if (suffix == "st") return ImageType::St; + if (suffix == "ima" || suffix == "img") return ImageType::Ibm; + return ImageType::Adf; +} + +void MainWindow::updateWriteOptions() +{ + const ImageType type = writeImageType(); + const bool flux = type == ImageType::Scp || type == ImageType::Ipf; + m_verify->setEnabled(!flux); + m_precomp->setEnabled(!flux); + m_index->setEnabled(type == ImageType::Adf); +} + +void MainWindow::browseReadFile() +{ + const QStringList filters{ + "Amiga Disk Files (*.adf)", "IBM PC Disk Files (*.ima)", + "Atari ST Disk Files (*.st)", "SuperCard Pro Files (*.scp)"}; + const QString file = QFileDialog::getSaveFileName( + this, "Save Disk Image", m_readFile->text(), + filters.at(m_readFormat->currentIndex())); + if (!file.isEmpty()) m_readFile->setText(file); +} + +void MainWindow::browseWriteFile() +{ + const QString file = QFileDialog::getOpenFileName( + this, "Select a Disk Image File", m_writeFile->text(), + "Supported Images (*.adf *.scp *.ipf *.ima *.img *.st);;" + "Amiga Disk Files (*.adf *.ipf);;IBM PC Disk Files (*.ima *.img);;" + "Atari ST Disk Files (*.st);;SuperCard Pro Files (*.scp);;All Files (*)"); + if (!file.isEmpty()) m_writeFile->setText(file); +} + +void MainWindow::readDisk() +{ + if (!requirePort()) return; + if (m_readFile->text().isEmpty()) { + QMessageBox::warning(this, "Read Disk", "Select an output file first."); + return; + } + saveSettings(); + const std::wstring port = wstr(selectedPort()); + const std::wstring file = wstr(m_readFile->text()); + const int format = m_readFormat->currentIndex(); + const int tracks = m_tracks->checkedId(); + const int density = m_density->currentIndex(); + const bool accurateFlux = m_flux->isChecked(); + OperationDialog dialog("Reading Disk", this); + dialog.start([=](OperationDialog &ui) { + ADFWriter writer; + ui.setStatus("Opening serial port and setting up device..."); + if (!writer.openDevice(port)) { + ui.showError("Serial Port Error", qstr(writer.getLastError())); + return; + } + bool hd = density == 2; + const FirmwareVersion version = writer.getFirwareVersion(); + if (density == 0 && ((version.major > 1) || version.minor >= 9)) { + ui.setStatus("Checking disk density..."); + if (writer.GuessDiskDensity(hd) != ADFResult::adfrComplete) { + ui.showError("Disk Density", "Unable to determine the disk density."); + writer.closeDevice(); + return; + } + } + ui.setRange(tracks * 2); + auto callback = [&ui](int track, DiskSurface side, int retries, + int good, int bad, int total, + CallbackOperation operation) { + if (ui.aborted()) return WriteResponse::wrAbort; + ui.setOperation(operation); + ui.setTrack(track, side, good, bad, total); + ui.setProgress(track * 2 + (side == DiskSurface::dsUpper ? 1 : 0)); + if (retries > 20) { + const int answer = ui.askRetry("Disk Read Errors", + "The disk has checksum errors or missing data."); + if (answer == QMessageBox::Abort) return WriteResponse::wrAbort; + if (answer == QMessageBox::Retry) return WriteResponse::wrRetry; + return WriteResponse::wrSkipBadChecksums; + } + return WriteResponse::wrContinue; + }; + ADFResult result = ADFResult::adfrFileError; + if (format == 0) result = writer.DiskToADF(file, hd, tracks, callback); + else if (format == 3) + result = writer.DiskToSCP(file, hd, tracks, 3, callback, accurateFlux); + else result = writer.diskToIBMST(file, hd, callback); + const std::string detail = writer.getLastError(); + writer.closeDevice(); + if (result == ADFResult::adfrComplete) + ui.showCompletion(format == 3 ? ":/drawbridge/scp.bmp" + : ":/drawbridge/fin.bmp", + "The disk image was read successfully."); + else if (result == ADFResult::adfrCompletedWithErrors) + ui.showCompletion(":/drawbridge/finpartial.bmp", + "The image was saved, but some sectors contain errors."); + else if (result != ADFResult::adfrAborted) + ui.showError("Read Failed", resultMessage(result, detail)); + }); + dialog.exec(); +} + +void MainWindow::writeDisk() +{ + if (!requirePort()) return; + if (m_writeFile->text().isEmpty()) { + QMessageBox::warning(this, "Write Disk", "Select an image file first."); + return; + } + saveSettings(); + const std::wstring port = wstr(selectedPort()); + const std::wstring file = wstr(m_writeFile->text()); + const ImageType type = writeImageType(); + const int density = m_density->currentIndex(); + const bool verify = m_verify->isChecked(); + const bool precomp = m_precomp->isChecked(); + const bool erase = m_erase->isChecked(); + const bool index = m_index->isChecked(); + OperationDialog dialog("Writing Disk", this); + dialog.start([=](OperationDialog &ui) { + ADFWriter writer; + ui.setStatus("Opening serial port and setting up device..."); + if (!writer.openDevice(port)) { + ui.showError("Serial Port Error", qstr(writer.getLastError())); + return; + } + bool hd = density == 2; + const bool flux = type == ImageType::Scp || type == ImageType::Ipf; + if (flux && density == 2) { + ui.showError("Disk Density", "SCP and IPF writing supports DD media only."); + writer.closeDevice(); + return; + } + const FirmwareVersion version = writer.getFirwareVersion(); + if (!flux && density == 0 && ((version.major > 1) || version.minor >= 9)) { + ui.setStatus("Checking disk density..."); + if (writer.GuessDiskDensity(hd) != ADFResult::adfrComplete) { + ui.showError("Disk Density", "Unable to determine the disk density."); + writer.closeDevice(); + return; + } + } + ui.setRange(160); + auto callback = [&ui](int track, DiskSurface side, bool verifyError, + CallbackOperation operation) { + if (ui.aborted()) return WriteResponse::wrAbort; + ui.setOperation(operation); + ui.setTrack(track, side); + ui.setProgress(track * 2 + (side == DiskSurface::dsUpper ? 1 : 0)); + if (verifyError) { + const int answer = ui.askRetry("Disk Write Error", + "Verify error writing track."); + if (answer == QMessageBox::Abort) return WriteResponse::wrAbort; + if (answer == QMessageBox::Retry) return WriteResponse::wrRetry; + return WriteResponse::wrSkipBadChecksums; + } + return WriteResponse::wrContinue; + }; + ADFResult result; + switch (type) { + case ImageType::Ipf: result = writer.IPFToDisk(file, erase, callback); break; + case ImageType::Scp: result = writer.SCPToDisk(file, erase, callback); break; + case ImageType::Adf: + result = writer.ADFToDisk(file, hd, verify, precomp, erase, index, callback); + break; + case ImageType::Ibm: + case ImageType::St: + result = writer.sectorFileToDisk(file, hd, verify, precomp, erase, + type == ImageType::St, callback); + break; + } + const std::string detail = writer.getLastError(); + writer.closeDevice(); + if (result == ADFResult::adfrComplete) + ui.showCompletion(verify ? ":/drawbridge/fin2.bmp" + : ":/drawbridge/dialog_o.bmp", + verify ? "The disk was written and verified successfully." + : "The disk was written successfully without verification."); + else if (result == ADFResult::adfrCompletedWithErrors) + ui.showCompletion(":/drawbridge/finpartial2.bmp", + "Writing completed with verification errors."); + else if (result != ADFResult::adfrAborted) + ui.showError("Write Failed", resultMessage(result, detail)); + }); + dialog.exec(); +} + +void MainWindow::runDiagnostics() +{ + if (!requirePort()) return; + QDialog dialog(this); + dialog.setWindowTitle("Drive and Arduino Diagnostics"); + dialog.setWindowFlag(Qt::WindowCloseButtonHint, false); + dialog.resize(700, 430); + auto *output = new QTextEdit; + output->setReadOnly(true); + auto *close = new QPushButton("Close"); + close->setEnabled(false); + auto *layout = new QVBoxLayout(&dialog); + layout->addWidget(output); + layout->addWidget(close, 0, Qt::AlignRight); + connect(close, &QPushButton::clicked, &dialog, &QDialog::accept); + const std::wstring port = wstr(selectedPort()); + std::thread worker([&] { + ADFWriter writer; + writer.runDiagnostics(port, + [&](bool error, const std::string message) { + const QString line = (error ? "DIAGNOSTICS FAILED: " : "") + qstr(message); + QMetaObject::invokeMethod(output, [=] { output->append(line); }); + }, + [&](bool question, const std::string message) { + bool answer = false; + QMetaObject::invokeMethod(&dialog, [&] { + answer = question + ? QMessageBox::question(&dialog, "Diagnostics Question", qstr(message)) + == QMessageBox::Yes + : QMessageBox::information(&dialog, "Diagnostics Prompt", qstr(message), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok; + }, Qt::BlockingQueuedConnection); + return answer; + }); + writer.closeDevice(); + QMetaObject::invokeMethod(close, [=] { close->setEnabled(true); }); + }); + dialog.exec(); + if (worker.joinable()) worker.join(); +} + +void MainWindow::configureDevice() +{ + if (!requirePort()) return; + QApplication::setOverrideCursor(Qt::WaitCursor); + ArduinoInterface interface; + const DiagnosticResponse response = interface.openPort(wstr(selectedPort())); + QApplication::restoreOverrideCursor(); + if (response != DiagnosticResponse::drOK) { + QMessageBox::critical(this, "DrawBridge Config", interface.getLastErrorStr().c_str()); + return; + } + const FirmwareVersion version = interface.getFirwareVersion(); + if (version.major == 1 && version.minor < 9) { + QMessageBox::warning(this, "DrawBridge Config", + "This feature requires firmware version 1.9 or newer."); + return; + } + bool advanced, plus, dd, slow, aligned; + bool error = interface.eeprom_IsAdvancedController(advanced) != DiagnosticResponse::drOK; + error |= interface.eeprom_IsDrawbridgePlusMode(plus) != DiagnosticResponse::drOK; + error |= interface.eeprom_IsDensityDetectDisabled(dd) != DiagnosticResponse::drOK; + error |= interface.eeprom_IsSlowSeekMode(slow) != DiagnosticResponse::drOK; + error |= interface.eeprom_IsIndexAlignMode(aligned) != DiagnosticResponse::drOK; + if (error) { + QMessageBox::critical(this, "DrawBridge Config", "Unable to read EEPROM settings."); + return; + } + QDialog dialog(this); + dialog.setWindowTitle("DrawBridge EEPROM Config"); + auto *layout = new QVBoxLayout(&dialog); + layout->addWidget(new QLabel("The following settings are saved to your DrawBridge board:")); + auto add = [&](const QString &text, bool checked) { + auto *box = new QCheckBox(text); + box->setChecked(checked); + layout->addWidget(box); + return box; + }; + auto *advancedBox = add("Force DiskChange detection support", advanced); + auto *plusBox = add("Set DrawBridge Plus mode", plus); + auto *ddBox = add("Force all disks to be detected as double density", dd); + auto *slowBox = add("Use slower disk seeking", slow); + auto *alignedBox = add("Always index-align all track writes", aligned); + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel); + layout->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + if (dialog.exec() == QDialog::Accepted) { + QApplication::setOverrideCursor(Qt::WaitCursor); + error = interface.eeprom_SetAdvancedController(advancedBox->isChecked()) != DiagnosticResponse::drOK; + error |= interface.eeprom_SetDrawbridgePlusMode(plusBox->isChecked()) != DiagnosticResponse::drOK; + error |= interface.eeprom_SetDensityDetectDisabled(ddBox->isChecked()) != DiagnosticResponse::drOK; + error |= interface.eeprom_SetSlowSeekMode(slowBox->isChecked()) != DiagnosticResponse::drOK; + error |= interface.eeprom_SetIndexAlignMode(alignedBox->isChecked()) != DiagnosticResponse::drOK; + QApplication::restoreOverrideCursor(); + if (error) QMessageBox::critical(this, "DrawBridge Config", "Unable to save EEPROM settings."); + } + interface.closePort(); +} + +void MainWindow::cleanDrive() +{ + if (!requirePort()) return; + const auto answer = QMessageBox::question(this, "Floppy Drive Cleaner", + "Insert a cleaning disk, add cleaning solution, and select Yes to start.\n\n" + "Do not use an ordinary floppy disk for this operation."); + if (answer != QMessageBox::Yes) return; + const std::wstring port = wstr(selectedPort()); + OperationDialog dialog("Floppy Drive Cleaner", this); + dialog.start([=](OperationDialog &ui) { + ADFWriter writer; + if (!writer.openDevice(port)) { + ui.showError("Drive Cleaner", qstr(writer.getLastError())); + return; + } + const ADFResult result = writer.runClean([&](uint16_t position, uint16_t maximum) { + if (position == 0) ui.setRange(maximum); + ui.setProgress(position); + return !ui.aborted(); + }); + writer.closeDevice(); + if (result == ADFResult::adfrComplete) + ui.showInformation("Drive Cleaner", "Cleaning cycle completed."); + else if (result == ADFResult::adfrCompletedWithErrors) + ui.showError("Drive Cleaner", "No cleaning disk was detected."); + else if (result != ADFResult::adfrAborted) + ui.showError("Drive Cleaner", "The cleaning cycle failed."); + }); + dialog.exec(); +} + +void MainWindow::showAbout() +{ + QMessageBox box(this); + box.setWindowTitle("About DrawBridge"); + box.setIconPixmap(QPixmap(":/drawbridge/floppy.bmp")); + box.setTextFormat(Qt::RichText); + box.setText("DrawBridge
Arduino Amiga Floppy Disk Reader and Writer
" + "Version 2.8.8 Qt

Copyright © 2017–2024 Robert Smith
" + "Project website · " + "Discord"); + box.exec(); +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + saveSettings(); + QMainWindow::closeEvent(event); +} diff --git a/ArduinoFloppyReader/ArduinoFloppyReaderQt/MainWindow.h b/ArduinoFloppyReader/ArduinoFloppyReaderQt/MainWindow.h new file mode 100644 index 0000000..7b71069 --- /dev/null +++ b/ArduinoFloppyReader/ArduinoFloppyReaderQt/MainWindow.h @@ -0,0 +1,112 @@ +#pragma once + +#include +#include + +#include +#include +#include + +class QButtonGroup; +class QCheckBox; +class QComboBox; +class QLabel; +class QLineEdit; +class QProgressBar; +class QPushButton; +class QTabWidget; +class QTextEdit; + +namespace ArduinoFloppyReader { +enum class CallbackOperation; +enum class DiskSurface; +} + +class OperationDialog final : public QDialog +{ + Q_OBJECT +public: + explicit OperationDialog(const QString &title, QWidget *parent = nullptr); + ~OperationDialog() override; + + void start(std::function operation); + bool aborted() const { return m_aborted.load(); } + void setRange(int maximum); + void setProgress(int value); + void setStatus(const QString &status); + void setOperation(ArduinoFloppyReader::CallbackOperation operation); + void setTrack(int track, ArduinoFloppyReader::DiskSurface surface, + int good = -1, int bad = -1, int total = -1); + int askRetry(const QString &title, const QString &message); + void showError(const QString &title, const QString &message); + void showInformation(const QString &title, const QString &message); + void showCompletion(const QString &asset, const QString &message); + +protected: + void reject() override; + +private: + void invoke(const std::function &function, bool blocking = false); + + QLabel *m_status; + QLabel *m_operation; + QLabel *m_cylinder; + QLabel *m_side; + QLabel *m_good; + QLabel *m_bad; + QProgressBar *m_progress; + QPushButton *m_abort; + std::atomic_bool m_aborted{false}; + std::thread m_thread; + bool m_finished = false; +}; + +class MainWindow final : public QMainWindow +{ + Q_OBJECT +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +protected: + void closeEvent(QCloseEvent *event) override; + +private: + enum class ImageType { Adf, Ibm, St, Scp, Ipf }; + + void buildInterface(); + QWidget *buildReadPage(); + QWidget *buildWritePage(); + void restoreSettings(); + void saveSettings(); + void refreshPorts(); + QString selectedPort() const; + bool requirePort(); + void updateReadOptions(); + void updateWriteOptions(); + ImageType writeImageType() const; + void browseReadFile(); + void browseWriteFile(); + void readDisk(); + void writeDisk(); + void runDiagnostics(); + void configureDevice(); + void cleanDrive(); + void showAbout(); + QComboBox *m_port; + QComboBox *m_density; + QPushButton *m_refresh; + QTabWidget *m_tabs; + + QComboBox *m_readFormat; + QLineEdit *m_readFile; + QButtonGroup *m_tracks; + QCheckBox *m_flux; + QLabel *m_scpNote; + + QLineEdit *m_writeFile; + QCheckBox *m_verify; + QCheckBox *m_precomp; + QCheckBox *m_erase; + QCheckBox *m_index; +}; diff --git a/ArduinoFloppyReader/ArduinoFloppyReaderQt/README.md b/ArduinoFloppyReader/ArduinoFloppyReaderQt/README.md new file mode 100644 index 0000000..04b939c --- /dev/null +++ b/ArduinoFloppyReader/ArduinoFloppyReaderQt/README.md @@ -0,0 +1,30 @@ +# DrawBridge Qt + +This is the Qt 6 desktop frontend for DrawBridge. It provides the same disk +reading, writing, diagnostics, cleaning, and EEPROM configuration workflows as +the Windows MFC application on Linux and macOS. + +## Requirements + +- A C++17 compiler +- CMake 3.21 or newer +- Qt 6.5 or newer with the Core, Gui, and Widgets modules +- On Linux, permission to access the selected serial device (commonly through + membership in the `dialout` group) +- The optional CAPSImg library at runtime for IPF writing + +## Build + +From the `ArduinoFloppyReader` directory: + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +``` + +The executable is created below `build/ArduinoFloppyReaderQt`. On macOS it is +created as `DrawBridge.app`. + +The application uses the existing platform-specific `SerialIO` implementation +rather than Qt Serial Port because DrawBridge needs custom two-megabaud and +flow-control handling. diff --git a/ArduinoFloppyReader/ArduinoFloppyReaderQt/main.cpp b/ArduinoFloppyReader/ArduinoFloppyReaderQt/main.cpp new file mode 100644 index 0000000..f549f45 --- /dev/null +++ b/ArduinoFloppyReader/ArduinoFloppyReaderQt/main.cpp @@ -0,0 +1,18 @@ +#include "MainWindow.h" + +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + QCoreApplication::setOrganizationName("RobSmithDev"); + QCoreApplication::setOrganizationDomain("robsmithdev.co.uk"); + QCoreApplication::setApplicationName("DrawBridge"); + QCoreApplication::setApplicationVersion("2.8.8"); + app.setWindowIcon(QIcon(":/drawbridge/app.ico")); + + MainWindow window; + window.show(); + return app.exec(); +} diff --git a/ArduinoFloppyReader/ArduinoFloppyReaderQt/resources.qrc b/ArduinoFloppyReader/ArduinoFloppyReaderQt/resources.qrc new file mode 100644 index 0000000..744472d --- /dev/null +++ b/ArduinoFloppyReader/ArduinoFloppyReaderQt/resources.qrc @@ -0,0 +1,12 @@ + + + ../ArduinoFloppyReaderWin/res/floppy.bmp + ../ArduinoFloppyReaderWin/res/fin.bmp + ../ArduinoFloppyReaderWin/res/fin2.bmp + ../ArduinoFloppyReaderWin/res/finpartial.bmp + ../ArduinoFloppyReaderWin/res/finpartial2.bmp + ../ArduinoFloppyReaderWin/res/scp.bmp + ../ArduinoFloppyReaderWin/res/dialog_o.bmp + ../ArduinoFloppyReaderWin/res/ArduinoFloppyReaderWin.ico + + diff --git a/ArduinoFloppyReader/CMakeLists.txt b/ArduinoFloppyReader/CMakeLists.txt new file mode 100644 index 0000000..3f75112 --- /dev/null +++ b/ArduinoFloppyReader/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.21) + +project(DrawBridge LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui Widgets) +find_package(Threads REQUIRED) + +add_library(drawbridge-core STATIC + lib/ADFWriter.cpp + lib/ArduinoInterface.cpp + lib/RotationExtractor.cpp + lib/SerialIO.cpp + lib/ftdi.cpp + lib/ibm_sectors.cpp + lib/pll.cpp + lib/capsapi/CapsPlug.cpp +) +target_include_directories(drawbridge-core PUBLIC lib) +target_link_libraries(drawbridge-core PUBLIC ${CMAKE_DL_LIBS} Threads::Threads) + +add_subdirectory(ArduinoFloppyReaderQt)