Resume-Website/blog/AirQualityMonitor/index.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>