Real-Time BLE Air Quality Monitoring with BleuIO and Adafruit IO

This project shows how to turn a BleuIO USB dongle into a tiny gateway that streams live air-quality data from a HibouAir sensor straight to Adafruit IO. The script listens for Bluetooth Low Energy (BLE) advertising packets, decodes CO2, temperature, and humidity, and posts fresh readings to your Adafruit IO feeds every few seconds. The result is a clean, shareable dashboard that updates in real time—perfect for demos, labs, offices, classrooms, and proofs of concept.

What is Adafruit IO—and why pair it with BleuIO?

Adafruit IO is a cloud platform for makers and developers that lets you collect, visualize, and route device data using a simple REST API or MQTT. You don’t need any Adafruit hardware; if you can make an HTTPS request, you can send data. BleuIO fits in beautifully here: the dongle handles the BLE side—scanning and parsing sensor frames—while a short Python script formats those values and pushes them to Adafruit IO. In practice that means you can take any BLE-advertising sensor, translate its packets into numbers, and land them on an IoT-friendly dashboard without servers or containers.

Requirements

To complete this project, you will need:

  • BleuIO BLE USB Dongle – acts as the BLE central device to capture advertising packets.
  • HibouAir Air quality monitor – broadcasts environmental data such as CO2, temperature, and humidity.
  • Python libraries – install them with: pip install pyserial requests
  • Adafruit IO account – free to sign up at io.adafruit.com.
  • Adafruit IO Key – available under your account’s “My Key” page for authentication.

How it works

When you start the script, it opens the BleuIO serial port and switches the dongle into central role the very first time the program runs. From then on it repeatedly performs a short BLE scan that filters for HibouAir advertising frames. The scanner always picks the latest matching packet and decodes the fields we care about: CO2 (ppm), temperature (°C), and humidity (%rH). The script then posts each value to its own Adafruit IO feed over HTTPS. Because Adafruit IO is designed for live IoT data, your dashboard widgets update as soon as new points arrive. The loop cadence is configurable (10 seconds by default), which keeps you comfortably under Adafruit IO’s free-tier request limits.

The code (key points)

The script is intentionally small and readable. It opens the serial device (for example /dev/cu.usbmodemXXXX on macOS or COM7 on Windows), sends the BleuIO commands to scan for a few seconds, and parses the returned “Device Data [ADV]” lines.

A compact decoder extracts CO2, temperature, and humidity from the HibouAir manufacturer data, including the byte order and scaling.

To make the setup painless, credentials are read from variables (AIO_USER, AIO_KEY) and feed names default to co2, temperature, and humidity. Each value is sent to the REST endpoint /api/v2/{username}/feeds/{feed_key}/data with a simple JSON body {"value": <number>}.

The script includes gentle sanity checks (for example, temperature range and humidity bounds) to ignore any malformed frames, and it prints a concise log line each time it pushes fresh data.

Here is the GitHub link with the full source so you can clone and run it as-is or adapt it to other sensors.

How to run the code

Before running, set your serial port and Adafruit IO credentials.

On macOS you can list ports with ls /dev/cu.usbmodem*;

on Windows use Device Manager to find the COM number. Update username and AIO key information, then run the script.
The program will put BleuIO into central mode on first launch and, every cycle, will scan, decode, and push CO2, temperature, and humidity to the three feeds.
If you see an HTTP 401 error, double-check the AIO key; a 404 usually means a feed name typo. If the script can’t open the serial port, confirm the path and that no other program is holding it open.

Creating Adafruit IO feeds, key, and dashboard

Log in to Adafruit IO and create three feeds named co2, temperature, and humidity. Your AIO Key is available under your account’s “My Key” page; copy it and keep it private. With feeds in place, open the Dashboards section and create a new dashboard for this project (for example, “HibouAir Live”). Add a few blocks: a gauge or line chart for CO₂ (with a range that makes sense for your space), another gauge or slide for temperature, and a slide or line chart for humidity so you can see the trend over time. Each block points to its corresponding feed. As the script posts to those feeds, the blocks will animate and refresh automatically. You can reorder blocks, tweak colors and ranges, and share a read-only link if you want others to watch along.

Output

Once everything is connected, the dashboard shows a live CO2 number in gauge an line chart, an updating temperature value, and a humidity box that advances with each new reading. The values move in near real time as the script cycles, and any spikes or changes in air quality appear immediately.

Use cases

Real-time air-quality dashboards are useful in far more places than a lab bench. Facility manager can watch CO2 levels across meeting rooms to optimize ventilation; schools and libraries can surface temperature and humidity alongside occupancy schedules; small manufacturers can keep an eye on comfort and safety in production spaces; and hobbyists can monitor their home offices or studios. Because the pipeline is “BLE sensor → BleuIO → HTTPS → Adafruit IO,” you can swap HibouAir for other BLE advertisers and reuse the same approach to visualize anything from soil moisture to ambient light.

This project highlights how quickly you can go from BLE broadcast to live cloud dashboard with BleuIO and Adafruit IO. There’s no server to maintain, no container to deploy—just a tiny USB dongle, an air quality monitoring device like HibouAir, a short Python script, and a few clicks on the Adafruit IO site. The result is a shareable, real-time view of your environment that’s easy to extend, brand, and automate.

Share this post on :

Streaming BLE Sensor Data into Microsoft Power BI using BleuIO

In this project, we demonstrate how to stream Bluetooth Low Energy (BLE) sensor data directly into Microsoft Power BI using the BleuIO USB dongle. By combining a HibouAir environmental sensor with BleuIO and a simple Python script, we can capture live readings of CO2, temperature, and humidity and display them in real time on a Power BI dashboard.

The goal of this project is to make BLE data visualization simple and accessible. Instead of dealing with complex server setups or containers, BleuIO provides an easy way to turn raw BLE advertising packets into meaningful insights that anyone can understand at a glance.

Why Power BI?

Microsoft Power BI is a business analytics platform designed to turn raw data into interactive dashboards and reports. One of its most powerful features is the ability to handle real-time streaming datasets, allowing live updates from sensors or IoT devices.

For IoT developers and organizations, this is a game-changer. Imagine watching air quality readings from your office appear in real time, or combining BLE sensor data with other business metrics to get a fuller picture of your environment. By using BleuIO as a BLE-to-cloud bridge, developers can integrate IoT data into Power BI dashboards quickly, without heavy infrastructure.

Requirements

To follow this tutorial, you will need:

  • A BleuIO USB dongle.
  • A HibouAir air quality monitor (for CO2, temperature, and humidity).
  • A free or paid Microsoft Power BI account.
  • The Python libraries pyserial and requests, which can be installed with: pip install pyserial requests

