728x90
void clearOnlyItem(QTableWidget* table)
{
	table->setRowCount(0);
}

 

728x90
728x90

Windows / PCAN USB를 가지고 ISOTP 혹은 CAN-FD로 간단하게 채팅할 수 있는 프로그램을 제작해 보았습니다. PCAN API C++ 관련 자료가 많이 없어 이 API를 가지고 개발하시려는 분들께 도움이 됐으면 좋겠습니다.

 

 

GitHub - psy1064/PCAN-Chat

Contribute to psy1064/PCAN-Chat development by creating an account on GitHub.

github.com

 

728x90
728x90

QString의 toInt는 정수형태의 문자열을 정수로 변환시켜주는 함수입니다. 그런데 이 호출하는 함수에 맞지 않는 문자열 값을 가지고 있으면 제대로 처리가 되지 않습니다.

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QString sUInt   = "123";
    QString sInt    = "-123";
    QString sFloat  = "123.12";

    bool ok;

    qDebug() << sUInt.toUInt(&ok) << ok;
    qDebug() << sInt.toUInt(&ok) << ok;
    qDebug() << sFloat.toUInt(&ok) << ok;

    qDebug() << sUInt.toInt(&ok) << ok;
    qDebug() << sInt.toInt(&ok) << ok;
    qDebug() << sFloat.toInt(&ok) << ok;

    return a.exec();
}

/* 실행결과
123 true
0 false
0 false
123 true
-123 true
0 false
*/

위 실행결과를 보면 알 수 있듯이 해당 함수를 통해 얻고 싶은 자료형이 아닌 문자열이 호출되면 0이 반환되고 인자로 넘겨준 flag에는 false 가 나오게 됩니다.

 

이를 방지하기 위해서는 toInt(), toFloat가 아닌 atoi, atof를 사용하는 것이 좋습니다.

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QString sUInt   = "123";
    QString sInt    = "-123";
    QString sFloat  = "123.12";

    bool ok;

    qDebug() << sUInt.toUInt(&ok) << ok;
    qDebug() << sInt.toUInt(&ok) << ok;
    qDebug() << sFloat.toUInt(&ok) << ok;

    qDebug() << sUInt.toInt(&ok) << ok;
    qDebug() << sInt.toInt(&ok) << ok;
    qDebug() << sFloat.toInt(&ok) << ok;

    qDebug() << atoi(sUInt.toLocal8Bit().constData());
    qDebug() << atoi(sInt.toLocal8Bit().constData());
    qDebug() << atoi(sFloat.toLocal8Bit().constData());

    return a.exec();
}
/* 실행결과
123 true
0 false
0 false
123 true
-123 true
0 false
123
-123
123
*/

 

728x90
728x90

Qt에서는 기본적으로 Drag&Drop Action을 지원합니다. 아래의 코드를 사용하면 QTreeWidget에 기본적으로 구현된 내부 Drag & Drop Action이 실행됩니다.

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    ui->originTree->setSelectionMode(QAbstractItemView::SingleSelection);
    ui->originTree->setDragEnabled(true);
    ui->originTree->viewport()->setAcceptDrops(true);
    ui->originTree->setDropIndicatorShown(true);
}

 

기존에 제공하는 로직이 아닌 다른 동을 하기 위해서는 dropEvent를 overriding 처리를 통해 직접 구현할 수 있습니다.

