Program Listing for File devicecontrolwidget.cpp

Return to documentation for file (src/widget/devicecontrolwidget.cpp)

/*
 * DeviceControlWidget - Widget for managing USB/serial devices.
 */

#include <QList>
#include <QMessageBox>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QSettings>
#include <QStandardItem>
#include <QStringList>
#include <QTabWidget>
#include <QWidget>
#include <thread>
#include "dialog/adddevicedialog.h"
#include "dialog/cueinterpreterdialog.h"
#include "dialog/preferencesdialog.h"
#include "dialog/sectionmapdialog.h"
#include "devicecontrolwidget.h"
#include "ui_devicecontrolwidget.h"
#include "controller/devicecontroller.h"
#include "controller/devicethreadcontroller.h"

namespace PixelMaestroStudio {
    DeviceControlWidget::DeviceControlWidget(QWidget *parent) :
            QWidget(parent),
            ui(new Ui::DeviceControlWidget),
            maestro_control_widget_(*dynamic_cast<MaestroControlWidget*>(parent)) {
        ui->setupUi(this);

        // Add saved serial devices to device list.
        QSettings settings;
        int num_serial_devices = settings.beginReadArray(PreferencesDialog::devices);
        for (int device = 0; device < num_serial_devices; device++) {
            settings.setArrayIndex(device);

            QString device_name = settings.value(PreferencesDialog::device_port).toString();
            serial_devices_.push_back(DeviceController(device_name));

            // If the device is set to auto-connect, try connecting
            DeviceController& serial_device = serial_devices_.last();
            if (serial_device.get_autoconnect()) {
                serial_device.connect();
            }

        }
        settings.endArray();

        refresh_device_list();
    }

    QByteArray* DeviceControlWidget::get_maestro_cue() {
        return &maestro_cue_;
    }

    void DeviceControlWidget::on_addDeviceButton_clicked() {
        AddDeviceDialog dialog(&serial_devices_, nullptr, this);
        dialog.exec();

        refresh_device_list();
    }

    void DeviceControlWidget::on_editDeviceButton_clicked() {
        DeviceController* device = nullptr;
        int selected_device = ui->serialOutputListWidget->currentRow();
        if (selected_device >= 0) {
            device = &serial_devices_[selected_device];
        }

        AddDeviceDialog dialog(&serial_devices_, device, this);
        dialog.exec();

        refresh_device_list();
    }

    void DeviceControlWidget::on_connectPushButton_clicked() {
        int selected = ui->serialOutputListWidget->currentRow();
        if (selected < 0) return;

        DeviceController device = serial_devices_.at(selected);
        if (!device.get_open()) {
            if (device.connect()) {
                refresh_device_list();
            }
            else {
                QString error = "Unable to connect to device at " + device.get_port_name();
                if (device.get_device()) error += ": " + device.get_device()->errorString();

                QMessageBox::warning(this, "Unable to Connect", error);
            }
        }
        else {
            QMessageBox::information(this, "Device Already Connected", "This device is already connected.");
        }
    }


    void DeviceControlWidget::on_previewButton_clicked() {
        CueInterpreterDialog dialog(this,
                                    reinterpret_cast<uint8_t*>(maestro_cue_.data()),
                                    static_cast<uint32_t>(maestro_cue_.size()));
        dialog.exec();
    }

    void DeviceControlWidget::on_disconnectPushButton_clicked() {
        int selected_index = ui->serialOutputListWidget->currentRow();
        if (selected_index < 0) return;

        DeviceController device = serial_devices_.at(selected_index);

        if (device.disconnect()) {
            refresh_device_list();
        }
        else {
            QMessageBox::warning(this, "Unable to Disconnect", QString("Unable to disconnect device on port " + device.get_port_name() + ": " + device.get_device()->errorString()));
        }
    }