Setup: Power BI Streaming Dataset

Before writing any code, we need to set up a streaming dataset in Power BI.

  1. Log in to Power BI Service and go to My workspace.
  2. Select New → Streaming dataset → API.
  3. Define the fields you’ll collect from the sensor:
    • CO2 (Number)
    • temperature (Number)
    • humidity (Number)
    • timestamp (DateTime or Text)
  4. Toggle Historic data analysis ON if you want Power BI to store rows for reporting.
  5. Save the dataset and copy the Push URL that Power BI generates. This will look something like: https://api.powerbi.com/beta/.../datasets/{id}/rows?key=...

This Push URL is what the Python script will use to send live sensor data to your dashboard.

The Script

We wrote a Python script that takes care of the entire process. Once it starts, the script connects to the BleuIO dongle through the serial port and switches it into central mode (this is done only the first time it runs). From then on, it performs a BLE scan every 10 seconds, specifically looking for HibouAir sensor advertising data. When the sensor is found, the script decodes the broadcast payload into CO2, temperature, and humidity values. These values are then packaged into the required JSON format and pushed directly to Power BI, where they appear instantly on your dashboard.

Before running the script, make sure to update two important details:

  • Dongle port location: On macOS it will look like /dev/cu.usbmodemXXXX, while on Windows it will appear as COMX.
  • Power BI Push URL: Use the one you generated earlier during the dataset setup.

We’ve published the full script on GitHub here:
GitHub Link for Script

To run it:

python script.py

Setup Dashboard

With the script running and sending live data, the next step is to build your Power BI dashboard.

  1. Go to My workspace in Power BI and click New → Dashboard.
  2. Give the dashboard a descriptive name, for example HibouAir Live Data.
  3. Select + Add a tile → Custom streaming data, then choose the dataset you created earlier.
  4. Pick a visualization type that suits your needs:
    • A Card to display the current CO₂ value.
    • A Gauge to track temperature within a target range.
    • A Line chart to watch humidity changes over time.
  5. Map the fields (CO2, temperature, humidity, timestamp) to each visual and pin them to your dashboard.

Within seconds of running the script, you’ll see live sensor readings begin to appear in your Power BI dashboard — updating automatically with every scan.

Output

Here’s what the final result looks like when the dashboard starts receiving data from the HibouAir sensor.

Use Cases

This project shows just one way to use BLE and Power BI together, but the possibilities are broad. For example, you could build air quality monitoring dashboards in offices, schools, or factories to help maintain healthier environments. In agriculture, farmers could create smart dashboards that combine soil and environmental sensors to optimize crop conditions. The same method can be applied to cold chain logistics, where monitoring temperature and humidity is essential for transporting food or medicine. Fitness and health enthusiasts could stream real-time data from BLE wearables into personal dashboards, making progress more visible and motivating. And for developers, Power BI is an excellent tool for rapid IoT prototyping, offering instant visualization of new sensor data without building a complex backend system.

With BleuIO and Microsoft Power BI, it’s easy to transform BLE sensor broadcasts into live dashboards. This integration makes it possible to visualize environmental data in real time, share insights instantly, and build prototypes faster than ever before. Whether you’re a developer, researcher, or business professional, combining BLE sensors with Power BI opens the door to smarter, data-driven decisions.

Share this post on :

BleuIO Firmware Update v2.7.9.6 – Secure Your AUTOEXEC List with Password Protection

We are happy to announce the release of BleuIO firmware v2.7.9.6.
This update brings an important new feature designed to make your device more secure: password protection for the AUTOEXEC list.

With this functionality, you can lock your AUTOEXEC list using a password. Once locked, no one can read, modify, or delete the commands inside unless they enter the correct password. The password will remain active even after power cycles, ensuring your setup stays protected. Please note that updating the firmware to a different version will clear the password.

What’s New in v2.7.9.6

Added Functionality

  • Ability to lock the AUTOEXEC list with a password.
  • Password protection persists through power cycles.
  • Updating firmware to another version will clear the password.

New Commands

We’ve introduced three new AT commands to manage this feature:

  1. AT+SETAUTOEXECPWD
    • Create or change a password for the AUTOEXEC list.Requires the old password if you want to change it.Maximum length: 255 ASCII characters.
    Example: AT+SETAUTOEXECPWD=my_password
  2. AT+ENTERAUTOEXECPWD
    • Used to enter the password when prompted.Grants access to locked commands.
  3. AT+CLRAUTOEXECPWD
    • Clears the existing password (requires entering the password first).Returns the AUTOEXEC list to an unprotected state.

How to Use the New Feature

The recommended way to use this functionality is:

  1. Add commands to your AUTOEXEC list as usual.
  2. Lock the list by setting a password with AT+SETAUTOEXECPWD.
  3. If you need to make changes later:
    • Use AT+CLRAUTOEXECPWD to remove the password.
    • Modify your AUTOEXEC list.
    • Lock it again using AT+SETAUTOEXECPWD.

When a password is set, the following commands will require authentication first:

  • AT+CLRAUTOEXECPWD
  • AT+SETAUTOEXECPWD
  • AT+AUTOEXEC
  • AT+AUTOEXECLOOP
  • AT+CLRAUTOEXEC

Why This Matters

The AUTOEXEC list is a powerful tool that allows you to automate commands when your BleuIO dongle starts up. By adding password protection, you ensure that only authorized users can view or change these startup configurations. This is especially important in production environments where security and stability are critical.

Update Today

We encourage all users to update to v2.7.9.6 to take advantage of this new security feature.
For full release notes and instructions, visit our documentation page.

Share this post on :

Ambient-Adaptive CO2 Bar with BleuIO & HibouAir

This tutorial walks you through a tiny, privacy-first web app that reads only the advertised CO2 level from a nearby HibouAir sensor using a BleuIO USB BLE dongle. There’s no pairing, cloud, or backend—just your browser, the dongle, and a page that decodes a numeric CO2 value broadcast in BLE advertisements and renders it as a color bar (default window 400–2000 ppm) with a simple “High CO2” warning when your threshold is crossed.

This project is a follow-up to the Ambient-Adaptive Noise Bar with BleuIO & HibouAir. We reused the same structure and UI, but swap the decoder to read CO2 instead of noise.

What you’ll build

A single HTML file that talks to BleuIO over the Web Serial API. The page puts BleuIO in the central role, periodically runs a targeted scan for your HibouAir Board ID, and parses the Manufacturer Specific Data (MSD) bytes in each advertisement to extract CO2 (ppm). The value drives a horizontal gradient bar; cross the threshold and a warning banner appears. Everything runs locally in the browser.