void dragDropTree::dropEvent(QDropEvent *event)
{
    QByteArray encoded = event->mimeData()->data("application/x-qabstractitemmodeldatalist");
    // 드래그 해온 아이템들에 대한 정보
    QDataStream stream(&encoded, QIODevice::ReadOnly);

    while (!stream.atEnd())
    {
        int row, col;
        QMap<int,  QVariant> roleDataMap;
        QTreeWidgetItem* pCloneItem = new QTreeWidgetItem;

        stream >> row >> col >> roleDataMap;
        for ( auto [role, value] : roleDataMap.asKeyValueRange() ) {
            pCloneItem->setData(col, role, value);
        }

        QModelIndex index = indexAt(event->position().toPoint());
        // 복사할 위치
        auto pItem = itemAt(event->position().toPoint());

        auto dropIndicator = dropIndicatorPosition();       // 아이템이 삽입되는 위치

        switch ( dropIndicator ) {
        case OnItem:            // 아이템으로
            pItem->addChild(pCloneItem);
            break;
        case AboveItem:         // 아이템 상단으로
            if ( !pItem->parent()) {
                insertTopLevelItem(index.row(), pCloneItem);
            } else {
                auto parent = pItem->parent();
                parent->insertChild(index.row(), pCloneItem);
            }
            break;
        case BelowItem:         // 아이템 하단으로
            if ( !pItem->parent()) {
                insertTopLevelItem(index.row()+1, pCloneItem);
            } else {
                auto parent = pItem->parent();
                parent->insertChild(index.row()+1, pCloneItem);
            }
            break;
        case OnViewport:        // 그냥 트리 위젯 공간 안
            insertTopLevelItem(topLevelItemCount(), pCloneItem);
            break;
        }

    } // https://stackoverflow.com/questions/1723989/how-to-decode-application-x-qabstractitemmodeldatalist-in-qt-for-drag-and-drop
}
class dragDropTree : public QTreeWidget {
    Q_OBJECT

protected:
    void dropEvent(QDropEvent *event);
};

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;

    dragDropTree* pOrigin;
};

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    pOrigin = new dragDropTree;
    pOrigin->setObjectName("Origin");

    pOrigin->setSelectionMode(QAbstractItemView::SingleSelection);
    pOrigin->setDragEnabled(true);
    pOrigin->viewport()->setAcceptDrops(true);
    pOrigin->setDropIndicatorShown(true);

    auto item = new QTreeWidgetItem(pOrigin);
    item->setText(0, "text");

    item = new QTreeWidgetItem(pOrigin);
    item->setText(0, "text2");

    ui->horizontalLayout->addWidget(pOrigin);
}

MainWindow::~MainWindow()
{
    delete ui;
}
728x90
728x90

PCAN ISOTP API Write 호출 시 비동기 처리가 되기 때문에 loop back을 이용해서 제대로 모든 데이터가 처리가 되었는지 확인할 수 있습니다.

void PCANISOTP::Write(const char *pData, int nSize)
{
    cantp_msg message = {};
    cantp_can_msgtype msgtype = PCANTP_CAN_MSGTYPE_EXTENDED | PCANTP_CAN_MSGTYPE_FD | PCANTP_CAN_MSGTYPE_BRS;

    cantp_netaddrinfo isotp_nai = {};
    isotp_nai.source_addr = N_SA;
    isotp_nai.target_addr = N_TA_PHYS;
    isotp_nai.target_type = PCANTP_ISOTP_ADDRESSING_PHYSICAL;
    isotp_nai.msgtype = PCANTP_ISOTP_MSGTYPE_DIAGNOSTIC;
    isotp_nai.format = PCANTP_ISOTP_FORMAT_NORMAL;

    status = CANTP_MsgDataAlloc_2016(&message, PCANTP_MSGTYPE_ISOTP);
    if (CANTP_StatusIsOk_2016(status)) {
        // initialize ISOTP message
        status = CANTP_MsgDataInit_2016(&message, PCANTP_MAPPING_FLOW_CTRL_NONE, msgtype, nSize, pData, &isotp_nai);
        if (CANTP_StatusIsOk_2016(status)) {
            // write message
            do {
                status = CANTP_Write_2016(channel, &message);
                if (CANTP_StatusIsOk_2016(status)) {
                    debug(QString::asprintf("Successfully queued ISOTP message: Length %i (sts=0x%04X).", nSize, (int)status));
                }
                else {
                    debug(QString::asprintf("Failed to write ISOTP message: Length %i (sts=0x%04X).", nSize, (int)status));
                    return;
                }

            } while ( !checkWriteState(message) );
            emit emit_sendISOTPPacketData(enTx, message);
        }
        else {
            debug(QString::asprintf("Failed to initialize ISOTP message: Length %i (sts=0x%04X).", nSize, (int)status));
            return;
        }
        // release message

        if (!CANTP_StatusIsOk_2016(status)) {
            debug(QString::asprintf("Failed to deallocate message (sts=0x%04X).", status));
            return;
        }
    }
    else {
        debug(QString::asprintf("Failed to allocate message (sts=0x%04X).", status));
        return;
    }
}

