diff --git a/SkywardHub.pro b/SkywardHub.pro
index c5f3472aabce1eb94f5af97510dfac0dbd3c196f..bb8bef9b122c16d782de33cee6f9aee3dd6eee0f 100644
--- a/SkywardHub.pro
+++ b/SkywardHub.pro
@@ -19,6 +19,7 @@ INCLUDEPATH += \
     libs/mavlink-skyward-lib
 
 SOURCES += \
+    src/shared/Modules/CsvLogger/CsvLogger.cpp \
     src/shared/Modules/Empty/EmptyModule.cpp \
     src/shared/Modules/ValuesConverterViewer/ValuesViewerConfigPanel.cpp \
     src/shared/Modules/ValuesConverterViewer/ValueElement.cpp \
@@ -75,6 +76,7 @@ SOURCES += \
     src/entrypoints/groundstation/main.cpp
 
 HEADERS += \
+    src/shared/Modules/CsvLogger/CsvLogger.h \
     src/shared/Modules/Empty/EmptyModule.h \
     src/shared/Modules/ValuesConverterViewer/ValueElement.h \
     src/shared/Modules/ValuesConverterViewer/ValuesViewerConfigPanel.h \
diff --git a/src/shared/Components/FilterSelector/FilterSelector.cpp b/src/shared/Components/FilterSelector/FilterSelector.cpp
index 47fdea567c12b5e36ac71c28a10c33548f5eefce..962ffcc213d5eb8d6f5d915951a034cad38be092 100644
--- a/src/shared/Components/FilterSelector/FilterSelector.cpp
+++ b/src/shared/Components/FilterSelector/FilterSelector.cpp
@@ -15,7 +15,7 @@ void FilterSelector::setupUi()
     topic = new QComboBox;
     topic->setSizeAdjustPolicy(QComboBox::AdjustToContents);
     topic->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
-    topic->addItems(messages.keys());
+    topic->addItems(listOfMessages().keys());
     layout->addWidget(topic);
 
     fieldsWidget = new QListWidget;
@@ -32,14 +32,14 @@ void FilterSelector::setupUi()
     if (filter.getTopic().toString() != Topic().toString())
         currentMessage = filter.getTopic().toString();
     else
-        currentMessage = messages.keys().at(0);
+        currentMessage = listOfMessages().keys().at(0);
     topic->setCurrentText(currentMessage);
     setTopic(currentMessage);
 
     connect(topic, &QComboBox::currentTextChanged, this,
             [=](QString key)
             {
-                if (messages.contains(key))
+                if (listOfMessages().contains(key))
                 {
                     filter = getFilter();
                     setTopic(key);
@@ -74,18 +74,22 @@ void FilterSelector::selectFilter(const Filter &filter,
 
 FilterSelector::FilterSelector()
 {
-    parseMessagesList();
     setupUi();
 }
 
 FilterSelector::FilterSelector(const Filter &filter) : filter(filter)
 {
-    parseMessagesList();
     setupUi();
 }
 
-void FilterSelector::parseMessagesList()
+QMap<QString, QList<QString>> FilterSelector::listOfMessages(){
+    static auto messages = parseMessagesList();
+    return messages;
+}
+
+QMap<QString, QList<QString>> FilterSelector::parseMessagesList()
 {
+    QMap<QString, QList<QString>> out;
     mavlink_message_info_t messagesList[256] = MAVLINK_MESSAGE_INFO;
 
     for (unsigned int i = 0; i < 255; i++)
@@ -98,9 +102,10 @@ void FilterSelector::parseMessagesList()
             for (unsigned int ii = 0; ii < messagesList[i].num_fields; ii++)
                 fields.append(messagesList[i].fields[ii].name);
 
-            messages[QString("Mav/") + messagesList[i].name] = fields;
+            out[QString("Mav/") + messagesList[i].name] = fields;
         }
     }
+    return out;
 }
 
 void FilterSelector::setTopic(QString topic)
@@ -109,7 +114,7 @@ void FilterSelector::setTopic(QString topic)
     fieldsWidget->clear();
 
     // Add the new topic fields
-    fieldsWidget->addItems(messages[topic]);
+    fieldsWidget->addItems(listOfMessages()[topic]);
     for (int i = 0; i < fieldsWidget->count(); i++)
     {
         auto item = fieldsWidget->item(i);
@@ -137,4 +142,4 @@ Filter FilterSelector::getFilter()
     }
 
     return filter;
-}
\ No newline at end of file
+}
diff --git a/src/shared/Components/FilterSelector/FilterSelector.h b/src/shared/Components/FilterSelector/FilterSelector.h
index 422a4954b726a50c2b2bfd86388fbda200080482..5d462bfa8196db0fe3f163bb3e78ced3d1ec0281 100644
--- a/src/shared/Components/FilterSelector/FilterSelector.h
+++ b/src/shared/Components/FilterSelector/FilterSelector.h
@@ -20,11 +20,13 @@ public:
     static void selectFilter(const Filter& filter, Handler handler);
     Filter getFilter();
 