Why a CO2-only, browser-based monitor?

CO2 is a practical proxy for ventilation. Elevated levels are associated with stale air, drowsiness, and reduced productivity. Many spaces—meeting rooms, classrooms, offices, homes—benefit from quick visual feedback so people know when to air out the room. Reading only a single, device-computed number from BLE advertisements keeps the design simple, fast, and privacy-preserving.

Hardware & software

How it works (at a glance)

BLE devices periodically broadcast short advertisement packets with real-time CO2 values. We can read them without pairing.

This page filters to a specific Board ID, captures the advertisement line, extracts the longest hex payload, and then decodes CO2 from a fixed position inside the MSD. The result is mapped to a 0–100% fill of the bar (for a display window of 400–2000 ppm), and we show a banner when CO2 ≥ threshold (default 1000 ppm).

Below is the exact function used in this project:

function decodeCo2FromAdv(hex) {
  // sanitize → bytes
  hex = (hex || '').replace(/[^0-9A-F]/gi, '');
  if (hex.length % 2) hex = hex.slice(0, -1);
  const b = new Uint8Array(hex.length / 2);
  for (let i = 0; i < b.length; i++) b[i] = parseInt(hex.substr(i*2,2), 16);

  // locate MSD anchor and read CO2 at fixed offset (big-endian)
  for (let i = 0; i <= b.length - 5; i++) {
    if (b[i] === 0x5B && b[i+1] === 0x07 && b[i+2] === 0x05) {
      const idx = i + 23;                // CO2 MSB position in this layout
      if (idx + 1 < b.length) {
        return (b[idx] << 8) | b[idx+1]; // ppm
      }
    }
  }
  return null;
}

The BLE flow

When you click Connect, the page opens a serial session to BleuIO and sends:

  • AT+CENTRAL once, to enter scanning mode
  • AT+FINDSCANDATA=<BOARD_ID>=3 every cycle to run a 3-second targeted scan
  • The reader consumes lines until BleuIO prints SCAN COMPLETE, then waits and repeats

Each time an advertisement arrives, the page extracts the hex payload, decodes CO2, updates the bar, and toggles the High CO2 banner if the threshold is exceeded.

Output

You’ll see a horizontal color bar labeled with the current CO2 ppm. The bar fills from left to right as values rise within the 400–2000 ppm window. A bold High CO2 banner appears when the reading crosses your threshold (default 1000 ppm), serving as a polite nudge to improve ventilation.

Use cases

This simple CO2 bar works well anywhere people gather and air can get stale. In meeting rooms and classrooms it provides a live cue to crack a window or switch on ventilation as occupancy rises. In open offices it nudges teams toward timely air exchanges, helping reduce stuffiness and afternoon dips in alertness. At home it’s a lightweight way to keep bedrooms and living spaces fresh during gatherings or winter months with closed windows. Shared studios and makerspaces also benefit from quick, ambient feedback without the overhead of dashboards or wall displays.

Because the app reads only a single numeric value that HibouAir already broadcasts, it avoids handling personal data and is easy to deploy in privacy-sensitive environments.

Accuracy & practical notes

This is a lightweight indicator, not a calibration tool. CO2 readings in advertisements update periodically and represent the sensor’s current value. Placement matters: keep your HibouAir within a reasonable range of BleuIO to reduce missed packets. If your environment regularly exceeds the default window, you can adjust the display range and threshold in the code.

Extend the project

You can grow this prototype in several practical directions. Start by logging readings to CSV or IndexedDB for simple trend analysis over days or weeks. If you have multiple sensors, add a multi-device view that scans several Board IDs and presents compact tiles in one page. For automation, trigger a webhook or send a serial command to control a fan or relay whenever CO2 exceeds your threshold. You can also pair it with the earlier Noise Bar and show Noise + CO2 side-by-side for a fuller picture of comfort and productivity.

Share this post on :

BleuIO Firmware Release v2.7.85 – Bug Fixes & Improved Stability

We are excited to announce the release of BleuIO Firmware v2.7.85. This update brings important fixes and improvements to ensure a smoother and more reliable experience when working with your BleuIO dongle.

What’s New in v2.7.85

This release focuses on bug fixes and performance improvements:

  • AT+CANCELCONNECT fix – Resolved an issue where the command would incorrectly return an error.
  • Improved JSON compatibility – Event message codes in verbose mode now include quotes, ensuring proper JSON format for easier parsing.
  • Better disconnection handling – Fixed a bug where AT+GAPDISCONNECTALL would sometimes fail to disconnect all connected devices.
  • Multi-device connection fix – Corrected an error where AT+GAPCONNECT returned “Connection limit exceeded” incorrectly when attempting to connect to the 8th device.

By upgrading to the latest firmware, you ensure your BleuIO dongle works at its best with improved stability and compatibility. If you are using BleuIO for projects that involve multiple device connections, JSON parsing, or handling connections/disconnections dynamically, this update is especially important.

Get the Latest Firmware

We recommend all users download and install the v2.7.85 firmware to take advantage of these improvements.
Download the latest firmware here

Share this post on :

Build a BLE Web App in Flutter Using BleuIO

This guide shows how to create a web-only Flutter application that talks to a BleuIO USB BLE dongle directly from the browser. The app is a minimal console: click Connect, send AT commands like AT+CENTRAL or AT+GAPSCAN=3, and watch responses appear in real time. Treat it as a starting point for richer BLE web apps—dashboards, scanners, or device tools—without native installs.

Requirements

What this post is about

The focus is Flutter for the web. Instead of a mobile build, you’ll run Flutter in the browser and use the Web Serial API to communicate with BleuIO. The included script acts as an example of how to open the serial port, send AT commands, and stream lines back into a Flutter UI. You can use the same pattern to build any BLE-adjacent web app: scan for devices, filter output, parse manufacturer data, or add visualizations—completely in Flutter Web.

How it works

Flutter Web can call browser APIs through dart:html and dart:js_util. The app asks Chrome/Edge to show the serial-port picker, opens the selected BleuIO port at 115200, and writes commands terminated by \r\n. A small pre-newline and micro delays are used so commands don’t concatenate (avoiding errors like AT+CENTRALA). A read loop collects bytes, splits them into lines, and renders them into a console-style panel. Everything runs locally in the browser; nothing is recorded or sent to a server.

Guide

Create a web-only Flutter app

From Terminal, create a project that targets only the web so you don’t see iOS/Android code-sign prompts:

flutter create --platforms=web bleuio-flutter
cd bleuio-flutter

