├── public ├── favicon.ico ├── style.css ├── index.html └── script.js ├── database.rules.json ├── README.md └── functions └── index.js /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olivierlourme/iot-store-display/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font-family: Roboto, Helvetica, Arial, sans-serif; 4 | margin: 0 5 | } 6 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "devices-ids": { 4 | ".read": true, 5 | ".write": false 6 | }, 7 | "devices-telemetry": { 8 | ".read": true, 9 | ".write": false 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iot-store-display 2 | 3 | ## Description 4 | **iot-store-display** hosts the Firebase aspects of **an IoT project** relying on [GCP-Cloud IoT Core](https://cloud.google.com/iot-core/). We called our GCP project **hello-cloud-iot-core**. 5 | 6 | **Devices** publish weather data messages to a **telemetry topic** connected to the MQTT bridge of Cloud Iot Core. 7 | 8 | A device is an [ESP32](https://www.espressif.com/en/products/hardware/esp32/overview) Wifi chip associated with a DHT22 temperature/humidity sensor. The ESP32 runs [Mongoose OS](https://mongoose-os.com/). 9 | 10 | All data messages are then forwarded to a **Cloud Pub/Sub** topic of the project and this is where the files in this repository intervene: 11 | * A **Cloud Function for Firebase** is triggered upon a publication to the Cloud Pub/Sub topic. Its aim is to log the last weather data as it is published and also to store it in a **Firebase Realtime Database**. The Cloud Function implementation is the `index.js` file, hosted in the [functions](functions) directory. 12 | * The read/write rules of this database are described in the `database.rules.json` file. 13 | * A **web app** hosted by **Firebase Hosting** is in charge of plotting the *n* last weather data each time a new data arrives in the Realtime Database. The files for this web app are hosted in the [public](public) directory. 14 | 15 | ## Getting started 16 | **Cloud IoT Core architecture**, **devices firmware & provision** and **Cloud Function & web app setups** are all extensively described in [this Medium post](https://medium.com/@o.lourme/gcp-cloudiotcore-esp32-mongooseos-1st-5c88d8134ac7). 17 | 18 | ## Live demo 19 | A live demo corresponding to the web app presented in this repository is avalaible [here](https://hello-cloud-iot-core.firebaseapp.com/). There are two devices, one "indoor" and one "outdoor". Each one makes a measure every two minutes. The last 750 measures are plotted for each device, corresponding to approximately 24 hours. -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 |35 | GCP Cloud IoT Core, ESP32 & Mongoose OS rock! See 36 | Medium 37 | and Github. 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | // The "Cloud Functions for Firebase" SDK to create Cloud Functions and setup triggers: 2 | const functions = require('firebase-functions'); 3 | 4 | // The Firebase Admin SDK to access the Firebase Realtime Database: 5 | const admin = require('firebase-admin'); 6 | admin.initializeApp(); 7 | 8 | // Definition of the Cloud Function reacting to publication on the telemetry topic: 9 | exports.detectTelemetryEvents = functions.pubsub.topic('weather-telemetry-topic').onPublish( 10 | (message, context) => { 11 | // The onPublish() trigger function requires a handler function that receives 12 | // 2 arguments: one related to the message published and 13 | // one related to the context of the message. 14 | 15 | // Firebase SDK for Cloud Functions has a 'json' helper property to decode 16 | // the message. We also round numbers to match DHT22 accuracy. 17 | const temperature = message.json.temperature.toFixed(1); 18 | const humidity = Math.round(message.json.humidity); 19 | if((temperature<-40) || (temperature>80) || (humidity <0) || (humidity > 100)) return; 20 | // A Pub/Sub message has an 'attributes' property. This property has itself some properties, 21 | // one of them being 'deviceId' to know which device published the message: 22 | const deviceId = message.attributes.deviceId; 23 | // The date the message was issued lies in the context object not in the message object: 24 | const timestamp = context.timestamp 25 | // Log telemetry activity: 26 | console.log(`Device=${deviceId}, Temperature=${temperature}°C, Humidity=${humidity}%, Timestamp=${timestamp}`); 27 | // Push to Firebase Realtime Database telemetry data sorted by device: 28 | return admin.database().ref(`devices-telemetry/${deviceId}`).push({ 29 | timestamp: timestamp, 30 | temperature: temperature, 31 | humidity: humidity 32 | }) 33 | }); 34 | -------------------------------------------------------------------------------- /public/script.js: -------------------------------------------------------------------------------- 1 | // A web app that lively plots data from Firebase Realtime Database nodes, thanks to plotly.js 2 | 3 | // Firebase initialization is already done as we use "simpler project configuration". 4 | // See https://firebase.google.com/docs/web/setup?authuser=0#host_your_web_app_using_firebase_hosting 5 | 6 | // Number of last records to display: 7 | const nbOfElts = 750; 8 | 9 | // Declaration of an array of devices ids 10 | let devicesIds = []; 11 | // Declaration of an array of devices aliases 12 | let devicesAliases = []; 13 | // How these arrays will be generated in the code? 14 | // Devices ids and aliases are read at startup from a specific node of the Firebase 15 | // Realtime Database named 'devices-id'. 16 | // Devices ids are keys of simple key/value objects whose values are either an alias 17 | // of the device or a boolean valuing true. 18 | // For instance, if the database is: 19 | // 20 | // hello-cloud-iot-core 21 | // | 22 | // +-devices-ids 23 | // | | 24 | // | +-esp32_1B2B04: 'outdoor' 25 | // | | 26 | // | +-esp32_ABB3B4: true 27 | // | 28 | // +-devices-telemetry 29 | // | 30 | // +-esp32_1B2B04... 31 | // | 32 | // +-esp32_ABB3B4... 33 | // 34 | // We will then have the following features: 35 | // * devicesIDs array will be equal to ['esp32_1B2B04', 'esp32_ABB3B4']. 36 | // * devicesAliases array will be equal to ['outdoor', 'esp32_ABB3B4']. 37 | // * Data from devices whose ids are in deviceIds array will be ploted. 38 | // * In the charts legend, 'esp32_1B2B04' device will be marked as 'outdoor' 39 | // but 'esp32_ABB3B4' device will still be marked as 'esp32_ABB3B4' 40 | // as the value for this key is a boolean valuing true. 41 | 42 | // To get a list of devices ids, why did we add this specific 'devices-ids' node 43 | // in the database instead of reading the children of 'devices-telemetry' node 44 | // (i.e. 'esp32_1B2B04' and 'esp32_ABB3B4')? 45 | // Because: 46 | // * This reading is a non shallow one, the whole data of 'devices-telemetry' node 47 | // would have been read! Thus, and it is common in NoSQL database, some parts 48 | // of data are duplicated ("denormalization"). 49 | // * It allows us to choose the devices whose data will be plotted or not. 50 | 51 | // Today (Feb. 24, 2019), we still have to create manually the 'devices-ids' node 52 | // in the database. TODO: a UI to select devices whose data should be plotted. 53 | 54 | // Get references to the DOM node that welcomes the plots drawn by Plotly.js 55 | const temperaturePlotDiv = document.getElementById('temperaturePlot'); 56 | const humidityPlotDiv = document.getElementById('humidityPlot'); 57 | 58 | // Get a reference to Firebase Realime Database: 59 | const db = firebase.database(); 60 | 61 | // Declaration of 3 objects named timestamps, temperatures and humidities 62 | let timestamps; 63 | let temperatures; 64 | let humidities; 65 | // Each of these objects will have n property, n being the number of devices ids 66 | // present in the devicesIds array. 67 | // Each property will be named with each device id. 68 | // Each property is an array of 'nbOfElts' elements. 69 | // For instance, if devicesIDs array is equal to ['esp32_1B2B04', 'esp32_ABB3B4'] 70 | // and if nbOfElts equals to 150: 71 | // * temperatures.esp32_1B2B04 is an array of the last 150 temperatures measured 72 | // by 'esp32_1B2B04' device. The related array of timestamps is timestamps.esp32_1B2B04. 73 | // * temperatures.esp32_ABB3B4 is also an array of the last 150 temperatures measured 74 | // by esp32_ABB3B4. The related array of timestamps is timestamps.esp32_ABB3B4. 75 | 76 | // For temperature and humidity, the common plotly.js layout 77 | const commonLayout = { 78 | titlefont: { 79 | family: 'Courier New, monospace', 80 | size: 16, 81 | color: '#000' 82 | }, 83 | xaxis: { 84 | linecolor: 'black', 85 | linewidth: 2 86 | }, 87 | yaxis: { 88 | titlefont: { 89 | family: 'Courier New, monospace', 90 | size: 14, 91 | color: '#000' 92 | }, 93 | linecolor: 'black', 94 | linewidth: 2, 95 | }, 96 | margin: { 97 | r: 50, 98 | pad: 0 99 | } 100 | }; 101 | // Specific layout aspects for temperature chart 102 | let temperatureLayout = JSON.parse(JSON.stringify(commonLayout)); 103 | temperatureLayout.title = 'Temperature live plot'; 104 | temperatureLayout.yaxis.title = 'Temp (°C)'; 105 | // Specific layout aspects for humidity chart 106 | let humidityLayout = JSON.parse(JSON.stringify(commonLayout)); 107 | humidityLayout.title = 'Humidity live plot'; 108 | humidityLayout.yaxis.title = 'Humidity (%)'; 109 | 110 | // Okay, let's start! 111 | // Make ONCE an array of devices ids and devices aliases 112 | db.ref('devices-ids').once('value', (snapshot) => { 113 | snapshot.forEach(childSnapshot => { 114 | const childKey = childSnapshot.key; 115 | devicesIds.push(childKey); 116 | const childData = childSnapshot.val(); 117 | let deviceAlias; 118 | if(childData == true) { 119 | deviceAlias = childKey; // alias is 'esp32_1B2B04' for instance 120 | } else { 121 | deviceAlias = childData; // alias is 'outdoor' for instance 122 | } 123 | devicesAliases.push(deviceAlias); 124 | }); 125 | //console.log(devicesAliases); 126 | if (devicesIds.length != 0) { 127 | // objects 1st property (an array) initialization... 128 | timestamps = { [devicesIds[0]]: [] }; 129 | temperatures = { [devicesIds[0]]: [] }; 130 | humidities = { [devicesIds[0]]: [] }; 131 | // ...and the rest of properties (somme arrays) initialization 132 | for (let i = 1; i < devicesIds.length; i++) { 133 | timestamps[devicesIds[i]] = []; 134 | temperatures[devicesIds[i]] = []; 135 | humidities[devicesIds[i]] = []; 136 | } 137 | //console.log('At startup timestamps =', timestamps); 138 | //console.log('At startup temperatures =', temperatures); 139 | } else console.log('No device id was found.') 140 | }) 141 | .then(() => { // We start building database nodes listeners only when we have devices ids. 142 | for (let i = 0; i < devicesIds.length; i++) { 143 | db.ref(`devices-telemetry/${devicesIds[i]}`).limitToLast(nbOfElts).on('value', ts_measures => { 144 | //console.log(ts_measures.val()); 145 | // We reinitialize the arrays to welcome timestamps, temperatures and humidities values: 146 | timestamps[devicesIds[i]] = []; 147 | temperatures[devicesIds[i]] = []; 148 | humidities[devicesIds[i]] = []; 149 | 150 | ts_measures.forEach(ts_measure => { 151 | timestamps[devicesIds[i]].push(moment(ts_measure.val().timestamp).format('YYYY-MM-DD HH:mm:ss')); 152 | temperatures[devicesIds[i]].push(ts_measure.val().temperature); 153 | humidities[devicesIds[i]].push(ts_measure.val().humidity); 154 | }); 155 | 156 | // plotly.js: See https://plot.ly/javascript/getting-started/ 157 | // Temperatures 158 | let temperatureTraces = []; // array of plotly temperature traces (n devices => n traces) 159 | for (let i = 0; i < devicesIds.length; i++) { 160 | temperatureTraces[i] = { 161 | x: timestamps[devicesIds[i]], 162 | y: temperatures[devicesIds[i]], 163 | name: devicesAliases[i] 164 | } 165 | } 166 | let temperatureData = []; // last plotly object to build 167 | for (let i = 0; i < devicesIds.length; i++) { 168 | temperatureData.push(temperatureTraces[i]); 169 | } 170 | Plotly.newPlot(temperaturePlotDiv, temperatureData, temperatureLayout, { responsive: true }); 171 | 172 | // Humidities 173 | let humidityTraces = []; // array of plotly humidity traces (n devices => n traces) 174 | for (let i = 0; i < devicesIds.length; i++) { 175 | humidityTraces[i] = { 176 | x: timestamps[devicesIds[i]], 177 | y: humidities[devicesIds[i]], 178 | name: devicesAliases[i] 179 | } 180 | } 181 | let humidityData = []; // last plotly object to build 182 | for (let i = 0; i < devicesIds.length; i++) { 183 | humidityData.push(humidityTraces[i]); 184 | } 185 | Plotly.newPlot(humidityPlotDiv, humidityData, humidityLayout, { responsive: true }); 186 | }); 187 | } 188 | }) 189 | .catch(err => { 190 | console.err('An error occured:', err); 191 | }); 192 | --------------------------------------------------------------------------------