bool PCANISOTP::checkWriteState(cantp_msg& rx_msg)
{
    cantp_msg loopback_msg;

    bool bRes = false;
    cantp_msgprogress progress;
    cantp_status result;
    memset(&loopback_msg, 0, sizeof(loopback_msg));
    memset(&progress, 0, sizeof(progress));

    HANDLE receive_event = NULL;

    status = CANTP_GetValue_2016(channel, PCANTP_PARAMETER_RECEIVE_EVENT, &receive_event, sizeof(receive_event));
    if (!CANTP_StatusIsOk_2016(status) || receive_event == NULL) {
        debug(QString("Failed to make loop back receive event"));
        return false;
    }

    do {
        int nResult = WaitForSingleObject(receive_event, 10);
        if (nResult == WAIT_OBJECT_0) {
            status = CANTP_MsgDataAlloc_2016(&loopback_msg, PCANTP_MSGTYPE_NONE);
            // Read transmission confirmation.

            result = CANTP_Read_2016(channel, &loopback_msg, 0, PCANTP_MSGTYPE_ANY);
            if (CANTP_StatusIsOk_2016(result, PCANTP_STATUS_OK, false)) {
                if (((PCANTP_MSGTYPE_ISOTP & loopback_msg.type) == PCANTP_MSGTYPE_ISOTP) && (loopback_msg.msgdata.isotp->flags == PCANTP_MSGFLAG_LOOPBACK)
                    && ((loopback_msg.msgdata.isotp->netaddrinfo.msgtype & PCANTP_ISOTP_MSGTYPE_FLAG_INDICATION_TX) == PCANTP_ISOTP_MSGTYPE_FLAG_INDICATION_TX)) {
                    // The message is being received, wait and show progress
                    do {
                        result = CANTP_GetMsgProgress_2016(channel, &loopback_msg, PCANTP_MSGDIRECTION_RX, &progress);
                    } while (progress.state == PCANTP_MSGPROGRESS_STATE_PROCESSING);

                    if ( CANTP_MsgEqual_2016(&rx_msg, &loopback_msg, true) ) {
                        qDebug() << "Equal";
                        bRes = true;
                    }
                }
            }
        }
    } while ( !bRes );
    CANTP_MsgDataFree_2016(&loopback_msg);

    return bRes;
}

checkWriteState 함수를 통해 loop back read를 진행 후 전송한 cantp_msg를 CANTP_MsgEqual_2016 호출로 비교해서 같다면 전송이 성공했다는 방식으로 처리를 할 수 있습니다.

728x90
728x90

PCAN ISOTP API를 이용해서 데이터를 수신할 때 단순히 Read하는 것이 아니라 ISOTP Read 과정이 모두 완료 되었는지 확인하고 Read를 진행할 수 있는 방법이 있습니다.

int PCANISOTP::read_segmented_message(cantp_handle channel, int &nbMsgRead)
{
    cantp_msg rx_msg;
    cantp_msgprogress progress;     // ISO-TP 메시지가 다 들어왔는지 체크하기 위함

    memset(&progress, 0, sizeof(cantp_msgprogress));
    status = CANTP_Read_2016(channel, &rx_msg, NULL, PCANTP_MSGTYPE_NONE);

    if (CANTP_StatusIsOk_2016(status, PCANTP_STATUS_OK, false) ) {
        if (rx_msg.msgdata.any->flags & PCANTP_MSGFLAG_LOOPBACK) {
            return -1;
        } // loopback message의 경우 pass

        if (((PCANTP_MSGTYPE_ISOTP & rx_msg.type) == PCANTP_MSGTYPE_ISOTP)
            && ((rx_msg.msgdata.isotp->netaddrinfo.msgtype & PCANTP_ISOTP_MSGTYPE_FLAG_INDICATION_RX) == PCANTP_ISOTP_MSGTYPE_FLAG_INDICATION_RX)) {
            // The message is being received, wait and show progress
            do {
                status = CANTP_GetMsgProgress_2016(channel, &rx_msg, PCANTP_MSGDIRECTION_RX, &progress);
            } while (progress.state == PCANTP_MSGPROGRESS_STATE_PROCESSING);

            // The message is received, read it
            status = CANTP_Read_2016(channel, &rx_msg, NULL, PCANTP_MSGTYPE_NONE);
        } // 잔여 데이터가 있을 경우 계속 체크 후 다 들어왔다면 다시 한번 Read
        emit emit_sendISOTPPacketData(enRx, rx_msg);
    }

    return 1;
}

위 코드에서 cantp_msgprogress 구조체를 사용하는데 메시지 처리 상태를 확인할 수 있습니다. 진행 상태, 진행률 등을 볼 수 있으며 이 상태를 체크하면서 while로 state가 PCANTP_MSGPROGRESS_STATE_COMPLETED이 될 때 까지 기다립니다.

