diff --git a/src/backend/datasources/MQTTClient.cpp b/src/backend/datasources/MQTTClient.cpp index 506fa1c53..9e75f413c 100644 --- a/src/backend/datasources/MQTTClient.cpp +++ b/src/backend/datasources/MQTTClient.cpp @@ -1,1421 +1,1428 @@ /*************************************************************************** File : MQTTClient.cpp Project : LabPlot Description : Represents a MQTT Client -------------------------------------------------------------------- Copyright : (C) 2018 Kovacs Ferencz (kferike98@gmail.com) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301 USA * * * ***************************************************************************/ #ifdef HAVE_MQTT #include "backend/datasources/MQTTClient.h" #include "backend/datasources/MQTTSubscription.h" #include "backend/datasources/MQTTTopic.h" #include "backend/datasources/filters/AsciiFilter.h" #include "backend/datasources/filters/FITSFilter.h" #include "backend/datasources/filters/BinaryFilter.h" #include "backend/core/Project.h" #include "kdefrontend/spreadsheet/PlotDataDialog.h" #include "commonfrontend/spreadsheet/SpreadsheetView.h" #include "kdefrontend/datasources/MQTTErrorWidget.h" #include #include #include #include #include #include #include #include #include #include #include /*! \class MQTTClient \brief The MQTT Client connects to the broker set in ImportFileWidget. It manages the MQTTSubscriptions, and the MQTTTopics. \ingroup datasources */ MQTTClient::MQTTClient(const QString& name) : Folder(name), m_paused(false), m_prepared(false), m_keepLastValues(false), m_filter(nullptr), m_updateTimer(new QTimer(this)), m_willTimer(new QTimer(this)), m_client(new QMqttClient(this)), m_mqttTest(false), m_mqttRetain(false), m_mqttUseWill(false), m_mqttUseID(false), m_loaded(false), m_sampleSize(1), m_keepNValues(0), m_updateInterval(1000), m_disconnectForWill(false), m_mqttUseAuthentication(false), m_subscriptionsLoaded(0), m_subscriptionCountToLoad(0), m_mqttFirstConnectEstablished(false) { qDebug()<<"MQTTClient constructor"; connect(m_updateTimer, &QTimer::timeout, this, &MQTTClient::read); m_willStatistics.fill(false, 15); connect(m_client, &QMqttClient::connected, this, &MQTTClient::onMqttConnect); connect(m_willTimer, &QTimer::timeout, this, &MQTTClient::updateWillMessage); connect(m_client, &QMqttClient::errorChanged, this, &MQTTClient::mqttErrorChanged); } MQTTClient::~MQTTClient() { emit clientAboutToBeDeleted(m_client->hostname()); //stop reading before deleting the objects pauseReading(); qDebug()<<"destructor"; if (m_filter) delete m_filter; qDebug()<<"delete timers"; delete m_updateTimer; delete m_willTimer; qDebug()<<"disocnnect"; m_client->disconnectFromHost(); qDebug()<<"delete client"; delete m_client; } /*! * depending on the update type, periodically or on data changes, starts the timer. */ void MQTTClient::ready() { if (m_updateType == TimeInterval) m_updateTimer->start(m_updateInterval); } /*! * \brief Updates the MQTTTopics of the client */ void MQTTClient::updateNow() { qDebug()<<"Update now"; m_updateTimer->stop(); read(); if (m_updateType == TimeInterval && !m_paused) m_updateTimer->start(m_updateInterval); } /*! * \brief Continue reading from messages after it was paused. */ void MQTTClient::continueReading() { qDebug()<<"continue reading"; m_paused = false; if (m_updateType == TimeInterval) m_updateTimer->start(m_updateInterval); } /*! * \brief Pause the reading from messages. */ void MQTTClient::pauseReading() { qDebug()<<"pause reading"; m_paused = true; if (m_updateType == TimeInterval) m_updateTimer->stop(); } /*! * \brief Sets the filter of the MQTTClient. * * \param f a pointer to the new filter */ void MQTTClient::setFilter(AbstractFileFilter* f) { m_filter = f; } /*! * \brief Returns the filter of the MQTTClient. */ AbstractFileFilter* MQTTClient::filter() const { return m_filter; } /*! * \brief Sets the MQTTclient's update interval to \c interval * \param interval */ void MQTTClient::setUpdateInterval(int interval) { qDebug()<<"Update interval " << interval; m_updateInterval = interval; if(!m_paused) m_updateTimer->start(m_updateInterval); } /*! * \brief Returns the MQTTClient's update interval to \c interval * \param interval */ int MQTTClient::updateInterval() const { return m_updateInterval; } /*! * \brief Sets how many values we should store * \param keepNValues */ void MQTTClient::setKeepNValues(int keepNValues) { qDebug()<<"Keep N Values" << keepNValues; m_keepNValues = keepNValues; } /*! * \brief Returns how many values we should store */ int MQTTClient::keepNValues() const { return m_keepNValues; } /*! * \brief Provides information about whether the reading is paused or not * * \return true if the reading is paused * \return false otherwise */ bool MQTTClient::isPaused() const { return m_paused; } /*! * \brief Sets the size rate to sampleSize * \param sampleSize */ void MQTTClient::setSampleSize(int sampleSize) { qDebug()<<"Sample rate: " << sampleSize; m_sampleSize = sampleSize; } /*! * \brief Returns the size rate */ int MQTTClient::sampleSize() const { return m_sampleSize; } /*! * \brief Sets the MQTTClient's reading type to readingType * \param readingType */ void MQTTClient::setReadingType(ReadingType readingType) { qDebug()<<"Read Type : " << static_cast(readingType); m_readingType = readingType; } /*! * \brief Returns the MQTTClient's reading type */ MQTTClient::ReadingType MQTTClient::readingType() const { return m_readingType; } /*! * \brief Sets the MQTTClient's update type to updatetype and handles this change * \param updatetype */ void MQTTClient::setUpdateType(UpdateType updatetype) { qDebug()<<"Update Type : " << static_cast(updatetype); if (updatetype == NewData) { m_updateTimer->stop(); } m_updateType = updatetype; } /*! * \brief Returns the MQTTClient's update type */ MQTTClient::UpdateType MQTTClient::updateType() const { return m_updateType; } /*! * \brief Returns the MQTTClient's icon */ QIcon MQTTClient::icon() const { QIcon icon; icon = QIcon::fromTheme("labplot-MQTT"); return icon; } /*! * \brief Sets the host and port for the client. * * \param host the hostname of the broker we want to connect to * \param port the port used by the broker */ void MQTTClient::setMqttClientHostPort(const QString& host, const quint16& port) { m_client->setHostname(host); m_client->setPort(port); } /*! * \brief Returns hostname of the broker the client is connected to. */ QString MQTTClient::clientHostName() const{ return m_client->hostname(); } /*! * \brief Returns the port used by the broker. */ quint16 MQTTClient::clientPort() const { return m_client->port(); } /*! * \brief Sets the flag on the given value. * If set true it means that the broker requires authentication, otherwise it doesn't. * * \param use */ void MQTTClient::setMQTTUseAuthentication(bool use) { m_mqttUseAuthentication = use; } /*! * \brief Returns whether the broker requires authentication or not. */ bool MQTTClient::mqttUseAuthentication() const { return m_mqttUseAuthentication; } /*! * \brief Sets the username and password for the client. * * \param username the username used for authentication * \param password the password used for authentication */ void MQTTClient::setMqttClientAuthentication(const QString& username, const QString& password) { m_client->setUsername(username); m_client->setPassword(password); } /*! * \brief Returns the username used for authentication. */ QString MQTTClient::clientUserName() const{ return m_client->username(); } /*! * \brief Returns the password used for authentication. */ QString MQTTClient::clientPassword() const{ return m_client->password(); } /*! * \brief Sets the flag on the given value. * If set true it means that user wants to set the client ID, otherwise it's not the case. * * \param use */ void MQTTClient::setMQTTUseID(bool use) { m_mqttUseID = use; } /*! * \brief Returns whether the user wants to set the client ID or not. */ bool MQTTClient::mqttUseID() const { return m_mqttUseID; } /*! * \brief Sets the ID of the client * * \param id */ void MQTTClient::setMqttClientId(const QString &id){ m_client->setClientId(id); } /*! * \brief Returns the ID of the client */ QString MQTTClient::clientID () const{ return m_client->clientId(); } /*! * \brief Sets the flag on the given value. * If retain is true we interpret retain messages, otherwise we do not * * \param retain */ void MQTTClient::setMqttRetain(bool retain) { m_mqttRetain = retain; } /*! * \brief Returns the flag, which set to true means that interpret retain messages, otherwise we do not */ bool MQTTClient::mqttRetain() const { return m_mqttRetain; } /*! * \brief Returns the name of every MQTTTopics which already received a message, and is child of the MQTTClient */ QVector MQTTClient::topicNames() const { return m_topicNames; } /*! * \brief Adds the initial subscriptions that were set in ImportFileWidget * * \param filter the name of the subscribed topic * \param qos the qos level of the subscription */ void MQTTClient::addInitialMqttSubscriptions(const QMqttTopicFilter& filter, const quint8& qos) { m_subscribedTopicNameQoS[filter] = qos; } /*! * \brief Returns the name of every MQTTSubscription of the MQTTClient */ QVector MQTTClient::mqttSubscriptions() const { return m_subscriptions; } /*! * \brief Adds a new MQTTSubscription to the MQTTClient * * \param topic, the name of the topic * \param QoS */ void MQTTClient::addMQTTSubscription(const QString& topic, quint8 QoS) { //Check whether the subscription already exists, if not we can add it if(!m_subscriptions.contains(topic)) { QMqttTopicFilter filter {topic}; QMqttSubscription* temp = m_client->subscribe(filter, QoS); if (temp) { qDebug()<topic()<<" "<qos(); m_subscriptions.push_back(temp->topic().filter()); m_subscribedTopicNameQoS[temp->topic().filter()] = temp->qos(); qDebug()<<"New MQTTSubscription"; MQTTSubscription* newSubscription = new MQTTSubscription(temp->topic().filter()); newSubscription->setMQTTClient(this); qDebug()<<"Add child"; addChild(newSubscription); qDebug()<<"Add to vector"; m_mqttSubscriptions.push_back(newSubscription); qDebug()<<"Added topic"; qDebug()<<"Check for inferior subscriptions"; //Search for inferior subscriptions, that the new subscription contains bool found = false; QVector inferiorSubscriptions; for(int i = 0; i < m_mqttSubscriptions.size(); ++i) { if(checkTopicContains(topic, m_mqttSubscriptions[i]->subscriptionName()) && topic != m_mqttSubscriptions[i]->subscriptionName()) { found = true; inferiorSubscriptions.push_back(m_mqttSubscriptions[i]); } } //If there are some inferior subscripitons, we have to deal with them if(found) { for(int sub = 0; sub < inferiorSubscriptions.size(); ++sub) { qDebug()<<"Inferior subscription: "<subscriptionName(); //We have to reparent every topic of the inferior subscription, so no data is lost QVector topics = inferiorSubscriptions[sub]->topics(); qDebug()<< topics.size(); for(int i = 0; i < topics.size() ; ++i) { qDebug()<topicName(); topics[i]->reparent(newSubscription); } //Then remove the subscription and every connected informaiton QMqttTopicFilter unsubscribeFilter {inferiorSubscriptions[sub]->subscriptionName()}; m_client->unsubscribe(unsubscribeFilter); for (int j = 0; j < m_mqttSubscriptions.size(); ++j) { if(m_mqttSubscriptions[j]->subscriptionName() == inferiorSubscriptions[sub]->subscriptionName()) { m_mqttSubscriptions.remove(j); } } m_subscriptions.removeAll(inferiorSubscriptions[sub]->subscriptionName()); m_subscribedTopicNameQoS.remove(inferiorSubscriptions[sub]->subscriptionName()); removeChild(inferiorSubscriptions[sub]); } } - connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::mqttSubscribtionMessageReceived); + connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::mqttSubscriptionMessageReceived); emit mqttTopicsChanged(); } } } /*! * \brief Removes a MQTTSubscription from the MQTTClient * * \param name, the name of the subscription to remove */ void MQTTClient::removeMQTTSubscription(const QString &name) { //We can only remove the subscription if it exists if(m_subscriptions.contains(name)) { qDebug()<<"Start to remove subscription in MQTTClient: "<unsubscribe(filter); qDebug()<<"QMqttClient's unsubscribe occured"; //Remove every connected information m_subscriptions.removeAll(name); for (int i = 0; i < m_mqttSubscriptions.size(); ++i) { if(m_mqttSubscriptions[i]->subscriptionName() == name) { qDebug()<<"Subscription name"<subscriptionName() << " "<name(); MQTTSubscription* removeSubscription = m_mqttSubscriptions[i]; m_mqttSubscriptions.remove(i); //Remove every topic of the subscription as well QVector topics = removeSubscription->topics(); for (int j = 0; j < topics.size(); ++j) { qDebug()<<"Removing topic name: "<topicName(); m_topicNames.removeAll(topics[j]->topicName()); } //Remove the MQTTSubscription removeChild(removeSubscription); qDebug()<<"removed child"; break; } } QMapIterator j(m_subscribedTopicNameQoS); while(j.hasNext()) { j.next(); if(j.key().filter() == name) { m_subscribedTopicNameQoS.remove(j.key()); qDebug()<<"Removed from TopicNameQoS map "<subscribe(filter, QoS); if (temp) { //Add the MQTTSubscription and other connected data qDebug()<topic()<<" "<qos(); m_subscriptions.push_back(temp->topic().filter()); m_subscribedTopicNameQoS[temp->topic().filter()] = temp->qos(); qDebug()<<"New MQTTSubscription"; MQTTSubscription* newSubscription = new MQTTSubscription(temp->topic().filter()); newSubscription->setMQTTClient(this); qDebug()<<"Add child"; addChild(newSubscription); qDebug()<<"Add to vector"; m_mqttSubscriptions.push_back(newSubscription); qDebug()<<"Added topic"; //Search for the subscription the topic belonged to bool found = false; MQTTSubscription* superiorSubscription; for(int i = 0; i < m_mqttSubscriptions.size(); ++i) { if(checkTopicContains(m_mqttSubscriptions[i]->subscriptionName(), topic) && topic != m_mqttSubscriptions[i]->subscriptionName()) { found = true; superiorSubscription = m_mqttSubscriptions[i]; break; } } if(found) { qDebug()<<"Superior subscription: "<subscriptionName(); //Search for topics belonging to the superior(old) subscription //which are also contained by the new subscription QVector topics = superiorSubscription->topics(); qDebug()<< topics.size(); QVector inferiorTopics; for(int i = 0; i < topics.size(); ++i) { if(checkTopicContains(topic, topics[i]->topicName())) { inferiorTopics.push_back(topics[i]); } } //Reparent these topics, in order to avoid data loss for(int i = 0; i < inferiorTopics.size() ; ++i) { qDebug()<topicName(); inferiorTopics[i]->reparent(newSubscription); } } - connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::mqttSubscribtionMessageReceived); + connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::mqttSubscriptionMessageReceived); } } } /*! * \brief Reparents the given MQTTTopic to the given MQTTSubscription * * \param topic, the name of the MQTTTopic * \param parent, the name of the MQTTSubscription */ void MQTTClient::reparentTopic(const QString& topic, const QString& parent) { //We can only reparent if the parent containd the topic if(m_subscriptions.contains(parent) && m_topicNames.contains(topic)) { //search for the parent MQTTSubscription bool found = false; MQTTSubscription* superiorSubscription; for(int i = 0; i < m_mqttSubscriptions.size(); ++i) { if(m_mqttSubscriptions[i]->subscriptionName() == parent) { found = true; superiorSubscription = m_mqttSubscriptions[i]; break; } } if(found) { qDebug()<<"Superior subscription: "<subscriptionName(); //get every topic of the MQTTClient QVector topics = children(AbstractAspect::Recursive); qDebug()<< topics.size(); //Search for the given topic among the MQTTTopics for(int i = 0; i < topics.size(); ++i) { qDebug()<topicName()<<" "<parentAspect()->name(); if(topic == topics[i]->topicName()) { qDebug()<topicName()<<" "<subscriptionName(); //if found, it is reparented to the parent MQTTSubscription topics[i]->reparent(superiorSubscription); break; } } } qDebug()<<"reparent done"; } } /*! *\brief Checks if a topic contains another one * * \param superior the name of a topic * \param inferior the name of a topic * \return true if superior is equal to or contains(if superior contains wildcards) inferior, * false otherwise */ bool MQTTClient::checkTopicContains(const QString &superior, const QString& inferior) { if (superior == inferior) return true; else { if(superior.contains("/")) { QStringList superiorList = superior.split('/', QString::SkipEmptyParts); QStringList inferiorList = inferior.split('/', QString::SkipEmptyParts); //a longer topic can't contain a shorter one if(superiorList.size() > inferiorList.size()) return false; bool ok = true; for(int i = 0; i < superiorList.size(); ++i) { if(superiorList.at(i) != inferiorList.at(i)) { if((superiorList.at(i) != "+") && !(superiorList.at(i) == "#" && i == superiorList.size() - 1)) { qDebug() < 0 && differIndex < firstList.size() -1) { + if(differIndex > 0) { for(int j = differIndex +1; j < firstList.size(); ++j) { if(firstList.at(j) != secondtList.at(j)) { differ = true; break; } } } else differ = true; if(!differ) { for(int i = 0; i < firstList.size(); ++i) { if(i != differIndex) { commonTopic.append(firstList.at(i)); } else { //we put "+" wildcard at the level where they differ commonTopic.append("+"); } if(i != firstList.size() - 1) commonTopic.append("/"); } } } } qDebug() << "Common topic: "<stop(); } /*! * \brief Returns whether the user wants to use will message or not */ bool MQTTClient::mqttWillUse() const{ return m_mqttUseWill; } /*! * \brief Sets the will topic of the client * * \param topic */ void MQTTClient::setWillTopic(const QString& topic) { m_willTopic = topic; } /*! * \brief Returns the will topic of the client */ QString MQTTClient::willTopic() const{ return m_willTopic; } /*! * \brief Sets the retain flag of the client's will message * * \param retain */ void MQTTClient::setWillRetain(bool retain) { m_willRetain = retain; } /*! * \brief Returns the retain flag of the client's will message */ bool MQTTClient::willRetain() const { return m_willRetain; } /*! * \brief Sets the QoS level of the client's will message * * \param QoS */ void MQTTClient::setWillQoS(quint8 QoS) { m_willQoS = QoS; } /*! * \brief Returns the QoS level of the client's will message */ quint8 MQTTClient::willQoS() const { return m_willQoS; } /*! * \brief Sets the will message type of the client * * \param messageType */ void MQTTClient::setWillMessageType(WillMessageType messageType) { m_willMessageType = messageType; } /*! * \brief Returns the will message type of the client */ MQTTClient::WillMessageType MQTTClient::willMessageType() const { return m_willMessageType; } /*! * \brief Sets the own will message of the user * * \param ownMessage */ void MQTTClient::setWillOwnMessage(const QString& ownMessage) { m_willOwnMessage = ownMessage; } /*! * \brief Returns the own will message of the user */ QString MQTTClient::willOwnMessage() const { return m_willOwnMessage; } /*! * \brief Updates the will message of the client */ void MQTTClient::updateWillMessage() { QVector topics = children(AbstractAspect::Recursive); const AsciiFilter* asciiFilter = nullptr; const MQTTTopic* willTopic = nullptr; qDebug()<<"Searching for topic"; //Search for the will topic for (int i = 0; i < topics.count(); ++i) { if(topics[i]->topicName() == m_willTopic) { willTopic = topics[i]; break; } } //if the will topic is found we can update the will message if(willTopic != nullptr) { //To update the will message we have to disconnect first, then after setting everything connect again if(m_mqttUseWill && (m_client->state() == QMqttClient::ClientState::Connected) ) { //Disconnect only once (disconnecting may take a while) if(!m_disconnectForWill) { qDebug() << "Disconnecting from host"; m_client->disconnectFromHost(); m_disconnectForWill = true; } //Try to update again updateWillMessage(); } //If client is disconnected we can update the settings else if(m_mqttUseWill && (m_client->state() == QMqttClient::ClientState::Disconnected) && m_disconnectForWill) { m_client->setWillQoS(m_willQoS); qDebug()<<"Will QoS" << m_willQoS; m_client->setWillRetain(m_willRetain); qDebug()<<"Will retain" << m_willRetain; m_client->setWillTopic(m_willTopic); qDebug()<<"Will Topic" << m_willTopic; //Set the will message according to m_willMessageType switch (m_willMessageType) { case WillMessageType::OwnMessage: m_client->setWillMessage(m_willOwnMessage.toUtf8()); qDebug()<<"Will own message" << m_willOwnMessage; break; case WillMessageType::Statistics: { qDebug()<<"Start will statistics"; asciiFilter = dynamic_cast(willTopic->filter()); //If the topic's asciiFilter was found, get the needed statistics if(asciiFilter != nullptr) { qDebug()<<"Checking column mode"; //Statistics is only possible if the data stored in the MQTTTopic is of type integer or numeric if((asciiFilter->mqttColumnMode() == AbstractColumn::ColumnMode::Integer) || (asciiFilter->mqttColumnMode() == AbstractColumn::ColumnMode::Numeric)) { - m_client->setWillMessage(asciiFilter->mqttColumnStatistics(tempTopic).toUtf8()); + m_client->setWillMessage(asciiFilter->mqttColumnStatistics(willTopic).toUtf8()); qDebug() << "Will statistics message: "<< QString(m_client->willMessage()); } //Otherwise set empty message else { m_client->setWillMessage(QString("").toUtf8()); qDebug() << "Will statistics message: "<< QString(m_client->willMessage()); } } break; } case WillMessageType::LastMessage: m_client->setWillMessage(m_willLastMessage.toUtf8()); qDebug()<<"Will last message:\n" << m_willLastMessage; break; default: break; } m_disconnectForWill = false; //Reconnect with the updated message m_client->connectToHost(); qDebug()<< "Reconnect to host"; } } } /*! * \brief Returns the MQTTClient's will update type */ MQTTClient::WillUpdateType MQTTClient::willUpdateType() const{ return m_willUpdateType; } /*! * \brief Sets the MQTTClient's will update type * * \param willUpdateType */ void MQTTClient::setWillUpdateType(WillUpdateType willUpdateType) { m_willUpdateType = willUpdateType; } /*! * \brief Returns the time interval of updating the MQTTClient's will message */ int MQTTClient::willTimeInterval() const{ return m_willTimeInterval; } /*! * \brief Sets the time interval of updating the MQTTClient's will message, if update type is TimePeriod * * \param interval */ void MQTTClient::setWillTimeInterval(int interval) { m_willTimeInterval = interval; } /*! * \brief Clear the lastly received message by the will topic * Called when the will topic is changed */ void MQTTClient::clearLastMessage() { m_willLastMessage.clear(); } /*! * \brief Sets true the corresponding flag of the statistic type, * what means that the given statistic type will be added to the will message * * \param statistics */ void MQTTClient::addWillStatistics(WillStatistics statistic){ m_willStatistics[static_cast(statistic)] = true; } /*! * \brief Sets false the corresponding flag of the statistic type, * what means that the given statistic will no longer be added to the will message * * \param statistics */ void MQTTClient::removeWillStatistics(WillStatistics statistic) { m_willStatistics[static_cast(statistic)] = false; } /*! * \brief Returns a bool vector, meaning which statistic types are included in the will message * If the corresponding value is true, the statistic type is included, otherwise it isn't */ QVector MQTTClient::willStatistics() const{ return m_willStatistics; } /*! * \brief Starts the will timer, which will update the will message */ void MQTTClient::startWillTimer() const{ if(m_willUpdateType == WillUpdateType::TimePeriod) m_willTimer->start(m_willTimeInterval); } /*! * \brief Stops the will timer */ void MQTTClient::stopWillTimer() const{ m_willTimer->stop(); } //############################################################################## //################################# SLOTS #################################### //############################################################################## /*! *\brief called periodically when update type is TimeInterval */ void MQTTClient::read() { if (m_filter == nullptr) return; if (!m_prepared) { qDebug()<<"Read & Connect"; //connect to the broker m_client->connectToHost(); qDebug()<<"connectTOHost called"; m_prepared = true; } if((m_client->state() == QMqttClient::ClientState::Connected) && m_mqttFirstConnectEstablished) { qDebug()<<"Read"; //Signal for every MQTTTopic that they can read emit readFromTopics(); } } /*! *\brief called when the client successfully connected to the broker */ void MQTTClient::onMqttConnect() { qDebug() << "on mqtt connect"; if(m_client->error() == QMqttClient::NoError) { //if this is the first connection (after setting the options in ImportFileWidget or loading saved project) if(!m_mqttFirstConnectEstablished) { qDebug()<<"connection made in MQTTClient"; //Subscribe to initial or loaded topics QMapIterator i(m_subscribedTopicNameQoS); while(i.hasNext()) { i.next(); qDebug()<subscribe(i.key(), i.value()); if(temp) { qDebug()<topic()<<" "<qos(); //If we didn't load the MQTTClient from xml we have to add the MQTTSubscriptions if(!m_loaded) { m_subscriptions.push_back(temp->topic().filter()); qDebug()<<"New MQTTSubscription"; MQTTSubscription* newSubscription = new MQTTSubscription(temp->topic().filter()); newSubscription->setMQTTClient(this); qDebug()<<"Add child"; addChild(newSubscription); qDebug()<<"Add to vector"; m_mqttSubscriptions.push_back(newSubscription); } - connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::mqttSubscribtionMessageReceived); + connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::mqttSubscriptionMessageReceived); qDebug()<<"Added topic"; } } m_mqttFirstConnectEstablished = true; //Signal that the initial subscriptions were made emit mqttSubscribed(); } //if there was already a connection made(happens after updating will message) else { qDebug() << "Resubscribing after will set"; //Only the client has to make the subscriptions again, every other connected data is still avialable QMapIterator i(m_subscribedTopicNameQoS); while(i.hasNext()) { i.next(); QMqttSubscription *temp = m_client->subscribe(i.key(), i.value()); if(temp) { qDebug()<topic()<<" "<qos(); - connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::mqttSubscribtionMessageReceived); + connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::mqttSubscriptionMessageReceived); } else qDebug()<<"Couldn't subscribe after will change"; } } } } /*! *\brief called when a message is received by a topic belonging to one of subscriptions of the client. * It passes the message to the appropriate MQTTSubscription which will pass it to the appropriate MQTTTopic */ -void MQTTClient::mqttSubscribtionMessageReceived(const QMqttMessage& msg) { +void MQTTClient::mqttSubscriptionMessageReceived(const QMqttMessage& msg) { //Decide to interpret retain message or not if(!msg.retain() || (msg.retain() && m_mqttRetain) ) { qDebug()<<"message received from "<subscriptionName(), msg.topic().name())) { m_mqttSubscriptions[i]->messageArrived(QString(msg.payload()), msg.topic().name()); break; } } //if the message was received by the will topic, update the last message received by it if(msg.topic().name() == m_willTopic) m_willLastMessage = QString(msg.payload()); } } /*! *\brief Handles some of the possible errors of the client, using MQTTErrorWidget */ void MQTTClient::mqttErrorChanged(QMqttClient::ClientError clientError) { if(clientError != QMqttClient::ClientError::NoError) { MQTTErrorWidget* errorWidget = new MQTTErrorWidget(clientError, this); errorWidget->show(); } } /*! *\brief Called when a subscription is loaded. * Checks whether every saved subscription was loaded or not. * If everything is loaded, it makes the conneciton and starts the reading * * \param name, the name of the subscription */ void MQTTClient::subscriptionLoaded(const QString &name) { qDebug()<writeStartElement("MQTTClient"); writeBasicAttributes(writer); writeCommentElement(writer); //general writer->writeStartElement("general"); writer->writeAttribute("subscriptionCount", QString::number(m_mqttSubscriptions.size())); writer->writeAttribute("updateType", QString::number(m_updateType)); writer->writeAttribute("readingType", QString::number(m_readingType)); writer->writeAttribute("keepValues", QString::number(m_keepNValues)); if (m_updateType == TimeInterval) writer->writeAttribute("updateInterval", QString::number(m_updateInterval)); if (m_readingType != TillEnd) writer->writeAttribute("sampleSize", QString::number(m_sampleSize)); writer->writeAttribute("host", m_client->hostname()); writer->writeAttribute("port", QString::number(m_client->port())); writer->writeAttribute("username", m_client->username()); writer->writeAttribute("pasword", m_client->password()); writer->writeAttribute("clientId", m_client->clientId()); writer->writeAttribute("useRetain", QString::number(m_mqttRetain)); writer->writeAttribute("useWill", QString::number(m_mqttUseWill)); writer->writeAttribute("willTopic", m_willTopic); writer->writeAttribute("willOwnMessage", m_willOwnMessage); writer->writeAttribute("willQoS", QString::number(m_willQoS)); writer->writeAttribute("willRetain", QString::number(m_willRetain)); writer->writeAttribute("willMessageType", QString::number(static_cast(m_willMessageType))); writer->writeAttribute("willUpdateType", QString::number(static_cast(m_willUpdateType))); writer->writeAttribute("willTimeInterval", QString::number(m_willTimeInterval)); for( int i = 0; i < m_willStatistics.count(); ++i){ writer->writeAttribute("willStatistics"+QString::number(i), QString::number(m_willStatistics[i])); } writer->writeAttribute("useID", QString::number(m_mqttUseID)); writer->writeAttribute("useAuthentication", QString::number(m_mqttUseAuthentication)); writer->writeEndElement(); //filter m_filter->save(writer); //MQTTSubscription for(auto* sub : children(IncludeHidden)) sub->save(writer); writer->writeEndElement(); // "MQTTClient" } /*! Loads from XML. */ bool MQTTClient::load(XmlStreamReader* reader, bool preview) { qDebug()<<"Start loading MQTTClient"; if (!readBasicAttributes(reader)) return false; QString attributeWarning = i18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs; QString str; while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement() && reader->name() == "MQTTClient") break; if (!reader->isStartElement()) continue; if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (reader->name() == "general") { qDebug()<<"MQTTClient general"; attribs = reader->attributes(); str = attribs.value("subscriptionCount").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'subscriptionCount'")); else m_subscriptionCountToLoad = str.toInt(); str = attribs.value("keepValues").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'keepValues'")); else m_keepNValues = str.toInt(); str = attribs.value("updateType").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'updateType'")); else m_updateType = static_cast(str.toInt()); str = attribs.value("readingType").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'readingType'")); else m_readingType = static_cast(str.toInt()); if (m_updateType == TimeInterval) { str = attribs.value("updateInterval").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'updateInterval'")); else m_updateInterval = str.toInt(); } if (m_readingType != TillEnd) { str = attribs.value("sampleSize").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'sampleSize'")); else m_sampleSize = str.toInt(); } str = attribs.value("host").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'host'")); else m_client->setHostname(str); str =attribs.value("port").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'port'")); else m_client->setPort(str.toUInt()); str = attribs.value("useAuthentication").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'useAuthentication'")); else m_mqttUseAuthentication = str.toInt(); if(m_mqttUseAuthentication) { str =attribs.value("username").toString(); if(!str.isEmpty()) m_client->setUsername(str); str =attribs.value("password").toString(); if(!str.isEmpty()) m_client->setPassword(str); } str = attribs.value("useID").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'useID'")); else m_mqttUseID = str.toInt(); if(m_mqttUseID) { str =attribs.value("clientId").toString(); if(!str.isEmpty()) m_client->setClientId(str); } str =attribs.value("useRetain").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'useRetain'")); else m_mqttRetain = str.toInt(); str =attribs.value("useWill").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'useWill'")); else m_mqttUseWill = str.toInt(); if(m_mqttUseWill) { str =attribs.value("willTopic").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willTopic'")); else m_willTopic = str; str =attribs.value("willOwnMessage").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willOwnMessage'")); else m_willOwnMessage = str; str =attribs.value("willQoS").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willQoS'")); else m_willQoS = str.toUInt(); str =attribs.value("willRetain").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willRetain'")); else m_willRetain = str.toInt(); str =attribs.value("willMessageType").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willMessageType'")); else m_willMessageType = static_cast(str.toInt()); str =attribs.value("willUpdateType").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willUpdateType'")); else m_willUpdateType = static_cast(str.toInt()); str =attribs.value("willTimeInterval").toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willTimeInterval'")); else m_willTimeInterval = str.toInt(); for( int i = 0; i < m_willStatistics.count(); ++i){ str =attribs.value("willStatistics"+QString::number(i)).toString(); if(str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willTimeInterval'")); else m_willStatistics[i] = str.toInt(); } } } else if (reader->name() == "asciiFilter") { qDebug()<<"load filter"; m_filter = new AsciiFilter(); if (!m_filter->load(reader)) return false; } else if(reader->name() == "MQTTSubscription") { qDebug()<<"Load MQTTSubscription"; MQTTSubscription* subscription = new MQTTSubscription(""); subscription->setMQTTClient(this); connect(subscription, &MQTTSubscription::loaded, this, &MQTTClient::subscriptionLoaded); if (!subscription->load(reader, preview)) { delete subscription; return false; } m_mqttSubscriptions.push_back(subscription); addChildFast(subscription); } else {// unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } return !reader->hasError(); } #endif diff --git a/src/backend/datasources/MQTTClient.h b/src/backend/datasources/MQTTClient.h index 3e25c1d85..44d17d1f9 100644 --- a/src/backend/datasources/MQTTClient.h +++ b/src/backend/datasources/MQTTClient.h @@ -1,240 +1,240 @@ #ifndef MQTTCLIENT_H #define MQTTCLIENT_H #ifdef HAVE_MQTT #include "backend/core/Folder.h" #include #include #include #include #include #include #include #include class QString; class AbstractFileFilter; class MQTTSubscription; class QAction; class MQTTClient : public Folder{ Q_OBJECT public: enum UpdateType { TimeInterval = 0, NewData }; enum ReadingType { ContinuousFixed = 0, FromEnd, TillEnd }; enum WillMessageType { OwnMessage = 0, Statistics, LastMessage }; enum WillUpdateType { TimePeriod = 0, OnClick }; enum WillStatistics { Minimum = 0, Maximum, ArithmeticMean, GeometricMean, HarmonicMean, ContraharmonicMean, Median, Variance, StandardDeviation, MeanDeviation, MeanDeviationAroundMedian, MedianDeviation, Skewness, Kurtosis, Entropy }; explicit MQTTClient(const QString& name); virtual ~MQTTClient() override; void ready(); UpdateType updateType() const; void setUpdateType(UpdateType); ReadingType readingType() const; void setReadingType(ReadingType); int sampleSize() const; void setSampleSize(int); bool isPaused() const; void setUpdateInterval(int); int updateInterval() const; void setKeepNValues(int); int keepNValues() const; void setKeepLastValues(bool); bool keepLastValues() const; void setMqttClientHostPort(const QString&, const quint16&); void setMqttClientAuthentication(const QString&, const QString&); void setMqttClientId(const QString&); QMqttClient mqttClient() const; void addInitialMqttSubscriptions(const QMqttTopicFilter&, const quint8&); QVector mqttSubscriptions() const; bool checkTopicContains(const QString& superior, const QString& inferior); QString checkCommonLevel(const QString& first, const QString& second); QString clientHostName() const; quint16 clientPort() const; QString clientPassword() const; QString clientUserName() const; QString clientID () const; void updateNow(); void pauseReading(); void continueReading(); void setFilter(AbstractFileFilter*); AbstractFileFilter* filter() const; QIcon icon() const override; void save(QXmlStreamWriter*) const override; bool load(XmlStreamReader*, bool preview) override; QVector topicNames() const; bool checkAllArrived(); void setMqttWillUse(bool); bool mqttWillUse() const; void setWillTopic(const QString&); QString willTopic() const; void setWillRetain(bool); bool willRetain() const; void setWillQoS(quint8); quint8 willQoS() const; void setWillMessageType(WillMessageType); WillMessageType willMessageType() const; void setWillOwnMessage(const QString&); QString willOwnMessage() const; WillUpdateType willUpdateType() const; void setWillUpdateType(WillUpdateType); int willTimeInterval() const; void setWillTimeInterval(int); void startWillTimer() const; void stopWillTimer() const; void updateWillMessage() ; void setMqttRetain(bool); bool mqttRetain() const; void setMQTTUseID(bool); bool mqttUseID() const; void setMQTTUseAuthentication(bool); bool mqttUseAuthentication() const; void clearLastMessage(); void addWillStatistics(WillStatistics); void removeWillStatistics(WillStatistics); QVector willStatistics() const; void addMQTTSubscription(const QString&, quint8); void removeMQTTSubscription(const QString&); void addBeforeRemoveSubscription(const QString&, quint8); void reparentTopic(const QString& topic, const QString& parent); private: //void initActions(); UpdateType m_updateType; ReadingType m_readingType; bool m_paused; bool m_prepared; bool m_keepLastValues; int m_sampleSize; int m_keepNValues; int m_updateInterval; AbstractFileFilter* m_filter; QTimer* m_updateTimer; /* QAction* m_reloadAction; QAction* m_toggleLinkAction; QAction* m_showEditorAction; QAction* m_showSpreadsheetAction; QAction* m_plotDataAction;*/ QMqttClient* m_client; QMap m_subscribedTopicNameQoS; QVector m_subscriptions; QVector m_topicNames; bool m_mqttTest; bool m_mqttUseWill; QString m_willMessage; QString m_willTopic; bool m_willRetain; quint8 m_willQoS; WillMessageType m_willMessageType; QString m_willOwnMessage; QString m_willLastMessage; QTimer* m_willTimer; int m_willTimeInterval; WillUpdateType m_willUpdateType; QVector m_willStatistics; bool m_mqttFirstConnectEstablished; bool m_mqttRetain; bool m_mqttUseID; bool m_mqttUseAuthentication; QVector m_mqttSubscriptions; bool m_disconnectForWill; bool m_loaded; int m_subscriptionsLoaded; int m_subscriptionCountToLoad; public slots: void read(); private slots: void onMqttConnect(); - void mqttSubscribtionMessageReceived(const QMqttMessage&); + void mqttSubscriptionMessageReceived(const QMqttMessage&); void mqttErrorChanged(QMqttClient::ClientError); void subscriptionLoaded(const QString&); signals: void mqttSubscribed(); void mqttTopicsChanged(); void readFromTopics(); void clientAboutToBeDeleted(const QString&); }; #endif #endif // MQTTCLIENT_H diff --git a/src/backend/datasources/filters/AsciiFilter.cpp b/src/backend/datasources/filters/AsciiFilter.cpp index 0fe090f39..feb0fa15f 100644 --- a/src/backend/datasources/filters/AsciiFilter.cpp +++ b/src/backend/datasources/filters/AsciiFilter.cpp @@ -1,2984 +1,2988 @@ /*************************************************************************** File : AsciiFilter.cpp Project : LabPlot Description : ASCII I/O-filter -------------------------------------------------------------------- Copyright : (C) 2009-2018 Stefan Gerlach (stefan.gerlach@uni.kn) Copyright : (C) 2009-2017 Alexander Semke (alexander.semke@web.de) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301 USA * * * ***************************************************************************/ #include "backend/datasources/LiveDataSource.h" #include "backend/core/column/Column.h" #include "backend/core/Project.h" #include "backend/datasources/filters/AsciiFilter.h" #include "backend/datasources/filters/AsciiFilterPrivate.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "backend/worksheet/plots/cartesian/XYCurve.h" #include "backend/lib/macros.h" #include "backend/lib/trace.h" #ifdef HAVE_MQTT #include "backend/datasources/MQTTClient.h" #include "backend/datasources/MQTTTopic.h" #endif #include #include #include #include #include /*! \class AsciiFilter \brief Manages the import/export of data organized as columns (vectors) from/to an ASCII-file. \ingroup datasources */ AsciiFilter::AsciiFilter() : AbstractFileFilter(), d(new AsciiFilterPrivate(this)) {} AsciiFilter::~AsciiFilter() {} /*! reads the content of the device \c device. */ void AsciiFilter::readDataFromDevice(QIODevice& device, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode, int lines) { d->readDataFromDevice(device, dataSource, importMode, lines); } void AsciiFilter::readFromLiveDeviceNotFile(QIODevice &device, AbstractDataSource* dataSource) { d->readFromLiveDevice(device, dataSource); } qint64 AsciiFilter::readFromLiveDevice(QIODevice& device, AbstractDataSource* dataSource, qint64 from) { return d->readFromLiveDevice(device, dataSource, from); } #ifdef HAVE_MQTT /*! reads the content of a message received by the topic. */ void AsciiFilter::readMQTTTopic(const QString& message, const QString& topic, AbstractDataSource*dataSource) { d->readMQTTTopic(message, topic, dataSource); } /*! provides a preview of the data received by the topic. */ void AsciiFilter::mqttPreview(QVector& list, const QString& message, const QString& topic) { d->mqttPreview(list, message, topic); } /*! Returns the statistical data, that the MQTTTopic needs for the will message. */ QString AsciiFilter::mqttColumnStatistics(const MQTTTopic* topic) const{ return d->mqttColumnStatistics(topic); } /*! Returns the column mode of the last column (the value column of the MQTTTopic). */ AbstractColumn::ColumnMode AsciiFilter::mqttColumnMode() const{ return d->mqttColumnMode(); } /*! After the MQTTTopic is loaded, prepares the filter for reading. */ void AsciiFilter::setPreparedForMQTT(bool prepared, MQTTTopic* topic, const QString& separator) { d->setPreparedForMQTT(prepared, topic, separator); } #endif /*! returns the separator used by the filter. */ QString AsciiFilter::separator() const { return d->separator(); } /*! returns the separator used by the filter. */ int AsciiFilter::isPrepared() { return d->isPrepared(); } /*! reads the content of the file \c fileName. */ void AsciiFilter::readDataFromFile(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode) { d->readDataFromFile(fileName, dataSource, importMode); } QVector AsciiFilter::preview(const QString& fileName, int lines) { return d->preview(fileName, lines); } QVector AsciiFilter::preview(QIODevice &device) { return d->preview(device); } /*! reads the content of the file \c fileName to the data source \c dataSource. */ //void AsciiFilter::read(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode) { // d->read(fileName, dataSource, importMode); //} /*! writes the content of the data source \c dataSource to the file \c fileName. */ void AsciiFilter::write(const QString& fileName, AbstractDataSource* dataSource) { d->write(fileName, dataSource); // emit() } /*! loads the predefined filter settings for \c filterName */ void AsciiFilter::loadFilterSettings(const QString& filterName) { Q_UNUSED(filterName); } /*! saves the current settings as a new filter with the name \c filterName */ void AsciiFilter::saveFilterSettings(const QString& filterName) const { Q_UNUSED(filterName); } /*! returns the list with the names of all saved (system wide or user defined) filter settings. */ QStringList AsciiFilter::predefinedFilters() { return QStringList(); } /*! returns the list of all predefined separator characters. */ QStringList AsciiFilter::separatorCharacters() { return (QStringList() << "auto" << "TAB" << "SPACE" << "," << ";" << ":" << ",TAB" << ";TAB" << ":TAB" << ",SPACE" << ";SPACE" << ":SPACE" << "2xSPACE" << "3xSPACE" << "4xSPACE" << "2xTAB"); } /*! returns the list of all predefined comment characters. */ QStringList AsciiFilter::commentCharacters() { return (QStringList() << "#" << "!" << "//" << "+" << "c" << ":" << ";"); } /*! returns the list of all predefined data types. */ QStringList AsciiFilter::dataTypes() { const QMetaObject& mo = AbstractColumn::staticMetaObject; const QMetaEnum& me = mo.enumerator(mo.indexOfEnumerator("ColumnMode")); QStringList list; for (int i = 0; i <= 100; ++i) // me.keyCount() does not work because we have holes in enum if (me.valueToKey(i)) list << me.valueToKey(i); return list; } QString AsciiFilter::fileInfoString(const QString& fileName) { QString info(i18n("Number of columns: %1", AsciiFilter::columnNumber(fileName))); info += QLatin1String("
"); info += i18n("Number of lines: %1", AsciiFilter::lineNumber(fileName)); return info; } /*! returns the number of columns in the file \c fileName. */ int AsciiFilter::columnNumber(const QString& fileName, const QString& separator) { KFilterDev device(fileName); if (!device.open(QIODevice::ReadOnly)) { DEBUG("Could not open file " << fileName.toStdString() << " for determining number of columns"); return -1; } QString line = device.readLine(); line.remove(QRegExp("[\\n\\r]")); QStringList lineStringList; if (separator.length() > 0) lineStringList = line.split(separator); else lineStringList = line.split(QRegExp("\\s+")); DEBUG("number of columns : " << lineStringList.size()); return lineStringList.size(); } size_t AsciiFilter::lineNumber(const QString& fileName) { KFilterDev device(fileName); if (!device.open(QIODevice::ReadOnly)) { DEBUG("Could not open file " << fileName.toStdString() << " to determine number of lines"); return 0; } if (!device.canReadLine()) return -1; size_t lineCount = 0; while (!device.atEnd()) { device.readLine(); lineCount++; } //TODO: wc is much faster but not portable /* QElapsedTimer myTimer; myTimer.start(); QProcess wc; wc.start(QString("wc"), QStringList() << "-l" << fileName); size_t lineCount = 0; while (wc.waitForReadyRead()) lineCount = wc.readLine().split(' ')[0].toInt(); lineCount++; // last line not counted DEBUG(" Elapsed time counting lines : " << myTimer.elapsed() << " ms"); */ return lineCount; } /*! returns the number of lines in the device \c device and 0 if sequential. resets the position to 0! */ size_t AsciiFilter::lineNumber(QIODevice &device) { if (device.isSequential()) return 0; if (!device.canReadLine()) DEBUG("WARNING in AsciiFilter::lineNumber(): device cannot 'readLine()' but using it anyway."); size_t lineCount = 0; device.seek(0); while (!device.atEnd()) { device.readLine(); lineCount++; } device.seek(0); return lineCount; } void AsciiFilter::setCommentCharacter(const QString& s) { d->commentCharacter = s; } QString AsciiFilter::commentCharacter() const { return d->commentCharacter; } void AsciiFilter::setSeparatingCharacter(const QString& s) { d->separatingCharacter = s; } QString AsciiFilter::separatingCharacter() const { return d->separatingCharacter; } void AsciiFilter::setDateTimeFormat(const QString &f) { d->dateTimeFormat = f; } QString AsciiFilter::dateTimeFormat() const { return d->dateTimeFormat; } void AsciiFilter::setNumberFormat(QLocale::Language lang) { d->numberFormat = lang; } QLocale::Language AsciiFilter::numberFormat() const { return d->numberFormat; } void AsciiFilter::setAutoModeEnabled(const bool b) { d->autoModeEnabled = b; } bool AsciiFilter::isAutoModeEnabled() const { return d->autoModeEnabled; } void AsciiFilter::setHeaderEnabled(const bool b) { d->headerEnabled = b; } bool AsciiFilter::isHeaderEnabled() const { return d->headerEnabled; } void AsciiFilter::setSkipEmptyParts(const bool b) { d->skipEmptyParts = b; } bool AsciiFilter::skipEmptyParts() const { return d->skipEmptyParts; } void AsciiFilter::setCreateIndexEnabled(bool b) { d->createIndexEnabled = b; } bool AsciiFilter::createIndexEnabled() const{ return d->createIndexEnabled; } void AsciiFilter::setSimplifyWhitespacesEnabled(bool b) { d->simplifyWhitespacesEnabled = b; } bool AsciiFilter::simplifyWhitespacesEnabled() const { return d->simplifyWhitespacesEnabled; } void AsciiFilter::setNaNValueToZero(bool b) { if (b) d->nanValue = 0; else d->nanValue = NAN; } bool AsciiFilter::NaNValueToZeroEnabled() const { if (d->nanValue == 0) return true; return false; } void AsciiFilter::setRemoveQuotesEnabled(bool b) { d->removeQuotesEnabled = b; } bool AsciiFilter::removeQuotesEnabled() const { return d->removeQuotesEnabled; } void AsciiFilter::setVectorNames(const QString& s) { d->vectorNames.clear(); if (!s.simplified().isEmpty()) d->vectorNames = s.simplified().split(' '); } QStringList AsciiFilter::vectorNames() const { return d->vectorNames; } QVector AsciiFilter::columnModes() { return d->columnModes; } void AsciiFilter::setStartRow(const int r) { d->startRow = r; } int AsciiFilter::startRow() const { return d->startRow; } void AsciiFilter::setEndRow(const int r) { d->endRow = r; } int AsciiFilter::endRow() const { return d->endRow; } void AsciiFilter::setStartColumn(const int c) { d->startColumn = c; } int AsciiFilter::startColumn() const { return d->startColumn; } void AsciiFilter::setEndColumn(const int c) { d->endColumn = c; } int AsciiFilter::endColumn() const { return d->endColumn; } //##################################################################### //################### Private implementation ########################## //##################################################################### AsciiFilterPrivate::AsciiFilterPrivate(AsciiFilter* owner) : q(owner), commentCharacter("#"), separatingCharacter("auto"), numberFormat(QLocale::C), autoModeEnabled(true), headerEnabled(true), skipEmptyParts(false), simplifyWhitespacesEnabled(true), nanValue(NAN), removeQuotesEnabled(false), createIndexEnabled(false), startRow(1), endRow(-1), startColumn(1), endColumn(-1), m_actualStartRow(1), m_actualRows(0), m_maxActualRows (0), m_actualCols(0), m_prepared(false), m_lastRowNum (0), mqttPreviewFirstEmptyColCount (0), m_columnOffset(0) { } /*! * get a single line from device */ QStringList AsciiFilterPrivate::getLineString(QIODevice& device) { QString line; do { // skip comment lines in data lines if (!device.canReadLine()) DEBUG("WARNING in AsciiFilterPrivate::getLineString(): device cannot 'readLine()' but using it anyway."); // line = device.readAll(); line = device.readLine(); } while (line.startsWith(commentCharacter)); line.remove(QRegExp("[\\n\\r]")); // remove any newline if (simplifyWhitespacesEnabled) line = line.simplified(); DEBUG("data line : \'" << line.toStdString() << '\''); QStringList lineStringList = line.split(m_separator, (QString::SplitBehavior)skipEmptyParts); //TODO: remove quotes here? QDEBUG("data line, parsed: " << lineStringList); return lineStringList; } /*! * returns -1 if the device couldn't be opened, 1 if the current read position in the device is at the end and 0 otherwise. */ int AsciiFilterPrivate::prepareDeviceToRead(QIODevice& device) { DEBUG("AsciiFilterPrivate::prepareDeviceToRead(): is sequential = " << device.isSequential() << ", can readLine = " << device.canReadLine()); if (!device.open(QIODevice::ReadOnly)) return -1; if (device.atEnd() && !device.isSequential()) // empty file return 1; ///////////////////////////////////////////////////////////////// // Find first data line (ignoring comment lines) DEBUG(" Skipping " << startRow - 1 << " lines"); for (int i = 0; i < startRow - 1; ++i) { QString line; if (!device.canReadLine()) DEBUG("WARNING in AsciiFilterPrivate::prepareDeviceToRead(): device cannot 'readLine()' but using it anyway."); line = device.readLine(); DEBUG(" line = " << line.toStdString()); if (device.atEnd()) { if (device.isSequential()) break; else return 1; } } // Parse the first line: // Determine the number of columns, create the columns and use (if selected) the first row to name them QString firstLine; do { // skip comment lines if (!device.canReadLine()) DEBUG("WARNING in AsciiFilterPrivate::prepareDeviceToRead(): device cannot 'readLine()' but using it anyway."); firstLine = device.readLine(); if (device.atEnd()) { if (device.isSequential()) break; else return 1; } } while (firstLine.startsWith(commentCharacter)); DEBUG(" device position after first line and comments = " << device.pos()); firstLine.remove(QRegExp("[\\n\\r]")); // remove any newline if (simplifyWhitespacesEnabled) firstLine = firstLine.simplified(); DEBUG("First line: \'" << firstLine.toStdString() << '\''); // determine separator and split first line QStringList firstLineStringList; if (separatingCharacter == "auto") { DEBUG("automatic separator"); QRegExp regExp("(\\s+)|(,\\s+)|(;\\s+)|(:\\s+)"); firstLineStringList = firstLine.split(regExp, (QString::SplitBehavior)skipEmptyParts); if (!firstLineStringList.isEmpty()) { int length1 = firstLineStringList.at(0).length(); if (firstLineStringList.size() > 1) { int pos2 = firstLine.indexOf(firstLineStringList.at(1), length1); m_separator = firstLine.mid(length1, pos2 - length1); } else { //old: separator = line.right(line.length() - length1); m_separator = ' '; } } } else { // use given separator // replace symbolic "TAB" with '\t' m_separator = separatingCharacter.replace(QLatin1String("2xTAB"), "\t\t", Qt::CaseInsensitive); m_separator = separatingCharacter.replace(QLatin1String("TAB"), "\t", Qt::CaseInsensitive); // replace symbolic "SPACE" with ' ' m_separator = m_separator.replace(QLatin1String("2xSPACE"), QLatin1String(" "), Qt::CaseInsensitive); m_separator = m_separator.replace(QLatin1String("3xSPACE"), QLatin1String(" "), Qt::CaseInsensitive); m_separator = m_separator.replace(QLatin1String("4xSPACE"), QLatin1String(" "), Qt::CaseInsensitive); m_separator = m_separator.replace(QLatin1String("SPACE"), QLatin1String(" "), Qt::CaseInsensitive); firstLineStringList = firstLine.split(m_separator, (QString::SplitBehavior)skipEmptyParts); } DEBUG("separator: \'" << m_separator.toStdString() << '\''); DEBUG("number of columns: " << firstLineStringList.size()); QDEBUG("first line: " << firstLineStringList); DEBUG("headerEnabled: " << headerEnabled); //optionally, remove potential spaces in the first line if (simplifyWhitespacesEnabled) { for (int i = 0; i < firstLineStringList.size(); ++i) firstLineStringList[i] = firstLineStringList[i].simplified(); } if (headerEnabled) { // use first line to name vectors vectorNames = firstLineStringList; QDEBUG("vector names =" << vectorNames); m_actualStartRow = startRow + 1; } else m_actualStartRow = startRow; // set range to read if (endColumn == -1) { if (headerEnabled || vectorNames.size() == 0) endColumn = firstLineStringList.size(); // last column else //number of vector names provided in the import dialog (not more than the maximal number of columns in the file) endColumn = qMin(vectorNames.size(), firstLineStringList.size()); } if (createIndexEnabled) { vectorNames.prepend(i18n("Index")); endColumn++; } m_actualCols = endColumn - startColumn + 1; //TEST: readline-seek-readline fails /* qint64 testpos = device.pos(); DEBUG("read data line @ pos " << testpos << " : " << device.readLine().toStdString()); device.seek(testpos); testpos = device.pos(); DEBUG("read data line again @ pos " << testpos << " : " << device.readLine().toStdString()); */ ///////////////////////////////////////////////////////////////// // parse first data line to determine data type for each column if (!device.isSequential()) firstLineStringList = getLineString(device); columnModes.resize(m_actualCols); int col = 0; if (createIndexEnabled) { columnModes[0] = AbstractColumn::Integer; col = 1; } for (auto& valueString: firstLineStringList) { // parse columns available in first data line if (simplifyWhitespacesEnabled) valueString = valueString.simplified(); if (col == m_actualCols) break; columnModes[col++] = AbstractFileFilter::columnMode(valueString, dateTimeFormat, numberFormat); } // parsing more lines to better determine data types for (unsigned int i = 0; i < m_dataTypeLines; ++i) { if (device.atEnd()) // EOF reached break; firstLineStringList = getLineString(device); if (createIndexEnabled) col = 1; else col = 0; for (auto& valueString: firstLineStringList) { if (simplifyWhitespacesEnabled) valueString = valueString.simplified(); if (col == m_actualCols) break; AbstractColumn::ColumnMode mode = AbstractFileFilter::columnMode(valueString, dateTimeFormat, numberFormat); // numeric: integer -> numeric if (mode == AbstractColumn::Numeric && columnModes[col] == AbstractColumn::Integer) columnModes[col] = mode; // text: non text -> text if (mode == AbstractColumn::Text && columnModes[col] != AbstractColumn::Text) columnModes[col] = mode; col++; } } QDEBUG("column modes = " << columnModes); // ATTENTION: This resets the position in the device to 0 m_actualRows = (int)AsciiFilter::lineNumber(device); const int actualEndRow = (endRow == -1 || endRow > m_actualRows) ? m_actualRows : endRow; m_actualRows = actualEndRow - m_actualStartRow + 1; DEBUG("start/end column: " << startColumn << ' ' << endColumn); DEBUG("start/end row: " << m_actualStartRow << ' ' << actualEndRow); DEBUG("actual cols/rows (w/o header): " << m_actualCols << ' ' << m_actualRows); if (m_actualRows == 0 && !device.isSequential()) return 1; return 0; } /*! reads the content of the file \c fileName to the data source \c dataSource. Uses the settings defined in the data source. */ void AsciiFilterPrivate::readDataFromFile(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode) { DEBUG("AsciiFilterPrivate::readDataFromFile(): fileName = \'" << fileName.toStdString() << "\', dataSource = " << dataSource << ", mode = " << ENUM_TO_STRING(AbstractFileFilter, ImportMode, importMode)); KFilterDev device(fileName); readDataFromDevice(device, dataSource, importMode); } qint64 AsciiFilterPrivate::readFromLiveDevice(QIODevice& device, AbstractDataSource* dataSource, qint64 from) { DEBUG("AsciiFilterPrivate::readFromLiveDevice(): bytes available = " << device.bytesAvailable() << ", from = " << from); if (device.bytesAvailable() <= 0) { DEBUG(" No new data available"); return 0; } // may be also a matrix? LiveDataSource* spreadsheet = dynamic_cast(dataSource); if (spreadsheet->sourceType() != LiveDataSource::SourceType::FileOrPipe) if (device.isSequential() && device.bytesAvailable() < (int)sizeof(quint16)) return 0; if (!m_prepared) { DEBUG("Preparing .."); switch (spreadsheet->sourceType()) { case LiveDataSource::SourceType::FileOrPipe: { const int deviceError = prepareDeviceToRead(device); if (deviceError != 0) { DEBUG("Device error = " << deviceError); return 0; } break; } case LiveDataSource::SourceType::NetworkTcpSocket: case LiveDataSource::SourceType::NetworkUdpSocket: case LiveDataSource::SourceType::LocalSocket: case LiveDataSource::SourceType::SerialPort: m_actualRows = 1; if (createIndexEnabled) { m_actualCols = 2; columnModes << AbstractColumn::Integer << AbstractColumn::Numeric; vectorNames << i18n("Index") << i18n("Value"); } else { m_actualCols = 1; columnModes << AbstractColumn::Numeric; vectorNames << i18n("Value"); } QDEBUG(" vector names = " << vectorNames); } // prepare import for spreadsheet spreadsheet->setUndoAware(false); spreadsheet->resize(AbstractFileFilter::Replace, vectorNames, m_actualCols); DEBUG(" data source resized to col: " << m_actualCols); DEBUG(" data source rowCount: " << spreadsheet->rowCount()); //columns in a file data source don't have any manual changes. //make the available columns undo unaware and suppress the "data changed" signal. //data changes will be propagated via an explicit Column::setChanged() call once new data was read. for (int i = 0; i < spreadsheet->childCount(); i++) { spreadsheet->child(i)->setUndoAware(false); spreadsheet->child(i)->setSuppressDataChangedSignal(true); } int keepNValues = spreadsheet->keepNValues(); if (keepNValues == 0) spreadsheet->setRowCount(m_actualRows > 1 ? m_actualRows : 1); else { spreadsheet->setRowCount(keepNValues); m_actualRows = keepNValues; } m_dataContainer.resize(m_actualCols); DEBUG(" Setting data .."); for (int n = 0; n < m_actualCols; ++n) { // data() returns a void* which is a pointer to any data type (see ColumnPrivate.cpp) spreadsheet->child(n)->setColumnMode(columnModes[n]); switch (columnModes[n]) { case AbstractColumn::Numeric: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Integer: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Text: { QVector* vector = static_cast*>(spreadsheet->child(n)->data()); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::DateTime: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } //TODO case AbstractColumn::Month: case AbstractColumn::Day: break; } } DEBUG("Prepared!"); } qint64 bytesread = 0; #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportTotal: "); #endif LiveDataSource::ReadingType readingType; if (!m_prepared) { readingType = LiveDataSource::ReadingType::TillEnd; } else { //we have to read all the data when reading from end //so we set readingType to TillEnd if (spreadsheet->readingType() == LiveDataSource::ReadingType::FromEnd) readingType = LiveDataSource::ReadingType::TillEnd; //if we read the whole file we just start from the beginning of it //and read till end else if (spreadsheet->readingType() == LiveDataSource::ReadingType::WholeFile) readingType = LiveDataSource::ReadingType::TillEnd; else readingType = spreadsheet->readingType(); } DEBUG(" Reading type = " << ENUM_TO_STRING(LiveDataSource, ReadingType, readingType)); //move to the last read position, from == total bytes read //since the other source types are sequencial we cannot seek on them if (spreadsheet->sourceType() == LiveDataSource::SourceType::FileOrPipe) device.seek(from); //count the new lines, increase actualrows on each //now we read all the new lines, if we want to use sample rate //then here we can do it, if we have actually sample rate number of lines :-? int newLinesForSampleSizeNotTillEnd = 0; int newLinesTillEnd = 0; QVector newData; if (readingType != LiveDataSource::ReadingType::TillEnd) newData.resize(spreadsheet->sampleSize()); int newDataIdx = 0; { #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportReadingFromFile: "); #endif while (!device.atEnd()) { DEBUG(" source type = " << ENUM_TO_STRING(LiveDataSource, SourceType, spreadsheet->sourceType())); if (readingType != LiveDataSource::ReadingType::TillEnd) { switch (spreadsheet->sourceType()) { // different sources need different read methods case LiveDataSource::SourceType::LocalSocket: newData[newDataIdx++] = device.readAll(); break; case LiveDataSource::SourceType::NetworkUdpSocket: newData[newDataIdx++] = device.read(device.bytesAvailable()); break; case LiveDataSource::SourceType::FileOrPipe: newData.push_back(device.readLine()); break; case LiveDataSource::SourceType::NetworkTcpSocket: //TODO: check serial port case LiveDataSource::SourceType::SerialPort: newData[newDataIdx++] = device.read(device.bytesAvailable()); } } else { // ReadingType::TillEnd switch (spreadsheet->sourceType()) { // different sources need different read methods case LiveDataSource::SourceType::LocalSocket: newData.push_back(device.readAll()); break; case LiveDataSource::SourceType::NetworkUdpSocket: newData.push_back(device.read(device.bytesAvailable())); break; case LiveDataSource::SourceType::FileOrPipe: newData.push_back(device.readLine()); break; case LiveDataSource::SourceType::NetworkTcpSocket: //TODO: check serial port case LiveDataSource::SourceType::SerialPort: newData.push_back(device.read(device.bytesAvailable())); } } newLinesTillEnd++; if (readingType != LiveDataSource::ReadingType::TillEnd) { newLinesForSampleSizeNotTillEnd++; //for Continuous reading and FromEnd we read sample rate number of lines if possible //here TillEnd and Whole file behave the same if (newLinesForSampleSizeNotTillEnd == spreadsheet->sampleSize()) break; } } QDEBUG(" data read: " << newData); } //now we reset the readingType if (spreadsheet->readingType() == LiveDataSource::ReadingType::FromEnd) readingType = spreadsheet->readingType(); //we had less new lines than the sample size specified if (readingType != LiveDataSource::ReadingType::TillEnd) QDEBUG("Removed empty lines: " << newData.removeAll("")); //back to the last read position before counting when reading from files if (spreadsheet->sourceType() == LiveDataSource::SourceType::FileOrPipe) device.seek(from); const int spreadsheetRowCountBeforeResize = spreadsheet->rowCount(); int currentRow = 0; // indexes the position in the vector(column) int linesToRead = 0; int keepNValues = spreadsheet->keepNValues(); DEBUG("Increase row count"); if (m_prepared) { //increase row count if we don't have a fixed size //but only after the preparation step if (keepNValues == 0) { if (readingType != LiveDataSource::ReadingType::TillEnd) m_actualRows += qMin(newData.size(), spreadsheet->sampleSize()); else { //we don't increase it if we reread the whole file, we reset it if (!(spreadsheet->readingType() == LiveDataSource::ReadingType::WholeFile)) m_actualRows += newData.size(); else m_actualRows = newData.size(); } //appending if (spreadsheet->readingType() == LiveDataSource::ReadingType::WholeFile) linesToRead = m_actualRows; else linesToRead = m_actualRows - spreadsheetRowCountBeforeResize; } else { // fixed size if (readingType == LiveDataSource::ReadingType::TillEnd) { //we had more lines than the fixed size, so we read m_actualRows number of lines if (newLinesTillEnd > m_actualRows) { linesToRead = m_actualRows; //TODO after reading we should skip the next data lines //because it's TillEnd actually } else linesToRead = newLinesTillEnd; } else { //we read max sample rate number of lines when the reading mode //is ContinuouslyFixed or FromEnd, WholeFile is disabled linesToRead = qMin(spreadsheet->sampleSize(), newLinesTillEnd); } } DEBUG(" actual rows = " << m_actualRows); if (linesToRead == 0) return 0; } else { linesToRead = newLinesTillEnd; if (headerEnabled) --m_actualRows; } DEBUG(" lines to read = " << linesToRead); //TODO: check other source types if (spreadsheet->sourceType() == LiveDataSource::SourceType::NetworkUdpSocket) { if (m_actualRows < linesToRead) { DEBUG(" SET actual rows to " << linesToRead); m_actualRows = linesToRead; } } //new rows/resize columns if we don't have a fixed size //TODO if the user changes this value..m_resizedToFixedSize..setResizedToFixedSize if (keepNValues == 0) { #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportResizing: "); #endif if (spreadsheet->rowCount() < m_actualRows) spreadsheet->setRowCount(m_actualRows); if (!m_prepared) currentRow = 0; else { // indexes the position in the vector(column) if (spreadsheet->readingType() == LiveDataSource::ReadingType::WholeFile) currentRow = 0; else currentRow = spreadsheetRowCountBeforeResize; } // if we have fixed size, we do this only once in preparation, here we can use // m_prepared and we need something to decide whether it has a fixed size or increasing for (int n = 0; n < m_actualCols; ++n) { // data() returns a void* which is a pointer to any data type (see ColumnPrivate.cpp) switch (columnModes[n]) { case AbstractColumn::Numeric: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Integer: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Text: { QVector* vector = static_cast*>(spreadsheet->child(n)->data()); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::DateTime: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } //TODO case AbstractColumn::Month: case AbstractColumn::Day: break; } } } else { //when we have a fixed size we have to pop sampleSize number of lines if specified //here popping, setting currentRow if (!m_prepared) { if (spreadsheet->readingType() == LiveDataSource::ReadingType::WholeFile) currentRow = 0; else currentRow = m_actualRows - qMin(newLinesTillEnd, m_actualRows); } else { if (readingType == LiveDataSource::ReadingType::TillEnd) { if (newLinesTillEnd > m_actualRows) { currentRow = 0; } else { if (spreadsheet->readingType() == LiveDataSource::ReadingType::WholeFile) currentRow = 0; else currentRow = m_actualRows - newLinesTillEnd; } } else { //we read max sample size number of lines when the reading mode //is ContinuouslyFixed or FromEnd currentRow = m_actualRows - qMin(spreadsheet->sampleSize(), newLinesTillEnd); } } if (m_prepared) { #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportPopping: "); #endif for (int row = 0; row < linesToRead; ++row) { for (int col = 0; col < m_actualCols; ++col) { switch (columnModes[col]) { case AbstractColumn::Numeric: { QVector* vector = static_cast* >(spreadsheet->child(col)->data()); vector->pop_front(); vector->resize(m_actualRows); m_dataContainer[col] = static_cast(vector); break; } case AbstractColumn::Integer: { QVector* vector = static_cast* >(spreadsheet->child(col)->data()); vector->pop_front(); vector->resize(m_actualRows); m_dataContainer[col] = static_cast(vector); break; } case AbstractColumn::Text: { QVector* vector = static_cast*>(spreadsheet->child(col)->data()); vector->pop_front(); vector->resize(m_actualRows); m_dataContainer[col] = static_cast(vector); break; } case AbstractColumn::DateTime: { QVector* vector = static_cast* >(spreadsheet->child(col)->data()); vector->pop_front(); vector->resize(m_actualRows); m_dataContainer[col] = static_cast(vector); break; } //TODO case AbstractColumn::Month: case AbstractColumn::Day: break; } } } } } // from the last row we read the new data in the spreadsheet qDebug() << "reading from line" << currentRow << " till end" << newLinesTillEnd; qDebug() << "Lines to read:" << linesToRead <<", actual rows:" << m_actualRows << ", actual cols:" << m_actualCols; newDataIdx = 0; if (readingType == LiveDataSource::ReadingType::FromEnd) { if (m_prepared) { if (newData.size() > spreadsheet->sampleSize()) newDataIdx = newData.size() - spreadsheet->sampleSize(); //since we skip a couple of lines, we need to count those bytes too for (int i = 0; i < newDataIdx; ++i) bytesread += newData.at(i).size(); } } qDebug() << "newDataIdx: " << newDataIdx; //TODO static int indexColumnIdx = 0; { #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportFillingContainers: "); #endif int row = 0; if (readingType == LiveDataSource::ReadingType::TillEnd || (readingType == LiveDataSource::ReadingType::ContinuousFixed)) { if (headerEnabled) { if (!m_prepared) { row = 1; bytesread += newData.at(0).size(); } } } if (spreadsheet->sourceType() == LiveDataSource::SourceType::FileOrPipe) { if (readingType == LiveDataSource::ReadingType::WholeFile) { if (headerEnabled) { row = 1; bytesread += newData.at(0).size(); } } } for (; row < linesToRead; ++row) { DEBUG("Reading row " << row << " of " << linesToRead); QString line; if (readingType == LiveDataSource::ReadingType::FromEnd) line = newData.at(newDataIdx++); else line = newData.at(row); //when we read the whole file we don't care about the previous position //so we don't have to count those bytes if (readingType != LiveDataSource::ReadingType::WholeFile) { if (spreadsheet->sourceType() == LiveDataSource::SourceType::FileOrPipe) { bytesread += line.size(); } } DEBUG("line bytes: " << line.size() << " line: " << line.toStdString()); if (simplifyWhitespacesEnabled) line = line.simplified(); if (line.isEmpty() || line.startsWith(commentCharacter)) // skip empty or commented lines continue; QLocale locale(numberFormat); QStringList lineStringList; // only FileOrPipe support multiple columns if (spreadsheet->sourceType() == LiveDataSource::SourceType::FileOrPipe) lineStringList = line.split(m_separator, (QString::SplitBehavior)skipEmptyParts); else lineStringList << line; QDEBUG(" line = " << lineStringList << ", separator = \'" << m_separator << "\'"); if (createIndexEnabled) { if (spreadsheet->keepNValues() == 0) lineStringList.prepend(QString::number(currentRow)); else lineStringList.prepend(QString::number(indexColumnIdx++)); } QDEBUG(" column modes = " << columnModes); for (int n = 0; n < m_actualCols; ++n) { DEBUG(" actual col = " << n); if (n < lineStringList.size()) { QString valueString = lineStringList.at(n); DEBUG(" value string = " << valueString.toStdString()); // set value depending on data type switch (columnModes[n]) { case AbstractColumn::Numeric: { DEBUG(" Numeric"); bool isNumber; const double value = locale.toDouble(valueString, &isNumber); static_cast*>(m_dataContainer[n])->operator[](currentRow) = (isNumber ? value : nanValue); qDebug() << "dataContainer[" << n << "] size:" << static_cast*>(m_dataContainer[n])->size(); break; } case AbstractColumn::Integer: { DEBUG(" Integer"); bool isNumber; const int value = locale.toInt(valueString, &isNumber); DEBUG(" container size = " << m_dataContainer.size() << ", current row = " << currentRow); static_cast*>(m_dataContainer[n])->operator[](currentRow) = (isNumber ? value : 0); qDebug() << "dataContainer[" << n << "] size:" << static_cast*>(m_dataContainer[n])->size(); break; } case AbstractColumn::DateTime: { const QDateTime valueDateTime = QDateTime::fromString(valueString, dateTimeFormat); static_cast*>(m_dataContainer[n])->operator[](currentRow) = valueDateTime.isValid() ? valueDateTime : QDateTime(); break; } case AbstractColumn::Text: if (removeQuotesEnabled) valueString.remove(QRegExp("[\"\']")); static_cast*>(m_dataContainer[n])->operator[](currentRow) = valueString; break; case AbstractColumn::Month: //TODO break; case AbstractColumn::Day: //TODO break; } } else { DEBUG(" missing columns in this line"); switch (columnModes[n]) { case AbstractColumn::Numeric: static_cast*>(m_dataContainer[n])->operator[](currentRow) = nanValue; break; case AbstractColumn::Integer: static_cast*>(m_dataContainer[n])->operator[](currentRow) = 0; break; case AbstractColumn::DateTime: static_cast*>(m_dataContainer[n])->operator[](currentRow) = QDateTime(); break; case AbstractColumn::Text: static_cast*>(m_dataContainer[n])->operator[](currentRow) = ""; break; case AbstractColumn::Month: //TODO break; case AbstractColumn::Day: //TODO break; } } } currentRow++; } } if (m_prepared) { //notify all affected columns and plots about the changes PERFTRACE("AsciiLiveDataImport, notify affected columns and plots"); const Project* project = spreadsheet->project(); QVector curves = project->children(AbstractAspect::Recursive); QVector plots; for (int n = 0; n < m_actualCols; ++n) { Column* column = spreadsheet->column(n); //determine the plots where the column is consumed for (const auto* curve: curves) { if (curve->xColumn() == column || curve->yColumn() == column) { CartesianPlot* plot = dynamic_cast(curve->parentAspect()); if (plots.indexOf(plot) == -1) { plots << plot; plot->setSuppressDataChangedSignal(true); } } } column->setChanged(); } //loop over all affected plots and retransform them for (auto* plot: plots) { plot->setSuppressDataChangedSignal(false); plot->dataChanged(); } } m_prepared = true; return bytesread; } /*! reads the content of device \c device to the data source \c dataSource. Uses the settings defined in the data source. */ void AsciiFilterPrivate::readDataFromDevice(QIODevice& device, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode, int lines) { DEBUG("AsciiFilterPrivate::readDataFromDevice(): dataSource = " << dataSource << ", mode = " << ENUM_TO_STRING(AbstractFileFilter, ImportMode, importMode) << ", lines = " << lines); if (!m_prepared) { const int deviceError = prepareDeviceToRead(device); if (deviceError != 0) { DEBUG("Device error = " << deviceError); return; } // matrix data has only one column mode (which is not text) if (dynamic_cast(dataSource)) { auto mode = columnModes[0]; if (mode == AbstractColumn::Text) mode = AbstractColumn::Numeric; for (auto& c: columnModes) if (c != mode) c = mode; } m_columnOffset = dataSource->prepareImport(m_dataContainer, importMode, m_actualRows, m_actualCols, vectorNames, columnModes); m_prepared = true; } DEBUG("locale = " << QLocale::languageToString(numberFormat).toStdString()); QLocale locale(numberFormat); // Read the data int currentRow = 0; // indexes the position in the vector(column) if (lines == -1) lines = m_actualRows; //skip data lines, if required DEBUG(" Skipping " << m_actualStartRow - 1 << " lines"); for (int i = 0; i < m_actualStartRow - 1; ++i) device.readLine(); DEBUG(" Reading " << qMin(lines, m_actualRows) << " lines"); for (int i = 0; i < qMin(lines, m_actualRows); ++i) { QString line = device.readLine(); line.remove(QRegExp("[\\n\\r]")); // remove any newline if (simplifyWhitespacesEnabled) line = line.simplified(); if (line.isEmpty() || line.startsWith(commentCharacter)) // skip empty or commented lines continue; QStringList lineStringList = line.split(m_separator, (QString::SplitBehavior)skipEmptyParts); //prepend the index if required //TODO: come up maybe with a solution with adding the index inside of the loop below, //without conversion to string, prepending to the list and then conversion back to integer. if (createIndexEnabled) lineStringList.prepend(QString::number(i+1)); // remove left white spaces if (skipEmptyParts) { for (int n = 0; n < lineStringList.size(); ++n) { QString valueString = lineStringList.at(n); if (!QString::compare(valueString, " ")) { lineStringList.removeAt(n); n--; } } } for (int n = 0; n < m_actualCols; ++n) { if (n < lineStringList.size()) { QString valueString = lineStringList.at(n); // set value depending on data type switch (columnModes[n]) { case AbstractColumn::Numeric: { bool isNumber; const double value = locale.toDouble(valueString, &isNumber); static_cast*>(m_dataContainer[n])->operator[](currentRow) = (isNumber ? value : nanValue); break; } case AbstractColumn::Integer: { bool isNumber; const int value = locale.toInt(valueString, &isNumber); static_cast*>(m_dataContainer[n])->operator[](currentRow) = (isNumber ? value : 0); break; } case AbstractColumn::DateTime: { const QDateTime valueDateTime = QDateTime::fromString(valueString, dateTimeFormat); static_cast*>(m_dataContainer[n])->operator[](currentRow) = valueDateTime.isValid() ? valueDateTime : QDateTime(); break; } case AbstractColumn::Text: if (removeQuotesEnabled) valueString.remove(QRegExp("[\"\']")); static_cast*>(m_dataContainer[n])->operator[](currentRow) = valueString; break; case AbstractColumn::Month: // never happens case AbstractColumn::Day: break; } } else { // missing columns in this line switch (columnModes[n]) { case AbstractColumn::Numeric: static_cast*>(m_dataContainer[n])->operator[](currentRow) = nanValue; break; case AbstractColumn::Integer: static_cast*>(m_dataContainer[n])->operator[](currentRow) = 0; break; case AbstractColumn::DateTime: static_cast*>(m_dataContainer[n])->operator[](currentRow) = QDateTime(); break; case AbstractColumn::Text: static_cast*>(m_dataContainer[n])->operator[](currentRow) = ""; break; case AbstractColumn::Month: // never happens case AbstractColumn::Day: break; } } } currentRow++; emit q->completed(100 * currentRow/m_actualRows); } DEBUG(" Read " << currentRow << " lines"); dataSource->finalizeImport(m_columnOffset, startColumn, endColumn, currentRow, dateTimeFormat, importMode); } /*! * preview for special devices (local/UDP/TCP socket or serial port) */ QVector AsciiFilterPrivate::preview(QIODevice &device) { DEBUG("AsciiFilterPrivate::preview(): bytesAvailable = " << device.bytesAvailable() << ", isSequential = " << device.isSequential()); QVector dataStrings; if (!(device.bytesAvailable() > 0)) { DEBUG("No new data available"); return dataStrings; } if (device.isSequential() && device.bytesAvailable() < (int)sizeof(quint16)) return dataStrings; #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportTotal: "); #endif int linesToRead = 0; QVector newData; //TODO: serial port "read(nBytes)"? while (!device.atEnd()) { if (device.canReadLine()) newData.push_back(device.readLine()); else // UDP fails otherwise newData.push_back(device.readAll()); linesToRead++; } QDEBUG(" data = " << newData); if (linesToRead == 0) return dataStrings; int col = 0; int colMax = newData.at(0).size(); if (createIndexEnabled) colMax++; columnModes.resize(colMax); if (createIndexEnabled) { columnModes[0] = AbstractColumn::ColumnMode::Integer; col = 1; vectorNames.prepend(i18n("Index")); } vectorNames.append(i18n("Value")); QDEBUG(" vector names = " << vectorNames); for (const auto& valueString: newData.at(0).split(' ', QString::SkipEmptyParts)) { if (col == colMax) break; columnModes[col++] = AbstractFileFilter::columnMode(valueString, dateTimeFormat, numberFormat); } for (int i = 0; i < linesToRead; ++i) { QString line = newData.at(i); if (simplifyWhitespacesEnabled) line = line.simplified(); if (line.isEmpty() || line.startsWith(commentCharacter)) // skip empty or commented lines continue; QLocale locale(numberFormat); QStringList lineStringList = line.split(' ', QString::SkipEmptyParts); if (createIndexEnabled) lineStringList.prepend(QString::number(i)); QStringList lineString; for (int n = 0; n < lineStringList.size(); ++n) { if (n < lineStringList.size()) { QString valueString = lineStringList.at(n); switch (columnModes[n]) { case AbstractColumn::Numeric: { bool isNumber; const double value = locale.toDouble(valueString, &isNumber); lineString += QString::number(isNumber ? value : nanValue, 'g', 16); break; } case AbstractColumn::Integer: { bool isNumber; const int value = locale.toInt(valueString, &isNumber); lineString += QString::number(isNumber ? value : 0); break; } case AbstractColumn::DateTime: { const QDateTime valueDateTime = QDateTime::fromString(valueString, dateTimeFormat); lineString += valueDateTime.isValid() ? valueDateTime.toString(dateTimeFormat) : QLatin1String(" "); break; } case AbstractColumn::Text: if (removeQuotesEnabled) valueString.remove(QRegExp("[\"\']")); lineString += valueString; break; case AbstractColumn::Month: // never happens case AbstractColumn::Day: break; } } else // missing columns in this line lineString += QLatin1String(""); } dataStrings << lineString; } return dataStrings; } /*! * generates the preview for the file \c fileName reading the provided number of \c lines. */ QVector AsciiFilterPrivate::preview(const QString& fileName, int lines) { QVector dataStrings; KFilterDev device(fileName); const int deviceError = prepareDeviceToRead(device); if (deviceError != 0) { DEBUG("Device error = " << deviceError); return dataStrings; } //number formatting DEBUG("locale = " << QLocale::languageToString(numberFormat).toStdString()); QLocale locale(numberFormat); // Read the data if (lines == -1) lines = m_actualRows; // set column names for preview if (!headerEnabled) { int start = 0; if (createIndexEnabled) start = 1; for (int i=start;iwriteStartElement( "asciiFilter"); writer->writeAttribute( "commentCharacter", d->commentCharacter); writer->writeAttribute( "separatingCharacter", d->separatingCharacter); writer->writeAttribute( "autoMode", QString::number(d->autoModeEnabled)); writer->writeAttribute( "createIndex", QString::number(d->createIndexEnabled)); writer->writeAttribute( "header", QString::number(d->headerEnabled)); writer->writeAttribute( "vectorNames", d->vectorNames.join(' ')); writer->writeAttribute( "skipEmptyParts", QString::number(d->skipEmptyParts)); writer->writeAttribute( "simplifyWhitespaces", QString::number(d->simplifyWhitespacesEnabled)); writer->writeAttribute( "nanValue", QString::number(d->nanValue)); writer->writeAttribute( "removeQuotes", QString::number(d->removeQuotesEnabled)); writer->writeAttribute( "startRow", QString::number(d->startRow)); writer->writeAttribute( "endRow", QString::number(d->endRow)); writer->writeAttribute( "startColumn", QString::number(d->startColumn)); writer->writeAttribute( "endColumn", QString::number(d->endColumn)); writer->writeEndElement(); } /*! Loads from XML. */ bool AsciiFilter::load(XmlStreamReader* reader) { KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs = reader->attributes(); QString str = attribs.value("commentCharacter").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("commentCharacter").toString()); else d->commentCharacter = str; str = attribs.value("separatingCharacter").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("separatingCharacter").toString()); else d->separatingCharacter = str; str = attribs.value("createIndex").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("createIndex").toString()); else d->createIndexEnabled = str.toInt(); str = attribs.value("autoMode").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("autoMode").toString()); else d->autoModeEnabled = str.toInt(); str = attribs.value("header").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("header").toString()); else d->headerEnabled = str.toInt(); str = attribs.value("vectorNames").toString(); d->vectorNames = str.split(' '); //may be empty str = attribs.value("simplifyWhitespaces").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("simplifyWhitespaces").toString()); else d->simplifyWhitespacesEnabled = str.toInt(); str = attribs.value("nanValue").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("nanValue").toString()); else d->nanValue = str.toDouble(); str = attribs.value("removeQuotes").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("removeQuotes").toString()); else d->removeQuotesEnabled = str.toInt(); str = attribs.value("skipEmptyParts").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("skipEmptyParts").toString()); else d->skipEmptyParts = str.toInt(); str = attribs.value("startRow").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("startRow").toString()); else d->startRow = str.toInt(); str = attribs.value("endRow").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("endRow").toString()); else d->endRow = str.toInt(); str = attribs.value("startColumn").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("startColumn").toString()); else d->startColumn = str.toInt(); str = attribs.value("endColumn").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("endColumn").toString()); else d->endColumn = str.toInt(); return true; } int AsciiFilterPrivate::isPrepared() { return m_prepared; } #ifdef HAVE_MQTT /*! * \brief Offers a preview about the data received by the topic * \param list * \param message * \param topic */ void AsciiFilterPrivate::mqttPreview(QVector& list, const QString& message, const QString& topic) { QVector dataStrings; if (!message.isEmpty()) { qDebug()<<"ascii mqtt preview" < newData; QStringList newDataList = message.split(QRegExp("\n|\r\n|\r"), QString::SkipEmptyParts); for (auto& valueString: newDataList) { QStringList splitString = valueString.split(' ', QString::SkipEmptyParts); for ( const auto& valueString2: splitString) { if (!valueString2.isEmpty() && !valueString2.startsWith(commentCharacter) ) { linesToRead++; newData.push_back(valueString2); } } } qDebug() <<" data investigated, lines to read: "< list.size()) { linesToRead = list.size(); qDebug()<<"Lines to bigger: " < numeric if (mode == AbstractColumn::Numeric && columnModes[colSize - 1] == AbstractColumn::Integer) { columnModes[colSize - 1] = mode; qDebug()<<"-----setting column mode" << " "< text if ( (mode == AbstractColumn::Text) && (columnModes[colSize - 1] != AbstractColumn::Text) ) { columnModes[colSize - 1] = mode; qDebug()<<"-----setting column mode" << " "< 0) ) if (createIndexEnabled) lineString += QString::number(i); //If this is the first not empty topic, add nan value to all the empty topics before this one if(!list.isEmpty() && mqttPreviewFirstEmptyColCount > 0){ for(int j = 0; j < mqttPreviewFirstEmptyColCount; j++) { lineString += QString::number(nanValue, 'g', 16); qDebug()<<"first column updated with nan"; } } //Add the actual value to the topic switch (columnModes[colSize - 1]) { case AbstractColumn::Numeric: { bool isNumber; const double value = locale.toDouble(line, &isNumber); lineString += QString::number(isNumber ? value : nanValue, 'g', 16); break; } case AbstractColumn::Integer: { bool isNumber; const int value = locale.toInt(line, &isNumber); lineString += QString::number(isNumber ? value : 0); break; } case AbstractColumn::DateTime: { const QDateTime valueDateTime = QDateTime::fromString(line, dateTimeFormat); lineString += valueDateTime.isValid() ? valueDateTime.toString(dateTimeFormat) : QLatin1String(" "); break; } case AbstractColumn::Text: if (removeQuotesEnabled) line.remove(QRegExp("[\"\']")); lineString += line; break; case AbstractColumn::Month: // never happens case AbstractColumn::Day: break; } qDebug()<<"column updated with value"; // if the list was empty, or this is the first not empty topic, we append the new line if(list.isEmpty() || (!list.isEmpty() && mqttPreviewFirstEmptyColCount > 0)) dataStrings << lineString; //Otherwise we update the already existing line if (!list.isEmpty() && mqttPreviewFirstEmptyColCount == 0) dataStrings[i] = lineString; } //The first empty topics were taken care of if(mqttPreviewFirstEmptyColCount > 0) { mqttPreviewFirstEmptyColCount = 0; } } //the message is empty and there were only empty messages before this else if (list.isEmpty() || (!list.isEmpty() && mqttPreviewFirstEmptyColCount > 0) ) { //increment the counter mqttPreviewFirstEmptyColCount ++; qDebug()<<"first column empty: "<< mqttPreviewFirstEmptyColCount; //determine number of columns int colSize; if (mqttPreviewFirstEmptyColCount == 1){ if (createIndexEnabled) colSize = 2; else colSize = 1; } else colSize = columnModes.size() + 1; columnModes.resize(colSize); //add index if needed if (mqttPreviewFirstEmptyColCount == 1) if (createIndexEnabled) { columnModes[0] = AbstractColumn::ColumnMode::Integer; vectorNames.prepend("index"); } //Add new column fot the mepty topic vectorNames.append( topic); columnModes[colSize-1] = AbstractColumn::ColumnMode::Numeric; qDebug()<<"Column mode set for empty column"; //Add a NaN value to the topic's column QStringList lineString; if (mqttPreviewFirstEmptyColCount == 1) { //Add index if needed if(createIndexEnabled) lineString += QString::number(0); lineString += QString::number(nanValue, 'g', 16); //Append since this is the first empty column dataStrings << lineString; qDebug()<<"new line added with nan"; } else { //Update the already existing line dataStrings = list; dataStrings[0] += QString::number(nanValue, 'g', 16); qDebug()<<"line expanded with nan"; } } //The message is empty but there already was a non empty message else if(!list.isEmpty()) { //Append vector name vectorNames.append( topic); qDebug()<<"vector name set on : " << topic; int colSize = columnModes.size() + 1; columnModes.resize(colSize); columnModes[colSize-1] = AbstractColumn::ColumnMode::Numeric; dataStrings = list; //Add as many NaN values as many lines the list already has for (int i = 0; i < dataStrings.size(); ++i) { dataStrings[i] += QString::number(nanValue, 'g', 16); } } //update the list list = dataStrings; } /*! * \brief Returns the statistical data that is needed by the topic for its MQTTClient's will message * \param topic */ QString AsciiFilterPrivate::mqttColumnStatistics(const MQTTTopic* topic) const{ qDebug()<<"MQTT Column Statistics"; Column* tempColumn = topic->child(m_actualCols - 1); QString statistics; QVector willStatistics = topic->mqttClient()->willStatistics(); //Add every statistical data to the string, the flag of which is set true for(int i = 0; i <= willStatistics.size(); i++) { if(willStatistics[i]) { switch (static_cast(i) ) { case MQTTClient::WillStatistics::ArithmeticMean: statistics += QLatin1String("Arithmetic mean: ") + QString::number(tempColumn->statistics().arithmeticMean) + "\n"; break; case MQTTClient::WillStatistics::ContraharmonicMean: statistics += QLatin1String("Contraharmonic mean: ") + QString::number(tempColumn->statistics().contraharmonicMean) + "\n"; break; case MQTTClient::WillStatistics::Entropy: statistics += QLatin1String("Entropy: ") + QString::number(tempColumn->statistics().entropy) + "\n"; break; case MQTTClient::WillStatistics::GeometricMean: statistics += QLatin1String("Geometric mean: ") + QString::number(tempColumn->statistics().geometricMean) + "\n"; break; case MQTTClient::WillStatistics::HarmonicMean: statistics += QLatin1String("Harmonic mean: ") + QString::number(tempColumn->statistics().harmonicMean) + "\n"; break; case MQTTClient::WillStatistics::Kurtosis: statistics += QLatin1String("Kurtosis: ") + QString::number(tempColumn->statistics().kurtosis) + "\n"; break; case MQTTClient::WillStatistics::Maximum: statistics += QLatin1String("Maximum: ") + QString::number(tempColumn->statistics().maximum) + "\n"; break; case MQTTClient::WillStatistics::MeanDeviation: statistics += QLatin1String("Mean deviation: ") + QString::number(tempColumn->statistics().meanDeviation) + "\n"; break; case MQTTClient::WillStatistics::MeanDeviationAroundMedian: statistics += QLatin1String("Mean deviation around median: ") + QString::number(tempColumn->statistics().meanDeviationAroundMedian) + "\n"; break; case MQTTClient::WillStatistics::Median: statistics += QLatin1String("Median: ") + QString::number(tempColumn->statistics().median) + "\n"; break; case MQTTClient::WillStatistics::MedianDeviation: statistics += QLatin1String("Median deviation: ") + QString::number(tempColumn->statistics().medianDeviation) + "\n"; break; case MQTTClient::WillStatistics::Minimum: statistics += QLatin1String("Minimum: ") + QString::number(tempColumn->statistics().minimum) + "\n"; break; case MQTTClient::WillStatistics::Skewness: statistics += QLatin1String("Skewness: ") + QString::number(tempColumn->statistics().skewness) + "\n"; break; case MQTTClient::WillStatistics::StandardDeviation: statistics += QLatin1String("Standard deviation: ") + QString::number(tempColumn->statistics().standardDeviation) + "\n"; break; case MQTTClient::WillStatistics::Variance: statistics += QLatin1String("Variance: ") + QString::number(tempColumn->statistics().variance) + "\n"; break; default: break; } } } return statistics; } AbstractColumn::ColumnMode AsciiFilterPrivate::mqttColumnMode() const{ return columnModes[m_actualCols - 1]; } /*! * \brief reads the content of a message received by the topic. * Uses the settings defined in the MQTTTopic's MQTTClient * \param message * \param topic * \param dataSource */ void AsciiFilterPrivate::readMQTTTopic(const QString& message, const QString& topic, AbstractDataSource*dataSource) { //If the message is empty, there is nothing to do if (message.isEmpty()) { DEBUG("No new data available"); return; } MQTTTopic* spreadsheet = dynamic_cast(dataSource); int keepNValues = spreadsheet->keepNValues(); if (!m_prepared) { qDebug()<<"Start prepare mqtt"; //Prepare the filter const int mqttPrepareError = prepareMQTTTopicToRead(message, topic); if (mqttPrepareError != 0) { DEBUG("Mqtt Prepare Error = " << mqttPrepareError); qDebug()<setUndoAware(false); spreadsheet->resize(AbstractFileFilter::Replace, vectorNames, m_actualCols); qDebug() << "fds resized to col: " << m_actualCols; qDebug() << "fds rowCount: " << spreadsheet->rowCount(); //columns in a MQTTTopic don't have any manual changes. //make the available columns undo unaware and suppress the "data changed" signal. //data changes will be propagated via an explicit Column::setChanged() call once new data was read. for (int i = 0; i < spreadsheet->childCount(); i++) { spreadsheet->child(i)->setUndoAware(false); spreadsheet->child(i)->setSuppressDataChangedSignal(true); } if (keepNValues == 0) spreadsheet->setRowCount(m_actualRows > 1 ? m_actualRows : 1); else { spreadsheet->setRowCount(spreadsheet->keepNValues()); m_actualRows = spreadsheet->keepNValues(); } m_dataContainer.resize(m_actualCols); for (int n = 0; n < m_actualCols; ++n) { // data() returns a void* which is a pointer to any data type (see ColumnPrivate.cpp) spreadsheet->child(n)->setColumnMode(columnModes[n]); switch (columnModes[n]) { case AbstractColumn::Numeric: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Integer: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Text: { QVector* vector = static_cast*>(spreadsheet->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::DateTime: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } //TODO case AbstractColumn::Month: case AbstractColumn::Day: break; } } qDebug() << "prepared!"; } #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportTotal: "); #endif MQTTClient::ReadingType readingType; if (!m_prepared) { //if filter is not prepared we read till the end readingType = MQTTClient::ReadingType::TillEnd; } else { //we have to read all the data when reading from end //so we set readingType to TillEnd if (spreadsheet->readingType() == MQTTClient::ReadingType::FromEnd) { readingType = MQTTClient::ReadingType::TillEnd; } else { readingType = static_cast(spreadsheet->readingType()); } } //count the new lines, increase actualrows on each //now we read all the new lines, if we want to use sample rate //then here we can do it, if we have actually sample rate number of lines :-? int newLinesForSampleSizeNotTillEnd = 0; int newLinesTillEnd = 0; QVector newData; if (readingType != MQTTClient::ReadingType::TillEnd) { newData.reserve(spreadsheet->sampleSize()); newData.resize(spreadsheet->sampleSize()); } int newDataIdx = 0; bool sampleSizeReached = false; { #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportReadingFromFile: "); #endif QStringList newDataList = message.split(QRegExp("\n|\r\n|\r"), QString::SkipEmptyParts); for (auto& valueString: newDataList) { - QStringList splitString = valueString.split(m_separator, static_cast(skipEmptyParts)); - for (const auto& valueString2: splitString) { - if (!valueString2.isEmpty() && !valueString2.startsWith(commentCharacter)) { - if (readingType != MQTTClient::ReadingType::TillEnd) - newData[newDataIdx++] = valueString2; - else - newData.push_back(valueString2); - newLinesTillEnd++; - - if (readingType != MQTTClient::ReadingType::TillEnd) { - newLinesForSampleSizeNotTillEnd++; - //for Continous reading and FromEnd we read sample rate number of lines if possible - qDebug()<<"new lines for sample rate: "<sampleSize()) { - sampleSizeReached = true; - break; + if(!valueString.startsWith(commentCharacter)) { + QStringList splitString = valueString.split(m_separator, static_cast(skipEmptyParts)); + for (const auto& valueString2: splitString) { + if (!valueString2.isEmpty() && !valueString2.startsWith(commentCharacter)) { + if (readingType != MQTTClient::ReadingType::TillEnd) + newData[newDataIdx++] = valueString2; + else + newData.push_back(valueString2); + newLinesTillEnd++; + + if (readingType != MQTTClient::ReadingType::TillEnd) { + newLinesForSampleSizeNotTillEnd++; + //for Continous reading and FromEnd we read sample rate number of lines if possible + qDebug()<<"new lines for sample rate: "<sampleSize()) { + sampleSizeReached = true; + break; + } } } } + //if the sample size limit is reached we brake from the outer loop as well + if(sampleSizeReached) + break; } - //if the sample size limit is reached we brake from the outer loop as well - if(sampleSizeReached) - break; } } qDebug()<<"Processing message done"; //now we reset the readingType if (static_cast(spreadsheet->readingType()) == MQTTClient::ReadingType::FromEnd) readingType = static_cast(spreadsheet->readingType()); //we had less new lines than the sample rate specified if (readingType != MQTTClient::ReadingType::TillEnd) qDebug() << "Removed empty lines: " << newData.removeAll(""); qDebug()<<"Create index enabled: "<rowCount();; if(m_prepared ) { if (keepNValues == 0) m_actualRows = spreadsheetRowCountBeforeResize; else { //if the keepNValues changed since the last read we have to manage the columns accordingly if(m_actualRows != spreadsheet->keepNValues()) { if(m_actualRows < spreadsheet->keepNValues()) { qDebug()<setRowCount(spreadsheet->keepNValues()); qDebug()<<"rowcount set to" << spreadsheet->keepNValues(); } //Calculate the difference between the old and new keepNValues int rowDiff = 0; if(m_actualRows > spreadsheet->keepNValues()) { rowDiff = m_actualRows - spreadsheet->keepNValues(); } if(m_actualRows < spreadsheet->keepNValues()) { rowDiff =spreadsheet->keepNValues() - m_actualRows; } qDebug()<<"last value changed: "<keepNValues()<<" "<keepNValues(); switch (columnModes[n]) { case AbstractColumn::Numeric: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); m_dataContainer[n] = static_cast(vector); //if the keepNValues got smaller then we move the last keepNValues count of data //in the first keepNValues places if(m_actualRows > spreadsheet->keepNValues()) { for(int i = 0; i < spreadsheet->keepNValues(); i++) { static_cast*>(m_dataContainer[n])->operator[] (i) = static_cast*>(m_dataContainer[n])->operator[](m_actualRows - spreadsheet->keepNValues() + i); qDebug()<< "overwrite row"<keepNValues() + i; } } //if the keepNValues got bigger we move the existing values to the last m_actualRows positions //then fill the remaining lines with NaN if(m_actualRows < spreadsheet->keepNValues()) { vector->reserve( spreadsheet->keepNValues()); vector->resize( spreadsheet->keepNValues()); qDebug()<<"actual rows < keepn"; for(int i = 1; i <= m_actualRows; i++) { static_cast*>(m_dataContainer[n])->operator[] (spreadsheet->keepNValues() - i) = static_cast*>(m_dataContainer[n])->operator[](spreadsheet->keepNValues() - i - rowDiff); qDebug()<< "overwrite row "<keepNValues() - i<<" "<keepNValues() - i - rowDiff; qDebug()<*>(m_dataContainer[n])->operator[](spreadsheet->keepNValues() - i - rowDiff); } for(int i = 0; i < rowDiff; i++) { static_cast*>(m_dataContainer[n])->operator[](i) = nanValue; qDebug()<* vector = static_cast* >(spreadsheet->child(n)->data()); m_dataContainer[n] = static_cast(vector); //if the keepNValues got smaller then we move the last keepNValues count of data //in the first keepNValues places if(m_actualRows > spreadsheet->keepNValues()) { for(int i = 0; i < spreadsheet->keepNValues(); i++) { static_cast*>(m_dataContainer[n])->operator[] (i) = static_cast*>(m_dataContainer[n])->operator[](m_actualRows - spreadsheet->keepNValues() + i); qDebug()<< "overwrite row "<keepNValues() + i; } } //if the keepNValues got bigger we move the existing values to the last m_actualRows positions //then fill the remaining lines with 0 if(m_actualRows < spreadsheet->keepNValues()) { vector->reserve( spreadsheet->keepNValues()); vector->resize( spreadsheet->keepNValues()); for(int i = 1; i <= m_actualRows; i++) { static_cast*>(m_dataContainer[n])->operator[] (spreadsheet->keepNValues() - i) = static_cast*>(m_dataContainer[n])->operator[](spreadsheet->keepNValues() - i - rowDiff); qDebug()<< "overwrite row"<keepNValues() - i<<" "<keepNValues() - i - rowDiff; qDebug()<*>(m_dataContainer[n])->operator[](spreadsheet->keepNValues() - i - rowDiff); } for(int i = 0; i < rowDiff; i++){ static_cast*>(m_dataContainer[n])->operator[](i) = 0; qDebug()<* vector = static_cast*>(spreadsheet->child(n)->data()); m_dataContainer[n] = static_cast(vector); //if the keepNValues got smaller then we move the last keepNValues count of data //in the first keepNValues places if(m_actualRows > spreadsheet->keepNValues()) { for(int i = 0; i < spreadsheet->keepNValues(); i++) { static_cast*>(m_dataContainer[n])->operator[] (i) = static_cast*>(m_dataContainer[n])->operator[](m_actualRows - spreadsheet->keepNValues() + i); qDebug()<< "overwrite row"<keepNValues() + i; } } //if the keepNValues got bigger we move the existing values to the last m_actualRows positions //then fill the remaining lines with "" if(m_actualRows < spreadsheet->keepNValues()) { vector->reserve( spreadsheet->keepNValues()); vector->resize( spreadsheet->keepNValues()); for(int i = 1; i <= m_actualRows; i++) { static_cast*>(m_dataContainer[n])->operator[] (spreadsheet->keepNValues() - i) = static_cast*>(m_dataContainer[n])->operator[](spreadsheet->keepNValues() - i - rowDiff); } for(int i = 0; i < rowDiff; i++) static_cast*>(m_dataContainer[n])->operator[](i) = ""; } break; } case AbstractColumn::DateTime: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); m_dataContainer[n] = static_cast(vector); //if the keepNValues got smaller then we move the last keepNValues count of data //in the first keepNValues places if(m_actualRows > spreadsheet->keepNValues()) { for(int i = 0; i < spreadsheet->keepNValues(); i++) { static_cast*>(m_dataContainer[n])->operator[] (i) = static_cast*>(m_dataContainer[n])->operator[](m_actualRows - spreadsheet->keepNValues() + i); qDebug()<< "overwrite row"<keepNValues() + i; } } //if the keepNValues got bigger we move the existing values to the last m_actualRows positions //then fill the remaining lines with null datetime if(m_actualRows < spreadsheet->keepNValues()) { vector->reserve( spreadsheet->keepNValues()); vector->resize( spreadsheet->keepNValues()); for(int i = 1; i <= m_actualRows; i++) { static_cast*>(m_dataContainer[n])->operator[] (spreadsheet->keepNValues() - i) = static_cast*>(m_dataContainer[n])->operator[](spreadsheet->keepNValues() - i - rowDiff); } for(int i = 0; i < rowDiff; i++) static_cast*>(m_dataContainer[n])->operator[](i) = QDateTime(); } break; } //TODO case AbstractColumn::Month: case AbstractColumn::Day: break; } } //if the keepNValues got smaller resize the spreadsheet if(m_actualRows > spreadsheet->keepNValues()) spreadsheet->setRowCount(spreadsheet->keepNValues()); //set the new row count m_actualRows = spreadsheet->keepNValues(); qDebug()<<"actual rows: "<sampleSize()); else { m_actualRows += newData.size(); } } //fixed size if (keepNValues != 0) { if (readingType == MQTTClient::ReadingType::TillEnd) { //we had more lines than the fixed size, so we read m_actualRows number of lines if (newLinesTillEnd > m_actualRows) { linesToRead = m_actualRows; } else linesToRead = newLinesTillEnd; } else { //we read max sample size number of lines when the reading mode //is ContinouslyFixed or FromEnd if(spreadsheet->sampleSize() <= spreadsheet->keepNValues()) linesToRead = qMin(spreadsheet->sampleSize(), newLinesTillEnd); else linesToRead = qMin(spreadsheet->keepNValues(), newLinesTillEnd); } } else { linesToRead = m_actualRows - spreadsheetRowCountBeforeResize; } if (linesToRead == 0) return; } else { if(keepNValues != 0) { linesToRead = newLinesTillEnd > m_actualRows ? m_actualRows : newLinesTillEnd; } else { linesToRead = newLinesTillEnd; } } qDebug()<<"linestoread = "<rowCount() < m_actualRows) spreadsheet->setRowCount(m_actualRows); if (!m_prepared) currentRow = 0; else { // indexes the position in the vector(column) currentRow = spreadsheetRowCountBeforeResize; } // if we have fixed size, we do this only once in preparation, here we can use // m_prepared and we need something to decide whether it has a fixed size or increasing for (int n = 0; n < m_actualCols; ++n) { // data() returns a void* which is a pointer to any data type (see ColumnPrivate.cpp) switch (columnModes[n]) { case AbstractColumn::Numeric: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Integer: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Text: { QVector* vector = static_cast*>(spreadsheet->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::DateTime: { QVector* vector = static_cast* >(spreadsheet->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } //TODO case AbstractColumn::Month: case AbstractColumn::Day: break; } } } else { //when we have a fixed size we have to pop sampleSize number of lines if specified //here popping, setting currentRow if (!m_prepared) currentRow = m_actualRows - qMin(newLinesTillEnd, m_actualRows); else { if (readingType == MQTTClient::ReadingType::TillEnd) { if (newLinesTillEnd > m_actualRows) currentRow = 0; else { currentRow = m_actualRows - newLinesTillEnd; } } else { //we read max sample rate number of lines when the reading mode //is ContinouslyFixed or FromEnd currentRow = m_actualRows - linesToRead; } } if (m_prepared) { #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportPopping: "); #endif for (int row = 0; row < linesToRead; ++row) { for (int col = 0; col < m_actualCols; ++col) { switch (columnModes[col]) { case AbstractColumn::Numeric: { QVector* vector = static_cast* >(spreadsheet->child(col)->data()); vector->pop_front(); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[col] = static_cast(vector); break; } case AbstractColumn::Integer: { QVector* vector = static_cast* >(spreadsheet->child(col)->data()); vector->pop_front(); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[col] = static_cast(vector); break; } case AbstractColumn::Text: { QVector* vector = static_cast*>(spreadsheet->child(col)->data()); vector->pop_front(); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[col] = static_cast(vector); break; } case AbstractColumn::DateTime: { QVector* vector = static_cast* >(spreadsheet->child(col)->data()); vector->pop_front(); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[col] = static_cast(vector); break; } //TODO case AbstractColumn::Month: case AbstractColumn::Day: break; } } } } } // from the last row we read the new data in the spreadsheet qDebug() << "reading from line: " << currentRow << " lines till end: " << newLinesTillEnd; qDebug() << "Lines to read: " << linesToRead <<" actual rows: " << m_actualRows; newDataIdx = 0; //From end means that we read the last sample size amount of data if (readingType == MQTTClient::ReadingType::FromEnd) { if (m_prepared) { if (newData.size() > spreadsheet->sampleSize()) newDataIdx = newData.size() - spreadsheet->sampleSize(); } } qDebug() << "newDataIdx: " << newDataIdx; static int indexColumnIdx = 0; { #ifdef PERFTRACE_LIVE_IMPORT PERFTRACE("AsciiLiveDataImportFillingContainers: "); #endif int row = 0; for (; row < linesToRead; ++row) { QString line; if (readingType == MQTTClient::ReadingType::FromEnd) line = newData.at(newDataIdx++); else line = newData.at(row); qDebug ()<<"line "<*>(m_dataContainer[0])->operator[](currentRow) = (isNumber ? value : nanValue); //qDebug() << "dataContainer[" << n << "] size:" << static_cast*>(m_dataContainer[n])->size(); break; } case AbstractColumn::Integer: { bool isNumber; const int value = locale.toInt(tempIndex, &isNumber); static_cast*>(m_dataContainer[0])->operator[](currentRow) = (isNumber ? value : 0); //qDebug() << "dataContainer[" << n << "] size:" << static_cast*>(m_dataContainer[n])->size(); break; } case AbstractColumn::DateTime: { const QDateTime valueDateTime = QDateTime::fromString(tempIndex, dateTimeFormat); static_cast*>(m_dataContainer[0])->operator[](currentRow) = valueDateTime.isValid() ? valueDateTime : QDateTime(); break; } case AbstractColumn::Text: if (removeQuotesEnabled) tempIndex.remove(QRegExp("[\"\']")); static_cast*>(m_dataContainer[0])->operator[](currentRow) = tempIndex; break; case AbstractColumn::Month: //TODO break; case AbstractColumn::Day: //TODO break; } } //setting timestamp on current time static_cast*>(m_dataContainer[col])->operator[](currentRow) = QDateTime::currentDateTime(); QString valueString = line; qDebug()<<"putting in this line" << valueString<< " "<*>(m_dataContainer[m_actualCols - 1])->operator[](currentRow) = (isNumber ? value : nanValue); //qDebug() << "dataContainer[" << n << "] size:" << static_cast*>(m_dataContainer[n])->size(); break; } case AbstractColumn::Integer: { bool isNumber; const int value = locale.toInt(valueString, &isNumber); static_cast*>(m_dataContainer[m_actualCols - 1])->operator[](currentRow) = (isNumber ? value : 0); //qDebug() << "dataContainer[" << n << "] size:" << static_cast*>(m_dataContainer[n])->size(); break; } case AbstractColumn::DateTime: { const QDateTime valueDateTime = QDateTime::fromString(valueString, dateTimeFormat); static_cast*>(m_dataContainer[m_actualCols - 1])->operator[](currentRow) = valueDateTime.isValid() ? valueDateTime : QDateTime(); break; } case AbstractColumn::Text: if (removeQuotesEnabled) valueString.remove(QRegExp("[\"\']")); static_cast*>(m_dataContainer[m_actualCols - 1])->operator[](currentRow) = valueString; break; case AbstractColumn::Month: //TODO break; case AbstractColumn::Day: //TODO break; } currentRow++; qDebug()<<"adding data is ok, current row increment"; } } if (m_prepared) { qDebug()<<"notifying plots"; //notify all affected columns and plots about the changes PERFTRACE("AsciiLiveDataImport, notify affected columns and plots"); qDebug()<<"project "; const Project* project = spreadsheet->project(); qDebug()<<"Curves"; QVector curves = project->children(AbstractAspect::Recursive); QVector plots; qDebug()<<"Uploading plots"; for (int n = 0; n < m_actualCols; ++n) { Column* column = spreadsheet->column(n); //determine the plots where the column is consumed for (const auto* curve: curves) { if (curve->xColumn() == column || curve->yColumn() == column) { CartesianPlot* plot = dynamic_cast(curve->parentAspect()); if (plots.indexOf(plot) == -1) { plots << plot; plot->setSuppressDataChangedSignal(true); } } } column->setChanged(); } //loop over all affected plots and retransform them for (auto* const plot: plots) { //TODO setting this back to true triggers again a lot of retransforms in the plot (one for each curve). // plot->setSuppressDataChangedSignal(false); plot->dataChanged(); } } qDebug()<<"check m_prepared"; //Set prepared true if needed if(!m_prepared) m_prepared = true; qDebug()<<"return , comes end"; } /*! * \brief Prepares the filter to read from messages received by the MQTTTopic. * Returns whether the preparation was successfull or not * \param message * \param topic */ int AsciiFilterPrivate::prepareMQTTTopicToRead(const QString& message, const QString& topic) { vectorNames.append("value"); if (endColumn == -1) endColumn = 1; else endColumn++; vectorNames.prepend("timestamp"); endColumn++; if (createIndexEnabled) { vectorNames.prepend("index"); endColumn++; } m_actualCols = endColumn - startColumn + 1; qDebug()<<"actual cols"< 1) { int pos2 = firstLine.indexOf(firstLineStringList.at(1), length1); m_separator = firstLine.mid(length1, pos2 - length1); } else { //old: separator = line.right(line.length() - length1); m_separator = ' '; } } } else { // use given separator // replace symbolic "TAB" with '\t' m_separator = separatingCharacter.replace(QLatin1String("2xTAB"), "\t\t", Qt::CaseInsensitive); m_separator = separatingCharacter.replace(QLatin1String("TAB"), "\t", Qt::CaseInsensitive); // replace symbolic "SPACE" with ' ' m_separator = m_separator.replace(QLatin1String("2xSPACE"), QLatin1String(" "), Qt::CaseInsensitive); m_separator = m_separator.replace(QLatin1String("3xSPACE"), QLatin1String(" "), Qt::CaseInsensitive); m_separator = m_separator.replace(QLatin1String("4xSPACE"), QLatin1String(" "), Qt::CaseInsensitive); m_separator = m_separator.replace(QLatin1String("SPACE"), QLatin1String(" "), Qt::CaseInsensitive); firstLineStringList = firstLine.split(m_separator, (QString::SplitBehavior)skipEmptyParts); } DEBUG("separator: \'" << m_separator.toStdString() << '\''); DEBUG("number of columns: " << firstLineStringList.size()); QDEBUG("first line: " << firstLineStringList); DEBUG("headerEnabled = " << headerEnabled); // parse first data line to determine data type for each column columnModes.resize(m_actualCols); int col = 0; if (createIndexEnabled) { columnModes[0] = AbstractColumn::Integer; col = 1; } //set column mode for timestamp columnModes[col] = AbstractColumn::DateTime; col++; auto firstValue = firstLineStringList.takeFirst();//use first value to identify column mode while(firstValue.isEmpty() || firstValue.startsWith(commentCharacter) )//get the first usable value firstValue = firstLineStringList.takeFirst(); if (simplifyWhitespacesEnabled) firstValue = firstValue.simplified(); columnModes[m_actualCols-1] = AbstractFileFilter::columnMode(firstValue, dateTimeFormat, numberFormat); qDebug()<<"-----setting column mode" << " "< numeric if (mode == AbstractColumn::Numeric && columnModes[m_actualCols-1] == AbstractColumn::Integer) { columnModes[m_actualCols-1] = mode; qDebug()<<"-----setting column mode" << " "< text if (mode == AbstractColumn::Text && columnModes[m_actualCols-1] != AbstractColumn::Text) { columnModes[m_actualCols-1] = mode; qDebug()<<"-----setting column mode" << " "< numeric if (mode == AbstractColumn::Numeric && columnModes[m_actualCols-1] == AbstractColumn::Integer) { columnModes[m_actualCols-1] = mode; qDebug()<<"-----setting column mode" << " "< text if (mode == AbstractColumn::Text && columnModes[m_actualCols-1] != AbstractColumn::Text) { columnModes[m_actualCols-1] = mode; qDebug()<<"-----setting column mode" << " "<rowCount(); //set the column modes columnModes.resize(topic->columnCount()); for(int i = 0; i < topic->columnCount(); ++i) { columnModes[i] = topic->column(i)->columnMode(); } //set the data containers m_dataContainer.resize(m_actualCols); for (int n = 0; n < m_actualCols; ++n) { // data() returns a void* which is a pointer to any data type (see ColumnPrivate.cpp) topic->child(n)->setColumnMode(columnModes[n]); switch (columnModes[n]) { case AbstractColumn::Numeric: { QVector* vector = static_cast* >(topic->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Integer: { QVector* vector = static_cast* >(topic->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Text: { QVector* vector = static_cast*>(topic->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } case AbstractColumn::DateTime: { QVector* vector = static_cast* >(topic->child(n)->data()); vector->reserve(m_actualRows); vector->resize(m_actualRows); m_dataContainer[n] = static_cast(vector); break; } //TODO case AbstractColumn::Month: case AbstractColumn::Day: break; } } } } /*! * \brief Returns the separator used by the filter * \return */ QString AsciiFilterPrivate::separator() const { return m_separator; } #endif diff --git a/tests/import_export/CMakeLists.txt b/tests/import_export/CMakeLists.txt index 82fafb58b..58661ced8 100644 --- a/tests/import_export/CMakeLists.txt +++ b/tests/import_export/CMakeLists.txt @@ -1,3 +1,4 @@ add_subdirectory(ASCII) add_subdirectory(JSON) add_subdirectory(project) +add_subdirectory(MQTT) diff --git a/tests/import_export/MQTT/CMakeLists.txt b/tests/import_export/MQTT/CMakeLists.txt new file mode 100644 index 000000000..acc3e3969 --- /dev/null +++ b/tests/import_export/MQTT/CMakeLists.txt @@ -0,0 +1,38 @@ +add_executable (mqttunittest MQTTUnitTest.cpp) + +target_link_libraries(mqttunittest Qt5::Test) +target_link_libraries(mqttunittest KF5::Archive KF5::XmlGui ${GSL_LIBRARIES} ${GSL_CBLAS_LIBRARIES}) + +IF (Qt5SerialPort_FOUND) + target_link_libraries(mqttunittest Qt5::SerialPort ) +ENDIF () +IF (KF5SyntaxHighlighting_FOUND) + target_link_libraries(mqttunittest KF5::SyntaxHighlighting ) +ENDIF () +#TODO: KF5::NewStuff + +IF (CANTOR_LIBS_FOUND) + target_link_libraries(mqttunittest ${CANTOR_LIBS} ) +ENDIF () +IF (HDF5_FOUND) + target_link_libraries(mqttunittest ${HDF5_C_LIBRARIES} ) +ENDIF () +IF (FFTW_FOUND) + target_link_libraries(mqttunittest ${FFTW_LIBRARIES} ) +ENDIF () +IF (NETCDF_FOUND) + target_link_libraries(mqttunittest ${NETCDF_LIBRARY} ) +ENDIF () +IF (CFITSIO_FOUND) + target_link_libraries(mqttunittest ${CFITSIO_LIBRARY} ) +ENDIF () +IF (USE_LIBORIGIN) + target_link_libraries(mqttunittest liborigin-static ) +ENDIF () +IF (Qt5Mqtt_FOUND) + target_link_libraries(mqttunittest Qt5::Mqtt ) +ENDIF () + +target_link_libraries(mqttunittest labplot2lib) + +add_test(NAME mqttunittest COMMAND mqttunittest) diff --git a/tests/import_export/MQTT/MQTTUnitTest.cpp b/tests/import_export/MQTT/MQTTUnitTest.cpp new file mode 100644 index 000000000..9f7882864 --- /dev/null +++ b/tests/import_export/MQTT/MQTTUnitTest.cpp @@ -0,0 +1,398 @@ +/*************************************************************************** + File : MQTTUnitTest.cpp + Project : LabPlot + Description : Tests for MQTT related features + -------------------------------------------------------------------- + Copyright : (C) 2018 Kovacs Ferencz (kferike98@gmail.com) + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, * + * Boston, MA 02110-1301 USA * + * * + ***************************************************************************/ + +#include "MQTTUnitTest.h" +#include "backend/datasources/filters/AsciiFilter.h" +#include "backend/datasources/MQTTClient.h" +#include "backend/datasources/MQTTSubscription.h" +#include "backend/datasources/MQTTTopic.h" +#include "backend/core/Project.h" + +#include +#include +#include +#include +#include +#include + +void MQTTUnitTest::initTestCase() { + const QString currentDir = __FILE__; + m_dataDir = currentDir.left(currentDir.lastIndexOf(QDir::separator())) + QDir::separator() + QLatin1String("data") + QDir::separator(); + + // needed in order to have the signals triggered by SignallingUndoCommand, see LabPlot.cpp + //TODO: redesign/remove this + qRegisterMetaType("const AbstractAspect*"); + qRegisterMetaType("const AbstractColumn*"); +} + +//############################################################################## +//################### check superior and inferior relations ################## +//############################################################################## +void MQTTUnitTest::testContainFalse() { + MQTTClient* client = new MQTTClient("test"); + const QString fileName = m_dataDir + "contain_false.txt"; + QFile file(fileName); + + if(file.open(QIODevice::ReadOnly)) { + QTextStream in(&file); + + while(!in.atEnd()) { + QString line = in.readLine(); + QStringList topics = line.split(" ", QString::SkipEmptyParts); + QCOMPARE(client->checkTopicContains(topics[0], topics[1]), false); + } + + delete client; + file.close(); + } +} + +void MQTTUnitTest::testContainTrue() { + MQTTClient* client = new MQTTClient("test"); + const QString fileName = m_dataDir + "contain_true.txt"; + QFile file(fileName); + + if(file.open(QIODevice::ReadOnly)) { + QTextStream in(&file); + + while(!in.atEnd()) { + QString line = in.readLine(); + QStringList topics = line.split(" ", QString::SkipEmptyParts); + QCOMPARE(client->checkTopicContains(topics[0], topics[1]), true); + } + + delete client; + file.close(); + } +} + +void MQTTUnitTest::testCommonTrue(){ + MQTTClient* client = new MQTTClient("test"); + const QString fileName = m_dataDir + "common_true.txt"; + QFile file(fileName); + + if(file.open(QIODevice::ReadOnly)) { + QTextStream in(&file); + + while(!in.atEnd()) { + QString line = in.readLine(); + QStringList topics = line.split(" ", QString::SkipEmptyParts); + QCOMPARE(client->checkCommonLevel(topics[0], topics[1]), topics[2]); + } + + delete client; + file.close(); + } +} + +void MQTTUnitTest::testCommonFalse(){ + MQTTClient* client = new MQTTClient("test"); + const QString fileName = m_dataDir + "common_false.txt"; + QFile file(fileName); + + if(file.open(QIODevice::ReadOnly)) { + QTextStream in(&file); + + while(!in.atEnd()) { + QString line = in.readLine(); + QStringList topics = line.split(" ", QString::SkipEmptyParts); + QCOMPARE(client->checkCommonLevel(topics[0], topics[1]), ""); + } + + delete client; + file.close(); + } +} + + +void MQTTUnitTest::testIntegerMessage() { + AsciiFilter* filter = new AsciiFilter(); + filter->setAutoModeEnabled(true); + + Project* project = new Project(); + + MQTTClient* mqttClient = new MQTTClient("test"); + project->addChild(mqttClient); + mqttClient->setFilter(filter); + mqttClient->setReadingType(MQTTClient::TillEnd); + mqttClient->setKeepNValues(0); + mqttClient->setUpdateType(MQTTClient::UpdateType::NewData); + mqttClient->setMqttClientHostPort("broker.hivemq.com", 1883); + mqttClient->setMQTTUseAuthentication(false); + mqttClient->setMQTTUseID(false); + QMqttTopicFilter topicFilter {"labplot/mqttUnitTest"}; + mqttClient->addInitialMqttSubscriptions(topicFilter, 0); + mqttClient->read(); + mqttClient->ready(); + + QMqttClient* client = new QMqttClient(); + client->setHostname("broker.hivemq.com"); + client->setPort(1883); + client->connectToHost(); + + QTest::qWaitFor([&]() { + return (client->state() == QMqttClient::Connected); + }, 5000); + + QMqttSubscription* subscription = client->subscribe(topicFilter, 0); + if(subscription) { + const QString fileName = m_dataDir + "integer_message_1.txt"; + QFile file(fileName); + + if(file.open(QIODevice::ReadOnly)) { + qDebug()<<"publish"; + QTextStream in(&file); + QString message = in.readAll(); + qDebug()<publish(topicFilter.filter(), message.toUtf8(), 0); + } + file.close(); + + + QTimer timer; + timer.setSingleShot(true); + QEventLoop* loop = new QEventLoop(); + connect(mqttClient, &MQTTClient::mqttTopicsChanged, loop, &QEventLoop::quit); + connect( (&timer), &QTimer::timeout, loop, &QEventLoop::quit); + timer.start(5000); + loop->exec(); + + const MQTTTopic* testTopic = nullptr; + + if(timer.isActive()) { + qDebug()<<"search for children"; + QVector topic = mqttClient->children (AbstractAspect::Recursive); + for(int i = 0; i < topic.size(); ++i) { + if (topic[i]->topicName() == "labplot/mqttUnitTest") { + testTopic = topic[i]; + break; + } + } + + Column* value = testTopic->column(testTopic->columnCount() - 1); + QCOMPARE(value->columnMode(), Column::ColumnMode::Integer); + QCOMPARE(value->rowCount(), 3); + QCOMPARE(value->valueAt(0), 1); + QCOMPARE(value->valueAt(1), 2); + QCOMPARE(value->valueAt(2), 3); + + const QString fileName2 = m_dataDir + "integer_message_2.txt"; + QFile file2(fileName2); + + if(file2.open(QIODevice::ReadOnly)) { + qDebug()<<"publish"; + QTextStream in2(&file2); + QString message = in2.readAll(); + qDebug()<publish(topicFilter.filter(), message.toUtf8(), 0); + } + file2.close(); + + QTest::qWait(1000); + + QCOMPARE(value->rowCount(), 8); + QCOMPARE(value->valueAt(3), 6); + QCOMPARE(value->valueAt(4), 0); + QCOMPARE(value->valueAt(5), 0); + QCOMPARE(value->valueAt(6), 0); + QCOMPARE(value->valueAt(7), 3); + } + } +} + +void MQTTUnitTest::testNumericMessage() { + AsciiFilter* filter = new AsciiFilter(); + filter->setAutoModeEnabled(true); + + Project* project = new Project(); + + MQTTClient* mqttClient = new MQTTClient("test"); + project->addChild(mqttClient); + mqttClient->setFilter(filter); + mqttClient->setReadingType(MQTTClient::TillEnd); + mqttClient->setKeepNValues(0); + mqttClient->setUpdateType(MQTTClient::UpdateType::NewData); + mqttClient->setMqttClientHostPort("broker.hivemq.com", 1883); + mqttClient->setMQTTUseAuthentication(false); + mqttClient->setMQTTUseID(false); + QMqttTopicFilter topicFilter {"labplot/mqttUnitTest"}; + mqttClient->addInitialMqttSubscriptions(topicFilter, 0); + mqttClient->read(); + mqttClient->ready(); + + QMqttClient* client = new QMqttClient(); + client->setHostname("broker.hivemq.com"); + client->setPort(1883); + client->connectToHost(); + + QTest::qWaitFor([&]() { + return (client->state() == QMqttClient::Connected); + }, 5000); + + QMqttSubscription* subscription = client->subscribe(topicFilter, 0); + if(subscription) { + const QString fileName = m_dataDir + "numeric_message_1.txt"; + QFile file(fileName); + + if(file.open(QIODevice::ReadOnly)) { + qDebug()<<"publish"; + QTextStream in(&file); + QString message = in.readAll(); + qDebug()<publish(topicFilter.filter(), message.toUtf8(), 0); + } + file.close(); + + + QTimer timer; + timer.setSingleShot(true); + QEventLoop* loop = new QEventLoop(); + connect(mqttClient, &MQTTClient::mqttTopicsChanged, loop, &QEventLoop::quit); + connect( (&timer), &QTimer::timeout, loop, &QEventLoop::quit); + timer.start(5000); + loop->exec(); + + const MQTTTopic* testTopic = nullptr; + + if(timer.isActive()) { + qDebug()<<"search for children"; + QVector topic = mqttClient->children (AbstractAspect::Recursive); + for(int i = 0; i < topic.size(); ++i) { + if (topic[i]->topicName() == "labplot/mqttUnitTest") { + testTopic = topic[i]; + break; + } + } + + Column* value = testTopic->column(testTopic->columnCount() - 1); + QCOMPARE(value->columnMode(), Column::ColumnMode::Numeric); + QCOMPARE(value->rowCount(), 3); + QCOMPARE(value->valueAt(0), 1.5); + QCOMPARE(value->valueAt(1), 2.7); + QCOMPARE(value->valueAt(2), 3.9); + + const QString fileName2 = m_dataDir + "numeric_message_2.txt"; + QFile file2(fileName2); + + if(file2.open(QIODevice::ReadOnly)) { + qDebug()<<"publish"; + QTextStream in2(&file2); + QString message = in2.readAll(); + qDebug()<publish(topicFilter.filter(), message.toUtf8(), 0); + } + file2.close(); + + QTest::qWait(1000); + + QCOMPARE(value->rowCount(), 8); + QCOMPARE(value->valueAt(3), 6); + QCOMPARE((bool)std::isnan(value->valueAt(4)), true); + QCOMPARE((bool)std::isnan(value->valueAt(5)), true); + QCOMPARE((bool)std::isnan(value->valueAt(6)), true); + QCOMPARE(value->valueAt(7), 0.0098); + } + } +} + +void MQTTUnitTest::testTextMessage() { + AsciiFilter* filter = new AsciiFilter(); + filter->setAutoModeEnabled(true); + + Project* project = new Project(); + + MQTTClient* mqttClient = new MQTTClient("test"); + project->addChild(mqttClient); + mqttClient->setFilter(filter); + mqttClient->setReadingType(MQTTClient::TillEnd); + mqttClient->setKeepNValues(0); + mqttClient->setUpdateType(MQTTClient::UpdateType::NewData); + mqttClient->setMqttClientHostPort("broker.hivemq.com", 1883); + mqttClient->setMQTTUseAuthentication(false); + mqttClient->setMQTTUseID(false); + QMqttTopicFilter topicFilter {"labplot/mqttUnitTest"}; + mqttClient->addInitialMqttSubscriptions(topicFilter, 0); + mqttClient->read(); + mqttClient->ready(); + + QMqttClient* client = new QMqttClient(); + client->setHostname("broker.hivemq.com"); + client->setPort(1883); + client->connectToHost(); + + QTest::qWaitFor([&]() { + return (client->state() == QMqttClient::Connected); + }, 5000); + + QMqttSubscription* subscription = client->subscribe(topicFilter, 0); + if(subscription) { + const QString fileName = m_dataDir + "text_message.txt"; + QFile file(fileName); + + if(file.open(QIODevice::ReadOnly)) { + qDebug()<<"publish"; + QTextStream in(&file); + QString message = in.readAll(); + qDebug()<publish(topicFilter.filter(), message.toUtf8(), 0); + } + file.close(); + + + QTimer timer; + timer.setSingleShot(true); + QEventLoop* loop = new QEventLoop(); + connect(mqttClient, &MQTTClient::mqttTopicsChanged, loop, &QEventLoop::quit); + connect( (&timer), &QTimer::timeout, loop, &QEventLoop::quit); + timer.start(5000); + loop->exec(); + + const MQTTTopic* testTopic = nullptr; + + if(timer.isActive()) { + qDebug()<<"search for children"; + QVector topic = mqttClient->children (AbstractAspect::Recursive); + for(int i = 0; i < topic.size(); ++i) { + if (topic[i]->topicName() == "labplot/mqttUnitTest") { + testTopic = topic[i]; + break; + } + } + + Column* value = testTopic->column(testTopic->columnCount() - 1); + QCOMPARE(value->columnMode(), Column::ColumnMode::Text); + QCOMPARE(value->rowCount(), 5); + QCOMPARE(value->textAt(0), "ball"); + QCOMPARE(value->textAt(1), "cat"); + QCOMPARE(value->textAt(2), "dog"); + QCOMPARE(value->textAt(3), "house"); + QCOMPARE(value->textAt(4), "Barcelona"); + } + } +} + +QTEST_MAIN(MQTTUnitTest) diff --git a/tests/import_export/MQTT/MQTTUnitTest.h b/tests/import_export/MQTT/MQTTUnitTest.h new file mode 100644 index 000000000..80ba8b51e --- /dev/null +++ b/tests/import_export/MQTT/MQTTUnitTest.h @@ -0,0 +1,51 @@ +/*************************************************************************** + File : MQTTUnitTest.h + Project : LabPlot + Description : Tests for MQTT related features + -------------------------------------------------------------------- + Copyright : (C) 2018 Kovacs Ferencz (kferike98@gmail.com) + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, * + * Boston, MA 02110-1301 USA * + * * + ***************************************************************************/ +#include + +class MQTTUnitTest : public QObject { + Q_OBJECT + +private slots: + void initTestCase(); + + //check superior and inferior relations + void testContainFalse(); + void testContainTrue(); + + //check common topics + void testCommonTrue(); + void testCommonFalse(); + + //test for different tipes of messages + void testIntegerMessage(); + void testNumericMessage(); + void testTextMessage(); + + +private: + QString m_dataDir; +}; diff --git a/tests/import_export/MQTT/data/common_false.txt b/tests/import_export/MQTT/data/common_false.txt new file mode 100644 index 000000000..9748a9a3b --- /dev/null +++ b/tests/import_export/MQTT/data/common_false.txt @@ -0,0 +1,6 @@ +home/bathroom/sensor/temperature home/bathroom/sensor/temperature/1 +home/bathroom/sensor/temperature home/bathroom/sensor/temperature +home/bathroom/sensor/temperature home/bedroom/sensor/pressure +home/+/sensor/temperature home/bedroom/sensor/pressure +home/bathroom/+ home/bedroom/# +home/bathroom/sensor/temperature guesthouse/bathroom/sensor/temperature diff --git a/tests/import_export/MQTT/data/common_true.txt b/tests/import_export/MQTT/data/common_true.txt new file mode 100644 index 000000000..3ce9e0cec --- /dev/null +++ b/tests/import_export/MQTT/data/common_true.txt @@ -0,0 +1,4 @@ +home/bathroom/sensor/temperature home/bedroom/sensor/temperature home/+/sensor/temperature +home/+/sensor/temperature home/+/sensor/pressure home/+/sensor/+ +home/bathroom/# home/bedroom/# home/+/# +home/+/sensor/# home/+/switch/# home/+/+/# diff --git a/tests/import_export/MQTT/data/contain_false.txt b/tests/import_export/MQTT/data/contain_false.txt new file mode 100644 index 000000000..4932d244a --- /dev/null +++ b/tests/import_export/MQTT/data/contain_false.txt @@ -0,0 +1,5 @@ +home/+/sensor/temperature home/bedroom/sensor/pressure +home/+/# home/# +home/bedroom/sensor/+ home/bedroom/sensor +home home/bedroom +home/bedroom/sensor/+ home/bedroom/sensor/# diff --git a/tests/import_export/MQTT/data/contain_true.txt b/tests/import_export/MQTT/data/contain_true.txt new file mode 100644 index 000000000..b8d00a97b --- /dev/null +++ b/tests/import_export/MQTT/data/contain_true.txt @@ -0,0 +1,5 @@ +home/+/sensor/temperature home/bedroom/sensor/temperature +home/# home/+/# +home/bedroom/+/# home/bedroom/sensor/1 +home/# home/bedroom/# +home/bedroom/sensor/# home/bedroom/sensor/+ diff --git a/tests/import_export/MQTT/data/integer_message_1.txt b/tests/import_export/MQTT/data/integer_message_1.txt new file mode 100644 index 000000000..2d39a69d6 --- /dev/null +++ b/tests/import_export/MQTT/data/integer_message_1.txt @@ -0,0 +1,7 @@ +1 + + +2 + + +3 diff --git a/tests/import_export/MQTT/data/integer_message_2.txt b/tests/import_export/MQTT/data/integer_message_2.txt new file mode 100644 index 000000000..c31165b29 --- /dev/null +++ b/tests/import_export/MQTT/data/integer_message_2.txt @@ -0,0 +1,10 @@ +6 + + +house ball + + +3.5 + + +3 diff --git a/tests/import_export/MQTT/data/numeric_message_1.txt b/tests/import_export/MQTT/data/numeric_message_1.txt new file mode 100644 index 000000000..2609eae3b --- /dev/null +++ b/tests/import_export/MQTT/data/numeric_message_1.txt @@ -0,0 +1,7 @@ +1.5 + + +2.7 + + +3.9 diff --git a/tests/import_export/MQTT/data/numeric_message_2.txt b/tests/import_export/MQTT/data/numeric_message_2.txt new file mode 100644 index 000000000..1da4e583b --- /dev/null +++ b/tests/import_export/MQTT/data/numeric_message_2.txt @@ -0,0 +1,10 @@ +6 + + +house ball + + +car + + +0.0098 diff --git a/tests/import_export/MQTT/data/text_message.txt b/tests/import_export/MQTT/data/text_message.txt new file mode 100644 index 000000000..dee6181ea --- /dev/null +++ b/tests/import_export/MQTT/data/text_message.txt @@ -0,0 +1,18 @@ +ball + +cat + + +#comment + + +dog + + + +#comment something +#comment something new + +house #comment + +Barcelona