Arnov Sharma
Published © MIT

Room Air Quality Monitor

An average Room AQI Monitor with a Classic Twist

BeginnerFull instructions provided1 hour235

Things used in this project

Hardware components

PCBWay Custom PCB
PCBWay Custom PCB
×1
Raspberry Pi Pico W
Raspberry Pi Pico W
×1

Software apps and online services

Arduino IDE
Arduino IDE
Fusion
Autodesk Fusion

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

cad file

Schematics

SCH

Code

code

C/C++
#include <Adafruit_Protomatter.h>
#include <Adafruit_SGP40.h>
#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
// Wi-Fi credentials
const char* ssid = "SSID";
const char* password = "PASSWORD";
// Web server
WebServer server(80);
// Display pin definitions
#define R1 2
#define G1 3
#define B1 4
#define R2 5
#define G2 8
#define B2 9
#define A 10
#define B 16
#define C 18
#define D 20
#define CLK 11
#define LAT 12
#define OE 13
#define PANEL_WIDTH 64
#define PANEL_HEIGHT 32
#define NUM_PANELS 2
#define WIDTH (PANEL_WIDTH * NUM_PANELS)
#define HEIGHT PANEL_HEIGHT
uint8_t rgbPins[] = { R1, G1, B1, R2, G2, B2 };
uint8_t addrPins[] = { A, B, C, D };
Adafruit_Protomatter matrix(WIDTH, HEIGHT, 1, rgbPins, 4, addrPins, CLK, LAT, OE, false);
Adafruit_SGP40 sgp;
#define SDA_PIN 26
#define SCL_PIN 27
bool grid[WIDTH][HEIGHT];
bool newGrid[WIDTH][HEIGHT];
uint16_t vocIndex = 0;
void setup() {
Serial.begin(115200);
// I2C setup
Wire1.setSDA(SDA_PIN);
Wire1.setSCL(SCL_PIN);
Wire1.begin();
// Matrix setup
matrix.begin();
// SGP40 setup
if (!sgp.begin(&Wire1)) {
matrix.setTextColor(matrix.color565(255, 0, 0));
matrix.setCursor(10, HEIGHT / 2 - 4);
matrix.print("SGP40 Error");
matrix.show();
while (1);
}
// Wi-Fi setup
WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected. IP: " + WiFi.localIP().toString());
// Web server routes
server.on("/", handleRoot);
server.on("/voc", handleVOC);
server.begin();
// Initialize grid
randomSeed(analogRead(0));
for (int x = 0; x < WIDTH; x++) {
for (int y = 0; y < HEIGHT; y++) {
grid[x][y] = random(2);
}
}
}
void loop() {
vocIndex = sgp.measureVocIndex();
// Determine cell color based on air quality
uint16_t cellColor;
if (vocIndex <= 100) {
cellColor = matrix.color565(0, 255, 0); // Green
} else if (vocIndex <= 200) {
cellColor = matrix.color565(255, 255, 0); // Yellow
} else {
cellColor = matrix.color565(255, 0, 0); // Red
}
matrix.fillScreen(0); // Clear screen
// Update grid based on Game of Life rules
for (int x = 0; x < WIDTH; x++) {
for (int y = 0; y < HEIGHT; y++) {
int aliveNeighbors = countAliveNeighbors(x, y);
if (grid[x][y]) {
newGrid[x][y] = (aliveNeighbors == 2 || aliveNeighbors == 3);
} else {
newGrid[x][y] = (aliveNeighbors == 3);
}
if (newGrid[x][y]) {
matrix.drawPixel(x, y, cellColor);
}
}
}
memcpy(grid, newGrid, sizeof(grid));
matrix.show();
delay(100);
if (isGridEmpty()) {
resetGrid();
}
server.handleClient();
}
int countAliveNeighbors(int x, int y) {
int count = 0;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) continue;
int nx = (x + dx + WIDTH) % WIDTH;
int ny = (y + dy + HEIGHT) % HEIGHT;
if (grid[nx][ny]) count++;
}
}
return count;
}
bool isGridEmpty() {
for (int x = 0; x < WIDTH; x++) {
for (int y = 0; y < HEIGHT; y++) {
if (grid[x][y]) return false;
}
}
return true;
}
void resetGrid() {
for (int x = 0; x < WIDTH; x++) {
for (int y = 0; y < HEIGHT; y++) {
grid[x][y] = random(2);
}
}
}
void handleRoot() {
String html = "<!DOCTYPE html><html><head><meta http-equiv='refresh' content='2'>";
html += "<title>Air Quality Dashboard</title></head><body>";
html += "<h1>Room Air Quality</h1>";
html += "<p><strong>VOC Index:</strong> " + String(vocIndex) + "</p>";
html += "<p><strong>Status:</strong> " + interpretVOC(vocIndex) + "</p>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleVOC() {
server.send(200, "text/plain", String(vocIndex));
}
String interpretVOC(uint16_t voc) {
if (voc <= 100) return "Excellent";
if (voc <= 200) return "Good";
if (voc <= 400) return "Moderate";
if (voc <= 600) return "Poor";
return "Unhealthy";
}

Credits

Arnov Sharma
352 projects • 360 followers
I'm Arnov. I build, design, and experiment with tech—3D printing, PCB design, and retro consoles are my jam.

Comments