728x90
728x90

PEAK CAN ISO-TP API를 사용하면 PC에서도 USB 등을 이용하여 ISO-TP 통신을 사용할 수 있습니다.

 

PCAN-ISO-TP API: PEAK-System

Description ISO-TP (ISO 15765-2) is an international standard for the transfer of data packages via CAN. Above CAN (OSI layers 1 and 2), the protocol covers the OSI layers 3 (Network Layer) and 4 (Transport Layer). It can transmit data packages of up to 4

www.peak-system.com

 

이 API를 사용하면서 가장 헷갈리는 부분 중 하나가 Mapping 함수인데 간단히 설명하려고 합니다.

 

먼저 설명에 앞서 ISO-TP의 사양을 간단히 설명하면 CAN 혹은 CAN-FD의 메시지를 사용해서 통신을 하는 구조입니다. 메시지를 보낼 때 사이즈가 DLC(한번에 보내기로 결정한 데이터 사이즈)보다 크다면 여러 메시지를 주고 받으면서 통신을 진행합니다. 이때 First Frame, ConsecutiveFrame, FlowControl이라는 메시지를 사용하는데 FirstFrame과 Consecutive Frame은 송신 측에서 데이터를 보낼 때 사용하고 FlowControl은 수신측에서 수신 상태를 알려줄 때 사용합니다.

 

이 CAN Message를 보낼 때 ID를 사용하게 되는데 이 때 Mapping으로 ID를 미리 지정해두게 됩니다.

 

만약 내가 이 프로그램에서 ISOTP 메시지를 보낼 때 CAN ID를 0x01로 그리고 상대쪽에서 이 메시지에 대한 FlowControl ID가 0x02라면 아래와 같이 등록합니다. format, msgtype, target_type은 상황에 맞게 설정합니다. Target, Source Address는 Normal Format에서는 크게 중요치 않습니다.

m_SendCANID = 0x01;
m_SendFlowID = 0x02;

status = initSendMsgMappings(channel, m_SendCANID, m_SendFlowID);
if ( !CANTP_StatusIsOk_2016(status) ) {
    debug(QString::asprintf("Failed to Send initialize mapping"));
    return bResult;
} // ISO-TP 주소 매핑
// CAN ID = 내가 쓸 때의 ISOTP CAN ID
// FlowID = 상대방(GUI)에서 응답하는 FlowControl ID
// PCAN-ISO-TP API Usermanual p.265 / ISO-TP Document p.40 참고

cantp_status PCANISOTP::initSendMsgMappings(cantp_handle channel, uint32_t can_id, uint32_t can_id_flow_ctrl)
{
    cantp_mapping mapping_phys_tx;

    // clean variables (it is common to leave can_tx_dl uninitialized which can lead to invalid 0xCC values)
    memset(&mapping_phys_tx, 0, sizeof(mapping_phys_tx));

    // configure a mapping to transmit physical message
    mapping_phys_tx.can_id = can_id;
    mapping_phys_tx.can_id_flow_ctrl = can_id_flow_ctrl;
    mapping_phys_tx.can_msgtype = PCANTP_CAN_MSGTYPE_EXTENDED;
    mapping_phys_tx.netaddrinfo.format = PCANTP_ISOTP_FORMAT_NORMAL;
    mapping_phys_tx.netaddrinfo.msgtype = PCANTP_ISOTP_MSGTYPE_DIAGNOSTIC;
    mapping_phys_tx.netaddrinfo.target_type = PCANTP_ISOTP_ADDRESSING_PHYSICAL;
    mapping_phys_tx.netaddrinfo.source_addr = N_SA;
    mapping_phys_tx.netaddrinfo.target_addr = N_TA_PHYS;
    status = CANTP_AddMapping_2016(channel, &mapping_phys_tx);
    if (!CANTP_StatusIsOk_2016(status)) {
        return status;
    }

    return status;
}

 

Write 할 때 msgDataAlloc 후 MsgDataInit을 하는데 이 때 Mapping 할 때 등록했던 각종 정보를 그대로 전달합니다. 전달받은 netaddrinfo 정보를 토대로 Mapping 된 정보에서 CAN, Flow ID를 가져옵니다.

