Monitor Air Quality with a Fleet of Sensors

In this tutorial you will learn how to set up a fleet of devices for yourself or third parties to collect air quality data. You will then create a web app that shows the most recent reading for any device a user has access to.

Air quality dashboard in a web browser with PM2.5 readings from three different sensor machines displayed.

Requirements

You can create one or more machines to measure air quality. For each machine, you need the following hardware:

Set up one device for development

In this section we’ll set up one air sensing machine as our development device.

1

Navigate to the Viam app in a web browser. Create an account and log in.

2

Click the dropdown in the upper-right corner of the FLEET page and use the + button to create a new organization for your air quality machine company. Name the organization and click Create.

3

Click FLEET in the upper-left corner of the page and click LOCATIONS. A new location called First Location is automatically generated for you. Use the menu next to edit the location name to Development, then click Save.

4

Connect a PM sensor to a USB port on the machine’s SBC. Then connect your device to power.

If the computer does not already have a Viam-compatible operating system installed, follow the operating system setup section of the Quickstart guide to install a compatible operating system. You do not need to follow the “Install viam-server” section; you will do that in the next step!

Enable serial communication so that the SBC can communicate with the air quality sensor. For example, if you are using a Raspberry Pi, SSH to it and enable serial communication in raspi-config.

5

Add a new machine using the button in the top right corner of the LOCATIONS tab in the app. Follow the Set up your machine part instructions to install viam-server on the machine and connect it to the Viam app.

When your machine shows as connected, continue to the next step.

6

Navigate to the CONFIGURE tab of the machine, click the + button and select Component or service. Click sensor, then search for sds011 and add the sds001:v1 module. Name the sensor PM_sensor and click Create.

The Add Module button that appears after you click the model name.
7

In the newly created PM_sensor card, replace the contents of the attributes box (the empty curly braces {}) with the following:

{
  "usb_interface": "<REPLACE WITH THE PATH YOU IDENTIFY>"
}
8

To figure out which port your sensor is connected to on your board, SSH to your board and run the following command:

ls /dev/serial/by-id

This should output a list of one or more USB devices attached to your board, for example usb-1a86_USB_Serial-if00-port0. If the air quality sensor is the only device plugged into your board, you can be confident that the only device listed is the correct one. If you have multiple devices plugged into different USB ports, you may need to choose one path and test it, or unplug something, to figure out which path to use.

Now that you have found the identifier, put the full path to the device into your config, for example:

{
  "usb_interface": "/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0"
}
9

Save the config.

Configure tab showing PM sensor and the sensor module configured.
10

On your sensor configuration panel, click on the TEST panel to check that you are getting readings from your sensor.

The sensor readings on the control tab.

If you do not see readings, check the LOGS tab for errors, double-check that serial communication is enabled on the single board computer, and check that the usb_interface path is correctly specified (click below).

Click here for usb_interface troubleshooting help

If you only have one USB device plugged into each of your boards, the usb_interface value you configured in the sensor config is likely (conveniently) the same for all of your machines. If not, you can use fragment overwrite to modify the value on any machine for which it is different:

  1. If you’re not getting sensor readings from a given machine, check the path of the USB port using the same process by which you found the first USB path.

  2. If the path to your sensor on one machine is different from the one you configured in the fragment, add a fragment overwrite to the config of that machine to change the path without needing to remove the entire fragment. Follow the instructions to add a fragment overwrite to your machine’s config, using the following JSON template:

    "fragment_mods": [
    {
      "fragment_id": "<REPLACE WITH YOUR FRAGMENT ID>",
      "mods": [
        {
          "$set": {
            "components.PM_sensor.attributes.usb_interface": "<REPLACE WITH THE PATH TO THE SENSOR ON YOUR MACHINE>"
          }
        }
      ]
    }
    ],
    

    Replace the values with your fragment ID and with the USB path you identify. If you named your sensor something other than PM_sensor, change the sensor name in the template above.

  3. Repeat this process for each machine that needs a different usb_interface value. If you have lots of machines with one usb_interface value, and lots of machines with a second one, you might consider duplicating the fragment, editing that value, and using that second fragment instead of the first one for the applicable machines, rather than using a fragment overwrite for each of the machines. You have options.

11

Click + and add the data management service. Toggle Syncing to the on position and set the sync interval to 0.05 minutes so that data syncs to the cloud every 3 seconds.

Add a tag to all your data so that you can query data from all your air quality sensors more easily in later steps. In the Tags field, type air-quality and click + Tag: air-quality when it appears to create a new tag. This tag will now automatically be applied to all data collected by this data manager.

