673 lines
22 KiB
HTML
673 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Building an Air Quality Monitor - Tech Blog</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--deep-navy: #0A1128;
|
|
--rich-gold: #D4AF37;
|
|
--warm-gold: #F4E5C2;
|
|
--cream: #FFF8E7;
|
|
--charcoal: #2C2C2C;
|
|
--silver: #C0C0C0;
|
|
--burgundy: #7C2D37;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Didot', 'Bodoni MT', 'Playfair Display', Georgia, serif;
|
|
line-height: 1.8;
|
|
color: var(--charcoal);
|
|
background: linear-gradient(180deg, var(--deep-navy) 0%, #1a2847 100%);
|
|
position: relative;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Art Deco geometric background pattern */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-image:
|
|
repeating-linear-gradient(45deg, transparent, transparent 35px, rgba(212, 175, 55, 0.03) 35px, rgba(212, 175, 55, 0.03) 70px),
|
|
repeating-linear-gradient(-45deg, transparent, transparent 35px, rgba(212, 175, 55, 0.03) 35px, rgba(212, 175, 55, 0.03) 70px);
|
|
z-index: -1;
|
|
}
|
|
|
|
/* Navigation */
|
|
nav {
|
|
background: rgba(10, 17, 40, 0.95);
|
|
border-bottom: 2px solid var(--rich-gold);
|
|
padding: 1.0rem 0;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.nav-content {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
padding: 0 2rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.nav-logo {
|
|
font-size: 1.5rem;
|
|
font-weight: 300;
|
|
color: var(--warm-gold);
|
|
text-decoration: none;
|
|
letter-spacing: 0.2em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.nav-back {
|
|
color: var(--silver);
|
|
text-decoration: none;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
font-size: 0.9rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
transition: color 0.3s;
|
|
}
|
|
|
|
.nav-back:hover {
|
|
color: var(--rich-gold);
|
|
}
|
|
|
|
.nav-back::before {
|
|
content: '←';
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
/* Article Container */
|
|
.article-container {
|
|
max-width: 1000px;
|
|
margin: 4rem auto;
|
|
padding: 0 2rem;
|
|
}
|
|
|
|
/* Article Header */
|
|
.article-header {
|
|
background: var(--cream);
|
|
padding: 4rem 4rem 3rem;
|
|
border: 2px solid var(--rich-gold);
|
|
position: relative;
|
|
margin-bottom: 3rem;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
/* Corner decorations */
|
|
.article-header::before,
|
|
.article-header::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 60px;
|
|
height: 60px;
|
|
border: 2px solid var(--rich-gold);
|
|
}
|
|
|
|
.article-header::before {
|
|
top: 0;
|
|
left: 0;
|
|
border-right: none;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.article-header::after {
|
|
bottom: 0;
|
|
right: 0;
|
|
border-left: none;
|
|
border-top: none;
|
|
}
|
|
|
|
.article-number {
|
|
font-size: 4rem;
|
|
font-weight: 300;
|
|
color: var(--rich-gold);
|
|
line-height: 1;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.article-meta {
|
|
display: flex;
|
|
gap: 2rem;
|
|
margin-bottom: 2rem;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
font-size: 0.85rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.15em;
|
|
}
|
|
|
|
.article-category {
|
|
color: var(--burgundy);
|
|
font-weight: 700;
|
|
position: relative;
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
.article-category::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 50%;
|
|
transform: translateY(-50%) rotate(45deg);
|
|
width: 6px;
|
|
height: 6px;
|
|
background: var(--rich-gold);
|
|
}
|
|
|
|
.article-title {
|
|
font-size: 3.5rem;
|
|
font-weight: 400;
|
|
color: var(--deep-navy);
|
|
line-height: 1.2;
|
|
letter-spacing: 0.02em;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.article-subtitle {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
font-size: 1.25rem;
|
|
line-height: 1.6;
|
|
color: var(--charcoal);
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* Tech Stack */
|
|
.tech-stack {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
margin-top: 2rem;
|
|
padding-top: 2rem;
|
|
border-top: 2px solid var(--rich-gold);
|
|
}
|
|
|
|
.tech-item {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
color: var(--deep-navy);
|
|
padding: 0.75rem 1.5rem;
|
|
background: linear-gradient(135deg, rgba(212, 175, 55, 0.15), rgba(212, 175, 55, 0.05));
|
|
border-left: 3px solid var(--rich-gold);
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* Featured Image */
|
|
.featured-image {
|
|
width: 100%;
|
|
height: 500px;
|
|
background: linear-gradient(135deg, var(--deep-navy) 0%, var(--burgundy) 100%);
|
|
border: 2px solid var(--rich-gold);
|
|
margin-bottom: 3rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
/* Sunburst pattern on image */
|
|
.featured-image::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: repeating-conic-gradient(
|
|
from 0deg,
|
|
rgba(212, 175, 55, 0.1) 0deg 10deg,
|
|
transparent 10deg 20deg
|
|
);
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
.image-placeholder {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
color: var(--warm-gold);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
z-index: 1;
|
|
}
|
|
|
|
.image-placeholder-icon {
|
|
font-size: 4rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.image-placeholder-text {
|
|
font-size: 1.2rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Article Content */
|
|
article {
|
|
background: var(--cream);
|
|
padding: 4rem;
|
|
border: 2px solid var(--rich-gold);
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
font-size: 1.1rem;
|
|
line-height: 1.8;
|
|
color: var(--charcoal);
|
|
}
|
|
|
|
article p {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
article h2 {
|
|
font-family: 'Didot', 'Bodoni MT', 'Playfair Display', Georgia, serif;
|
|
font-size: 2.5rem;
|
|
font-weight: 400;
|
|
color: var(--deep-navy);
|
|
margin: 3rem 0 1.5rem;
|
|
position: relative;
|
|
padding-bottom: 1rem;
|
|
}
|
|
|
|
article h2::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100px;
|
|
height: 2px;
|
|
background: linear-gradient(to right, var(--rich-gold), transparent);
|
|
}
|
|
|
|
article h3 {
|
|
font-family: 'Didot', 'Bodoni MT', 'Playfair Display', Georgia, serif;
|
|
font-size: 1.8rem;
|
|
font-weight: 400;
|
|
color: var(--deep-navy);
|
|
margin: 2.5rem 0 1rem;
|
|
}
|
|
|
|
article ul, article ol {
|
|
margin: 1.5rem 0;
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
article li {
|
|
margin-bottom: 1rem;
|
|
padding-left: 0.5rem;
|
|
}
|
|
|
|
article ul li::marker {
|
|
color: var(--rich-gold);
|
|
}
|
|
|
|
article ol li::marker {
|
|
color: var(--rich-gold);
|
|
font-weight: 700;
|
|
}
|
|
|
|
article a {
|
|
color: var(--burgundy);
|
|
text-decoration: none;
|
|
border-bottom: 1px solid var(--burgundy);
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
article a:hover {
|
|
color: var(--rich-gold);
|
|
border-bottom-color: var(--rich-gold);
|
|
}
|
|
|
|
/* Decorative dividers */
|
|
.deco-divider {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 3rem 0;
|
|
position: relative;
|
|
}
|
|
|
|
.deco-divider::before,
|
|
.deco-divider::after {
|
|
content: '';
|
|
flex: 1;
|
|
height: 1px;
|
|
background: linear-gradient(to right, transparent, var(--rich-gold), transparent);
|
|
}
|
|
|
|
.diamond {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: var(--rich-gold);
|
|
transform: rotate(45deg);
|
|
margin: 0 2rem;
|
|
}
|
|
|
|
/* Blockquote */
|
|
blockquote {
|
|
margin: 2.5rem 0;
|
|
padding: 2rem 3rem;
|
|
background: linear-gradient(135deg, rgba(212, 175, 55, 0.08), rgba(212, 175, 55, 0.03));
|
|
border-left: 4px solid var(--rich-gold);
|
|
font-style: italic;
|
|
font-size: 1.2rem;
|
|
color: var(--deep-navy);
|
|
position: relative;
|
|
}
|
|
|
|
blockquote::before {
|
|
content: '"';
|
|
position: absolute;
|
|
top: 1rem;
|
|
left: 1rem;
|
|
font-size: 4rem;
|
|
color: var(--rich-gold);
|
|
opacity: 0.3;
|
|
font-family: Georgia, serif;
|
|
line-height: 1;
|
|
}
|
|
|
|
/* Code blocks */
|
|
pre {
|
|
background: var(--deep-navy);
|
|
border: 1px solid var(--rich-gold);
|
|
border-radius: 4px;
|
|
padding: 2rem;
|
|
margin: 2rem 0;
|
|
overflow-x: auto;
|
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
code {
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 0.9rem;
|
|
line-height: 1.6;
|
|
color: var(--warm-gold);
|
|
}
|
|
|
|
/* Footer */
|
|
footer {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: var(--warm-gold);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
font-size: 0.9rem;
|
|
letter-spacing: 0.1em;
|
|
position: relative;
|
|
}
|
|
|
|
.footer-ornament {
|
|
width: 100px;
|
|
height: 2px;
|
|
background: linear-gradient(to right, transparent, var(--rich-gold), transparent);
|
|
margin: 0 auto 2rem;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.article-header {
|
|
padding: 2rem 1.5rem;
|
|
}
|
|
|
|
.article-title {
|
|
font-size: 2.5rem;
|
|
}
|
|
|
|
.article-subtitle {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
article {
|
|
padding: 2rem 1.5rem;
|
|
}
|
|
|
|
article h2 {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
article h3 {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.featured-image {
|
|
height: 300px;
|
|
}
|
|
|
|
.nav-content {
|
|
padding: 0 1rem;
|
|
}
|
|
|
|
.article-container {
|
|
padding: 0 1rem;
|
|
margin: 2rem auto;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Navigation -->
|
|
<nav>
|
|
<div class="nav-content">
|
|
<a href="#" class="nav-logo">Projects</a>
|
|
<a href="#" class="nav-back">Back to Home</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Article Container -->
|
|
<div class="article-container">
|
|
<!-- Article Header -->
|
|
<header class="article-header">
|
|
<div class="article-number">01</div>
|
|
<div class="article-meta">
|
|
<span class="article-category">IoT & Hardware</span>
|
|
<span class="article-date">2025</span>
|
|
</div>
|
|
<h1 class="article-title">Building an Air Quality Monitor</h1>
|
|
<p class="article-subtitle">
|
|
Measuring home air quality for health and better understanding of my environment.
|
|
</p>
|
|
|
|
<!-- Tech Stack -->
|
|
<div class="tech-stack">
|
|
<span class="tech-item">ESP32</span>
|
|
<span class="tech-item">BME280</span>
|
|
<span class="tech-item">SPS30</span>
|
|
<span class="tech-item">SCD40</span>
|
|
<span class="tech-item">InfluxDB</span>
|
|
<span class="tech-item">I2C</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Featured Image -->
|
|
<div class="featured-image">
|
|
<div class="image-placeholder">
|
|
<div class="image-placeholder-icon">⚗</div>
|
|
<div class="image-placeholder-text">Air Quality Sensor Array</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Article Content -->
|
|
<article>
|
|
<h2>The Inspiration</h2>
|
|
<p>
|
|
Many years ago, I watched a <a href="https://youtu.be/1Nh_vxpycEA" target="_blank">Tom Scott video</a> about how air quality can affect cognitive performance.
|
|
From that point on, I felt very conscientious(paranoid) about the quality of air in my home.
|
|
To boot, I don't have centralized air, so the idea that my home might
|
|
have more carbon dioxide than necessary is a concern.
|
|
</p>
|
|
|
|
<p>
|
|
Rather than let this rational concept vex me for the foreseeable future, I figured I would put my
|
|
skills to the test and actually measure my indoor air quality.
|
|
</p>
|
|
|
|
<div class="deco-divider">
|
|
<div class="diamond"></div>
|
|
</div>
|
|
|
|
<h2>The Solution</h2>
|
|
<p>
|
|
My solution: multiple small devices placed around my home, taking readings from the air
|
|
and aggregating data into a dashboard to show me what the problems are and in what areas. Easy
|
|
enough, surely.
|
|
</p>
|
|
|
|
<p>
|
|
I bought an ESP32 dev board (DOIT v1) which would serve as a microcontroller that
|
|
could connect to the internet and relay data back to a server. I also purchased an SCD40 from
|
|
Digikey, an I2C module for detecting CO2. Since I would be taking periodic samples of the air,
|
|
the slowness of I2C was not a problem. In fact, it would allow me to add modules as needed during
|
|
the prototyping phase.
|
|
</p>
|
|
|
|
<h3>Initial Implementation</h3>
|
|
<p>
|
|
Thankfully, there were already premade libraries by Adafruit for the SCD40. It made programming
|
|
a cinch. I could take periodic readings and send the data to a time-series database (InfluxDB)
|
|
using nothing but POST requests. No heavy lifting from the ESP32—I could do all my querying on
|
|
my homelab.
|
|
</p>
|
|
<img src="CO2_levels.png">
|
|
<p>
|
|
The initial CO2 readings were not great, but not particularly concerning or unusual for the time
|
|
of year. There was also an issue of calibration, but I'll talk about that later.
|
|
</p>
|
|
|
|
<div class="deco-divider">
|
|
<div class="diamond"></div>
|
|
</div>
|
|
|
|
<h2>Expanding the Sensor Array</h2>
|
|
<p>
|
|
I wasn't quite satisfied yet. The National Weather Service measures things like Carbon Dioxide
|
|
(CO2), Ozone (O3), and many Volatile Organic Compounds (VOCs) such as smoke or exhaust from cars.
|
|
In the summer, Canada has had some wildfires which produce a lot of VOCs. It would be good to
|
|
know how much is leaking into my home. Also, any fumes or exhaust that my oven produces can have
|
|
an effect on air quality. I have a small animal, which are much more sensitive and vulnerable to these.
|
|
</p>
|
|
|
|
<p>
|
|
For this, I purchased an SPS30 from Digikey, as well as a BME280 to measure things like temperature,
|
|
humidity, and air pressure. Thanks to the I2C design, it was as simple as plug and play (and code).
|
|
A real boon for prototyping.
|
|
</p>
|
|
|
|
<h3>Testing and Validation</h3>
|
|
<img src="open_window.png" style="width:100%; height:100%; object-fit: cover;">
|
|
<p>
|
|
I tested the system by opening my window. There was a clear drop in temperature, CO2 levels, and
|
|
humidity levels. This is actually recommended by the manufacturers to help calibrate the sensors.
|
|
</p>
|
|
|
|
<div class="deco-divider">
|
|
<div class="diamond"></div>
|
|
</div>
|
|
|
|
<h2>The Calibration Challenge</h2>
|
|
<p>
|
|
This is the tricky part. These components work well and were programmed easily. Knowing that
|
|
they're accurate is the hard part, and that's the real product that actual air quality sensor
|
|
companies create. This is why I didn't show the PM (particulate matter) graphs from the SPS30—I
|
|
have no clue how accurate they are.
|
|
</p>
|
|
|
|
<p>
|
|
At the very least, I can verify the humidity and temperature readings and approximate the CO2
|
|
from the weather report. The SPS30 also takes about a week to fully calibrate internally.
|
|
</p>
|
|
|
|
<blockquote>
|
|
That's really the drawback of building your own air quality monitor. It's much cheaper and you
|
|
can configure things how you like with no worry about malicious things like data stealing, but
|
|
you have no idea how accurate it is.
|
|
</blockquote>
|
|
|
|
<p>
|
|
When you buy a pre-configured air quality monitor, what you're really buying is peace of mind.
|
|
You can be assured that the monitors are calibrated properly and to within specifications (assuming
|
|
you buy from a reputable company). Mine seems accurate, but without proper tooling, I have little
|
|
way of knowing.
|
|
</p>
|
|
|
|
<div class="deco-divider">
|
|
<div class="diamond"></div>
|
|
</div>
|
|
|
|
<h2>Next Steps: PCB Design</h2>
|
|
<p>
|
|
Since this is a prototype, I can't keep it forever. It's wasting my precious breakout boards.
|
|
Also, it's a liability when you have a pet who loves stringy things. I took it upon myself to
|
|
whip up a circuit design schematic. From here, I could just send the design to some PCB manufacturing
|
|
website and they would send me the board. All I would have to do is solder on the components.
|
|
</p>
|
|
<img src="pcb-schematic.png">
|
|
<p>
|
|
But before I pull the trigger on that, I want to take some time to both calibrate the parts and
|
|
decide if I think they provide enough of a picture of the quality of my air. Maybe I might want
|
|
some spectroscopy sensor to detect certain gases and their quantity.
|
|
</p>
|
|
|
|
<h3>Outdoor Monitoring Concept</h3>
|
|
<p>
|
|
I also had the idea that I could get a pretty good indication of their calibration (and how badly
|
|
insulated my home is) if I made a monitor for outside. There are more challenges involved with this.
|
|
I need to design some sort of casing that can keep the elements off of the board while also being
|
|
able to let it breathe and actually read the air.
|
|
</p>
|
|
|
|
<p>
|
|
I learned an old solder quick-fix once: you could take clear acrylic nail polish and coat traces
|
|
on boards to make them non-conductive on those spots. Dipping a whole board in nail polish might
|
|
not be the right move either, but maybe some Scotchgard might help. I'll have to do some research
|
|
and experimenting before I pull the trigger on that either.
|
|
</p>
|
|
|
|
<div class="deco-divider">
|
|
<div class="diamond"></div>
|
|
</div>
|
|
|
|
<h2>Future Enhancements</h2>
|
|
<p>
|
|
The last thing I would want to work on would be OTA (Over The Air) updates. It can be kind of a
|
|
pain to connect it and flash the firmware every time. If I could configure OTA, that would solve
|
|
that hassle.
|
|
</p>
|
|
|
|
<p>
|
|
If I were to fully dive into this multiple monitor configuration, I might have to consider creating
|
|
a server for them to "phone home" to. I plug in a domain name and they all register in the same
|
|
little UI. This would make updating and version tracking seamless.
|
|
</p>
|
|
|
|
<p>
|
|
This project demonstrates that building DIY environmental monitoring systems is accessible to
|
|
makers and engineers, but it also highlights the real value proposition of commercial products:
|
|
calibration and validation. The journey continues, with PCB design, weatherproofing, and OTA
|
|
updates on the horizon.
|
|
</p>
|
|
</article>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer>
|
|
<div class="footer-ornament"></div>
|
|
<p>Tech Chronicles 2025</p>
|
|
</footer>
|
|
</body>
|
|
</html>
|