void PCANISOTP::Write(const char *pData, int nSize)
{
    cantp_msg message = {};
    cantp_can_msgtype msgtype = PCANTP_CAN_MSGTYPE_EXTENDED | PCANTP_CAN_MSGTYPE_FD | PCANTP_CAN_MSGTYPE_BRS;

    cantp_netaddrinfo isotp_nai = {};
    isotp_nai.source_addr = N_SA;
    isotp_nai.target_addr = N_TA_PHYS;
    isotp_nai.target_type = PCANTP_ISOTP_ADDRESSING_PHYSICAL;
    isotp_nai.msgtype = PCANTP_ISOTP_MSGTYPE_DIAGNOSTIC;
    isotp_nai.format = PCANTP_ISOTP_FORMAT_NORMAL;

    status = CANTP_MsgDataAlloc_2016(&message, PCANTP_MSGTYPE_ISOTP);
    if (CANTP_StatusIsOk_2016(status)) {
        // initialize ISOTP message
        status = CANTP_MsgDataInit_2016(&message, PCANTP_MAPPING_FLOW_CTRL_NONE, msgtype, nSize, pData, &isotp_nai);
        ...
}

 

이제 수신쪽 매핑입니다. 만약 상대쪽에서 ISOTP 메시지를 보낼 때 CAN ID를 0x03로 그리고 이 프로그램에서 메시지에 대한 FlowControl ID가 0x04라면 아래와 같이 등록합니다.

m_SendCANID = 0x03;
m_SendFlowID = 0x04;

status = initRecvMsgMappings(channel, m_RecvCANID, m_RecvFlowID);
if ( !CANTP_StatusIsOk_2016(status) ) {
    debug(QString::asprintf("Failed to Recv initialize mapping"));
    return bResult;
} // ISO-TP 주소 매핑
// CAN ID = 상대방의 ISOTP CAN ID
// FlowID = 내가 보내는 FlowControl ID

cantp_status PCANISOTP::initRecvMsgMappings(cantp_handle channel, uint32_t can_id, uint32_t can_id_flow_ctrl)
{
    cantp_mapping mapping_phys_rx;

    // clean variables (it is common to leave can_tx_dl uninitialized which can lead to invalid 0xCC values)
    memset(&mapping_phys_rx, 0, sizeof(mapping_phys_rx));

    // configure a mapping to transmit physical message
    mapping_phys_rx.can_id = can_id;
    mapping_phys_rx.can_id_flow_ctrl = can_id_flow_ctrl;
    mapping_phys_rx.can_msgtype = PCANTP_CAN_MSGTYPE_EXTENDED;
    mapping_phys_rx.netaddrinfo.format = PCANTP_ISOTP_FORMAT_NORMAL;
    mapping_phys_rx.netaddrinfo.msgtype = PCANTP_ISOTP_MSGTYPE_DIAGNOSTIC;
    mapping_phys_rx.netaddrinfo.target_type = PCANTP_ISOTP_ADDRESSING_PHYSICAL;
    mapping_phys_rx.netaddrinfo.source_addr = N_TA_PHYS;
    mapping_phys_rx.netaddrinfo.target_addr = N_SA;
    status = CANTP_AddMapping_2016(channel, &mapping_phys_rx);
    if (!CANTP_StatusIsOk_2016(status)) {
        return status;
    }

    return status;
}

 

자세한 내용은 PCAN ISOTP API 매뉴얼 참고 바랍니다.

728x90
728x90

Qt에서 멀티 쓰레딩을 사용하기 위해서 QThread를 상속받아서 처리하는 예제를 소개한 적이 있습니다.

 

Qt QThread 사용하기(화면 실시간 갱신하기)

센서값을 수집해서 실시간으로 실행창에서 최신화하고 갱신하는 방법에 대해 소개해 드리겠습니다. 먼저 QThread를 상속받는 클래스를 하나 만들어 줍니다. 생성된 qTh.h 파일에 소스코드를 아래

1d1cblog.tistory.com

QtConcurrent를 사용하면 간단하게 비동기 멀티 쓰레딩을 할 수 있습니다. 이를 Qt에서 제공하는 Example을 같이 살펴보며 필요한 부분에 대해 같이 공부해보려 합니다.

 

Example은 Qt Creator > Examples에서 concurrent로 검색하여 첫 번째 예제인 Image Scaling을 사용하려 합니다.

전체 함수를 다 보는 것보다 필요한 부분만 순서대로 보겠습니다. 주석에 나와있는 번호를 따라가면 UI 생성 및 Connect 관계에 대해서는 쉽게 파악이 가능합니다.

 

그리고 중간에 비동기 처리를 확인하기 위해 Debug Message를 출력하는 함수를 추가로 사용하였습니다.

void DebugMsg(const QString& sMsg)
{
    qDebug() << QTime::currentTime().toString("[mm:ss:zzz]") + sMsg;
}

 

[3] 부분인 DownloadDialog를 실행 후 확인을 눌렀을 때 진행되는 부분부터 보겠습니다. DownloadDialog에서는 이미지의 URL들을 관리하고 확인을 눌렀을 때 getUrls를 통해 Url 정보를 가져옵니다.

//! [3]
void Images::process()
{
    // Clean previous state
    replies.clear();
    addUrlsButton->setEnabled(false);

    if (downloadDialog->exec() == QDialog::Accepted) {

        const auto urls = downloadDialog->getUrls();
        if (urls.empty())
            return;

        cancelButton->setEnabled(true);

        initLayout(urls.size());

        downloadFuture = download(urls);
        DebugMsg("Downloading");
        statusBar->showMessage(tr("Downloading..."));
//! [3]

        //! [4]
        downloadFuture
                .then([this](auto) {
                    cancelButton->setEnabled(false);
                    updateStatus(tr("Scaling..."));
                    DebugMsg("Scaling");
                    //! [16]
                    scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
                                                               downloadFuture.results()));
                    //! [16]
                })
        //! [4]
        //! [5]
                .onCanceled([this] {
                    updateStatus(tr("Download has been canceled."));
                })
                .onFailed([this](QNetworkReply::NetworkError error) {
                    updateStatus(tr("Download finished with error: %1").arg(error));
                    // Abort all pending requests
                    abortDownload();
                })
                .onFailed([this](const std::exception &ex) {
                    updateStatus(tr(ex.what()));
                })
        //! [5]
                .then([this]() {
                    cancelButton->setEnabled(false);
                    addUrlsButton->setEnabled(true);
                });
    }
}

 

