Condition Monitoring Dashboard Tutorial

This tutorial will guide you through building an industrial condition monitoring dashboard.

Step 01

Set up environment

This application is written in JavaScript and Arduino C++. You must set up the Arduino IDE to program the hardware to collect your sensor values. You will also need to acquire the hardware used for this tutorial.

Programmable Logic Controller Hardware and Sensors

This tutorial uses industrial-quality hardware that is designed to withstand harsh environments like those found in manufacturing and other industrial applications. You could use other hardware vendors by following this tutorial as a model for how to implement the same PubNub connectivity with other PLC vendors.

For this tutorial, we are using the ProductivityOpen Controllers by Automation Direct. We chose this hardware because it supports using C++ to program allowing for using our PubNub Arduino SDK. Automation Direct also offers fast shipping and amazing customer service.

Required PLC Hardware

You'll need one of each of these components:

P1AM-100 PLC - This is the Programmable Logic Controller that will be the brains of our sensor data collection.

P1-4ADL2DAL-2 (Productivity1000 analog combo module) - This module will be used to collect the analog output values from our vibration sensors. You will also need a separate 24v power supply for this module as it is isolated from the P1AM-100. You may also want to purchase one of the recommended removable terminal blocks to make connecting 24v power and the sensors easier.

P1AM-ETH - This module is required to connect the P1AM-100 PLC to the internet and enables streaming sensor data over the PubNub network. An ethernet connection is required for the tutorial.

P1-01AC (Optional) - This power supply connects to the P1AM-100 PLC to enable you to power it from an AC power source. You could also use an external 24v power supply to power the P1AM-100 PLC or one of the other power supply modules available from Automation Direct for the P1AM-100.

You'll also need two sensors for collecting vibration data. These will connect directly to the P1-4ADL2DAL-2 (Productivity1000 analog combo module). You can use any sensor that is analog voltage output type with this tutorial.

We used the SINVT Voltage-Output-Type Tilt Switch by WitMotion because it is industrial quality and pairs nicely with our other selected hardware.

Assembling Your Vibration Monitoring PLC

Each of the ProductivityOpen modules link together. Open the lock tabs, connect each module, and then push the locking tabs back down. The modules should be connected in this order (There is only one possibility):

P1-01AC > P1AM-ETH > P1AM-100 > P1-4ADL2DAL-2

Next, you'll need to connect the P1-01AC to AC power.

The last step is to connect your sensors and 24v power to the P1-4ADL2DAL-2.

  • Connect an external 24v power supply to the 24v and 0v pins on the P1-4ADL2DAL-2.

  • Connect the positive wires from both sensors to the 24v pin.

  • Connect the negative wires from both sensors to the 0v pin.

  • Connect the X and Y analog outputs from one sensor to V1+ and V2+ inputs.

  • Connect the X and Y analog outputs from the other sensor to V3+ and V4+ inputs.

  • Other wires from the sensors are not used.

Industrial automation modules with AC power input, Ethernet connection, and DC power wiring annotations for sensor outputs.

Arduino IDE Setup

Arduino provides a guide for downloading and installing the Arduino IDE.

Once the application is installed, you must configure it to support the hardware used in this tutorial and add the PubNub Arduino SDK library.

Set Up P1AM-100 for Arduino IDE

Open the Arduino IDE and open Preferences from the File menu.

In the Additional Boards Manager URLs area, add the following URL:

https://raw.githubusercontent.com/facts-engineering/facts-engineering.github.io/master/package_productivity-P1AM-boardmanagermodule_index.json

Then click OK and go to Tools > Board: > Boards Manager. Search for 'P1AM'.

Install the P1AM-100 board by Facts Engineering. Close the Boards Manager window.

Go to Tools > Board: > P1AM-100 and select 'P1AM-100'. You are now ready to develop with the P1AM-100 programmable logic controller using the Arduino IDE.

Set Up PubNub Arduino SDK

In the Arduino IDE, open the Library Manager by going to Tools > Manage Libraries...

Search for 'PubNub.'

Install the PubNub Arduino SDK. Close the Library Manager.

You are now ready to develop with the PubNub Arduino SDK using the Arduino IDE.

Your development environment is now ready. Next, you need to configure your PubNub API keys.

Step 02

Create keys

Demo Keys

You're ready to go!

We’ve assigned demo keys which are set up to support the features of this tutorial app. These will be active for 72 hours but you can sign up for a free account at the end of the tutorial to create your own permanent keys.

The demo keys have the following functionality activated ready for your application:

  • Channel Presence Be notified when PubNub clients go online or offline.

  • Stream Controller You will be able to receive messages against a group of channels, or post one message to multiple channels.

  • Message Persistence You will be able to see user’s message history and meta data.