You’ll run exclusively against Chrome (or Edge). No mobile setup is needed.

Select Chrome as the build target

Use Chrome so the Web Serial API is available:

flutter run -d chrome

If you use an IDE, choose Chrome (web) in the device selector. Safari is not supported for Web Serial.

Add the example script

Open lib/main.dart and replace its contents with the example from the repository. That file defines a tiny Web-Serial service (connect, writeLine, continuous read loop) and a simple UI with a status, quick command buttons, a custom command input, and a full-width terminal output.

Try live commands

Click Connect BleuIO and choose your dongle. Send ATI to verify, then AT+CENTRAL to enter central role, and AT+GAPSCAN=3 to perform a three-second scan. The responses stream into the on-page console immediately. Because it’s just AT over serial, you can experiment with any command that BleuIO supports.

Understanding the example

The script is intentionally small so you can lift it into other projects. The service wraps Web Serial and exposes a line stream you can subscribe to from widgets. The UI is a single page that prints lines to a terminal-style view and keeps the scroll pinned to the bottom.

Extending this into a BLE web app

Once you’re comfortable with the console, you can add features that turn it into a BLE tool. Start by parsing common outputs such as GAP scan lines into structured objects with fields like address, RSSI, and name. Add filters and search to highlight target devices.

Use cases

This web-only approach is ideal for demos, workshops, and quick bring-up labs where you want a zero-install experience. It’s handy for field diagnostics when you need to peek at advertisements, confirm firmware state, or prove connectivity from any machine with Chrome. It also serves as a foundation for privacy-respecting dashboards that only read broadcast data and never require native packaging.

Source code & demo

Repository:
https://github.com/smart-sensor-devices-ab/bleuio-flutter

Live demo (open in Chrome/Edge, connect your dongle, and try AT commands):
https://smart-sensor-devices-ab.github.io/bleuio-flutter/

Output, hosted and real time

The live page streams output as soon as the device is connected. You can keep the tab open as a lightweight serial console while developing other features.

Flutter Web is a great fit for BLE-adjacent tooling when you want the reach of a URL and the feel of a native UI. Start with the example console today, then grow it into the BLE web application your project needs.

Share this post on :

Ambient-Adaptive Noise Bar with BleuIO & HibouAir

This tutorial shows how to build a small, browser-based monitor that reads only the advertised noise level from a nearby HibouAir sensor using a BleuIO USB BLE dongle. There is no pairing, no audio recording, and no microphone access. The page simply listens for Bluetooth Low Energy (BLE) advertisements, decodes a numeric noise value emitted by the sensor, and renders it as a color bar between 40–80 dBSPL. When the value exceeds a threshold you choose, a plain “shhhh” banner appears as a gentle cue to keep things quiet.

Why a Noise-Only, Privacy-Centric Monitor?

The goal is awareness, not surveillance. Many environments benefit from real-time feedback about loudness—libraries, classrooms, shared offices, and homes—yet microphones introduce privacy concerns and data-handling obligations. This project avoids all of that by reading a single numeric value that the HibouAir computes internally and broadcasts publicly. Because BLE advertisements are brief, connectionless, and contain no personally identifiable information or audio, the solution is both practical and privacy-preserving. It gives occupants a clear indication of ambient noise without storing, transmitting, or analyzing conversations.

What You’ll Build

You’ll create a single HTML file that talks to the BleuIO dongle through the Web Serial API. The page switches BleuIO to a central role, periodically runs a targeted scan for your HibouAir device ID, and parses the manufacturer-specific bytes in each advertisement to extract the noise reading. The value is then mapped to a 40–80 dB display range and presented as a horizontal color bar. If the measured level crosses your threshold, the banner appears. Everything runs locally in your browser; there is no backend server and nothing leaves your machine.

Hardware

Software

  • Google Chrome or Microsoft Edge on desktop (Web Serial enabled)
  • Any text editor to save and edit index.html

How It Works

BLE devices periodically broadcast short advertisement packets. These packets can include a manufacturer-specific data (MSD) field where vendors store compact sensor values. Because advertisements are public and unidirectional, you can read them without pairing or maintaining a connection, which makes them ideal for low-overhead telemetry and privacy-first designs.

What We Read

HibouAir encodes a noise metric inside its MSD block. This project looks only for that metric. The page filters scan results to a specific board ID so you capture advertisements from your own sensor. Each time the dongle reports an advertisement line, the page extracts the longest hex payload, finds the MSD anchor, and reads two bytes that represent the noise value.

The BLE Flow

When you click Connect, the browser opens a serial session to BleuIO. The page sends AT+CENTRAL once to set the dongle into scanning mode. Every few seconds, it issues AT+FINDSCANDATA=<BOARD_ID>=3 to perform a three-second targeted scan and then reads the output until the dongle prints “SCAN COMPLETE.” This cadence repeats continuously so your display stays current without spamming the serial interface.