가져온 Url을 Donwload 함수로 넘기게 됩니다.

//! [8]
QFuture<QByteArray> Images::download(const QList<QUrl> &urls)
{
//! [8]
//! [9]
    DebugMsg("DonwloadStart");
    QSharedPointer<QPromise<QByteArray>> promise(new QPromise<QByteArray>());
    promise->start();
//! [9]

    //! [10]
    for (const auto &url : urls) {
        DebugMsg("Donwload " + url.toString());
        QSharedPointer<QNetworkReply> reply(qnam.get(QNetworkRequest(url)));
        replies.push_back(reply);
    //! [10]

    //! [11]
        QtFuture::connect(reply.get(), &QNetworkReply::finished).then([=] {
            if (promise->isCanceled()) {
                if (!promise->future().isFinished())
                    promise->finish();
                return;
            }

            if (reply->error() != QNetworkReply::NoError) {
                if (!promise->future().isFinished())
                    throw reply->error();
            }
        //! [12]
            promise->addResult(reply->readAll());
            DebugMsg("Get Data");

            // Report finished on the last download
            if (promise->future().resultCount() == urls.size()) {
                DebugMsg("Finish promis");
                promise->finish();
            }
        //! [12]
        }).onFailed([promise] (QNetworkReply::NetworkError error) {
            promise->setException(std::make_exception_ptr(error));
            promise->finish();
        }).onFailed([promise] {
            const auto ex = std::make_exception_ptr(
                        std::runtime_error("Unknown error occurred while downloading."));
            promise->setException(ex);
            promise->finish();
        });
    }
    //! [11]

//! [13]
    DebugMsg("Return");
    return promise->future();
}
//! [13]

여기서 QPromise와 QFuture을 사용하게 됩니다. C++에서 Promise와 Furture은 같이 사용하게 되는데 Furture(미래)에 데이터를 돌려주겠다는 Promise(약속)이라고 생각하면 됩니다. QPromise의 비동기 처리를 QFurture에 전달하는 방식입니다. QFurtuer의 Connect를 통해 Signal 처리를 하게 되고 혹은 QFurture::waitforFinish를 통해 비동기 처리가 완료될 때까지 기다릴 수도 있습니다.

 

