diff --git a/README.md b/README.md
index 16b40bbf28344c5d2250a18237ec93bc06530467..788708185e5bfb8b33fb67956cd79dc8a936fc14 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,18 @@
 # README #
 
-NRP web-frontend 2.0 using React
+NRP web-frontend 4.0 using React
 
+### Prerequisites
+
+- "nvm install 14"
+
+### Install
+
+- "nvm use 14"
+- "npm install"
 
 ### Commands
 
+- "nvm use 14"
 - "npm start" (dev server)
 - "npm run build" (build for production)
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index e9b37333452f49bd17ee3b46dec3ecabf28d71f4..4c67903c555a9eafa5ae8f01b0cd24e5db610256 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1264,6 +1264,87 @@
         "minimist": "^1.2.0"
       }
     },
+    "@codemirror/autocomplete": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.1.0.tgz",
+      "integrity": "sha512-wtO4O5WDyXhhCd4q4utDIDZxnQfmJ++3dGBCG9LMtI79+92OcA1DVk/n7BEupKmjIr8AzvptDz7YQ9ud6OkU+A==",
+      "requires": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "@codemirror/commands": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.1.0.tgz",
+      "integrity": "sha512-qCj2YqmbBjj0P1iumnlL5lBqZvJPzT+t2UvgjcaXErp5ZvMqFRVgQyrEfdXX6SX5UcvcHKBjXqno+MkUp0aYvQ==",
+      "requires": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "@codemirror/language": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz",
+      "integrity": "sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==",
+      "requires": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0",
+        "style-mod": "^4.0.0"
+      }
+    },
+    "@codemirror/lint": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz",
+      "integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==",
+      "requires": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "crelt": "^1.0.5"
+      }
+    },
+    "@codemirror/search": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.1.0.tgz",
+      "integrity": "sha512-ye6m0jFHSgQ4qnfWVwArvm7XrCMNppMYnL5f4M0WdBScslnckomf5eVacYCw8P0UBWeq72lCSXA0/eo1piZxLA==",
+      "requires": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "crelt": "^1.0.5"
+      }
+    },
+    "@codemirror/state": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.1.tgz",
+      "integrity": "sha512-2s+aXsxmAwnR3Rd+JDHPG/1lw0YsA9PEwl7Re88gHJHGfxyfEzKBmsN4rr53RyPIR4lzbbhJX0DCq0WlqlBIRw=="
+    },
+    "@codemirror/theme-one-dark": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.0.0.tgz",
+      "integrity": "sha512-jTCfi1I8QT++3m21Ui6sU8qwu3F/hLv161KLxfvkV1cYWSBwyUanmQFs89ChobQjBHi2x7s2k71wF9WYvE8fdw==",
+      "requires": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/highlight": "^1.0.0"
+      }
+    },
+    "@codemirror/view": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.2.0.tgz",
+      "integrity": "sha512-3emW1symh+GoteFMBPsltjmF790U/trouLILATh3JodbF/z98HvcQh2g3+H6dfNIHx16uNonsAF4mNzVr1TJNA==",
+      "requires": {
+        "@codemirror/state": "^6.0.0",
+        "style-mod": "^4.0.0",
+        "w3c-keyname": "^2.2.4"
+      }
+    },
     "@csstools/convert-colors": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
@@ -1883,6 +1964,27 @@
         }
       }
     },
+    "@lezer/common": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz",
+      "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA=="
+    },
+    "@lezer/highlight": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz",
+      "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==",
+      "requires": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "@lezer/lr": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.3.tgz",
+      "integrity": "sha512-qpB7rBzH8f6Mzjv2AVZRahcm+2Cf7nbIH++uXbvVOL1yIRvVWQ3HAM/saeBLCyz/togB7LGo76qdJYL1uKQlqA==",
+      "requires": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
     "@material-ui/core": {
       "version": "4.11.3",
       "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz",
@@ -2980,6 +3082,41 @@
         "eslint-visitor-keys": "^2.0.0"
       }
     },