Source code

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>BleuIO + HibouAir — Simple Noise Bar</title>
    <style>
      :root {
        --val: 0;
      } /* percent of the 40–80 dB range */
      body {
        font-family: system-ui, Arial, sans-serif;
        margin: 24px;
      }
      button {
        padding: 10px 14px;
        margin-right: 8px;
        cursor: pointer;
      }
      .row {
        display: flex;
        align-items: center;
        gap: 12px;
        flex-wrap: wrap;
      }
      .bar-wrap {
        position: relative;
        width: 100%;
        max-width: 760px;
        height: 56px;
        border-radius: 12px;
        overflow: hidden;
        border: 1px solid #eee;
        margin-top: 12px;
        background: linear-gradient(
          90deg,
          #3aa0ff 0%,
          #41d17d 20%,
          #b5e04a 40%,
          #ffd54d 60%,
          #ff8a33 80%,
          #f44336 100%
        );
      }
      .bar-mask {
        position: absolute;
        inset: 0;
        background: #fff;
        transform-origin: left center;
        transform: translateX(
          calc(var(--val) * 1%)
        ); /* reveals gradient as value grows */
        mix-blend-mode: lighten;
      }
      .value {
        position: absolute;
        inset: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: 800;
        font-size: 22px;
        color: #111;
        text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
        pointer-events: none;
      }
      .shhh {
        display: none;
        margin-top: 10px;
        font-weight: 700;
        color: #f44336;
        font-size: 20px;
      }
      .shhh.show {
        display: block;
      }
      #log {
        height: 160px;
        overflow: auto;
        background: #0b0b0b;
        color: #9f9;
        padding: 8px;
        margin-top: 16px;
        font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
        border-radius: 8px;
      }
    </style>
  </head>
  <body>
    <h2>Ambient-Adaptive Noise Bar</h2>
    <div class="row">
      <button id="btnConnect">Connect BleuIO</button>
      <button id="btnStart" disabled>Start</button>
      <button id="btnStop" disabled>Stop</button>
      <label
        >Board ID: <input id="board" value="473DA5" style="width: 90px"
      /></label>
      <label
        >“Shhh” threshold (dBSPL):
        <input id="thresh" type="number" value="60" style="width: 70px" />
      </label>
    </div>

    <div class="bar-wrap" aria-label="Noise level">
      <div class="bar-mask" id="mask"></div>
      <div class="value" id="valLabel">dBSPL +40</div>
    </div>
    <div id="shhh" class="shhh">🤫 shhhh… too loud</div>

    <pre id="log"></pre>

    <script>
      let port,
        writer,
        running = false,
        timer = null;

      const logEl = document.getElementById('log');
      const btnConnect = document.getElementById('btnConnect');
      const btnStart = document.getElementById('btnStart');
      const btnStop = document.getElementById('btnStop');
      const valLabel = document.getElementById('valLabel');
      const shhhEl = document.getElementById('shhh');
      const threshInput = document.getElementById('thresh');
      const boardInput = document.getElementById('board');

      function log(s) {
        logEl.textContent += s + '\n';
        logEl.scrollTop = logEl.scrollHeight;
      }
      function sleep(ms) {
        return new Promise((r) => setTimeout(r, ms));
      }
      function clamp(x, min, max) {
        return Math.max(min, Math.min(max, x));
      }

      // Update UI from **dB** value: fill 40..80 => 0..100%, label as dBSPL +<db>
      function setUI(db, thresholdDb) {
        const dbClamped = clamp(db, 40, 80); // display window
        const pct = Math.round(((dbClamped - 40) / 40) * 100);
        document.documentElement.style.setProperty('--val', pct);
        valLabel.textContent = `dBSPL +${dbClamped}`;
        if (dbClamped >= thresholdDb) shhhEl.classList.add('show');
        else shhhEl.classList.remove('show');
      }

      // Parse hex string → byte array
      function hexToBytes(hex) {
        hex = (hex || '').replace(/[^0-9A-F]/gi, '');
        if (hex.length % 2) hex = hex.slice(0, -1);
        const arr = new Uint8Array(hex.length / 2);
        for (let i = 0; i < arr.length; i++)
          arr[i] = parseInt(hex.substr(i * 2, 2), 16);
        return arr;
      }

      // Robust decoder: find 5B 07 05 and read **noise bytes at offset +7 (big-endian)**.
      // dB = 120 - noiseAdv ; then clamp to 0..120 and show within 40..80 range.
      function decodeNoiseDbFromAdv(hex) {
        const bytes = hexToBytes(hex);
        // find sequence 5B 07 05
        for (let i = 0; i <= bytes.length - 3; i++) {
          if (
            bytes[i] === 0x5b &&
            bytes[i + 1] === 0x07 &&
            bytes[i + 2] === 0x05
          ) {
            const noiseIdx = i + 7; // your generator: i+7 = MSB, i+8 = LSB
            if (noiseIdx + 1 < bytes.length) {
              const noiseAdv = (bytes[noiseIdx] << 8) | bytes[noiseIdx + 1]; // big-endian
              const db = 120 - noiseAdv; // inverse of your generator
              return clamp(Math.round(db), 0, 120);
            }
          }
        }
        return null;
      }

      async function connectPort() {
        try {
          port = await navigator.serial.requestPort();
          await port.open({ baudRate: 115200 });
          writer = port.writable.getWriter();
          await send('AT'); // wake
          btnStart.disabled = false;
          log('Connected. BleuIO ready.');
        } catch (e) {
          alert('Serial open failed: ' + e);
        }
      }

      // robust sender: flush + tiny delay to avoid glued commands (e.g., "CENTRALA")
      async function send(cmd) {
        const enc = new TextEncoder();
        try {
          await writer.write(enc.encode('\r\n'));
          await sleep(10);
          await writer.write(
            enc.encode(cmd.endsWith('\r\n') ? cmd : cmd + '\r\n')
          );
          log('>> ' + cmd.trim());
          await sleep(30);
        } catch (e) {
          log('WRITE ERROR: ' + e);
        }
      }

      // Fresh reader per scan; stop on SCAN COMPLETE or timeout
      async function readFor(ms) {
        const reader = port.readable.getReader();
        const td = new TextDecoder();
        let buf = '',
          doneEarly = false;

        const killer = setTimeout(async () => {
          try {
            await reader.cancel();
          } catch {}
        }, ms);

        try {
          while (running) {
            const { value, done } = await reader.read();
            if (done) break;
            if (!value) continue;

            buf += td.decode(value);
            const lines = buf.split(/\r?\n/);
            buf = lines.pop();

            for (const line of lines) {
              if (!line.trim()) continue;
              log(line);

              if (line.includes('SCAN COMPLETE')) {
                doneEarly = true;
                break;
              }

              const matches = line.match(/[0-9A-F]{16,}/gi);
              const hex = matches
                ? matches.sort((a, b) => b.length - a.length)[0]
                : null;
              if (hex) {
                const db = decodeNoiseDbFromAdv(hex.toUpperCase());
                if (db !== null) setUI(db, Number(threshInput.value || 60));
              }
            }
            if (doneEarly) break;
          }
        } catch (e) {
          /* expected on cancel */
        } finally {
          clearTimeout(killer);
          reader.releaseLock();
        }
      }

      async function startLoop() {
        if (!port || !writer) return;

        running = true;
        btnStart.disabled = true;
        btnStop.disabled = false;

        await send('AT+CENTRAL'); // set once

        const scanSec = 3; // command duration
        const periodMs = 10000; // keep your current cadence (adjust if needed)

        const cycle = async () => {
          if (!running) return;

          const id = (boardInput.value || '473DA5').trim();
          const t0 = performance.now();

          try {
            await send(`AT+FINDSCANDATA=${id}=${scanSec}`);
            log('SCANNING...');
            await readFor(scanSec * 1000 + 500); // read until SCAN COMPLETE or timeout (~3.5s)
          } catch (e) {
            log('LOOP ERROR: ' + e);
          } finally {
            log('SCAN COMPLETE');
            if (running) {
              const elapsed = performance.now() - t0;
              const wait = Math.max(0, periodMs - elapsed);
              timer = setTimeout(cycle, wait);
            }
          }
        };

        cycle();
      }

      function stopLoop() {
        running = false;
        btnStart.disabled = false;
        btnStop.disabled = true;
        if (timer) clearTimeout(timer);
      }

      btnConnect.addEventListener('click', connectPort);
      btnStart.addEventListener('click', startLoop);
      btnStop.addEventListener('click', stopLoop);

      // init: show floor of the display window (40 dB)
      setUI(40, Number(threshInput.value || 60));
    </script>
  </body>