for문으로 전달받은 Url들을 요청하게 되는데 이에 대한 결과는 QNetworkReply::Finish Signal을 QtFurture::connect로 묶어 처리가 됩니다. 아래 로그를 확인하면 Download 후 바로 Promise가 가지고 있는 QFurture을 Return하게 됩니다. 그 후 요청한 Url에 대한 결과가 도착하고 Url 개수만큼의 완료가 진행 됐다면 QPromise는 종료합니다.

"[23:11:039]DonwloadStart"
"[23:11:039]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:504]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:505]Return"
"[23:11:505]Downloading"
"[23:11:724]Get Data"
"[23:11:724]Get Data"
"[23:11:724]Finish promis"

그리고 다시 Process 함수로 돌아와서 QPromise가 finish 됨에 따라 QFurtuer의 then의 lambda 함수가 실행됩니다.

//! [3]
void Images::process()
{
    // Clean previous state
    replies.clear();
    addUrlsButton->setEnabled(false);

    if (downloadDialog->exec() == QDialog::Accepted) {

        const auto urls = downloadDialog->getUrls();
        if (urls.empty())
            return;

        cancelButton->setEnabled(true);

        initLayout(urls.size());

        downloadFuture = download(urls);
        DebugMsg("Downloading");
        statusBar->showMessage(tr("Downloading..."));
//! [3]

        //! [4]
        downloadFuture
                .then([this](auto) {
                    cancelButton->setEnabled(false);
                    updateStatus(tr("Scaling..."));
                    DebugMsg("Scaling");
                    //! [16]
                    scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
                                                               downloadFuture.results()));
                    //! [16]
                })
        //! [4]
        //! [5]
                .onCanceled([this] {
                    updateStatus(tr("Download has been canceled."));
                })
                .onFailed([this](QNetworkReply::NetworkError error) {
                    updateStatus(tr("Download finished with error: %1").arg(error));
                    // Abort all pending requests
                    abortDownload();
                })
                .onFailed([this](const std::exception &ex) {
                    updateStatus(tr(ex.what()));
                })
        //! [5]
                .then([this]() {
                    cancelButton->setEnabled(false);
                    addUrlsButton->setEnabled(true);
                });
    }
}

여기서 QFurtherWatcher라는 객체에 QtConcurrent::run의 Return을 넘겨주게 됩니다. QFurtherWatcher는 QFurture에 대한 상태를 체크하고 QFurture가 Singal 상태가 됐을 때 실행할 Slot을 connect 시켜주었습니다.

Images::Images(QWidget *parent) : QWidget(parent), downloadDialog(new DownloadDialog(this))
{
    resize(800, 600);

    ...

//! [6]
    connect(&scalingWatcher, &QFutureWatcher<QList<QImage>>::finished, this, &Images::scaleFinished);
//! [6]
}

//! [14]
Images::OptionalImages Images::scaled(const QList<QByteArray> &data)
{
    DebugMsg("Scale Image");
    QList<QImage> scaled;
    for (const auto &imgData : data) {
        QImage image;
        image.loadFromData(imgData);
        if (image.isNull())
            return std::nullopt;

        scaled.push_back(image.scaled(100, 100, Qt::KeepAspectRatio));
    }

    return scaled;
}
//! [14]

//! [15]
void Images::scaleFinished()
{
    DebugMsg("Sacle Finished");
    const OptionalImages result = scalingWatcher.result();
    if (result.has_value()) {
        const auto scaled = result.value();
        showImages(scaled);
        updateStatus(tr("Finished"));
    } else {
        updateStatus(tr("Failed to extract image data."));
    }
    addUrlsButton->setEnabled(true);
}

최종적으로 실행된 로그를 보면 아래와 같습니다.

"[23:11:039]DonwloadStart"
"[23:11:039]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:504]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:505]Return"
"[23:11:505]Downloading"
"[23:11:724]Get Data"
"[23:11:724]Get Data"
"[23:11:724]Finish promis"
"[23:11:724]Scaling"
"[23:11:724]Scale Image"
"[23:11:752]Sacle Finished"

정리해보자면 QThread를 사용하지 않으면서 비동기 처리를 위해선 QFurture, QPromise 등을 사용하면서 종료를 대기 혹은 신호를 받거나 혹은 QFurtureWatcher에 QFurture를 등록 후 사용도 가능합니다.

 

QFurture를 간단히 사용하기 위해서는 Qt::Concorrent::run을 통해 함수 포인터와 인자를 넘겨줌으로써 그 처리의 QFurture를 반환받아 위처럼 사용도 가능합니다.

728x90

+ Recent posts