12

On the PM_sensor panel, click Add method to add data capture.

  • Type :Readings.
  • Frequency: 0.1

Save the config.

You can check that your sensor data is being synced by clicking on the menu and clicking View captured data.

Congratulations, if you made it this far, you now have a functional air sensing machine. Let’s create a dashboard for its measurements next.

Create a dashboard

The Viam TypeScript SDK allows you to build custom web interfaces to interact with your machines. For this project, you’ll use it to build a page that displays air quality sensor data for a given location. You’ll host the website on Viam Apps.

The full code is available for reference on GitHub.

Set up your TypeScript project

Complete the following steps on your laptop or desktop. You don’t need to install or edit anything else on your machine’s single-board computer (aside from viam-server which you already did); you’ll be running the TypeScript code from your personal computer.

1

Make sure you have the latest version of Node.JS installed on your computer.

2

Create a directory on your laptop or desktop for your project. Name it aqi-dashboard.

3

Create a file in your aqi-dashboard folder and name it package.json. The package.json file holds necessary metadata about your project. Paste the following contents into it:

{
  "name": "air-quality-dashboard",
  "description": "A dashboard for visualizing data from air quality sensors.",
  "scripts": {
    "start": "esbuild ./main.ts --bundle --outfile=static/main.js --servedir=static --format=esm",
    "build": "esbuild ./main.ts --bundle --outfile=static/main.js --format=esm"
  },
  "author": "<YOUR NAME>",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "*"
  },
  "dependencies": {
    "@viamrobotics/sdk": "^0.42.0",
    "bson": "^6.6.0",
    "js-cookie": "^3.0.5"
  }
}
4

Install the project’s dependencies by running the following command in your terminal:

npm install

Authenticate your code to your Viam app location

Your TypeScript code requires an API key to establish a connection to your machines. In the final dashboard, these will be provided to your webapp through the local storage of your browser. For development purposes, you will use an API key for your development machine.

1

Create another file inside the aqi-dashboard folder and name it main.ts. Paste the following code into main.ts:

// Air quality dashboard

import * as VIAM from "@viamrobotics/sdk";
import { BSON } from "bson";
import Cookies from "js-cookie";

let apiKeyId = "";
let apiKeySecret = "";
let hostname = "";
let machineId = "";

async function main() {
  const opts: VIAM.ViamClientOptions = {
    serviceHost: "https://app.viam.com",
    credentials: {
      type: "api-key",
      payload: apiKeySecret,
      authEntity: apiKeyId,
    },
  };

  // <Insert data client and query code here in later steps>

  // <Insert HTML block code here in later steps>
}

// <Insert getLastFewAv function definition here in later steps>

document.addEventListener("DOMContentLoaded", async () => {
  machineId = window.location.pathname.split("/")[2];
  ({
    id: apiKeyId,
    key: apiKeySecret,
    hostname: hostname,
  } = JSON.parse(Cookies.get(machineId)!));

  main().catch((error) => {
    console.error("encountered an error:", error);
  });
});
2

To test the dashboard with your development machine, you can temporarily set your machine’s API key, API key ID and machine ID at the top of the main() function.

You can obtain your API key and API key ID from your machines CONNECT tab. You can copy the machine ID using the menu at the top right and clicking Copy machine ID.

Add functionality to your code

1

Now that you have the connection code, you are ready to add code that establishes a connection from the computer running the code to the Viam Cloud where the air quality sensor data is stored. You’ll first create a client to obtain the organization and location ID. Then you’ll get a dataClient instance which accesses all the data in your location, and then query this data to get only the data tagged with the air-quality tag you applied with your data service configuration. The following code also queries the data for a list of the machines that have collected air quality data so that later, depending on the API key used with the code, your dashboard can show the data from any number of machines.

Paste the following code into the main function of your main.ts script, directly after the locationID line, in place of // <Insert data client and query code here in later steps>:

// Instantiate data_client and get all
// data tagged with "air-quality" from your location
const client = await VIAM.createViamClient(opts);
const machine = await client.appClient.getRobot(machineId);
const locationID = machine?.location;
const orgID = (await client.appClient.listOrganizations())[0].id;