</html>

Running It

Save the page as index.html and open it with Chrome or Edge on your desktop. Click Connect BleuIO and select the dongle’s serial port. Verify the Board ID matches your HibouAir unit, then click Start. You should see the serial log print “SCANNING…” followed by advertisement lines and “SCAN COMPLETE.” As new packets arrive, the color bar moves within the 40–80 dB range and the banner appears when the measured level meets or exceeds your threshold. The app continues scanning at a steady cadence without manual intervention.

Output

Use Cases

In a library or reading room, the bar provides a quiet, visual nudge to maintain a calm atmosphere without recording anyone. Classrooms can use it to keep group work from spilling into disruption while reassuring students and parents that no audio is captured. Open offices gain a neutral reference during focus periods, with the banner serving as a gentle reminder rather than an alarm. At home, the display helps keep late-night activities considerate of sleeping children or neighbors. Across all of these contexts, the design sidesteps privacy concerns by never collecting voice or content—only a simple loudness number the sensor already computes.

Accuracy and Limitations

The 40–80 dB window is a display choice that covers common indoor scenarios; this is not a calibrated sound level meter. BLE advertisements arrive periodically, so the value updates in small steps rather than continuously. Placement matters: keep the HibouAir and BleuIO within reasonable proximity to reduce missed packets. If the environment is unusually quiet or loud, you can shift the threshold or adjust the visual window to suit your space.

Share this post on :

Execute Python Scripts via BLE Using BleuIO and Your Mobile Phone

This project demonstrates how to execute a Python script wirelessly from your mobile phone using a BleuIO dongle. By sending simple text commands over Bluetooth Low Energy (BLE) through the Serial Port Service (SPS), the Python script can trigger actions. In our case, the script will retrieve and display CO2, temperature, and humidity values from a nearby BLE air quality monitoring sensor called HibouAir.

This approach opens up many possibilities for interactive projects, sensor monitoring, and remote control applications — all without the need for internet connectivity.

Prerequisites

Hardware

Software

  • Python 3 installed on your computer
  • pyserial library (pip install pyserial)
  • nRF Connect (iOS / Android)

Setting up the BleuIO Dongle

Begin by plugging your BleuIO dongle into the computer. Open a BLE terminal such as the BleuIO Web Terminal or any serial monitor and enable the Serial Port Service. You can do this by entering:


ATASPS1
AT+ADVSTART

This enables the SPS service, ensures incoming data is displayed as ASCII text, and starts advertising so your phone can connect.

The Python Script

The Python script connects to the BleuIO dongle over a serial port and listens for incoming BLE messages. When it receives one of four possible commands — CO2, TEMP, HUM, or ALL — it triggers a scan using the AT+FINDSCANDATA=220069=3 command. Here 220069 is the boardID of HibouAir sensor which we will use to retrieve air quality data in real time. This scans for nearby BLE devices broadcasting the matching advertising data. From the latest packet received, the script decodes and extracts the relevant sensor values.

Below is the complete script. Update the port variable to match your BleuIO’s serial port location.

import serial
import time
import re

def send_and_wait(ser, at_cmd):
    ser.write((at_cmd + '\r\n').encode())
    time.sleep(2)
    output = []
    while ser.in_waiting:
        line = ser.readline().decode('utf-8', errors='ignore').strip()
        output.append(line)
    return output

def extract_last_adv_hex(lines):
    adv_lines = [l for l in lines if "Device Data [ADV]:" in l]
    if not adv_lines:
        return None
    last_line = adv_lines[-1]
    match = re.search(r'([0-9A-Fa-f]{40,})$', last_line)
    if match:
        return match.group(1)
    return None

def parse_co2_temp_hum(adv):
    pos = adv.find("5B0705")
    if pos == -1:
        return None

    def parse_val(offset, reverse=True, scale=1.0, signed=False):
        raw = adv[pos + offset : pos + offset + 4]
        if reverse:
            raw = ''.join(reversed([raw[i:i+2] for i in range(0, 4, 2)]))
        val = int(raw, 16)
        if signed and val > 32767:
            val -= 65536
        return val / scale

    temp_raw = parse_val(22, scale=1, signed=True)
    temp = temp_raw / 10
    hum = parse_val(26, scale=10)
    co2 = int(adv[pos + 46 : pos + 50], 16)

    return {
        "temp": round(temp, 1),
        "hum": round(hum, 1),
        "co2": co2
    }

# --- Main Loop ---
port = "/dev/cu.usbmodem4048FDEBA6D01"
baudrate = 115200

try:
    with serial.Serial(port, baudrate, timeout=1) as ser:
        print("Listening for BLE SPS command...")
        while True:
            line = ser.readline().decode('utf-8', errors='ignore').strip()
            if line:
                #print(f" Received: {line}")
                cmd = line.replace("[Received]:", "").strip().upper()
                if cmd in ["CO2", "TEMP", "HUM", "ALL"]:
                    print(" Scanning for environmental data...")
                    results = send_and_wait(ser, "AT+FINDSCANDATA=0504220069=3")
                    adv = extract_last_adv_hex(results)
                    if adv:
                        data = parse_co2_temp_hum(adv)
                        if data:
                            if cmd == "CO2":
                                print(f" CO2: {data['co2']} ppm")
                            elif cmd == "TEMP":
                                print(f" Temp: {data['temp']} °C")
                            elif cmd == "HUM":
                                print(f" Hum: {data['hum']} %")
                            elif cmd == "ALL":
                                print(f" Temp: {data['temp']} °C")
                                print(f" Hum: {data['hum']} %")
                                print(f" CO2: {data['co2']} ppm")
                        else:
                            print(" Could not decode sensor values.")
                    else:
                        print(" No advertising data found.")
except serial.SerialException as e:
    print("Serial error:", e)

How It Works

Once the script is running, it keeps the serial connection to the BleuIO open and listens for BLE SPS messages sent from the phone. When a command is received, it is cleaned to remove any extra text added by the dongle, then matched against one of the four recognized keywords. If a match is found, the script asks the dongle to scan for nearby advertising packets from a device matching our filter. The script extracts the CO2, temperature, and humidity values, then displays the requested data based on the command.

For example, sending CO2 from the phone will only display the CO2 value, while sending ALL will show all three measurements.