My own keys

You'll need to create an account to use your own keys

  1. Sign in to your PubNub Dashboard.

  2. Go to Keysets using the left hand side navigation.

  3. Click the “Create New Keyset” button on the upper right hand-side of the portal.

  4. A pop-up appears. Give the keyset a name and click the “Create” button.

  5. Click on the newly created keyset.

  6. Enable three features for the keyset to be able to connect to the PubNub Network: Presence, Message Persistence and Stream Controller.

  7. Enable Presence by clicking on the slider to turn it on. A pop-up will require that you enter in “ENABLE”. Enter in “ENABLE” in all caps and then press the “Enable” button. You can leave the default settings.

  8. Enable Message Persistence by clicking on the slider to turn it on. You can leave the default settings.

  9. Enable Stream Controller by clicking on the slider to turn it on if it is already not enabled. You can leave the default settings.

  10. Click on the “Save Changes” button in the bottom right corner of the portal. After a few seconds, a confirmation message will appear in the upper-left corner of the portal confirming that these changes have been saved. 

  11. Scroll back to the top of the portal. Copy the Publish key by clicking on the copy icon and paste it below

  12. Copy the Subscribe Key by clicking on the copy icon and paste it below

Step 03

Build and run

Watch the video or follow the steps below to build and run your application.

Programming P1AM-100 as a Vibration Monitor

Open the Arduino IDE and connect your P1AM-100 with a usb cable. Go to Tools > Port and make sure your hardware is selected.

We'll start with a basic sketch that includes the P1AM library, the Ethernet library, and the PubNub libary.

Edit the mac address to match your P1AM-ETH module. You can find the mac address on the bottom of the module. If you've already assembled your hardware, you may need to disconnect the P1AM-ETH module from your P1AM-100 to record the mac address.

We'll also add a a few variables to store our sensor readings and compare to the last reading.

arduino
#include <P1AM.h>
#include <Ethernet.h>
#include <PubNub.h>

byte mac[] = { 0x60, 0x52, 0xD0, 0x07, 0x43, 0xD4 };

int channelRead1, channelRead2, channelRead3, channelRead4;
int channelLast1, channelLast2, channelLast3, channelLast4;

void setup(){ // the setup routine runs once:

}

void loop(){  // the loop routine runs over and over again forever:

}

Next, modify the setup() routine to create a serial connection, initialize the analog input module, and create a connection to PubNub.

Replace 'YOUR_PUB_KEY_HERE' and 'YOUR_SUB_KEY_HERE' with your PubNub API Keys.

arduino
void setup(){ // the setup routine runs once:

  Serial.begin(115200);  //initialize serial communication at 115200 bits per second 
  while (!P1.init()){ 
    ; //Wait for Modules to Sign on   
  }
  while (!Ethernet.begin(mac)) {
    Serial.println("Ethernet setup error");
    delay(1000);
  }
  Serial.println(Ethernet.localIP());
  PubNub.set_uuid("monitoring_1");
  PubNub.begin("YOUR_PUB_KEY_HERE", "YOUR_SUB_KEY_HERE");
}

Modify the loop routine to read each of the analog inputs, calculate the absolute value of the current reading from the last one (the vibration), and then publish the data to the 'monitoringdata' channel. This code will take a new reading and then publish it every 1/2 second.

arduino
void loop(){  // the loop routine runs over and over again forever:
  Ethernet.maintain();
  EthernetClient *client;

  // Get the readings from the 4 analog inputs 
  channelRead1 = P1.readAnalog(1, 1);
  channelRead2 = P1.readAnalog(1, 2);
  channelRead3 = P1.readAnalog(1, 3);
  channelRead4 = P1.readAnalog(1, 4);

  //Publish to PubNub the absolute value of the difference from the last 2 readings (the vibration)
  char msg[120];
  sprintf(msg, "{\"channel1\":%d,\"channel2\":%d,\"channel3\":%d,\"channel4\":%d}", abs(channelLast1-channelRead1), abs(channelLast2-channelRead2), abs(channelLast3-channelRead3), abs(channelLast4-channelRead4));
  if (channelLast1 > 0) { // Skip first publish
    client = PubNub.publish("monitoringchannel", msg);
    if (!client) {
        Serial.println("publishing error");
        delay(1000);
        return;
    }
    while (client->connected()) {
        while (client->connected() && !client->available());
        char c = client->read();
        Serial.print(c);
    }
    Serial.println();
    client->stop();
  }

  //Remember last reading for next reading
  channelLast1 = channelRead1;
  channelLast2 = channelRead2;
  channelLast3 = channelRead3;
  channelLast4 = channelRead4;

  Serial.println(msg);
  delay(500);
}