const myDataClient = client.dataClient;
const query = {
  $match: {
    tags: "air-quality",
    location_id: locationID,
    organization_id: orgID,
  },
};
const match = { $group: { _id: "$robot_id" } };
// Get a list of all the IDs of machines that have collected air quality data
const BSONQueryForMachineIDList = [
  BSON.serialize(query),
  BSON.serialize(match),
];
let machineIDs: any = await myDataClient?.tabularDataByMQL(
  orgID,
  BSONQueryForMachineIDList,
);
// Get all the air quality data
const BSONQueryForData = [BSON.serialize(query)];
let measurements: any = await myDataClient?.tabularDataByMQL(
  orgID,
  BSONQueryForData,
);
2

For this project, your dashboard will display the average of the last five readings from each air sensor. You need a function to calculate that average. The data returned by the query is not necessarily returned in order, so this function must put the data in order based on timestamps before averaging the last five readings.

Paste the following code into main.ts after the end of your main function, in place of // <Insert getLastFewAv function definition here in later steps>:

// Get the average of the last five readings from a given sensor
async function getLastFewAv(all_measurements: any[], machineID: string) {
  // Get just the data from this machine
  let measurements = new Array();
  for (const entry of all_measurements) {
    if (entry.robot_id == machineID) {
      measurements.push({
        PM25: entry.data.readings["pm_2.5"],
        time: entry.time_received,
      });
    }
  }

  // Sort the air quality data from this machine
  // by timestamp
  measurements = measurements.sort(function (a, b) {
    let x = a.time.toString();
    let y = b.time.toString();
    if (x < y) {
      return -1;
    }
    if (x > y) {
      return 1;
    }
    return 0;
  });

  // Add up the last 5 readings collected.
  // If there are fewer than 5 readings, add all of them.
  let x = 5; // The number of readings to average over
  if (x > measurements.length) {
    x = measurements.length;
  }
  let total = 0;
  for (let i = 1; i <= x; i++) {
    const reading: number = measurements[measurements.length - i].PM25;
    total += reading;
  }
  // Return the average of the last few readings
  return total / x;
}
3

Now that you’ve defined the function to sort and average the data for each machine, you’re done with all the dataClient code. The final piece you need to add to this script is a way to create some HTML to display data from each machine in your dashboard.

Paste the following code into the main function of main.ts, in place of // <Insert HTML block code here in later steps>:

// Instantiate the HTML block that will be returned
// once everything is appended to it
let htmlblock: HTMLElement = document.createElement("div");

// Display the relevant data from each machine to the dashboard
for (let m of machineIDs) {
  let insideDiv: HTMLElement = document.createElement("div");
  let avgPM: number = await getLastFewAv(measurements, m._id);
  // Color-code the dashboard based on air quality category
  let level: string = "blue";
  switch (true) {
    case avgPM < 12.1: {
      level = "good";
      break;
    }
    case avgPM < 35.5: {
      level = "moderate";
      break;
    }
    case avgPM < 55.5: {
      level = "unhealthy-sensitive";
      break;
    }
    case avgPM < 150.5: {
      level = "unhealthy";
      break;
    }
    case avgPM < 250.5: {
      level = "very-unhealthy";
      break;
    }
    case avgPM >= 250.5: {
      level = "hazardous";
      break;
    }
  }
  let machineName = (await client.appClient.getRobot(m._id))?.name;
  // Create the HTML output for this machine
  insideDiv.className = "inner-div " + level;
  insideDiv.innerHTML =
    "<p>" +
    machineName +
    ": " +
    avgPM.toFixed(2).toString() +
    " &mu;g/m<sup>3</sup></p>";
  htmlblock.appendChild(insideDiv);
}

// Output a block of HTML with color-coded boxes for each machine
return document.getElementById("insert-readings")?.replaceWith(htmlblock);

Style your dashboard