Testing the Project

Run the script on your computer, then use the nRF Connect app on your phone to connect to the BleuIO dongle. Locate the SPS write characteristic (UUID ending in ...5CBA) and send a command in Text mode. Try sending CO2, TEMP, HUM, and ALL to see the different outputs. You should see the values appear in your terminal almost instantly after each command is sent.

Output

Use Cases and Expanding the Idea

This setup goes beyond simply reading CO2, temperature, and humidity. At its core, it is a BLE command-to-script bridge — meaning you can trigger any Python action from your phone without needing Wi-Fi or internet access.

In real-world use, this could be applied to:

  • Remote environmental monitoring: Trigger on-demand scans for sensor readings in greenhouses, offices, or laboratories.
  • IoT control panels: Send commands to control connected devices, such as turning fans or air purifiers on when CO2 levels rise.
  • Data logging systems: Store sensor readings in a file or database only when requested, helping conserve storage and processing power.
  • Event-based automation: Pair with external hardware like Raspberry Pi GPIO pins to trigger physical actions when commands are received.

Because the commands are just plain text, the possibilities for customization are endless. You could add more commands to control different hardware, run different scripts, or even communicate with multiple BLE devices. This same approach can also integrate with web dashboards, cloud services, or data visualization tools to make the results more accessible and actionable.

This project shows how simple it is to control Python scripts and interact with BLE sensor data using BleuIO and your phone. By combining the SPS profile with AT commands, you can create flexible, interactive tools that can be extended for various IoT applications — from remote environmental monitoring to interactive control systems.

Share this post on :

Using BleuIO with Waveshare RP2350A USB Mini Based On Raspberry Pi RP2350A 

In this tutorial, we demonstrate how to connect the BleuIO Bluetooth Low Energy dongle to the Waveshare RP2350A USB Mini development board to create a dual-mode USB serial bridge. This example highlights the process of communicating with BleuIO using AT commands via USB host mode. It serves as a great starting point for anyone looking to build their own BLE application using this compact and powerful board.

About the Waveshare RP2350A USB Mini

The Waveshare RP2350A USB Mini is a development board built around the Raspberry Pi RP2350 microcontroller. It features a dual-core architecture, combining ARM Cortex-M33 and RISC-V Hazard3 cores, running at up to 150 MHz. The chip includes 520KB of SRAM and 2MB of onboard flash, making it suitable for advanced embedded applications.

What sets this board apart is its USB-A connector with support for USB host mode, allowing it to communicate directly with USB peripherals like the BleuIO dongle. This makes it an ideal host controller for Bluetooth Low Energy (BLE) applications.

Project Overview

This project demonstrates a dual-mode USB serial bridge implemented on the RP2350A board. The bridge allows the board to simultaneously function as:

  • A USB host using PIO-USB on GPIO 12/13, connected to a device like BleuIO.
  • A USB device, appearing as a virtual COM port when connected to a PC.

Data is transparently forwarded between the host and device interfaces, allowing you to communicate with BleuIO from a terminal on your PC.

This example project is useful for anyone looking to build a standalone USB host application using BleuIO. You can use the source code as a base and expand it into a more complex BLE project by sending and receiving AT commands directly from the RP2350A board.

Use Case

Imagine a scenario where you want to build a small, embedded BLE sensor gateway. Using the RP2350A as a USB host and BleuIO as the BLE interface, you can develop a powerful BLE solution without needing a full-sized computer. This approach is ideal for prototyping custom BLE applications, sensor data acquisition, or even building a mini BLE scanner.

Hardware Requirements

Software Requirements

Host Mode (PIO-USB on GPIO 12/13)

In host mode, the RP2350 board uses PIO-USB to emulate USB host functionality on GPIO pins 12 (D+) and 13 (D-). You can connect any CDC-compatible device, such as a USB-to-serial adapter or the BleuIO dongle, to these pins.

Once connected, the application automatically detects and configures the device. All incoming and outgoing serial data is forwarded and can be monitored through the debug UART interface.

Device Mode (Native USB Port)

When the RP2350A’s native USB port is connected to your PC, it appears as a virtual serial port. This allows you to communicate with the BLE dongle (connected via host mode) using a terminal application like PuTTY or Tera Term.

This enables full-duplex communication, letting you send AT commands to BleuIO and receive responses directly from your PC.

Debug Output (UART on GP0/GP1)

To observe internal debug messages, connect a UART adapter to GPIO 0 (TX) and GPIO 1 (RX) with a baud rate of 115200. This output includes information such as device enumeration, data flow, and potential errors—essential for troubleshooting and development.

Building the Project

To build the firmware, you can either use the provided build script or follow a manual setup.

You can find the complete source code for this project on GitHub:

GitHub Repository: bleuio-rp2350

Method 1: Using the build script

cd rp2350_serial_bridge
./build.sh

Method 2: Manual build with CMake

mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j4

After building, a rp2350_serial_bridge.uf2 file will be generated.

Flashing the Firmware

  1. Hold the BOOTSEL button on the RP2350 board and connect it to your PC via USB.
  2. The board will mount as a USB mass storage device.
  3. Copy the generated rp2350_serial_bridge.uf2 file to this drive.
  4. The board will reboot and start running the dual-mode USB bridge application.

You can now insert the BleuIO dongle into the USB-A port of the RP2350A board and begin communication from your PC.

Code Structure

  • main.c – Main entry point of the application.
  • serial_host_bridge.c/h – Handles the USB CDC host implementation.
  • tusb_config.h – TinyUSB configuration file.
  • CMakeLists.txt – Build configuration for CMake.

Getting Started with Your Own BLE App

This project is just a starting point. It demonstrates how to set up USB host/device mode and communicate with a CDC-based USB dongle like BleuIO. From here, you can extend the code to parse responses, interact with BLE devices, or trigger actions based on received data.

By combining the flexible RP2350A platform with BleuIO, developers can create their own powerful standalone BLE applications for IoT, sensors, or industrial control—without relying on a full computer.

Source Code

You can find the complete source code for this project on GitHub:

GitHub Repository: bleuio-rp2350

Share this post on :

Building a BLE-Powered Air Quality Dashboard with Grafana

Indoor air quality is an important factor in workplace health, home comfort, and industrial safety. Monitoring CO2, particulate matter, temperature, humidity, and other environmental metrics can help prevent health issues, optimize HVAC usage, and maintain compliance with building standards. In this tutorial, we’ll walk through building a complete air quality monitoring solution using the BleuIO USB dongle and the HibouAir BLE sensor. We’ll use Python to collect data, InfluxDB to store it, and Grafana to visualize it in real time.

