Real-Time CO₂ Monitoring App with Go and BleuIO
January 17, 2025Awareness of Air quality monitoring importance for health and productivity has been increasing lately, especially in indoor environments like offices and homes. In this tutorial, we’ll demonstrate how to create a real-time CO₂ monitoring application using Go, a modern programming language with a vibrant community, alongside the BleuIO BLE USB dongle and HibouAir, a BLE-enabled air quality sensor.
This project showcases how to use Go’s simplicity and performance to build an efficient application that scans for CO₂ data, decodes it, and provides real-time notifications on macOS when the CO₂ level exceeds a critical threshold. By using BleuIO’s integrated AT commands, you can focus on your application logic without worrying about complex embedded BLE programming.
Project Overview
The goal of this project is to:
- Use BleuIO to scan for BLE advertisements from HibouAir, which broadcasts real-time CO₂ levels.
- Decode the advertised data to extract CO₂ concentration.
- Send a real-time macOS notification when CO₂ levels exceed a specified threshold (1000 ppm in this example).
Notifications are implemented using the macOS osascript
utility, ensuring you are immediately alerted about high CO₂ levels on your laptop screen.
Why This Project Is Useful
When you’re focused on work, you might not notice subtle changes in your environment. This application ensures you’re notified directly on your laptop screen when CO₂ levels become unsafe. This is especially helpful for:
- Office Workers: Monitor meeting rooms or shared spaces where ventilation may be insufficient.
- Remote Workers: Ensure a healthy workspace at home without distractions.
- Educational Settings: Keep classrooms or labs safe for students and staff.
Technical Details
Tools and Devices
- Programming Language: Go – Chosen for its simplicity, performance, and active community.
- BLE USB Dongle: BleuIO – Simplifies BLE communication with built-in AT commands.
- CO₂ Monitoring Device: HibouAir – Provides real-time air quality metrics over BLE.
How It Works
- Initialize the Dongle:
- Set the BleuIO dongle to the central role to enable scanning for BLE devices.
- Scan for Advertised Data:
- Use the
AT+FINDSCANDATA
command to scan for HibouAir’s advertisements containing air quality data.
- Use the
- Decode CO₂ Information:
- Extract and convert the relevant part of the advertisement to get the CO₂ level in ppm.
- Send Notifications:
- Use Go’s
exec.Command
to invoke macOSosascript
and display a desktop notification if the CO₂ level exceeds the threshold.
- Use Go’s
Implementation
Here is the source code for the project:
package main
import (
"bufio"
"fmt"
"log"
"os/exec"
"strconv"
"strings"
"time"
"go.bug.st/serial"
)
func main() {
// Open the serial port
mode := &serial.Mode{
BaudRate: 9600,
}
port, err := serial.Open("/dev/cu.usbmodem4048FDE52CF21", mode)
if err != nil {
log.Fatalf("Failed to open port: %v", err)
}
defer port.Close()
// Initial setup: Set the dongle to central mode
err = setupDongle(port)
if err != nil {
log.Fatalf("Failed to set up dongle: %v", err)
}
// Repeatedly scan for advertised data and process it
for {
err := scanAndProcessData(port)
if err != nil {
log.Printf("Error during scan and process: %v", err)
}
time.Sleep(10 * time.Second) // Wait before the next scan (interval)
}
}
// setupDongle sets the dongle to central mode
func setupDongle(port serial.Port) error {
_, err := port.Write([]byte("AT+CENTRAL\r"))
if err != nil {
return fmt.Errorf("failed to write AT+CENTRAL: %w", err)
}
time.Sleep(1 * time.Second) // Ensure the command is processed
buf := make([]byte, 100)
_, err = port.Read(buf)
if err != nil {
return fmt.Errorf("failed to read response from AT+CENTRAL: %w", err)
}
fmt.Println("Dongle set to central mode.")
return nil
}
// scanAndProcessData scans for advertised data and processes it
func scanAndProcessData(port serial.Port) error {
_, err := port.Write([]byte("AT+FINDSCANDATA=220069=2\r"))
if err != nil {
return fmt.Errorf("failed to write AT+FINDSCANDATA: %w", err)
}
time.Sleep(3 * time.Second) // Wait for scan to complete
buf := make([]byte, 1000)
n, err := port.Read(buf)
if err != nil {
return fmt.Errorf("failed to read scan response: %w", err)
}
response := string(buf[:n])
// Extract the first advertised data
firstAdvertisedData := extractFirstAdvertisedData(response)
if firstAdvertisedData == "" {
fmt.Println("No advertised data found.")
return nil
}
// Extract the specific part (6th from last to 3rd from last) and convert to decimal
if len(firstAdvertisedData) >= 6 {
extractedHex := firstAdvertisedData[len(firstAdvertisedData)-6 : len(firstAdvertisedData)-2]
decimalValue, err := strconv.ParseInt(extractedHex, 16, 64)
if err != nil {
return fmt.Errorf("failed to convert hex to decimal: %w", err)
}
fmt.Printf("CO₂ Value: %d ppm\n", decimalValue)
// Send notification if CO₂ value exceeds 1000
if decimalValue > 1000 {
sendNotification("CO₂ Alert", fmt.Sprintf("High CO₂ level detected: %d ppm", decimalValue))
}
} else {
fmt.Println("Advertised data is too short to extract the desired part.")
}
return nil
}
// extractFirstAdvertisedData extracts the first advertised data from the response
func extractFirstAdvertisedData(response string) string {
scanner := bufio.NewScanner(strings.NewReader(response))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "Device Data [ADV]:") {
parts := strings.Split(line, ": ")
if len(parts) > 1 {
return parts[1]
}
}
}
if err := scanner.Err(); err != nil {
log.Printf("Error scanning response: %v", err)
}
return ""
}
// sendNotification sends a macOS notification with the specified title and message
func sendNotification(title, message string) {
script := `display notification "` + message + `" with title "` + title + `"`
cmd := exec.Command("osascript", "-e", script)
err := cmd.Run()
if err != nil {
log.Printf("Error sending notification: %v", err)
}
}
Source code
Source code is available on https://github.com/smart-sensor-devices-ab/monitor-realtime-co2-go
Output
This project demonstrates how to build a real-time CO₂ monitoring application using Go, BleuIO, and HibouAir. By using Go’s capabilities and BleuIO’s ease of use, you can focus on the logic of your application and quickly adapt the solution to your specific needs.