You have completed the main TypeScript file that gathers and sorts the data. Now, you’ll create a page to display the data.

  1. Create a folder called static inside your aqi-dashboard folder. Inside the static folder, create a file called index.html. This file specifies the contents of the webpage that you will see when you run your code. Paste the following into index.html:

    <!doctype html>
    <html>
    <head>
     <link rel="stylesheet" href="style.css">
    </head>
    <body>
     <div id="main">
       <div>
         <h1>Air Quality Dashboard</h1>
       </div>
       <script type="module" src="main.js"></script>
       <div>
         <h2>PM 2.5 readings</h2>
         <p>The following are averages of the last few readings from each machine:</p>
       </div>
       <div id="insert-readings">
         <p><i>Loading data...
           It may take a few moments for the data to load.
           Do not refresh page.</i></p>
       </div>
       <br>
       <div class="key">
         <h4 style="margin:5px 0px">Key:</h4>
         <p class="good">Good air quality</p>
         <p class="moderate">Moderate</p>
         <p class="unhealthy-sensitive">Unhealthy for sensitive groups</p>
         <p class="unhealthy">Unhealthy</p>
         <p class="very-unhealthy">Very unhealthy</p>
         <p class="hazardous">Hazardous</p>
       </div>
       <p>
         After the data has loaded, you can refresh the page for the latest readings.
       </p>
     </div>
    </body>
    </html>
    
  1. Now you’ll create a style sheet to specify the fonts, colors, and spacing of your dashboard. Create a new file inside your static folder and name it style.css.

  2. Paste the following into style.css:

    body {
      font-family: Helvetica;
      margin-left: 20px;
    }
    
    div {
      background-color: whitesmoke;
    }
    
    h1 {
      color: black;
    }
    
    h2 {
      font-family: Helvetica;
    }
    
    .inner-div {
      font-family: monospace;
      border: .2px solid;
      background-color: lightblue;
      padding: 20px;
      margin-top: 10px;
      max-width: 320px;
      font-size: large;
    }
    
    .key {
      max-width: 200px;
      padding: 0px 5px 5px;
    }
    
    .key p {
      padding: 4px;
      margin: 0px;
    }
    
    .good {
      background-color: lightgreen;
    }
    
    .moderate {
      background-color: yellow;
    }
    
    .unhealthy-sensitive {
      background-color: orange;
    }
    
    .unhealthy {
      background-color: red;
    }
    
    .very-unhealthy {
      background-color: violet;
    }
    
    .hazardous {
      color: white;
      background-color: purple;
    }
    
    #main {
      max-width:600px;
      padding:10px 30px 10px;
    }
    

Full tutorial code

You can find all the code in the GitHub repo for this tutorial.

Run the code

  1. In a command prompt terminal, navigate to your aqi-dashboard directory. Run the following command to start up your air quality dashboard:

    npm start
    
    Terminal window with the command 'npm start' run inside the aqi-dashboard folder. The output says 'start' and then 'esbuild' followed by the esbuild string from the package.json file you configured. Then there's 'Local:' followed by a URL and 'Network:' followed by a different URL.
  2. The terminal should output a line such as Local: http://127.0.0.1:8000/. Copy the URL the terminal displays and paste it into the address bar in your web browser. The data may take up to approximately 5 seconds to load, then you should see air quality data from all of your sensors. If the dashboard does not appear, right-click the page, select Inspect, and check for errors in the console.

    Air quality dashboard in a web browser with PM2.5 readings from three different sensor machines displayed.

Great work. You’ve learned how to configure a machine and you can view its data in a custom TypeScript dashboard.

Deploy the app to Viam apps

Let’s deploy this dashboard so you don’t have to run it locally. This will also allow others to use the dashboard.

1

Remove any API key or IDs before continuing.

2

Create a meta.json in your project folder using this template:

{
  "module_id": "<your-namespace>:air-quality",
  "visibility": "public",
  "url": "https://github.com/viam-labs/air-quality-fleet/",
  "description": "Display air quality data from a machine",
  "applications": [
    {
      "name": "air-quality",
      "type": "single_machine",
      "entrypoint": "static/index.html"
    }
  ]
}

In the Viam app, navigate to your organization settings through the menu in upper right corner of the page. Find the Public namespace and copy that string. Replace <your-namespace> with your public namespace.

3

Register your module with Viam:

viam module create --name="air-quality" --public-namespace="your-namespace"
4

Package your static files and your meta.json file and upload them to the Viam Registry:

npm run build
tar -czvf module.tar.gz static meta.json
viam module upload module.tar.gz --platform=any --version=0.0.1

For subsequent updates run these commands again with an updated version number.

5

Try your app by navigating to:

https://air-quality_your-public-namespace.viamapplications.com

Log in and select your development machine. Your dashboard should now load your data.

Organizing devices for third-party usage

Imagine you create an air quality monitoring company called Pollution Monitoring Made Simple. Anyone can sign up and order one of your sensing machines. When a new customer signs up, you assemble a new machine with a sensor, SBC, and power supply.

Before shipping the sensor machine to your new client, you provision the machine, so that the recipient only needs to connect the machine to their WiFi network for it to work.

To manage all your company’s air quality sensing machines together, you create one organization called Pollution Monitoring Made Simple. An organization is the highest level grouping, and often contains all the locations (and machines) of an entire company.

Inside that organization, you create a location for each customer. A location can represent either a physical location or some other conceptual grouping. You have some individual customers, for example Antonia, who has one sensor machine in her home and one outside. You have other customers who are businesses, for example RobotsRUs, who have two offices, one in New York and one in Oregon, with multiple sensor machines in each.