This project works entirely offline, runs on both macOS and Windows, and is suitable for personal, educational, or professional use. Whether you want to monitor a home office or manage sensors in an industrial setup, this system is simple to deploy and easy to scale.

What You Need

You will need one BleuIO USB dongle and one HibouAir BLE sensor. These two devices form the core of the setup. The BleuIO dongle will connect via a serial port and act as a BLE Central, continuously scanning for BLE advertisement packets sent by the HibouAir sensor.

For software, you’ll need Python 3, InfluxDB, and Grafana, all of which are available for both macOS and Windows. You’ll also install a few Python packages (pyserial and influxdb) to allow your script to read data over the serial port and write it to the database.

About Grafana

Grafana is an open-source analytics and visualization platform designed to display time-series data. It integrates seamlessly with databases like InfluxDB, allowing users to create interactive and customizable dashboards. In this project, Grafana serves as the front end for visualizing live air quality data coming from the HibouAir sensor. Its intuitive interface and support for powerful queries make it ideal for monitoring environmental conditions over time, setting alerts, and gaining actionable insights from BLE sensor data.

macOS Installation

On macOS, it’s easiest to install tools using Homebrew. You can install everything with the following commands:

brew install influxdb@1
brew install grafana
brew install python

Start the services:

brew services start influxdb@1
brew services start grafana

Windows Installation

For Windows, download installers from official sites:

Make sure Python and pip are added to your system path. You can then install Python packages via:

pip install pyserial influxdb

Also, identify your BleuIO COM port by opening Device Manager and checking Ports (COM & LPT). You’ll see something like COM3, which you’ll use in the Python script.

Setting Up InfluxDB

After installing InfluxDB, you need to create a database that will store sensor readings. On macOS, open your terminal. On Windows, use Command Prompt or PowerShell.

To enter the InfluxDB CLI:

influx

Inside the CLI, run:

> CREATE DATABASE hibouair
> SHOW DATABASES
> EXIT

This creates a database called hibouair, which your Python script will use to log environmental data.

You don’t need to set up authentication for local testing. If you do secure your instance later, remember to update Grafana and the Python script with your username and password.

Configuring Grafana

Once Grafana is installed and running, open your browser and go to:

http://localhost:3000

The default username and password are:

Username: admin
Password: admin

After logging in, you’ll be asked to set a new password. Once you’re in the Grafana dashboard:

  1. Go to Settings → Data Sources
  2. Click Add data source
  3. Choose InfluxDB
  4. Set the following:
    • URL: http://localhost:8086
    • Database: hibouair
    • User and Password: leave empty unless you’ve enabled authentication
    • HTTP Method: GET or POST (default is fine)
  5. Click Save & Test

You should see a success message confirming Grafana can read from InfluxDB.

Connecting BleuIO via Serial Port

Insert your BleuIO USB dongle.

  • On macOS, find the port using: ls /dev/cu.usbmodem* Example: /dev/cu.usbmodem4048FDEBA6D01
  • On Windows, check Device Manager → Ports (COM & LPT) and find something like COM3 or COM4.

This port will be used in your Python script to open a serial connection to the dongle.

The Python Script

The Python script initializes the BleuIO dongle in Central mode, sends a scan command (AT+FINDSCANDATA), and reads the BLE advertisement packets from the HibouAir sensor. The data, which arrives in hexadecimal format, is decoded to extract values like temperature, humidity, CO2, VOC, PM1, PM2.5, PM10, light, noise, and pressure. The script then writes this data to InfluxDB with a timestamp.

To avoid storing duplicate values, the script only logs the first valid reading per scan cycle. It waits for a configurable period (e.g., 20 seconds) before scanning again.

You can view and copy the complete Python script from the GitHub repository linked below.

GitHub: bleuio-hibouair-influx-grafana

To run the script:

python3 bleuio_to_influx.py

Make sure to replace the serial port path and board ID in the script with your actual values.

Creating a Dashboard in Grafana

With data flowing into InfluxDB, it’s time to visualize it in Grafana. Start by creating a new dashboard:

  1. From the left menu, go to Dashboards → New → Add new panel
  2. In the query editor, select your InfluxDB data source
  3. Enter the following query to display CO2 levels: SELECT last("co2") FROM "air_quality" WHERE $timeFilter GROUP BY time($__interval)

This query fetches the latest CO2 readings from the air_quality measurement. You can change "co2" to "temp", "hum", "voc", "pm25" or any other field depending on what you want to display.

Grafana will automatically plot the data on a line chart. You can switch to a gauge, bar, or other visualizations from the panel settings. You can also set thresholds, value ranges, and colors to improve readability.

Repeat this process to add more panels for temperature, humidity, VOCs, PM2.5, PM10, and other values.

Once your dashboard is complete, click Apply, then Save the dashboard to reuse it later.

Output

How It Works

This system leverages BLE advertisement broadcasting from HibouAir and AT command-based serial communication with the BleuIO dongle. The dongle scans for a specific board ID using AT+FINDSCANDATA=<board_id>=3, and the Python script reads and decodes each advertisement received. The decoded data is immediately sent to InfluxDB using the official client library.

Grafana queries this time-series data and displays it in real time. Since all components are local, there is no dependency on external servers or cloud APIs. The system is also modular, so you can easily scale it to support multiple sensors, extend the decoder for other BLE formats, or forward data to cloud-based platforms in the future.

Use Cases

This setup is ideal for a range of applications. In an office environment, monitoring CO2 and humidity can help optimize ventilation and improve productivity. In factories or workshops, tracking particulate matter and VOCs ensures air quality compliance and safety. For researchers and students, this project provides a hands-on, local, and open-source method to study environmental changes. Even in residential settings, it can help detect poor air circulation or pollutant spikes. The simplicity of this solution makes it accessible to both developers and non-developers alike.

Extending the Project

There are several ways to expand this project:

  • Add support for additional BLE data types, such as HibouAir’s type 08 advertisements for NO₂
  • Set up Grafana alerts to notify you when CO2 exceeds 800 ppm or humidity drops below 30%
  • Export historical data from InfluxDB to CSV for offline analysis
  • Run the Python script as a background service on system boot
  • Connect multiple sensors to cover multiple rooms or areas

With minimal changes, you can also run this setup on a Raspberry Pi or Linux server.

With just a BleuIO dongle, a HibouAir sensor, and a few open-source tools, you’ve built a fully functional air quality monitoring solution. The system is flexible, lightweight, and runs entirely offline. It provides real-time insights into indoor environmental conditions and can be adapted for a variety of applications.

Share this post on :