Your code should now look like this:

arduino
#include <P1AM.h>
#include <Ethernet.h>
#include <PubNub.h>

byte mac[] = { 0x60, 0x52, 0xD0, 0x07, 0x43, 0xD4 };

int channelRead1, channelRead2, channelRead3, channelRead4;
int channelLast1, channelLast2, channelLast3, channelLast4;

void setup(){ // the setup routine runs once:

  Serial.begin(115200);  //initialize serial communication at 115200 bits per second 
  while (!P1.init()){ 
    ; //Wait for Modules to Sign on   
  }
  while (!Ethernet.begin(mac)) {
    Serial.println("Ethernet setup error");
    delay(1000);
  }
  Serial.println(Ethernet.localIP());
  PubNub.set_uuid("monitoring_1");
  PubNub.begin("YOUR_PUB_KEY_HERE", "YOUR_SUB_KEY_HERE");
}

void loop(){  // the loop routine runs over and over again forever:
  Ethernet.maintain();
  EthernetClient *client;

  // Get the readings from the 4 analog inputs 
  channelRead1 = P1.readAnalog(1, 1);
  channelRead2 = P1.readAnalog(1, 2);
  channelRead3 = P1.readAnalog(1, 3);
  channelRead4 = P1.readAnalog(1, 4);

  //Publish to PubNub the absolute value of the difference from the last 2 readings (the vibration)
  char msg[120];
  sprintf(msg, "{\"channel1\":%d,\"channel2\":%d,\"channel3\":%d,\"channel4\":%d}", abs(channelLast1-channelRead1), abs(channelLast2-channelRead2), abs(channelLast3-channelRead3), abs(channelLast4-channelRead4));
  if (channelLast1 > 0) { // Skip first publish
    client = PubNub.publish("monitoringchannel", msg);
    if (!client) {
        Serial.println("publishing error");
        delay(1000);
        return;
    }
    while (client->connected()) {
        while (client->connected() && !client->available());
        char c = client->read();
        Serial.print(c);
    }
    Serial.println();
    client->stop();
  }

  //Remember last reading for next reading
  channelLast1 = channelRead1;
  channelLast2 = channelRead2;
  channelLast3 = channelRead3;
  channelLast4 = channelRead4;

  Serial.println(msg);
  delay(500);
}

You can now upload your code to the P1AM by clicking the upload button in the top left corner of the Arduino IDE.

Check everything is working properly by opening the serial monitor. You should see a success message when each reading is published to the PubNub network.

Check all connections if you see an error. Make sure you have an Ethernet cord connected and all sensors are plugged into the analog input module.

Building your Condition Monitoring Dashboard

Now you're ready to build a Condition Monitoring Dashboard to recive the vibration data stream. Our dashboard will have a graph for each sensor, a status indicator for the data stream, and a status indicator for the vibration values.

Create 3 files in a directory: dashboard.css, dashboard.js, and index.html.

In dashboard.css add the following code to format the dashboard styling:

html
body {
  font-size: .875rem;
}

.feather {
  width: 16px;
  height: 16px;
  vertical-align: text-bottom;
}

/*
 * Sidebar
 */

.sidebar {
  position: fixed;
  top: 0;
  /* rtl:raw:
  right: 0;
  */
  bottom: 0;
  /* rtl:remove */
  left: 0;
  z-index: 100; /* Behind the navbar */
  padding: 48px 0 0; /* Height of navbar */
  box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}

@media (max-width: 767.98px) {
  .sidebar {
    top: 5rem;
  }
}

.sidebar-sticky {
  position: relative;
  top: 0;
  height: calc(100vh - 48px);
  padding-top: .5rem;
  overflow-x: hidden;
  overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}

.sidebar .nav-link {
  font-weight: 500;
  color: #333;
}

.sidebar .nav-link .feather {
  margin-right: 4px;
  color: #727272;
}

.sidebar .nav-link.active {
  color: #2470dc;
}

.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
  color: inherit;
}

.sidebar-heading {
  font-size: .75rem;
  text-transform: uppercase;
}

/*
 * Navbar
 */

.navbar-brand {
  padding-top: .75rem;
  padding-bottom: .75rem;
  font-size: 1rem;
  background-color: rgba(0, 0, 0, .25);
  box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}

.navbar .navbar-toggler {
  top: .25rem;
  right: 1rem;
}

.navbar .form-control {
  padding: .75rem 1rem;
  border-width: 0;
  border-radius: 0;
}

.form-control-dark {
  color: #fff;
  background-color: rgba(255, 255, 255, .1);
  border-color: rgba(255, 255, 255, .1);
}