Organization and locations allow you to manage permissions:

  • When you provision Antonia’s machines, you create them inside a new location called Antonia's Home and grant Antonia operator access to the location. This will later allow her to view data from the air sensors at her home.
  • When you provision the machines for RobotsRUs, you create a location called RobotsRUs and two sub-locations for New York Office and Oregon Office. Then you create the machines in the sub-locations and grant RobotsRUs operator access to the RobotsRUs machines location.

You, as the organization owner, will be able to manage any necessary configuration changes for all air sensing machines in all locations created within the Pollution Monitoring Made Simple organization.

Diagram of the Pollution Monitoring Made Simple organization. In it are two locations: Antonia's HOme and Robots R Us. Robots R Us contains two sub-locations, each containing some machines. The Antonia's Home location contains two machines (and no sub-locations).

For more information, see Fleet Management and provisioning.

Organize your fleet

If you want to follow along, create the following locations:

  • Antonia's Home
  • RobotsRUs

For RobotsRUs crate two sublocations:

  1. Add a new location called Oregon Office using the same Add location button.
  2. Then, find the New parent location dropdown on the Oregon Office page.
  3. Select RobotsRUs and click Change.

Repeat to add the New York office: Add a new location called New York Office, then change its parent location to RobotsRUs.

The New York Office fleet page. The left Locations navigation panel lists Antonia's Home and RobotsRUs, with New York Office and Oregon Office nested inside RobotsRUs.

Getting machines ready for third parties

Continuing with our ficticious company, let’s assume you want to ship air sensing machines to customers as ready-to-go as possible. In other words, you want to provision devices.

Before an air sensing machine leaves your factory, you’d complete the following steps:

  1. You’d flash the single-board computer with an operating system
  2. You’d install viam-agent
  3. You’d provide a machine configuration template, a fragment.

Once a customer receives your machine, they will:

  1. Plug it in and turn it on.
  2. viam-agent will start a WiFi network
  3. The customer uses another device to connect to the machine’s WiFi network and the user gives the machine the password for their WiFi network.
  4. The machine can now connect to the internet and complete setup based on the fragment it knows about.

Create the fragment air sensing machines

In this section you will create the fragment, that is the configuration template that all other machines will use.

  1. Navigate to the FLEET page and go to the FRAGMENTS tab.
  2. Add the same components that you added to the development machine when you set up one device for development.
  3. As a shortcut, you can use the JSON mode on the machine you already configured and copy the machine’s configuration to the fragment.

Provision your machines

1

For each machine, flash the operating system to the device’s SD card. If you are using the Raspberry PI Imager, you must customize at least the hostname for the next steps to work.

Then run the following commands to download the preinstall script and make the script executable:

wget https://storage.googleapis.com/packages.viam.com/apps/viam-agent/preinstall.sh
chmod 755 preinstall.sh
2

Create a file called viam-defaults.json with the following configuration:

{
  "network_configuration": {
    "manufacturer": "Pollution Monitoring Made Simple",
    "model": "v1",
    "fragment_id": "<FRAGMENT-ID>",
    "hotspot_prefix": "air-quality",
    "hotspot_password": "WeLoveCleanAir123"
  }
}

Replace "<FRAGMENT-ID>" with the fragment ID from your fragment.

3

In Organize your fleet you created several locations. Navigate to one of the locations and create a machine. Select the part status dropdown to the right of your machine’s name on the top of the page.

Click the copy icon next to Machine cloud credentials. Paste the machine cloud credentials into a file on your harddrive called FILE>viam.json.

3

Run the preinstall script

Run the preinstall script without options and it will attempt to auto-detect a mounted root filesystem (or for Raspberry Pi, bootfs) and also automatically determine the architecture.

sudo ./preinstall.sh

Follow the instructions and provide the viam-defaults.json file and the machine cloud credentials file when prompted.

That’s it! Your device is now provisioned and ready for your end user!

Having trouble? See Provisioning for more information and troubleshooting.

Next steps

You can now set up one or more air quality sensors for yourself or others and access them with your dashboard. If you are selling your air quality sensing machines, they can also use your dashboard to view their data.

If you’re wondering what to do next, why not set up a text or email alert when your air quality passes a certain threshold? For instructions on setting up an email alert, see the Monitor Helmet Usage tutorial as an example. For an example of setting up text alerts, see the Detect a Person and Send a Photo tutorial.