+    "@uiw/codemirror-extensions-basic-setup": {
+      "version": "4.11.5",
+      "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.11.5.tgz",
+      "integrity": "sha512-aHtdF1JEzHmBVuWXemr8OH7SQP/LbXXZdiOo/4tcxjFpyTuVGzPteBdfQU0xPOk0m+5Oc1LPqM+HaNPXNzX6aA==",
+      "requires": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/commands": "^6.0.0",
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/lint": "^6.0.0",
+        "@codemirror/search": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0"
+      }
+    },
+    "@uiw/react-codemirror": {
+      "version": "4.11.5",
+      "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.11.5.tgz",
+      "integrity": "sha512-Bf8l3nVV4ekHbv4U0VrzUibl8+ucAY3UV0gk0xckbFnV1AlUxHcrYFiXSgy/rkyWBD7enHQENtM888B/3qBiwg==",
+      "requires": {
+        "@babel/runtime": "^7.18.6",
+        "@codemirror/theme-one-dark": "^6.0.0",
+        "@uiw/codemirror-extensions-basic-setup": "4.11.5",
+        "codemirror": "^6.0.0"
+      },
+      "dependencies": {
+        "@babel/runtime": {
+          "version": "7.18.9",
+          "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz",
+          "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==",
+          "requires": {
+            "regenerator-runtime": "^0.13.4"
+          }
+        }
+      }
+    },
     "@webassemblyjs/ast": {
       "version": "1.9.0",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@@ -4650,6 +4787,20 @@
         "q": "^1.1.2"
       }
     },
+    "codemirror": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
+      "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
+      "requires": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/commands": "^6.0.0",
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/lint": "^6.0.0",
+        "@codemirror/search": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0"
+      }
+    },
     "collect-v8-coverage": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
@@ -4984,6 +5135,11 @@
       "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
       "dev": true
     },
+    "crelt": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
+      "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
+    },
     "cross-fetch": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz",
@@ -7548,6 +7704,11 @@
         "slash": "^3.0.0"
       }
     },
+    "google-protobuf": {
+      "version": "3.21.0",
+      "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.0.tgz",
+      "integrity": "sha512-byR7MBTK4tZ5PZEb+u5ZTzpt4SfrTxv5682MjPlHN16XeqgZE2/8HOIWeiXe8JKnT9OVbtBGhbq8mtvkK8cd5g=="
+    },
     "graceful-fs": {
       "version": "4.2.6",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
@@ -15684,6 +15845,11 @@
         "schema-utils": "^2.7.0"
       }
     },
+    "style-mod": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
+      "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
+    },
     "stylehacks": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
@@ -16575,6 +16741,11 @@
         "browser-process-hrtime": "^1.0.0"
       }
     },
+    "w3c-keyname": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz",
+      "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg=="
+    },
     "w3c-xmlserializer": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
diff --git a/package.json b/package.json
index c992410e816558c807d4fb1748a5f1f9c65d4960..459c389fb63fafc700fc14f18e5056eb120b3a31 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
     "@material-ui/lab": "4.0.0-alpha.57",
     "bootstrap": "4.5",
     "flexlayout-react": "0.5.5",
+    "google-protobuf": "3.21.0",
     "jquery": "3.6.0",
     "jszip": "3.2.0",
     "mqtt": "4.3.5",
@@ -24,6 +25,7 @@
     "protobufjs": "6.11.2",
     "react": "^17.0.1",
     "react-bootstrap": "1.4.0",
+    "@uiw/react-codemirror": "4.11.5",
     "react-dom": "^17.0.1",
     "react-icons": "4.1.0",
     "react-router-dom": "5.2.0",
diff --git a/src/components/entry-page/entry-page.js b/src/components/entry-page/entry-page.js
index 038e1177f83dbc27e5350d94e3ed4837b2158f74..435685932b60b4e86da4b4aa7c0aacd9c83836d1 100644
--- a/src/components/entry-page/entry-page.js
+++ b/src/components/entry-page/entry-page.js
@@ -3,8 +3,8 @@ import React from 'react';
 import NrpHeader from '../nrp-header/nrp-header.js';
 
 import './entry-page.css';
-import PlaceholderImage from '../../assets/images/Artificial_Intelligence_2.jpg';
 import NrpCoreDashboard from '../nrp-core-dashboard/nrp-core-dashboard.js';