    void DeviceControlWidget::on_removeDeviceButton_clicked() {
        int selected = ui->serialOutputListWidget->currentRow();
        if (selected < 0) return;

        QMessageBox::StandardButton confirm;
        confirm = QMessageBox::question(this, "Remove Device", "Are you sure you want to remove this device from your saved devices?", QMessageBox::Yes | QMessageBox::No);
        if (confirm == QMessageBox::Yes) {
            serial_devices_.removeAt(selected);
            refresh_device_list();
        }
    }

    void DeviceControlWidget::on_uploadButton_clicked() {
        // "ROMEND" flags to the Arduino that we're done transmitting the Cuefile
        QByteArray out = maestro_cue_.append("ROMEND");
        write_to_device(serial_devices_[ui->serialOutputListWidget->currentRow()],
                static_cast<const char*>(maestro_cue_),
                maestro_cue_.size(),
                true);
    }

    void DeviceControlWidget::on_serialOutputListWidget_currentRowChanged(int currentRow) {
        if (currentRow < 0) return;

        DeviceController device = serial_devices_.at(currentRow);

        bool connected = device.get_open();

        ui->connectPushButton->setEnabled(!connected);
        ui->disconnectPushButton->setEnabled(connected);
        ui->uploadButton->setEnabled(connected);
        ui->uploadProgressBar->setValue(0);

        ui->editDeviceButton->setEnabled(currentRow >= 0);
        ui->removeDeviceButton->setEnabled(currentRow >= 0);
    }

    void DeviceControlWidget::refresh_device_list() {
        ui->serialOutputListWidget->clear();
        bool connected_devices = false;
        for (DeviceController device : serial_devices_) {
            QListWidgetItem* item = new QListWidgetItem(device.get_port_name());
            if (device.get_open()) {
                item->setTextColor(Qt::white);
                connected_devices = true;
            }
            else {
                item->setTextColor(Qt::gray);
            }
            ui->serialOutputListWidget->addItem(item);
        }

        // Display icon in tab
        QTabWidget* tab_widget = maestro_control_widget_.findChild<QTabWidget*>("tabWidget");
        QWidget* tab = tab_widget->findChild<QWidget*>("deviceTab");
        if (connected_devices) {
            tab_widget->setTabIcon(tab_widget->indexOf(tab), QIcon(":/icon_connected.png"));
        }
        else {
            tab_widget->setTabIcon(tab_widget->indexOf(tab), QIcon());
        }

        int selected = ui->serialOutputListWidget->currentRow();
        if (selected >= 0) {
            ui->uploadButton->setEnabled(serial_devices_[selected].get_open());
            ui->serialOutputListWidget->setCurrentRow(selected);
        }
        else {
            ui->uploadButton->setEnabled(false);
            ui->connectPushButton->setEnabled(false);
            ui->disconnectPushButton->setEnabled(false);
        }

        ui->editDeviceButton->setEnabled(selected >= 0);
        ui->removeDeviceButton->setEnabled(selected >= 0);
    }

    void DeviceControlWidget::run_cue(uint8_t *cue, int size) {
        CueController* controller = &this->maestro_control_widget_.get_maestro_controller()->get_maestro().get_cue_controller();

        for (DeviceController device : serial_devices_) {
            // TODO: Move to separate thread

            // Copy the Cue for each device
            QByteArray out = QByteArray(reinterpret_cast<const char*>(cue), size);

            /*
             * If the device has a Section map saved, apply it to the Cue.
             * This only applies to real-time updates, not Cuefiles.
             */
            SectionMapModel* model = device.section_map_model;
            if (model != nullptr && device.get_real_time_refresh_enabled()) {

                // Only check Section-based Cues
                if (!(out[(uint8_t)CueController::Byte::PayloadByte] == static_cast<char>(CueController::Handler::MaestroCueHandler) ||
                    out[(uint8_t)CueController::Byte::PayloadByte] == static_cast<char>(CueController::Handler::ShowCueHandler))) {

                    /*
                     * Grab the local section ID from the buffer.
                     * Then, grab the remote section ID from the map.
                     * If they're different, replace the local with the remote ID in the buffer.
                     */

                    // SectionByte is the same location for all Section-related handlers (as of v0.30)
                    uint8_t local_section_id = out[(uint8_t)SectionCueHandler::Byte::SectionByte];

                    // Make sure the cell actually exists in the model before swapping.
                    QStandardItem* cell = model->item(local_section_id, 1);
                    if (cell && !cell->text().isEmpty()) {
                        // If the remote section number is negative, exit immediately
                        int remote_section_id = cell->text().toInt();
                        if (remote_section_id < 0) return;
                        if (remote_section_id != local_section_id) {
                            // We have a match. Swap the values and reassmble the Cue.
                            out[(uint8_t)SectionCueHandler::Byte::SectionByte] = remote_section_id;
                            out[(uint8_t)CueController::Byte::ChecksumByte] = controller->checksum(reinterpret_cast<uint8_t*>(out.data()), out.size());
                        }
                    }
                }
            }

            if (device.get_open() && device.get_real_time_refresh_enabled()) {
                write_to_device(device,
                                out.data(),
                                out.size(),
                                false);
            }
        }

        update_cuefile_size();
    }