+    static QMap<QString, QList<QString>> listOfMessages();
+
 private:
     FilterSelector();
     explicit FilterSelector(const Filter& filter);
 
-    void parseMessagesList();
+    static QMap<QString, QList<QString>> parseMessagesList();
     void setupUi();
 
     void setTopic(QString topic);
@@ -35,8 +37,6 @@ private:
     QListWidget* fieldsWidget;
     QComboBox* topic;
 
-    QMap<QString, QList<QString>> messages;
-
 signals:
     void filterSelected(const Filter&);
 };
diff --git a/src/shared/Modules/CsvLogger/CsvLogger.cpp b/src/shared/Modules/CsvLogger/CsvLogger.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..64b02d3065bf00b5e1aefed53e4e2fac8b669a04
--- /dev/null
+++ b/src/shared/Modules/CsvLogger/CsvLogger.cpp
@@ -0,0 +1,94 @@
+#include "CsvLogger.h"
+
+#include <Components/FilterSelector/FilterSelector.h>
+
+CsvLogger::CsvLogger(QWidget* parent) : DefaultModule(parent), started(false), file(nullptr) {
+    defaultContextMenuSetup();
+    setupUi();
+}
+
+CsvLogger::~CsvLogger() { }
+
+QWidget* CsvLogger::toWidget(){
+    return this;
+}
+
+XmlObject CsvLogger::toXmlObject(){
+    XmlObject obj(getName(ModuleId::CSV_LOGGER));
+    return obj;
+}
+
+void CsvLogger::fromXmlObject(const XmlObject& obj){
+    Q_UNUSED(obj);
+}
+
+void CsvLogger::setupUi(){
+    QVBoxLayout* layout = new QVBoxLayout;
+
+    QComboBox* combo = new QComboBox;
+    combo->addItems(FilterSelector::listOfMessages().keys());
+    layout->addWidget(combo);
+
+    QHBoxLayout* bottomLayout = new QHBoxLayout;
+
+    QLineEdit* filenameEdit = new QLineEdit;
+    filenameEdit->setPlaceholderText("Type output file name here.");
+    bottomLayout->addWidget(filenameEdit);
+
+    QPushButton* startStopLogging = new QPushButton("Start logging");
+    bottomLayout->addWidget(startStopLogging);
+
+    QObject::connect(startStopLogging, &QPushButton::clicked, [this, combo, filenameEdit, startStopLogging](){
+        if(!started){
+            if(file){
+                file->close();
+                file->deleteLater();
+            }
+
+            file = new QFile(filenameEdit->text());
+            if(!file->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)){
+                file->deleteLater();
+                file = nullptr;
+
+                error("I/O Error", QString("Unable to open file \"") + filenameEdit->text() + "\".");
+                return;
+            }
+
+            const auto& properties = FilterSelector::listOfMessages()[combo->currentText()];
+            for(int i = 0; i < properties.size(); i++){
+                file->write((properties[i] + (i < properties.size()+1 ? "," : "\n")).toUtf8());
+            }
+
+            getCore()->getMessageBroker()->unsubscribe(lastSub, this);
+            lastSub = Filter::fromString(combo->currentText());
+            getCore()->getMessageBroker()->subscribe(
+                lastSub, this,
+                [this](const Message& message, const Filter& filter)
+                { received(message); });
+            started = true;
+        } else {
+            if(file){
+                file->close();
+                file->deleteLater();
+                file = nullptr;
+            }
+            started = false;
+            getCore()->getMessageBroker()->unsubscribe(lastSub, this);
+        }
+
+        startStopLogging->setText(started ? "Stop logging" : "Start logging");
+    });
+
+    layout->addLayout(bottomLayout);
+    setLayout(layout);
+}
+
+void CsvLogger::received(const Message& msg){
+    if(!file)
+        return;
+
+    const auto& properties = FilterSelector::listOfMessages()[lastSub.getTopic().toString()];
+    for(int i = 0; i < properties.size(); i++){
+        file->write((msg.getField(properties[i]).toString() + (i < properties.size()+1 ? "," : "\n")).toUtf8());
+    }
+}
diff --git a/src/shared/Modules/CsvLogger/CsvLogger.h b/src/shared/Modules/CsvLogger/CsvLogger.h
new file mode 100644
index 0000000000000000000000000000000000000000..f0fb1b94c753fa774c4153d3b865f6851a35dffd
--- /dev/null
+++ b/src/shared/Modules/CsvLogger/CsvLogger.h
@@ -0,0 +1,31 @@
+#ifndef CSVLOGGER_H
+#define CSVLOGGER_H
+
+#include <Core/Message/Message.h>
+#include <Modules/DefaultModule/DefaultModule.h>
+
+class CsvLogger : public DefaultModule
+{
+    Q_OBJECT
+
+public:
+    explicit CsvLogger(QWidget* parent = nullptr);
+    ~CsvLogger();
+
+    QWidget* toWidget() override;
+
+    XmlObject toXmlObject() override;
+    void fromXmlObject(const XmlObject& obj) override;
+
+private:
+    bool started;
+    QFile* file;
+    Filter lastSub;
+
+    void setupUi();
+
+private slots:
+    void received(const Message& msg);
+};
+
+#endif // CSVLOGGER_H
diff --git a/src/shared/Modules/ModuleInfo.h b/src/shared/Modules/ModuleInfo.h
index de76b3e92d2e1df726f478c22cbfc9ffa3e2f3b5..1d26c421bc49532a8165585efdd1497aff0f07c4 100644
--- a/src/shared/Modules/ModuleInfo.h
+++ b/src/shared/Modules/ModuleInfo.h
@@ -11,6 +11,7 @@ enum ModuleId
     SKYWARDHUB,
     COMMANDPAD,
     COMPACT_COMMAND_PAD,
