Final Project
BeeAir © 2025 by Kerstin Ogrissek is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
I worked on defining my final project idea and started getting used to the documentation process. It all begins in Week 1, where you can find my initial idea, some sketches, and links.
Curious to see my first sketches and research process?
Check them out here
Here’s an example of my concept:
A CO₂ Infopoint seamlessly integrated into the wall for Companys and open plan offices.
Time Line
2D and 3D Modeling
04.02.2025
The first prototype consists of an elegant wooden casing with a diffuse acrylic glass panel. This panel features a bee icon with a targeted opening for the CO₂ sensor, ensuring more accurate measurements.
Inside, a precisely fitting PLA frame securely holds the electronics and circuit boards. It is designed to slide into place without additional fasteners, staying in position due to its exact fit.
A dedicated groove accommodates the LED strip, while a subtle elevation in the center optimally positions the CO₂ sensor.
The LED strip emits a soft, diffused glow across the acrylic surface, preventing glare while creating a pleasant and evenly distributed light.
The acrylic panel itself is securely attached with magnets and includes two small foot supports to ensure a perfectly aligned installation.
At the bottom of the wooden frame, polygonal openings allow for clear sound output from the speaker. The rear side features carefully placed cutouts, making it easy to mount the enclosure flush against a wall.
Focus in March: PCB Design
In March, I primarily focused on designing the PCB for my final project. I reused the design of my polygon to define the cutout shape of the board, ensuring that it fits perfectly into my product.
First PCB Issue: Incorrect Voltage
With my first PCB version, I discovered that the voltage was incorrect. The CO₂ sensor requires a 5V supply, but I had mistakenly connected it to a 3.3V line. This oversight led to functionality issues, which I plan to fix in the updated design over the next few weeks.
Custom PCB for LCD Screen
For my LCD screen, I designed a custom PCB using the same polygon shape as the main board to maintain visual consistency. I used horizontal (lying) headers to connect the screen with flexible cables, allowing for more adaptability when integrating it into the housing.
Inlay Testing in April
In April, I tested the inlay for my final project and placed the PCB inside it for the first time. This allowed me to check the fit and alignment of the board within the housing and make any necessary adjustments early on.
For each component, I plan to design a dedicated PCB that fits precisely into the polygon-shaped slots of the 3D-printed parts. This approach ensures a clean and well-integrated assembly within the final product.
Materials
Electronic Link | Picture | Price | quantity |
---|---|---|---|
1. CO2 Sensor MQ-135 | ![]() |
Price: 2,80€ | 1 |
2. PCB-Pixel-LED Strip | ![]() |
Price: 8,29€ | 1x2m |
3. Joy-it com-lcd 16x2 Display-Modul 6.6 cm (2.6 Zoll) 16 x 4 Pixel | ![]() |
Price: 4,99€ | 1 |
4. Xiao ESP32-C6 | ![]() |
Price: 8,45 € | 2 |
5. SMD 1206, 470R resistor | ![]() |
Price: 0,9€ | 1 |
My first Material List was from Week1
Material List Updates (February – April)
Between February and April, I made several updates to my material list. Firstly, I replaced the originally planned CO₂ sensor—which was quite expensive—with a more affordable alternative. While this new sensor requires a 24-hour warm-up period, it consumes significantly less power afterward. I’m currently evaluating whether this option performs better overall. Additionally, I swapped out the Bluetooth transmitter for a Xiao ESP32-C6, which offers broader connectivity options and suits my project’s needs more effectively.
To improve clarity, I built my first prototype shell and created an electronic overview:
May was for finishing my final project
Final Project Summary: CO₂ Measurement System
Object 1: CO₂ Sensor Polygon (Main Unit)
Electronics & PCBs:
For the finalization of my project in May, I first improved the PCBs. I milled and soldered them again.
The LCD display PCB from Week 10 was still perfect and could be reused without changes. However, the CO₂ sensor board urgently needed a resistor for the LED strip, and I had to switch the power supply from 3.3 V to 5 V because 3.3 V was too weak.
After correcting that, I used a reflow oven to solder the smaller components, including Seeed modules.
Milled Parts & Case:
The housing for the CO₂ sensor and LED strip was optimized for CNC milling. I had to insert T-bones, for which I used a plugin I found in Fusion. Click herer to get Dogbone Plugin for Fusion
I also moved the LED strip to the inside of the product, embedding it more securely and avoiding unnecessary bending. This allowed me to avoid soldering on corners and gave the LED strip better protection.
For this, I added cable channels directly into the milling file.
After milling, I cut the side bevels on a table saw at a 30° angle. Note: Always do a test cut first and check whether everything fits properly!
And big thanks to Jonas – without my instructor, I probably would’ve forgotten to put on my safety glasses. A good instructor watches out for you, even from across the room.
Next, I used a router to smooth the edges, sanded everything (grit 180), applied fire-retardant varnish, sanded again, applied a second coat, and did a final sanding with 240 grit.
I then glued the parts together using wood glue and pressed them into place.
Lid & Laser-Cut Parts:
The lid was laser-cut. Since the glued parts had slightly changed dimensions, I had to adjust the laser files. I ended up creating two layers: one to hide the magnets and another to hold the acrylic plate in place.
The lid consists of two 3 mm thick plates – one from poplar plywood and one from translucent acrylic. I added vent holes to the center of the acrylic plate for the CO₂ sensor, and a few additional holes for visual dynamics. The two plates were then glued together.
To insert the magnets, I glued them into the wooden frame of both the housing and the lid using super glue. Make sure the poles match and place painter’s tape between them during gluing so they don’t snap together.
I used magnets 6 mm in diameter and 3 mm thick, and drilled 6 mm holes, which fit perfectly due to a slight kerf. (This might vary depending on your drill bit.)
Inlay & Integration:
For the inlay of the housing, I designed 3D-printed objects, which I had to adjust slightly so they would fit into the milled polygons. Some cable management adjustments were also needed.
The cables run underneath the polygons and are protected. I intentionally kept the PCB visible – I wanted it to serve as the “heart” of the product, clearly recognizable as a FabLab project.
Placing the CO₂ sensor in the center required precise measurement with calipers, but I was able to securely screw it in place.
At the end, all components for the CO₂ Sensor Polygon were assembled.
Object 2: LCD Display Unit (Small Polygon Cluster)
For the LCD display, I designed a housing in Fusion that included cable routing and a slightly raised frame to protect the screen. The design references the larger polygon, and I created several small polygon shapes for visual consistency.
Internally, everything was screwed in place. The back cover is form-fitted and also screwed on.
The resulting seam indicates where the object can be opened again, maintaining both functionality and aesthetics.
Both objects are currently powered via USB-C. While this is not an ideal long-term solution, it works well for the lab context.
Programming Code
For the programming part, I had to rethink my original approach from Week 16 Integration System. Initially, I used a simple if/else structure to define LED color output based on previously measured CO₂ levels.
However, I noticed that CO₂ values can vary significantly depending on humidity, time of day, and other environmental factors, so regular calibration was essential.
Now, the device calibrates itself on startup by measuring once per second and averaging the results over a full minute.
For this calibration, the sensor must be exposed to fresh air – either by opening all windows or by bringing the product outside (which I find less ideal).
While calibrating, the LEDs breathe in a soft white light pattern, which I found fitting.
After one minute, the device displays the air quality using a classic traffic light scheme: Green = Good, Yellow = Okay, Red = Bad.
Data is transmitted wirelessly via ESP-NOW from the CO₂ sensor to the LCD display. The LCD code detects whether the sensor is still calibrating or displaying live data. Depending on the reading, it shows messages like “Good Air,” “Okay Air,” or “Bad Air,” along with a smiley icon for visual feedback.
To avoid bugs and inform users about communication issues, I added error detection: If the LCD unit doesn’t receive a signal, it displays a warning. Similarly, if the sensor fails to send data 10 times in a row, the LED strip displays a loading circle animation.
Code for the sender Board:
#include <WiFi.h>
#include <esp_now.h>
#include <Adafruit_NeoPixel.h>
// --- NeoPixel definition ---
#define LED_PIN 1 // D1 = GPIO1
#define NUM_LEDS 40
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
// --- Sensor definition ---
#define MQ135_PIN A0
// --- ESP-NOW peer MAC (receiver) ---
uint8_t peerMAC[] = { 0xE4, 0xB0, 0x63, 0x41, 0xD1, 0x18 };
// --- Status codes ---
// 0 = Calibrating, 1 = good, 2 = okay, 3 = bad
uint8_t state = 0;
uint32_t baseAvg = 0; // Baseline value from calibration
uint8_t failCount = 0;
uint16_t spinnerPos = 0;
// Callback: Send status -> failCount
void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
if (status == ESP_NOW_SEND_SUCCESS) {
failCount = 0;
} else {
failCount++;
}
}
// "Breathing" effect in white (total duration in ms)
void breatheWhite(uint16_t durationMs) {
const int steps = 50;
const int halfMs = durationMs / 2;
const int delayMs = halfMs / steps;
// fade up
for (int i = 0; i <= steps; i++) {
strip.setBrightness(map(i, 0, steps, 0, 255));
strip.fill(strip.Color(255,255,255), 0, NUM_LEDS);
strip.show();
delay(delayMs);
}
// fade down
for (int i = steps; i >= 0; i--) {
strip.setBrightness(map(i, 0, steps, 0, 255));
strip.fill(strip.Color(255,255,255), 0, NUM_LEDS);
strip.show();
delay(delayMs);
}
strip.setBrightness(255);
}
// Spinner animation in case of connection problems
void showSpinner() {
strip.clear();
for (uint8_t i = 0; i < 3; i++) {
strip.setPixelColor((spinnerPos + i) % NUM_LEDS,
strip.Color(255,255,255));
}
strip.show();
spinnerPos = (spinnerPos + 1) % NUM_LEDS;
delay(100);
}
void setup() {
Serial.begin(115200);
// Initialize NeoPixel + white illumination
strip.begin();
strip.fill(strip.Color(255,255,255), 0, NUM_LEDS);
strip.setBrightness(255);
strip.show();
// Init WiFi & ESP-NOW
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
// Endless loop with white flash
while (true) {
strip.fill(strip.Color(255,255,255), 0, NUM_LEDS);
strip.show();
delay(100);
strip.clear();
strip.show();
delay(100);
}
}
esp_now_register_send_cb(onDataSent);
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, peerMAC, 6);
peer.channel = 0; peer.encrypt = false;
esp_now_add_peer(&peer);
Serial.println("Starting calibration (60s)...");
// Calibration: 60 readings in 1s intervals
uint32_t sum = 0;
for (int i = 0; i < 60; i++) {
int raw = analogRead(MQ135_PIN);
sum += raw;
// Send calibration status
state = 0;
esp_now_send(peerMAC, &state, 1);
// White "breathing"
breatheWhite(1000);
}
baseAvg = sum / 60;
Serial.printf("Calibration complete, baseline = %u\n", baseAvg);
// Turn off LEDs after calibration
strip.clear();
strip.show();
delay(500);
}
void loop() {
// 1) Read sensor
int raw = analogRead(MQ135_PIN);
// 2) Classification relative to baseline
if (raw <= baseAvg) state = 1; // good
else if (raw <= baseAvg * 1.2f) state = 2; // okay
else state = 3; // bad
// 3) Send status
esp_now_send(peerMAC, &state, 1);
// 4) Animation / color legend
if (failCount >= 10) {
showSpinner();
} else {
strip.clear();
switch (state) {
case 1: // good → Green every second LED
for (int i = 0; i < NUM_LEDS; i += 2)
strip.setPixelColor(i, strip.Color(0,150,0));
break;
case 2: // okay → Yellow
for (int i = 0; i < NUM_LEDS; i += 2)
strip.setPixelColor(i, strip.Color(150,150,0));
break;
case 3: // bad → Red
for (int i = 0; i < NUM_LEDS; i += 2)
strip.setPixelColor(i, strip.Color(150,0,0));
break;
}
strip.show();
delay(1000);
}
}
Code for the reciever:
#include <WiFi.h>
#include <esp_now.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// --- I²C LCD (0x27, 16×2) ---
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Timestamp of the last reception
unsigned long lastReceive = 0;
// Toggle for calibration animation
bool calToggle = false;
void onDataRecv(const esp_now_recv_info_t* info, const uint8_t* data, int len) {
// We expect data[0] = 0..3
uint8_t state = data[0];
lastReceive = millis();
lcd.clear();
switch(state) {
case 0: { // Calibrating
// Toggle eyes
const char* eyes = calToggle ? "O O" : "- -";
calToggle = !calToggle;
lcd.setCursor(0, 0);
lcd.print("Air:");
// Right-align eyes
lcd.setCursor(16 - strlen(eyes), 0);
lcd.print(eyes);
// Text "Calibrating" left, "..." right
lcd.setCursor(0, 1);
lcd.print("Calibrating");
lcd.setCursor(14, 1);
lcd.print("w");
break;
}
case 1: { // Good
// Eyes = "O O"
lcd.setCursor(0, 0);
lcd.print("Air:");
lcd.setCursor(13, 0);
lcd.print("O O");
// Rating
lcd.setCursor(0, 1);
lcd.print("Good");
// Mouth = "v"
lcd.setCursor(14, 1);
lcd.print("v");
break;
}
case 2: { // Okay
// Eyes = "- -"
lcd.setCursor(0, 0);
lcd.print("Air:");
lcd.setCursor(13, 0);
lcd.print("- -");
// Rating
lcd.setCursor(0, 1);
lcd.print("Okay");
// Mouth = "o"
lcd.setCursor(14, 1);
lcd.print("o");
break;
}
case 3: { // Bad
// Eyes = "X X"
lcd.setCursor(0, 0);
lcd.print("Air:");
lcd.setCursor(13, 0);
lcd.print("X X");
// Rating
lcd.setCursor(0, 1);
lcd.print("Bad");
// Mouth = "-"
lcd.setCursor(14, 1);
lcd.print("-");
break;
}
default: {
// Eyes = "? ?"
lcd.setCursor(0, 0);
lcd.print("Air:");
lcd.setCursor(13, 0);
lcd.print("? ?");
// Rating
lcd.setCursor(0, 1);
lcd.print("---");
// Mouth = "_"
lcd.setCursor(14, 1);
lcd.print("_");
}
}
Serial.printf("Received state=%d\n", state);
}
void setup() {
Serial.begin(115200);
// Initialize LCD
Wire.begin();
lcd.init();
lcd.backlight();
// Greeting
lcd.setCursor(0, 0);
lcd.print("Hello! :)");
lcd.setCursor(0, 1);
lcd.print("Waiting...");
// WiFi & ESP-NOW
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Init ERROR");
while (true) delay(100);
}
esp_now_register_recv_cb(onDataRecv);
Serial.println("ESP-NOW ready");
}
void loop() {
// After 60s without reception → placeholder face
if (lastReceive != 0 && millis() - lastReceive > 60000) {
lcd.clear();
// Eyes = "? ?"
lcd.setCursor(0, 0);
lcd.print("Air:");
lcd.setCursor(13, 0);
lcd.print("? ?");
// Rating
lcd.setCursor(0, 1);
lcd.print("---");
// Mouth = "_"
lcd.setCursor(14, 1);
lcd.print("_");
lastReceive = millis();
}
delay(100);
}
And now, I have completed my Fab Academy journey — I hope you enjoyed following along on my website. It has been a true pleasure to be part of the Fab Academy, where I had the opportunity to explore so many machines, tools, and workflows, and to grow both technically and creatively.
A heartfelt thank you to my instructor for the continuous support and guidance, and to the Fab Foundation for organizing this incredible program.
Material price at the end
No. | Item | Unit Price | Quantity | Subtotal |
---|---|---|---|---|
1 | CO₂ Sensor MQ-135 | €2.80 | 1 | €2.80 |
2 | PCB Pixel LED Strip (2 m) | €8.29 | 1 | €8.29 |
3 | Joy-it LCD 16x2 Display | €4.99 | 1 | €4.99 |
4 | Xiao ESP32-C6 | €8.45 | 2 | €16.90 |
5 | SMD 1206, 470 Ω Resistor | €0.90 | 1 | €0.90 |
6 | Acrylic sheet, translucent, 3 mm, A4 size | €4.00 | 1 | €4.00 |
7 | Poplar plywood, 3 mm, A4 size (for laser cutting) | €2.50 | 1 | €2.50 |
8 | Multiplex plywood, 12 mm, 800×300 mm | €16.00 | 1 | €16.00 |
9 | Redline RPLA filament, 400 g | €9.50 | 1 | €9.50 |
---------------- | ||||
Total | €65.98 |
License of My Product
For the finalization of my project, it was especially important to me to develop a sustainable and circular product with a modular design. The goal was to create a system that is ideal for use in FabLab workshops and can be shared as an open-source project. The product is released under an open-source license and still holds potential for further development and improvement — true to the principle: developing together and moving forward
Video
files
- Object: CO₂ Sensor Polygon (Main Unit) CO2 sensor PCB cutout Gerber Co2 sensor PCB Gerber
Co2 Inlay stl-file Polygon Inlay stl-file cable polygon Inlay stl-file
co2 sesor cover wood frame lasercut file co2 sensor Cover acrylic lasercut file co2 inlay fusion file co2 sensor Case fusion file
- Object: LCD Display Unit (Small Polygon Cluster)
LCD PCB Outline Gerber LCD PCB Outline Gerber
LCD Case stl-file LCD Cover stl-file LCD Case and Cover Fusion file
BeeAir © 2025 by Kerstin Ogrissek is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International