    void DeviceControlWidget::save_devices() {
        // Save devices
        QSettings settings;
        settings.remove(PreferencesDialog::devices);
        settings.beginWriteArray(PreferencesDialog::devices);
        for (int i = 0; i < ui->serialOutputListWidget->count(); i++) {
            settings.setArrayIndex(i);

            DeviceController* device = &serial_devices_[i];
            settings.setValue(PreferencesDialog::device_port, device->get_port_name());
            settings.setValue(PreferencesDialog::device_real_time_refresh, device->get_real_time_refresh_enabled());

            // Save the device's model
            SectionMapModel* model = device->section_map_model;
            if (model != nullptr) {
                settings.beginWriteArray(PreferencesDialog::section_map);

                QModelIndex parent = QModelIndex();
                for (int row = 0; row < model->rowCount(parent); row++) {
                    settings.setArrayIndex(row);

                    QStandardItem* local_section = model->item(row, 0);
                    settings.setValue(PreferencesDialog::section_map_local, local_section->text().toInt());

                    QStandardItem* remote_section = model->item(row, 1);
                    settings.setValue(PreferencesDialog::section_map_remote, remote_section->text().toInt());
                }
                settings.endArray();
            }
        }
        settings.endArray();
    }

    void DeviceControlWidget::set_progress_bar(int val) {
        ui->uploadProgressBar->setValue(val);
        // Disable upload button while sending data
        ui->uploadButton->setEnabled(!(val > 0 && val < 100));
    }

    void DeviceControlWidget::update_cuefile_size() {
        // Calculate and display the size of the current Maestro configuration
        QDataStream datastream(&maestro_cue_, QIODevice::Truncate);

        MaestroController* controller = maestro_control_widget_.get_maestro_controller();

        // Generate the Cuefile
        controller->save_maestro_to_datastream(datastream);

        ui->fileSizeLineEdit->setText(locale_.toString(maestro_cue_.size()));
    }

    void DeviceControlWidget::write_to_device(DeviceController& device, const char *out, const int size, bool progress) {
        DeviceThreadController* thread = new DeviceThreadController(device,
                                                          out,
                                                          size);

        connect(thread, &DeviceThreadController::finished, thread, &DeviceThreadController::deleteLater);

        if (progress) {
            connect(thread, &DeviceThreadController::progress_changed, this, &DeviceControlWidget::set_progress_bar);
        }

        /*
         * NOTE: Device pointer becomes invalid when using Thread::start(), likely due to invalid memory access.
         *      Need to make the DeviceThreadController and output thread-safe before sending it to a separate thread.
         *
         *      Maybe convert threads to QFuture and use QFutureWatcher to update progress? https://doc.qt.io/qt-5/qfuturewatcher.html#details
         *
         */

        //thread->start();
        thread->run();
    }

    DeviceControlWidget::~DeviceControlWidget() {
        for (DeviceController& device : serial_devices_) {
            device.disconnect();
        }
        delete ui;
    }
}