+//import TransceiverFunctionEditor from '../tf-editor/tf-editor';
 
 export default class EntryPage extends React.Component {
   render() {
@@ -33,6 +33,7 @@ export default class EntryPage extends React.Component {
           <div><b>!!! NRP Core testing !!!</b></div>
         </div>
         <NrpCoreDashboard />
+        {/*<TransceiverFunctionEditor experimentId='mqtt_simple_1'/>*/}
       </div>
     );
   }
diff --git a/src/components/experiment-overview/experiment-overview.js b/src/components/experiment-overview/experiment-overview.js
index 9f367bf2fca7a9148be051e61e2fe3392154b4c3..b43fb7a9d504792832640f57a7b35acfa5de78c6 100644
--- a/src/components/experiment-overview/experiment-overview.js
+++ b/src/components/experiment-overview/experiment-overview.js
@@ -106,7 +106,7 @@ export default class ExperimentOverview extends React.Component {
 
   onUpdatePublicExperiments(publicExperiments) {
     this.setState({
-      publicExperiments: publicExperiments.filter(exp => exp.configuration.maturity === 'production')
+      publicExperiments: publicExperiments //.filter(exp => exp.configuration.maturity === 'production')
     });
   }
 
diff --git a/src/components/nrp-core-dashboard/nrp-core-dashboard.js b/src/components/nrp-core-dashboard/nrp-core-dashboard.js
index 49aa07eed0ffc9a74bbbc956207eecd3b2d4a9cc..88489d1d385635993ec40d279564071663286227 100644
--- a/src/components/nrp-core-dashboard/nrp-core-dashboard.js
+++ b/src/components/nrp-core-dashboard/nrp-core-dashboard.js
@@ -15,8 +15,8 @@ export default class NrpCoreDashboard extends React.Component {
     MqttClientService.instance.connect(this.mqttBrokerUrl);
   }
 
-  onMqttClientConnected(mqttClient) {
-    mqttClient.subscribe('#', (err) => {
+  onMqttClientConnected(MqttClient) {
+    MqttClient.subscribe('#', (err) => {
       if (err) {
         console.error(err);
       }
diff --git a/src/components/tf-editor/tf-editor.css b/src/components/tf-editor/tf-editor.css
new file mode 100644
index 0000000000000000000000000000000000000000..8f9261144e0954c79323e82a01cd828764a2abe6
--- /dev/null
+++ b/src/components/tf-editor/tf-editor.css
@@ -0,0 +1,44 @@
+.tf-editor-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.tf-editor-icon {
+  width: 40px;
+  height: 40px;
+  background-color: yellow;
+  text-align: center;
+  border: 1px solid black;
+  margin: 5px;
+  font-size: 1.5em;
+}
+
+.tf-editor-file-ui {
+  padding: 10px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.tf-editor-file-ui-item {
+  margin: 5px;
+}
+
+.tf-editor-ui-save {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.tf-editor-text-saved {
+  color: green;
+}
+
+.tf-editor-text-unsaved {
+  color: orange;
+}
+
+.tf-editor-codemirror-container {
+  overflow: scroll;
+}
\ No newline at end of file
diff --git a/src/components/tf-editor/tf-editor.js b/src/components/tf-editor/tf-editor.js
new file mode 100644
index 0000000000000000000000000000000000000000..126ba788a2c147eba4dda1f982b59ad003d50462
--- /dev/null
+++ b/src/components/tf-editor/tf-editor.js
@@ -0,0 +1,143 @@
+import React from 'react';
+import CodeMirror from '@uiw/react-codemirror';
+import { Modal, Button } from 'react-bootstrap';
+
+import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service';
+
+
+import './tf-editor.css';
+
+export default class TransceiverFunctionEditor extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.testListTfFiles = ['cg_mqtt.py', 'cg_mqtt_2.py', 'cg_mqtt_3.py'];
+    this.state = {
+      selectedFilename: this.testListTfFiles[0],
+      code: '',
+      textChanges: '',
+      showDialogUnsavedChanges: false
+    };
+  }
+
+  async componentDidMount() {
+    this.loadFileContent(this.state.selectedFilename);
+  }
+
+  onChangeSelectedFile(event) {
+    let filename = event.target.value;
+    if (this.hasUnsavedChanges) {
+      this.pendingFileChange = {
+        newFilename: filename,
+        oldFilename: this.state.selectedFilename
+      };
+      this.setState({showDialogUnsavedChanges: true});
+    }
+    else {
+      this.loadFileContent(filename);
+    }
+  }
+
+  onUnsavedChangesDiscard() {
+    this.loadFileContent(this.pendingFileChange.newFilename);
+  }
+
+  async onUnsavedChangesSave() {
+    let success = await this.saveTF();
+    if (success) {
+      this.loadFileContent(this.pendingFileChange.newFilename);
+    }
+  }
+
+  async loadFileContent(filename) {
+    let fileContent = await ExperimentStorageService.instance.getFileText(this.props.experimentId, filename);
+    this.fileLoading = true;
+    this.setState({selectedFilename: filename, code: fileContent, showDialogUnsavedChanges: false});
+  }
+
+  onChangeCodemirror(change, viewUpdate) {
+    this.setState({code: change});
+    this.hasUnsavedChanges = !this.fileLoading;
+    this.fileLoading = false;
+    if (this.hasUnsavedChanges) {
+      this.setState({textChanges: 'unsaved changes'});
+    }
+  }
+
+  async saveTF() {
+    let response = await ExperimentStorageService.instance.setFile(
+      this.props.experimentId, this.state.selectedFilename, this.state.code);
+    if (response.ok) {
+      this.hasUnsavedChanges = false;
+      this.setState({textChanges: 'saved'});
+      setTimeout(() => {
+        this.setState({textChanges: ''});
+      }, 3000);
+      return true;
+    }
+    else {
+      console.error('Error trying to save TF!');
+      console.error(response);
+      return false;
+    }
+  }
+
+  render() {
+    return (
+      <div className='tf-editor-container'>
+        <div className='tf-editor-header'>
+          <div className='tf-editor-icon'>TF</div>
+          <div className='tf-editor-file-ui'>
+            <select
+              className='tf-editor-file-ui-item'
+              name="selectTFFile"
+              value={this.state.selectedFilename}
+              onChange={(event) => this.onChangeSelectedFile(event)}>
+              {this.testListTfFiles.map(file => {
+                return (<option key={file} value={file}>{file}</option>);
+              })}
+            </select>
+            <button className='tf-editor-file-ui-item' onClick={() => this.saveTF()}>Save</button>
+            <div className={this.hasUnsavedChanges ?
+              'tf-editor-text-unsaved' : 'tf-editor-text-saved'}>
+              {this.state.textChanges}
+            </div>
+          </div>
+        </div>
+
+        <div className='tf-editor-codemirror-container'>
+          <CodeMirror
+            value={this.state.code}
+            onChange={(change, viewUpdate) => this.onChangeCodemirror(change, viewUpdate)}/>
+        </div>
+
+        {this.state.showDialogUnsavedChanges ?
+          <div>
+            <Modal show={this.state.showDialogUnsavedChanges}
+              onHide={() => this.setState({showDialogUnsavedChanges: false})}>
+              <Modal.Header>
+                <Modal.Title>Unsaved Changes</Modal.Title>
+              </Modal.Header>
+              <Modal.Body>You have unsaved changes for "{this.pendingFileChange.oldFilename}".
+              What would you like to do?</Modal.Body>
+              <Modal.Footer>
+                <div>
+                  <Button variant="danger" onClick={() => this.setState({showDialogUnsavedChanges: false})}>
+                  Cancel
+                  </Button>
+                  <Button variant="danger" onClick={() => this.onUnsavedChangesDiscard()}>
+                  Discard changes
+                  </Button>
+                  <Button variant="light" onClick={() => this.onUnsavedChangesSave()}>
+                  Save
+                  </Button>
+                </div>
+              </Modal.Footer>
+            </Modal>
+          </div>
+          : null}
+      </div>
+    );
+  }
+}
diff --git a/src/services/__tests__/mqtt-client-service.test.js b/src/services/__tests__/mqtt-client-service.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..3f384cdd630949251c30d98e6a799b51229e2516
--- /dev/null
+++ b/src/services/__tests__/mqtt-client-service.test.js
@@ -0,0 +1,72 @@
+/**
+ * @jest-environment jsdom
+*/
+import '@testing-library/jest-dom';
+
+import MqttClientService from '../mqtt-client-service';
+
+let subscribeTopicAndValidate = (topic, callback) => {
+  let token = MqttClientService.instance.subscribeToTopic(topic, callback);
+  expect(token).toBeDefined();
+  expect(token.topic).toBe(topic);
+  expect(token.callback).toBe(callback);
+  expect(MqttClientService.instance.subTokensMap.get(topic).includes(token)).toBeTruthy();
+
+  return token;
+};
+
+let unsubscribeAndValidate = (token) => {
+  MqttClientService.instance.unsubscribe(token);
+  expect(MqttClientService.instance.subTokensMap.get(token.topic).includes(token)).toBeFalsy();
+};
+
+test('sub/unsub', async () => {
+  let topicA = 'topic/A';
+  let topicB = 'topic/B';
+
+  let sub1Callback = jest.fn();
+  let sub1Token = subscribeTopicAndValidate(topicA, sub1Callback);
+  let sub2Callback = jest.fn();
+  let sub2Token = subscribeTopicAndValidate(topicA, sub2Callback);
+  let sub3Callback = jest.fn();
+  let sub3Token = subscribeTopicAndValidate(topicB, sub3Callback);
+
+  expect(MqttClientService.instance.subTokensMap.get(topicA).length).toBe(2);
+  expect(MqttClientService.instance.subTokensMap.get(topicB).length).toBe(1);
+
+  MqttClientService.instance.onMessage(topicA, {});
+  MqttClientService.instance.onMessage(topicB, {});
+  expect(sub1Token.callback).toHaveBeenCalledTimes(1);
+  expect(sub2Token.callback).toHaveBeenCalledTimes(1);
+  expect(sub3Token.callback).toHaveBeenCalledTimes(1);
+
+  unsubscribeAndValidate(sub1Token);
+  expect(MqttClientService.instance.subTokensMap.get(topicA).length).toBe(1);
+  expect(MqttClientService.instance.subTokensMap.get(topicB).length).toBe(1);
+
+  MqttClientService.instance.onMessage(topicA, {});
+  MqttClientService.instance.onMessage(topicB, {});
+  expect(sub1Token.callback).toHaveBeenCalledTimes(1);
+  expect(sub2Token.callback).toHaveBeenCalledTimes(2);
+  expect(sub3Token.callback).toHaveBeenCalledTimes(2);
+
+  unsubscribeAndValidate(sub2Token);
+  expect(MqttClientService.instance.subTokensMap.get(topicA).length).toBe(0);
+  expect(MqttClientService.instance.subTokensMap.get(topicB).length).toBe(1);
+
+  MqttClientService.instance.onMessage(topicA, {});
+  MqttClientService.instance.onMessage(topicB, {});
+  expect(sub1Token.callback).toHaveBeenCalledTimes(1);
+  expect(sub2Token.callback).toHaveBeenCalledTimes(2);
+  expect(sub3Token.callback).toHaveBeenCalledTimes(3);
+
+  unsubscribeAndValidate(sub3Token);
+  expect(MqttClientService.instance.subTokensMap.get(topicA).length).toBe(0);
+  expect(MqttClientService.instance.subTokensMap.get(topicB).length).toBe(0);
+
+  MqttClientService.instance.onMessage(topicA, {});
+  MqttClientService.instance.onMessage(topicB, {});
+  expect(sub1Token.callback).toHaveBeenCalledTimes(1);
+  expect(sub2Token.callback).toHaveBeenCalledTimes(2);
+  expect(sub3Token.callback).toHaveBeenCalledTimes(3);
+});
diff --git a/src/services/experiments/files/experiment-storage-service.js b/src/services/experiments/files/experiment-storage-service.js
index baa4d79656644eb5abde013a80d36f8f9f5e69c2..36942d6c996b583b2dbb7d4d59b2cfd4aaea494f 100644
--- a/src/services/experiments/files/experiment-storage-service.js
+++ b/src/services/experiments/files/experiment-storage-service.js
@@ -191,6 +191,19 @@ class ExperimentStorageService extends HttpService {
   }
 
 
+  /**
+   * Gets a file from the storage as text.
+   * @param {string} experimentName - name of the experiment
+   * @param {string} filename - name of the file
+   * @param {Boolean} byName - whether to check for the file by name or not (default TRUE)
+   *
+   * @returns {Blob} the contents of the file as text
+   */
+  async getFileText(experimentName, filename, byName = true) {
+    return await (await this.getBlob(experimentName, filename, byName)).text();
+  }
+
+
   /**
    * Deletes an experiment entity (folder or file) from the storage.
    * Called by other functions, not to be called independently.
diff --git a/src/services/experiments/files/public-experiments-service.js b/src/services/experiments/files/public-experiments-service.js
index 86b3eb167f7fccb3825c69d05853a338d32dc5e1..88e10eaaa525decfdb7b6bdb0039d54bd51d2dc2 100644
--- a/src/services/experiments/files/public-experiments-service.js
+++ b/src/services/experiments/files/public-experiments-service.js
@@ -86,8 +86,8 @@ class PublicExperimentsService extends HttpService {
   sortExperiments(experimentList) {
     experimentList = experimentList.sort(
       (a, b) => {
-        let nameA = a.configuration.name.toLowerCase();
-        let nameB = b.configuration.name.toLowerCase();
+        let nameA = a.configuration.SimulationName.toLowerCase();
+        let nameB = b.configuration.SimulationName.toLowerCase();
         if (nameA < nameB) {
           return -1;
         }
diff --git a/src/services/mqtt-client-service.js b/src/services/mqtt-client-service.js
index ce09aa079385307634bda4ffd27c9d99c588cdaf..06b2193c3180dc803e3180d2c36bb18865db867c 100644
--- a/src/services/mqtt-client-service.js
+++ b/src/services/mqtt-client-service.js
@@ -1,7 +1,8 @@
 import mqtt from 'mqtt';
 import { EventEmitter } from 'events';
 
-//import * as proto from 'nrp-jsproto/engine_grpc_pb';
+//import { DataPackMessage } from 'nrp-jsproto/engine_grpc_pb';
+import jspb from '../../node_modules/google-protobuf/google-protobuf';
 
 let _instance = null;
 const SINGLETON_ENFORCER = Symbol();
@@ -16,7 +17,7 @@ export default class MqttClientService extends EventEmitter {
       throw new Error('Use ' + this.constructor.name + '.instance');
     }
 
-    //console.info(proto);
+    this.subTokensMap = new Map();
   }
 
   static get instance() {
@@ -36,7 +37,9 @@ export default class MqttClientService extends EventEmitter {
       this.emit(MqttClientService.EVENTS.CONNECTED, this.client);
     });
     this.client.on('error', this.onError);
-    this.client.on('message', this.onMessage);
+    this.client.on('message', (params) => {
+      this.onMessage(params);
+    });
   }
 
   onError(error) {
@@ -44,13 +47,90 @@ export default class MqttClientService extends EventEmitter {
   }
 
   onMessage(topic, payload, packet) {
-    console.info('MQTT message: [topic, payload, packet]');
-    console.info([topic, payload, packet]);
+    if (typeof payload === 'undefined') {
+      return;
+    }
+
+    //console.info('MQTT message: [topic, payload, packet]');
+    //console.info([topic, payload, packet]);
+    //Now we see which callbacks have been assigned for a topic
+    let subTokens = this.subTokensMap.get(topic);
+    if (typeof subTokens !== 'undefined') {
+      for (var token of subTokens) {
+        //Deserializatin of Data must happen here
+        token.callback(payload);
+      };
+    };
+
+    /*try {
+      if (topic.endsWith('/type')) {
+        let msg = String(payload);
+        console.info('"' + topic + '" message format = ' + msg);
+      }
+      else {
+        let msg = DataPackMessage.deserializeBinary(payload);
+        console.info('DataPackMessage');
+        console.info(msg);
+      }
+    }
+    catch (error) {
+      console.error(error);
+    }*/
+  }
+
+  //callback should have args topic, payload
+  subscribeToTopic(topic, callback) {
+    if (typeof callback !== 'function') {
+      console.error('trying to subscribe to topic "' + topic + '", but no callback function given!');
+      return;
+    }
+
+    const token = {
+      topic: topic,
+      callback: callback
+    };
+    if (this.subTokensMap.has(token.topic)){
+      this.subTokensMap.get(token.topic).push(token);
+    }
+    else{
+      this.subTokensMap.set(
+        token.topic,
+        [token]
+      );
+    }
+    //console.info('You have been subscribed to topic ' + topic);
+    //console.info(this.subTokensMap);
+    return token;
+  }
 
-    // step 0: deserialize the payload => messageData
-    // step 1: pick subs based on topic => subs
-    // step 2: foreach(sub) {sub.callback(messageData, topic);}
+  unsubscribe(unsubToken) {
+    if (this.subTokensMap.has(unsubToken.topic)){
+      let tokens = this.subTokensMap.get(unsubToken.topic);
+      let index = tokens.indexOf(unsubToken);
+      if (index !== -1) {
+        tokens.splice(index, 1);
+        //console.info('You have been unsubscribed from topic ' + unsubToken.topic);
+      }
+      else {
+        console.warn('Your provided token could not be found in the subscription list');
+      }
+    }
+    else{
+      console.warn('The topic ' + unsubToken.topic + ' was not found');
+    }
   }
+
+  static getProtoOneofData(protoMsg, oneofCaseNumber) {
+    return jspb.Message.getField(protoMsg, oneofCaseNumber);
+  }
+
+  /*static getDataPackMessageOneofCaseString(protoMsg) {
+    for (let dataCase in DataPackMessage.DataCase) {
+      if (DataPackMessage.DataCase[dataCase] === protoMsg.getDataCase()) {
+        return dataCase;
+      }
+    }
+  }*/
 }
 
 MqttClientService.EVENTS = Object.freeze({