+    CSV_LOGGER,
     BROKERTEST,
     GRAPH,
     OUTCOMINGMESSAGEVIEWER,
diff --git a/src/shared/Modules/ModulesList.cpp b/src/shared/Modules/ModulesList.cpp
index 2ac1ea1f7ad333c1d7ff73ad9e301fc6344389e2..0aed0068f943ee4bae614f2a47fe95e0c0fd5ec5 100644
--- a/src/shared/Modules/ModulesList.cpp
+++ b/src/shared/Modules/ModulesList.cpp
@@ -4,6 +4,7 @@
 #include <Components/ModulesPicker/ModulesPicker.h>
 #include <Modules/CommandPad/CommandPad.h>
 #include <Modules/CompactCommandPad/CompactCommandPad.h>
+#include <Modules/CsvLogger/CsvLogger.h>
 #include <Modules/Empty/EmptyModule.h>
 #include <Modules/FileStream/FileStreamModule.h>
 #include <Modules/Graph/Graph.h>
@@ -53,6 +54,13 @@ void ModulesList::createModuleList()
     compactCommandPad.addModuleSourceFiles("Modules/CompactCommandPad/");
     addModuleInfo(compactCommandPad);
 
+    ModuleInfo csvLogger(ModuleId::CSV_LOGGER,
+                                 "CsvLogger",
+                                 ModuleCategory::DATASOURCE);
+    csvLogger.setFactory([]() { return new CsvLogger(); });
+    csvLogger.addModuleSourceFiles("Modules/CsvLogger/");
+    addModuleInfo(csvLogger);
+
     ModuleInfo splitter(ModuleId::SPLITTER, "Splitter");
     splitter.setFactory([]() { return new Splitter(); });
     splitter.addModuleSourceFiles("Modules/Splitter/");