.form-control-dark:focus {
  border-color: transparent;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}

Next, you need to build out the structure of the dashboard. You'll need areas to show two charts, cards for displaying operational status, and cards for displaying vibration status. We'll also add some popup modals to explain what you're looking at. The page will include the PubNub Javascript SDK, Bootstrap 5, Chart.js library, and the dashboard.js file.

Make your index.html file look like this:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
    <title>Condition Monitoring Dashboard</title>
    <link rel="canonical" href="https://getbootstrap.com/docs/5.0/examples/dashboard/">
    <!-- Bootstrap core CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <!-- Custom styles for this template -->
    <link href="dashboard.css" rel="stylesheet">
  </head>
  <body>

<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
  <a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#">PubNub</a>
  <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="navbar-nav">
    <div class="nav-item text-nowrap">
      <!-- <a class="nav-link px-3" href="#">Sign out</a> -->
    </div>
  </div>
</header>
<div class="container-fluid">
  <div class="row">
    <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
      <div class="position-sticky pt-3">
        <ul class="nav flex-column">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="#">
              <span data-feather="home"></span>
              Dashboard
            </a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="#">
              <span data-feather="settings"></span>
              Settings
            </a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="#">
              <span data-feather="users"></span>
              User Manager
            </a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="#">
              <span data-feather="cpu"></span>
              Device Manager
            </a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="#">
              <span data-feather="database"></span>
              Integrations
            </a>
          </li>
        </ul>
      </div>
    </nav>

    <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
      <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
        <h1 class="h2">Condition Monitoring Dashboard</h1>
        <div class="btn-toolbar mb-2 mb-md-0">

        </div>
      </div>
      <div>
        </div>
        <h3>Compressor Sensor</h3>
        <canvas class="my-4 w-100 chartjs-render-monitor" id="sensorchart1" style="display: block; width: 1540px; height: 350px;" width="1540" height="350"></canvas>
        <div class="container">
          <div class="row">
            <div class="col">

              <div class="card text-white mb-3 bg-danger" id="C1OSD" style="max-width: 18rem;">
                <div class="card-header">Operational Status</div>
                <div class="card-body">
                  <h5 class="card-title">Danger</h5>
                  <p class="card-text">Data is not being received and is not being monitored. Check hardware immediately for faults.</p>
                  <button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#OPStatModal">
                    More Info
                  </button>
                </div>
              </div>
              <div class="card text-dark mb-3 bg-warning d-none" id="C1OSW" style="max-width: 18rem;">
                <div class="card-header">Operational Status</div>
                <div class="card-body">
                  <h5 class="card-title">Warning</h5>
                  <p class="card-text">Sensor output values are indicating low or no vibrations. The monitored device may be offline or inactive.</p>
                  <button type="button" class="btn btn-outline-dark" data-bs-toggle="modal" data-bs-target="#OPStatModal">
                    More Info
                  </button>
                </div>
              </div>
              <div class="card text-white mb-3 bg-success d-none" id="C1OSN" style="max-width: 18rem;">
                <div class="card-header">Operational Status</div>
                <div class="card-body">
                  <h5 class="card-title">Normal</h5>
                  <p class="card-text">Data is being recived and vibrations are shown above.</p>
                  <button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#OPStatModal">
                    More Info
                  </button>
                </div>
              </div>


            </div>
            <div class="col">
              <div class="card text-white mb-3 bg-danger d-none" id="C1VSD" style="max-width: 18rem;">
                <div class="card-header">Vibration Status</div>
                <div class="card-body">
                  <h5 class="card-title">Danger</h5>
                  <p class="card-text">Extreme vibrations detected. Check monitored hardware for potential issues. Exercise caution.</p>
                  <button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#VStatModal">
                    More Info
                  </button>
                </div>
              </div>
              <div class="card text-dark mb-3 bg-warning d-none" id="C1VSW" style="max-width: 18rem;">
                <div class="card-header">Vibration Status</div>
                <div class="card-body">
                  <h5 class="card-title">Medium</h5>
                  <p class="card-text">Significant vibrations detected within normal range.</p>
                  <button type="button" class="btn btn-outline-dark" data-bs-toggle="modal" data-bs-target="#VStatModal">
                    More Info
                  </button>
                </div>
              </div>
              <div class="card text-white mb-3 bg-success" id="C1VSS" style="max-width: 18rem;">
                <div class="card-header">Vibration Status</div>
                <div class="card-body">
                  <h5 class="card-title">Low</h5>
                  <p class="card-text">Little or no vibration activity.</p>
                  <button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#VStatModal">
                    More Info
                  </button>
                </div>
              </div>
            </div>
            <div class="card border-dark mb-3" style="max-width: 18rem;">
              <div class="card-header">Live Data Stream</div>
              <div class="card-body">
                <ul class="list-group card-text">
                  <li class="list-group-item">Last X: <span id="c1_last_x"></span></li>
                  <li class="list-group-item">Last Y: <span id="c1_last_y"></span></li>
                  <li class="list-group-item">X Max: <span id="c1_max_x"></span></li>
                  <li class="list-group-item">Y Max: <span id="c1_max_y"></span></li>
                </ul>
              </div>
            </div>
        </div>
      </div>
      <div>
        <h3>Fan Sensor</h3>
        <canvas class="my-4 w-100 chartjs-render-monitor" id="sensorchart2" style="display: block; width: 1540px; height: 350px;" width="1540" height="350"></canvas>
        <div class="container">
          <div class="row">
            <div class="col">

              <div class="card text-white mb-3 bg-danger" id="C2OSD" style="max-width: 18rem;">
                <div class="card-header">Operational Status</div>
                <div class="card-body">
                  <h5 class="card-title">Danger</h5>
                  <p class="card-text">Data is not being received and is not being monitored. Check hardware immediately for faults.</p>
                  <button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#OPStatModal">
                    More Info
                  </button>
                </div>
              </div>
              <div class="card text-dark mb-3 bg-warning d-none" id="C2OSW" style="max-width: 18rem;">
                <div class="card-header">Operational Status</div>
                <div class="card-body">
                  <h5 class="card-title">Warning</h5>
                  <p class="card-text">Sensor output values are indicating low or no vibrations. The monitored device may be offline or inactive.</p>
                  <button type="button" class="btn btn-outline-dark" data-bs-toggle="modal" data-bs-target="#OPStatModal">
                    More Info
                  </button>
                </div>
              </div>
              <div class="card text-white mb-3 bg-success d-none" id="C2OSN" style="max-width: 18rem;">
                <div class="card-header">Operational Status</div>
                <div class="card-body">
                  <h5 class="card-title">Normal</h5>
                  <p class="card-text">Data is being recived and vibrations are shown above.</p>
                  <button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#OPStatModal">
                    More Info
                  </button>
                </div>
              </div>

            </div>
            <div class="col">
              <div class="card text-white mb-3 bg-danger d-none" id="C2VSD" style="max-width: 18rem;">
                <div class="card-header">Vibration Status</div>
                <div class="card-body">
                  <h5 class="card-title">Danger</h5>
                  <p class="card-text">Extreme vibrations detected. Check monitored hardware for potential issues. Exercise caution.</p>
                  <button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#VStatModal">
                    More Info
                  </button>
                </div>
              </div>
              <div class="card text-dark mb-3 bg-warning d-none" id="C2VSW" style="max-width: 18rem;">
                <div class="card-header">Vibration Status</div>
                <div class="card-body">
                  <h5 class="card-title">Medium</h5>
                  <p class="card-text">Significant vibrations detected within normal range.</p>
                  <button type="button" class="btn btn-outline-dark" data-bs-toggle="modal" data-bs-target="#VStatModal">
                    More Info
                  </button>
                </div>
              </div>
              <div class="card text-white mb-3 bg-success" id="C2VSS" style="max-width: 18rem;">
                <div class="card-header">Vibration Status</div>
                <div class="card-body">
                  <h5 class="card-title">Low</h5>
                  <p class="card-text">Little or no vibration activity.</p>
                  <button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#VStatModal">
                    More Info
                  </button>
                </div>
              </div>
            </div>
            <div class="col">
              <div class="card border-dark mb-3" style="max-width: 18rem;">
                <div class="card-header">Live Data Stream</div>
                <div class="card-body">
                    <ul class="list-group card-text">
                    <li class="list-group-item">Last X: <span id="c2_last_x"></span></li>
                    <li class="list-group-item">Last Y: <span id="c2_last_y"></span></li>
                    <li class="list-group-item">X Max: <span id="c2_max_x"></span></li>
                    <li class="list-group-item">Y Max: <span id="c2_max_y"></span></li>
                  </ul>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

    </main>
  </div>
