# Fog Server Guide ## JavaScript * [javascript.info](https://https://javascript.info/) covers all aspects of JavaScript. It has very intuitive tutorials, covering from basic syntax, DOM events, to Promises and regex. * If you prefer to watch videos, watch [Modern JavaScript Tutorial](https://youtu.be/iWOYAxlnaww) by The Net Ninja. ### JavaScript in our Project We are more likely to use Asynchronous JavaScript. So this playlist, [Asynchronous JavaScript](https://youtu.be/ZcQyJ-gxke0) is more preferable in our case. Note: The nature of JavaScript is synchronous. Because making a database call takes some amount of time, we need to use promises or async/await methods. ```javascript= console.log('a'); // make a database call const patients = Patients.find({patientId: "1234"}); console.log(patients); console.log('b'); // result: a,b,{list of patients} ``` OR if like this: ```javascript= console.log('a'); // make a database call async function findPatients() { const patients = await Patients.find({patientId: "1234"}); return patients; } console.log(findPatients()); console.log('b'); // result: a,{list of patients},b ``` ## Node.js and MongoDB This [Node.js playlist](https://youtu.be/zb3Qk8SG5Ms) (consists of 12 videos) will give you a solid understanding of Node.js and MongoDB. In short, Node.js is a wrapper for Chrome's V8 engine, which compiles JavaScript into C++ code then into machine code. MongoDB is a NoSQL database which consists of **collections** (tables in SQL) and **documents** (rows in SQL). Documents may contain arrays and objects. A few things to note from the playlist: * Mongoose is a NPM library that manages interaction between Node.js and MongoDB. It helps users define data models/schemas and provides simpler syntax for users to write Mongo queries (so that users do not need raw Mongo queries, which are much longer) * `ejs` is a JavaScript templating library which renders contents on web. Since our project separates front-end (written in React Native) and backend completely, don't worry about `ejs`. In our case, instead of returning a html page in `router.get()/post()`, we will return json objects. ## Redis pubsub and Socket.io `Fog-edge->fog-fog` uses Redis pubsub protocol. Fog-edge publishes messages via channel 'heart-rate' by calling `redis.publish()`. Think of the relationship as multiple publishers -> one subscriber. Fog-fog serves as the subscriber by calling `redis.subscribe()`. `Fog-fog->client` uses Socket.io protocol. Whenever fog-fog receives a message from fog-edge, it will in turn send the message via socket.io by calling `io.send()`. The client code (react native) places a listener to the fog-fog ip (we are using ngrok) to subscribe to any incoming messages. ## Fog Server Code Walkthrough ### How to Start with Fog Server * Open Redis, go to Redis directory, `src/redis-server --protected-mode no` to launch Redis * Replace ip address of Redis (line 75 in index.js) as needed (I'll assume Redis is together with fog server on one machine, so 127.0.0.1 or localhost), yet not necessarily the case. You may install Redis on your Raspberry PI, and set the ip as your RPI ip. * `npm run dev` to start fog server; the default port is 8000 * (optional) use ngrok to pass through internal networks: * `./ngrok http -hostname=iomt-uci.ngrok.io 8000 * to set up ngrok, go to ngrok.io and download ngrok; set up authToken in your computer. Once you log in to ngrok using our iomt account, it provides a detailed instruction of how to do so. * Getting client mobile app ready ### Cautions with Fog Server * staff log in, use this: * username: staff@test.com * password: 123 * Valid patient phone numbers are `2001-2006`. * Valid device id numbers are `1-3`. * The fog server "subscribes" to channel "heart-rate" and expects incoming message strings in the following format: * if `str.split('|').length === 2`, the fog server will receive `<device id>|<room name>` * if `str.split('|').length === 3`, the fog server will receive `<device id>|<prediction label>|<placeholder char>` * if `str.split('|').length === 4`, the fog server will receive `<patient id>|<device id>|<patient name>|<bpm>` * overall, fog server consists of two parts: * receving and sending real-time data via Redis pubsub and socket.io * HTTP requests for signing in/signing up, connecting/disconnecting patients, and getting patient's historical data ### Database Schema Walkthrough All user types are defined under the `/models` folder. **Done in Phase 1** ```javascript= Device |_ _id: String, required |_ isConnected: Number, required |_ connectedTo: objectId, ref: 'Patient', default: null |_ manufacturer: String ``` * `connectedTo` refers to the _id that the current patient is connecting to this device --- ```javascript= Patient |_ email: String, required |_ password: String, required |_ firstName: String, required |_ lastName: String, required |_ phoneNum: String, required |_ deviceId: String, ref: 'Device', default: "0" |_ isCalling: Number, required, default: 0 |_ monitoredBy: Array of ObjectIds, ref: 'Staff' ``` * `deviceId` refers to the `_id` of a device that the current patient is connecting to * `isCalling` is either 0 or 1; 0 means not calling; 1 means calling * `monitoredBy`: refers to the `_id` of the staff that the patient belongs to; we don't actually have a function to assign a patient to a staff right now; these `ids` were manually set by me. Don't worry too much about these two functions: * `patientSchema.pre('save', function(next)` converts the raw password to a hash string * `patientSchema.methods.comparePassword = function(candidatePassword)` checks if user has inputted a correct password --- ```javascript= Staff |_ email: String, required |_ password: String, required |_ firstName: String, required |_ lastName: String, required |_ phoneNum: String, required |_ patients: Array of patient obejctIds, ref: 'Patient' ``` * `patients`: a list of patients monitored by this staff; we don't actually have a function to assign a patient to a staff right now; these ids were manually set by me. --- **To be implemented in Phase 2** Collection **Day**, aims to store historical records ```javascript= Day |_ patientId: String, required |_ patientName: String, required |_ today: String, required |_ bpm: Array, TODO IN PHASE 2 |_ locations: Array, TODO IN PHASE 2 ``` A few ideas: * may store bpm and locations together as an array * keep in mind to let client retrieve bpm history and location (also wait times) efficiently in terms of days, weeks, and months. ## Code Walkthrough ### Middlewares * `requireAuth.js`: checks whether a user is authenticated. **Don't worry about this file**. ### Routes Let's take a look at files inside the `/routes` folder. Essentially, functions in these files are API calls. Technically, they can all be placed within the same file. But for readability, it's usually to group routes that belong together. * `authRoutes.js`: takes care of authentication regarding staff/patient sign in/sign up. Logic is set; **Don't worry about this file**. * `connectRoutes.js`: takes care of connection between a patient and an edge device. Logic is set; **Not likely to be modified heavily**. * `historyRoutes.js`: takes care of retrieving historical records. **TODO in Phase 2**. * `trackRoutes.js`: sample code from Udemy course. **Can be deleted**. Here is a template to set up a route file: ```javascript= const express = require("express"); const mongoose = require("mongoose"); const requireAuth = require("../middlewares/requireAuth"); // import data models // const Day = mongoose.model("Day"); // set up router const router = express.Router(); router.use(requireAuth); // fill up with your specified API url, e.g. /bpm-history router.get("...", async (req, res) => { // retreive any query parameters or body message attached with the client request // call <Model>.find() or findOne() to pull up the appropriate document(s) // based on provided query parameters/body // send the necessary information requested by client in json format }); router.post("...", async (req, res) { ... }); // finally, remember to export this file module.exports = router; ``` **NOTES**: `GET` generally for retrieving data, or **read** `POST` generally making modification to server/database, or **write** ### index.js Let's take a look at `index.js`. * when importing data models, use `require()` without assigning variables * `require("./models/Patient");` * when importing packages, use * `const mongoose = require("mongoose");` * when importing files from other directories * `const authRoutes = require("./routes/authRoutes");` * when setting up with express * `const app = express();` * when making use code from other files * `app.use(authRoutes);` Here I will explain how to get data from fog-edge, send data in real-time via socket.io, and also store a copy to database. Explanation will be right underneath each code block. ```javascript= // redis config const redisPort = 6379; // replace ip if needed const host = "127.0.0.1"; // redis client const redis = require('redis'); const client = redis.createClient(redisPort, host); ``` In the code block above, we set up the connection to redis in node.js. ```javascript= // client listening messages client.on('message', function(channel, message) { console.log('from channel ' + channel + ": " + message); ``` Notice `.on()`, which is an ongoing observer. The redis subscriber will keep listening as new messages come in. It will only stop when the server is shut down. It's helpful to print out the message received from fog-edge. That's the message printed out onto the terminal. ```javascript= //////// REAL TIME DATA /////////// let list = message.split('|'); // "1|RoomA" ==> <device id>|<room name> // OR "1|N|P" ==> <device id>|<prediction label>|<char> // OR "1|1|Taiting Lu|92" ==> <patient id>|<device id>|<patient name>|<bpm> ``` We split the entire message by `'|'` when we receive it from fog-edge ```javascript= // if length === 4, it is bpm else if (list.length === 4) { const deviceId = list[1]; const bpm = list[3]; if (Object.keys(dataDict.getPatientData(deviceId)).length !== 0) { dataDict.updateBPM(deviceId, bpm); const jsonStr = dataDict.parseData(deviceId); io.send(jsonStr); } } ... }); ``` I'll use when `string.length === 4 ` as an example, which is to send/store bpm. But first, let's go to `connectRoutes.js` to take a look at `/patient-connect` function. ```javascript= ... const dataDict = require('../utils/dataDict.js'); ... router.post("/patient-connect", async (req, res) => { ... ////////////////////////////////////////////////////////////////////////////// // initialize data stream with deviceId, patientId, and patient's full name // ////////////////////////////////////////////////////////////////////////////// dataDict.initializePatient(deviceIdInput, patient._id, patient.firstName + " " + patient.lastName); ... }); ``` At the top, I imported `dataDict.js` from `/utils` folder. `dataDict.js` provides functions that can be used globally within Node.js project. Whenever a staff hits the "Connect" button on mobile app, he sends a http POST request to the server, which is `POST /patient-connect`. After going through logic to check if a patient and a device can be connected, the Node.js server also holds patient information temporially in memory. Let's a look at `initializePatient` function. ```javascript= // keys are deviceIds let dataStream = { "1": {}, "2": {}, "3": {} }; function initializePatient(deviceId, patientId, patientName) { dataStream[deviceId].patientId = patientId; dataStream[deviceId].patientName = patientName; dataStream[deviceId].bpm = 0; dataStream[deviceId].alarm = 0; dataStream[deviceId].location = "Hallway"; dataStream[deviceId].startTime = Date.now(); dataStream[deviceId].prediction = 'I'; dataStream[deviceId].lastUpdated = Date.now(); dataStream[deviceId].bpmHistory = [0,0,0,0,0]; } ``` At the moment the patient is connected to a device, we'll keep the above fields in memory. ```javascript= let dataStream = { "1": { patientId: "123456", patientName: "John", bpm: 0, alarm: 0, location: "Hallway", prediction: "I", lastUpdated: <some timestamp>, bpmHistory: [0,0,0,0,0] }, "2": {}, "3": {} } ``` Assume we have only one patient, John, connected to Device 1. Here is the current dataStream array. After having an idea of the dataStream array structure, let's retake another look at the scenario when receiving John's first heartbeat. ```javascript= else if (list.length === 4) { // retreive deviceId and bpm const deviceId = list[1]; const bpm = list[3]; if (Object.keys(dataDict.getPatientData(deviceId)).length !== 0) { dataDict.updateBPM(deviceId, bpm); const jsonStr = dataDict.parseData(deviceId); io.send(jsonStr); } } ``` Check if `dataStream["1"].length === 0`, here it's not. It means that patient John has been connected. (`reset` will be called when a patient has been disconnected, which will clear the value of `dataStream["1"]`.) So we proceed the update by calling `updateBPM`. Let's move back to `dataDict.js` to see what `updateBPM` does. ```javascript= function updateBPM(deviceId, bpm) { // here to process bpm strings; treat list like a queue of 5 elements dataStream[deviceId].bpm = bpm; // pop the front element; insert bpm at the end; dataStream[deviceId].bpmHistory.shift(); dataStream[deviceId].bpmHistory.push(bpm); // red = high rate; green = normal; orange = lower; gray = none let counter_red = 0; let counter_orange = 0; let counter_gray = 0; // loop through each element in bpmHistory; counter the number of instances for each scenario for (let beat of dataStream[deviceId].bpmHistory) { console.log(beat); if (beat >= 90) { counter_red += 1; } else if (beat >= 80) { continue; } else if (beat >= 20) { counter_orange += 1; } else { counter_gray += 1; } } // counter = 5 means the heart beat has met the <condition> 5 times // if none of the counters = 5, heart beat is normal // e.g. 85, 90, 100, 92, 87 // counter_gray = 0, counter_orange = 2, counter_red = 1, heart beat is still normal if (counter_gray === 5) { dataStream[deviceId].alarm = 0; } else if (counter_orange === 5) { dataStream[deviceId].alarm = 1; } else if (counter_red === 5) { dataStream[deviceId].alarm = 3; } else { dataStream[deviceId].alarm = 2; } // reset counters counter_red = 0; counter_orange = 0; counter_gray = 0; ////////////////////////////// TO DATABASE ///////////////////////////// // if (dataStream[deviceId].bpmBuffer.length === 10) { // // after collected 10 bpms, compute bpm avg // const avg = arr => arr.reduce((a,b) => parseInt(a)+parseInt(b), 0) / arr.length; // const bpmAvg = Math.floor(avg(dataStream[deviceId].bpmBuffer)); // // clear list once getting the avg for bpm // dataStream[deviceId].bpmBuffer = []; // // insert/update MongoDB // const filter = { // patientId: dataStream[deviceId].patientId, // patientName: dataStream[deviceId].patientName, // today: getToday() // }; // const update = { // "$push": { "bpm": bpmAvg } // }; // updateDB(filter, update); // } // helpful to check if a patient gets disconnected by // keeping a copy of the last updated date dataStream[deviceId].lastUpdated = Date.now(); } ``` **TODO in Phase 2** The commented code from line 49 to 69 is the *to-be-determined* logic for storing data inside database. *One solution:* Add another key in dataStream called `bpmBuffer`. Once we have kept 10 bpms locally on server, we will compute the average bpm (clears out the array afterwards). To insert/update MongoDB, we pull out the document that matches the `patientId`, `patientName`, and today's starting date in timestamp format (`filter` fields). Then, we perform an update to database using async/await, like so: ```javascript= async function updateDB(filter, update) { await Day.findOneAndUpdate(filter, update, { upsert: true }); } ``` `upsert:` if the document does not exist (matching the filter fields), create it; if it exists, update it. Let's move back to `index.js` ```javascript= else if (list.length === 4) { // retreive deviceId and bpm const deviceId = list[1]; const bpm = list[3]; if (Object.keys(dataDict.getPatientData(deviceId)).length !== 0) { dataDict.updateBPM(deviceId, bpm); const jsonStr = dataDict.parseData(deviceId); io.send(jsonStr); } } ``` After the server updates dataDict dictionary, it then calls `parseData` function (line 9) to *flatten* the the data structure, like so: ```javascript= { deviceId: "1", patientId: "123456", patientName: "John", bpm: 0, alarm: 0, location: "Hallway", prediction: "I", lastUpdated: <some timestamp>, bpmHistory: [0,0,0,0,0] } ``` The json object above will be sent to the client via socket.io by calling `io.send(jsonStr);` (line 10). Let's take a look at the `setInterval()` underneath the redis suscriber code. ```javascript= // constantly check if patient has left the device setInterval(() => { let dataStream = dataDict.getDataStream(); for (let deviceId in dataStream) { if (Object.keys(dataDict.getPatientData(deviceId)).length !== 0 && Date.now() - dataStream[deviceId].lastUpdated >= 5000) { dataDict.updateBPM(deviceId, 0); const jsonStr = dataDict.parseData(deviceId); io.send(jsonStr); } } }, 7000); ``` For every 7 seconds, I check whether the last updated time of a patient's data is more than 5 seconds from now. If it's the case, it indicates that the patient's finger may be disconnected to the device and/or the device may get disconnected from the network. So, this function will set the patient's bpm to 0 if his/her heart rate is not detectable after being connected. The reason that this function is needed because code inside the observer, `client.on()` will only be executed if there's an incoming message. If there isn't a new message detected for a long time, the observer will not automatically send bpm of 0 to the client. *TO BE CONTINUED.....* ## Resources * Fog Server API Documentation: https://github.com/iomt-uci/fog-server * JavaScript Written Tutorial: https://javascript.info/ * Modern JavaScript Tutorial: https://youtu.be/iWOYAxlnaww * **Asynchronous JavaScript Tutorial: https://youtu.be/ZcQyJ-gxke0** * **Node.js and MongoDB Tutorial: https://youtu.be/zb3Qk8SG5Ms**