</div>

<!-- Modal -->
<div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="staticBackdropLabel">Predictive Maintenance Dashboard</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        Welcome to the Predictive Maintenance Dashboard Demo powered by PubNub. This demo shows real data from an industrial programmable logic controller connected to vibration sensors.
         This data is used to anticipate when equipment is about to fail or when maintenance is needed. Some features of this dashboard are disabled. Send an email to
          <a href="mailto:devrel@pubnub.com">devrel@pubnub.com</a> to learn more about how to create your own predictive maintenance monitoring system powered by PubNub.
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Understood</button>
      </div>
    </div>
  </div>
</div>
<div class="modal fade" id="OPStatModal" tabindex="-1" role="dialog" aria-labelledby="OPStatModalLabel" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="OPStatModalLabel">Operational Status Info</h5>
        <button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <h5>Danger</h5>
        <p>Data is not being received and is not being monitored. Check hardware immediately for faults.</p>
        <h5>Warning</h5>
        <p>Sensor output values are indicating low or no vibrations. The monitored device may be offline or inactive.</p>
        <h5>Normal</h5>
        <p>Data is being recived and vibrations are shown.</p>            
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>
<div class="modal fade" id="VStatModal" tabindex="-1" role="dialog" aria-labelledby="VStatModalLabel" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="VStatModalLabel">Vibration Status Info</h5>
        <button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <h5>Danger</h5>
        <p>Extreme vibrations detected. Check monitored hardware for potential issues. Exercise caution.</p>
        <h5>Medium</h5>
        <p>Significant vibrations detected within normal range.</p>
        <h5>Low</h5>
        <p>Little or no vibration activity.</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>


      <script src="https://cdn.pubnub.com/sdk/javascript/pubnub.7.2.2.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
      <script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
      <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
      <script src="dashboard.js"></script>
  </body>
</html>

Next, populate the dashboard.js file. To make it easier to understand, this tutorial will review the file in 3 parts. Copy each part one after the other.

In the first part of the dashboard.js file, there's a function that gets the maximum value from the chart. The output values from this function are used to determine how significant the vibrations are. We also initialize are two charts that will display the sensor data.

javascript
(function () {
  feather.replace({ 'aria-hidden': 'true' })

  var getMax = function(chart) { //Returns the maximum value from the chart.
    datasets = chart.data.datasets;
    max = 0;
    for(var i=0; i<datasets.length; i++) {
        dataset=datasets[i]
        if(chart.data.datasets[i].hidden) {
            continue;
        }
        dataset.data.forEach(function(d) {
            if(typeof(d)=="number" && d>max) {
                max = d
            }
        })
    }
    return max;
  }

  // Graphs
  var sensorchart1 = new Chart(document.getElementById('sensorchart1'), {
    type: 'line',
    data: {
      datasets: [{
        label: 'Reported X Deviation',
        lineTension: 0.1,
        backgroundColor: 'transparent',
        borderColor: '#007bff',
        borderWidth: 4,
        pointBackgroundColor: '#007bff'
      },
      {
        label: 'Reported Y Deviation',
        lineTension: 0.1,
        backgroundColor: 'transparent',
        borderColor: '#FF0000',
        borderWidth: 5,
        pointBackgroundColor: '#FF0000'
      }]
    },
    options: {
      legend: {
        display: true
      }
    }
  })
  var sensorchart2 = new Chart(document.getElementById('sensorchart2'), {
    type: 'line',
    data: {
      datasets: [{
        label: 'Reported X Deviation',
        lineTension: 0.1,
        backgroundColor: 'transparent',
        borderColor: '#007bff',
        borderWidth: 4,
        pointBackgroundColor: '#007bff'
      },
      {
        label: 'Reported Y Deviation',
        lineTension: 0.1,
        backgroundColor: 'transparent',
        borderColor: '#FF0000',
        borderWidth: 5,
        pointBackgroundColor: '#FF0000'
      }]
    },
    options: {
      legend: {
        display: true
      }
    }
  })

Next, initialize the PubNub connection. Replace 'YOUR_PUB_KEY_HERE' and 'YOUR_SUB_KEY_HERE' with the same PubNub API keys you used for the Programmable Logic Controller.

Load the recent sensor history into both charts so there's already data displayed when the page loads.

Then create a PubNub listener that adds new values to the charts and rotates out old data. Each time your PLC publishes a new sensor reading both charts will update. We'll also update the max value displayed on the page.

javascript
  pubnub = new PubNub({ // Your PubNub keys here. Get them from https://dashboard.pubnub.com.
    publishKey : 'YOUR_PUB_KEY_HERE',
    subscribeKey : 'YOUR_SUB_KEY_HERE',
    uuid : 'dashboard'
  });

var lastUpdate;
const arrAvg = arr => arr.reduce((a,b) => a + b, 0) / arr.length

 pubnub.fetchMessages( // Load history into the charts
      {
          channels: ['monitoringchannel'],
          count: 40,
      },
      function (status, response) {
              response.channels['monitoringchannel'].forEach((message) => {
                  var date = new Date(Math.trunc((message.timetoken / 10000), 16)); 
                  lastUpdate = date;
                  var timedisplay = date.toLocaleString();
                  //console.log(message);
                  sensorchart1.data.labels.push(timedisplay);
                  sensorchart1.data.datasets[0].data.push([message.message.channel1]);
                  sensorchart1.data.datasets[1].data.push([message.message.channel2]);
                  sensorchart2.data.labels.push(timedisplay);
                  sensorchart2.data.datasets[0].data.push([message.message.channel3]);
                  sensorchart2.data.datasets[1].data.push([message.message.channel4]);

                  if (sensorchart1.data.datasets[0].data.length > 40) {
                    // Remove the oldest data and label
                    sensorchart1.data.datasets[0].data.shift();
                    sensorchart1.data.datasets[1].data.shift();
                    sensorchart1.data.labels.shift();
                  }
                  if (sensorchart2.data.datasets[0].data.length > 40) {
                    // Remove the oldest data and label
                    sensorchart2.data.datasets[0].data.shift();
                    sensorchart2.data.datasets[1].data.shift();
                    sensorchart2.data.labels.shift();
                  }
                  var maxarray = sensorchart1.data.datasets[0].data.concat(sensorchart1.data.datasets[1].data);
                  sensorchart1.options.scales = {
                    xAxes: [],
                    yAxes: [
                      {
                        ticks: {
                          beginAtZero: false,
                          max: Math.max(...maxarray)+1  
                        }
                      }
                    ]
                  };

                  sensorchart1.update();
                  maxarray = sensorchart2.data.datasets[0].data.concat(sensorchart2.data.datasets[1].data);
                  sensorchart2.options.scales = {
                    xAxes: [],
                    yAxes: [
                      {
                        ticks: {
                          beginAtZero: false,
                          max: Math.max(...maxarray)+1  
                        }
                      }
                    ]
                  };

                  sensorchart2.update();

              }); 
      }
  );

  pubnub.addListener({ // Subscribe for new values and update charts.
    message: function(message) {
          var date = new Date(Math.trunc((message.timetoken / 10000), 16)); 
          lastUpdate = date;
          var timedisplay = date.toLocaleString();
          //console.log(message);
          sensorchart1.data.labels.push(timedisplay);
          sensorchart1.data.datasets[0].data.push([message.message.channel1]);
          sensorchart1.data.datasets[1].data.push([message.message.channel2]);
          document.getElementById("c1_last_x").textContent=message.message.channel1;
          document.getElementById("c1_last_y").textContent=message.message.channel2;

          sensorchart2.data.labels.push(timedisplay);
          sensorchart2.data.datasets[0].data.push([message.message.channel3]);
          sensorchart2.data.datasets[1].data.push([message.message.channel4]);
          document.getElementById("c2_last_x").textContent=message.message.channel3;
          document.getElementById("c2_last_y").textContent=message.message.channel4;

          if (sensorchart1.data.datasets[0].data.length > 40) {
            // Remove the oldest data and label
            sensorchart1.data.datasets[0].data.shift();
            sensorchart1.data.datasets[1].data.shift();
            sensorchart1.data.labels.shift();
          }
          if (sensorchart2.data.datasets[0].data.length > 40) {
            // Remove the oldest data and label
            sensorchart2.data.datasets[0].data.shift();
            sensorchart2.data.datasets[1].data.shift();
            sensorchart2.data.labels.shift();
          }
          var maxarray = sensorchart1.data.datasets[0].data.concat(sensorchart1.data.datasets[1].data);
          sensorchart1.options.scales = {
            xAxes: [],
            yAxes: [
              {
                ticks: {
                  beginAtZero: false,
                  max: Math.max(...maxarray)+1  
                }
              }
            ]
          };

Now we need to update the status cards based on the max vibration detected and if data is being received. You can edit the range values in this code to adjust for the sensitivity of different applications.

Subscribe to 'monitoringchannel' to get any new data in the PubNub listener from the previous step.

The last step is to create a checkStatus() function that updates the page status if no sensor values have been received after a period of time (20 seconds).

javascript
          if (Math.max(...maxarray) < 3) { // low vibration. OS=warn and VS=low
            document.getElementById('C1OSD').classList.add("d-none");
            document.getElementById('C1OSW').classList.remove("d-none");
            document.getElementById('C1OSN').classList.add("d-none");
            document.getElementById('C1VSD').classList.add("d-none");
            document.getElementById('C1VSW').classList.add("d-none");
            document.getElementById('C1VSS').classList.remove("d-none");
          } else if (Math.max(...maxarray) >= 3 && Math.max(...maxarray) < 12 ) { // med vibration. OS=ok and VS=warn
            document.getElementById('C1OSD').classList.add("d-none");
            document.getElementById('C1OSW').classList.add("d-none");
            document.getElementById('C1OSN').classList.remove("d-none");
            document.getElementById('C1VSD').classList.add("d-none");
            document.getElementById('C1VSW').classList.remove("d-none");
            document.getElementById('C1VSS').classList.add("d-none");
          } else if (Math.max(...maxarray) >= 12) { // danger  OS=ok and VS=danger
            document.getElementById('C1OSD').classList.add("d-none");
            document.getElementById('C1OSW').classList.add("d-none");
            document.getElementById('C1OSN').classList.remove("d-none");
            document.getElementById('C1VSD').classList.remove("d-none");
            document.getElementById('C1VSW').classList.add("d-none");
            document.getElementById('C1VSS').classList.add("d-none");
          } 

          sensorchart1.update();
          maxarray = sensorchart2.data.datasets[0].data.concat(sensorchart2.data.datasets[1].data);
          sensorchart2.options.scales = {
            xAxes: [],
            yAxes: [
              {
                ticks: {
                  beginAtZero: false,
                  max: Math.max(...maxarray)+1  
                }
              }
            ]
          };

          document.getElementById("c2_max_x").textContent = Math.max(...sensorchart2.data.datasets[0].data);
          document.getElementById("c2_max_y").textContent = Math.max(...sensorchart2.data.datasets[1].data);

          if (Math.max(...maxarray) < 3) { // low vibration. OS=warn and VS=low
            document.getElementById('C2OSD').classList.add("d-none");
            document.getElementById('C2OSW').classList.remove("d-none");
            document.getElementById('C2OSN').classList.add("d-none");
            document.getElementById('C2VSD').classList.add("d-none");
            document.getElementById('C2VSW').classList.add("d-none");
            document.getElementById('C2VSS').classList.remove("d-none");
          } else if (Math.max(...maxarray) >= 3 && Math.max(...maxarray) < 12 ) { // med vibration. OS=ok and VS=warn
            document.getElementById('C2OSD').classList.add("d-none");
            document.getElementById('C2OSW').classList.add("d-none");
            document.getElementById('C2OSN').classList.remove("d-none");
            document.getElementById('C2VSD').classList.add("d-none");
            document.getElementById('C2VSW').classList.remove("d-none");
            document.getElementById('C2VSS').classList.add("d-none");
          } else if (Math.max(...maxarray) >= 12) { // danger  OS=ok and VS=danger
            document.getElementById('C2OSD').classList.add("d-none");
            document.getElementById('C2OSW').classList.add("d-none");
            document.getElementById('C2OSN').classList.remove("d-none");
            document.getElementById('C2VSD').classList.remove("d-none");
            document.getElementById('C2VSW').classList.add("d-none");
            document.getElementById('C2VSS').classList.add("d-none");
          } 
          sensorchart2.update();
      }
  });

pubnub.subscribe({
    channels: ['monitoringchannel'] // Listen for data.
});

function checkStatus() { // If the stream is offline update the status boxes
  var currenttime = new Date();
  if(currenttime-lastUpdate > 20*1000){ // Data stream went offline if no data in 20 seconds
    document.getElementById('C1OSD').classList.remove("d-none");
    document.getElementById('C1OSW').classList.add("d-none");
    document.getElementById('C1OSN').classList.add("d-none");
    document.getElementById('C1VSD').classList.add("d-none");
    document.getElementById('C1VSW').classList.add("d-none");
    document.getElementById('C1VSS').classList.remove("d-none");

    document.getElementById('C2OSD').classList.remove("d-none");
    document.getElementById('C2OSW').classList.add("d-none");
    document.getElementById('C2OSN').classList.add("d-none");
    document.getElementById('C2VSD').classList.add("d-none");
    document.getElementById('C2VSW').classList.add("d-none");
    document.getElementById('C2VSS').classList.remove("d-none");
  }
}

setInterval(checkStatus, 500);

const myModal = new bootstrap.Modal('#staticBackdrop'); // Show info to explain dashboard
myModal.show();

Now you're ready to try it out.

Mount your sensors and PLC to the equipment you want to monitor for vibrations. Open 'index.html' in your browser.

You should see sensor values and the vibration status populate on your dashboard. Try manually manipulating the sensors and watch both charts. Disconnect the PLC from the internet and wait 20 seconds to see what occurs if the data stream is interrupted.

Step 04

Congratulations - what's next?

Get the Full Source Code

More Resources for Building Real-time Solutions

PubNub Functions BasicsHow-to Build a Real-Time Active User CounterDeveloper Path Collection

Ready to get beyond Proof of Concept?

Schedule a 15 minute chat with one of our experts to get your project moving

Talk to an Expert
Keep building

Take it to production on your own keys

Create a free PubNub account to get permanent keys for this app.