μSense
Created September 27, 2025 © CC BY-NC-SA

Ai Forest Tag

A mesh network of sensors with AI that can monitor the enviroment,, collect and classify species of birds and animal in forest.

AdvancedProtip12 hours25
Ai Forest Tag

Things used in this project

Hardware components

ESP32-S3
Espressif ESP32-S3
×1
NextPCB  Custom PCB Board
NextPCB Custom PCB Board
×1
Bosch BME690
×1
NextPCB Mems Mic SD18OB261-060
×1
STMicroelectronics LSM303
×1
ESP32-C6
Espressif ESP32-C6
×1

Software apps and online services

Edge Impulse Studio
Edge Impulse Studio
SensiML Analytics Toolkit
SensiML Analytics Toolkit
Neuton
Neuton Tiny ML Neuton
Google Sheets
Google Sheets
Arduino IDE
Arduino IDE
ESP-IDF
Espressif ESP-IDF
Fusion
Autodesk Fusion

Story

Read more

Custom parts and enclosures

Tag case Hexaginal

TagSTL

Sketchfab still processing.

Schematics

BME690/688 ENV/gas snesor

ACC / MAG / Gyro snesor

MEMS MIC and RGB LED

ESP CHIP

Code

Master_Forest_Tag

Arduino
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <BLEDevice.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <map>

AsyncWebServer server(80);

const char* ap_ssid = "ForestMaster";
const char* ap_password = "password123"; // Change as needed

int sensor_count = 0;

std::map<std::string, unsigned long> last_seen;

// Structs
struct Species {
  String name;
  int estimated;
  String timestamp;
  bool detected;
};

Species birds[30];
Species animals[30];

struct Event {
  String name;
  int estimated;
  String timestamp;
  bool detected;
};

Event events[10];

struct EnvParam {
  String name;
  float value;
  String unit;
  String timestamp;
  bool alert;
  float min_val;
  float max_val;
};

EnvParam env_params[4];

// HTML strings
const String classifyHTML = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Forest Classify Dashboard</title>
    <style>
        body {
            background-color: white;
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .header {
            text-align: center;
            margin-bottom: 20px;
        }
        .header h1 {
            color: #333;
        }
        .sensor-info {
            font-weight: bold;
            color: #666;
            margin-bottom: 30px;
        }
        .dashboard {
            display: flex;
            width: 100%;
            max-width: 1200px;
            justify-content: space-between;
        }
        .section {
            width: 48%;
        }
        .section h2 {
            text-align: center;
            color: #444;
            margin-bottom: 20px;
        }
        .cards {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 15px;
        }
        .card {
            background-color: white;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 15px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            transition: box-shadow 0.3s;
        }
        .card:hover {
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
        }
        .card.detected {
            border-color: #4CAF50;
            background-color: #E8F5E9;
        }
        .card h3 {
            margin-top: 0;
            color: #333;
        }
        .card p {
            margin: 5px 0;
            color: #555;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>AI Forest Classify</h1>
    </div>
    <div class="sensor-info">
        Sensor Nodes Connected: <span id="sensor-count">Loading...</span>
    </div>
    <div class="dashboard">
        <div class="section" id="birds-section">
            <h2>Birds</h2>
            <div class="cards" id="birds-cards"></div>
        </div>
        <div class="section" id="animals-section">
            <h2>Animals</h2>
            <div class="cards" id="animals-cards"></div>
        </div>
    </div>

    <script>
        function renderCards(containerId, data) {
            const container = document.getElementById(containerId);
            container.innerHTML = '';
            data.forEach(item => {
                const card = document.createElement('div');
                card.className = 'card';
                if (item.detected) {
                    card.classList.add('detected');
                }
                card.innerHTML = `
                    <h3>${item.species}</h3>
                    <p>Estimated Number: ${item.estimated}</p>
                    <p>Sound Time: ${item.timestamp}</p>
                    <p>Currently Detected: ${item.detected ? 'Yes' : 'No'}</p>
                `;
                container.appendChild(card);
            });
        }

        function updateData() {
            fetch('/data_classify')
                .then(response => response.json())
                .then(data => {
                    renderCards('birds-cards', data.birds);
                    renderCards('animals-cards', data.animals);
                    document.getElementById('sensor-count').textContent = data.sensor_count;
                })
                .catch(error => console.error('Error:', error));
        }

        setInterval(updateData, 5000);
        updateData();
    </script>
</body>
</html>
)rawliteral";

const String guardianHTML = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Forest Guardians Dashboard</title>
    <style>
        body {
            background-color: white;
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .header {
            text-align: center;
            margin-bottom: 20px;
        }
        .header h1 {
            color: #333;
        }
        .sensor-info {
            font-weight: bold;
            color: #666;
            margin-bottom: 30px;
        }
        .dashboard {
            display: flex;
            width: 100%;
            max-width: 1200px;
            justify-content: center;
        }
        .section {
            width: 80%;
        }
        .section h2 {
            text-align: center;
            color: #444;
            margin-bottom: 20px;
        }
        .cards {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 15px;
        }
        .card {
            background-color: white;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 15px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            transition: box-shadow 0.3s;
        }
        .card:hover {
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
        }
        .card.detected {
            border-color: #4CAF50;
            background-color: #E8F5E9;
        }
        .card h3 {
            margin-top: 0;
            color: #333;
        }
        .card p {
            margin: 5px 0;
            color: #555;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>AI Forest Guardians</h1>
    </div>
    <div class="sensor-info">
        Sensor Nodes Connected: <span id="sensor-count">Loading...</span>
    </div>
    <div class="dashboard">
        <div class="section" id="events-section">
            <h2>Detected Events</h2>
            <div class="cards" id="events-cards"></div>
        </div>
    </div>

    <script>
        function renderCards(containerId, data) {
            const container = document.getElementById(containerId);
            container.innerHTML = '';
            data.forEach(item => {
                const card = document.createElement('div');
                card.className = 'card';
                if (item.detected) {
                    card.classList.add('detected');
                }
                card.innerHTML = `
                    <h3>${item.event}</h3>
                    <p>Estimated Occurrences: ${item.estimated}</p>
                    <p>Last Sound Time: ${item.timestamp}</p>
                    <p>Currently Detected: ${item.detected ? 'Yes' : 'No'}</p>
                `;
                container.appendChild(card);
            });
        }

        function updateData() {
            fetch('/data_guardian')
                .then(response => response.json())
                .then(data => {
                    renderCards('events-cards', data.events);
                    document.getElementById('sensor-count').textContent = data.sensor_count;
                })
                .catch(error => console.error('Error:', error));
        }

        setInterval(updateData, 5000);
        updateData();
    </script>
</body>
</html>
)rawliteral";

const String envHTML = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Forest Environment Dashboard</title>
    <style>
        body {
            background-color: white;
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .header {
            text-align: center;
            margin-bottom: 20px;
        }
        .header h1 {
            color: #333;
        }
        .sensor-info {
            font-weight: bold;
            color: #666;
            margin-bottom: 30px;
        }
        .dashboard {
            display: flex;
            width: 100%;
            max-width: 1200px;
            justify-content: center;
        }
        .section {
            width: 80%;
        }
        .section h2 {
            text-align: center;
            color: #444;
            margin-bottom: 20px;
        }
        .cards {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 15px;
        }
        .card {
            background-color: white;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 15px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            transition: box-shadow 0.3s;
        }
        .card:hover {
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
        }
        .card.alert {
            border-color: #4CAF50;
            background-color: #E8F5E9;
        }
        .card h3 {
            margin-top: 0;
            color: #333;
        }
        .card p {
            margin: 5px 0;
            color: #555;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>AI Forest Environment</h1>
    </div>
    <div class="sensor-info">
        Sensor Nodes Connected: <span id="sensor-count">Loading...</span>
    </div>
    <div class="dashboard">
        <div class="section" id="env-section">
            <h2>Environmental Parameters</h2>
            <div class="cards" id="env-cards"></div>
        </div>
    </div>

    <script>
        function renderCards(containerId, data) {
            const container = document.getElementById(containerId);
            container.innerHTML = '';
            data.forEach(item => {
                const card = document.createElement('div');
                card.className = 'card';
                if (item.alert) {
                    card.classList.add('alert');
                }
                card.innerHTML = `
                    <h3>${item.name}</h3>
                    <p>Current Value: ${item.value} ${item.unit}</p>
                    <p>Last Updated: ${item.timestamp}</p>
                    <p>Alert Status: ${item.alert ? 'Active' : 'Normal'}</p>
                `;
                container.appendChild(card);
            });
        }

        function updateData() {
            fetch('/data_env')
                .then(response => response.json())
                .then(data => {
                    renderCards('env-cards', data.params);
                    document.getElementById('sensor-count').textContent = data.sensor_count;
                })
                .catch(error => console.error('Error:', error));
        }

        setInterval(updateData, 5000);
        updateData();
    </script>
</body>
</html>
)rawliteral";

// BLE Callback
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    std::string addr = advertisedDevice.getAddress().toString();
    last_seen[addr] = millis();

    if (advertisedDevice.haveManufacturerData()) {
      std::string mdata = advertisedDevice.getManufacturerData();
      String data = String(mdata.c_str());

      // Parse data: category:name:estimated/value:detected/alert
      String parts[4];
      int idx = 0;
      int pos;
      String temp = data;
      while ((pos = temp.indexOf(':')) != -1 && idx < 3) {
        parts[idx++] = temp.substring(0, pos);
        temp = temp.substring(pos + 1);
      }
      parts[idx] = temp;

      String category = parts[0];
      String name = parts[1];
      String timestamp = String(millis() / 1000) + "s since boot";

      if (category == "bird") {
        int estimated = parts[2].toInt();
        bool detected = parts[3].toInt() == 1;
        for (int i = 0; i < 30; i++) {
          if (birds[i].name == name) {
            birds[i].estimated = estimated;
            birds[i].detected = detected;
            birds[i].timestamp = timestamp;
            break;
          }
        }
      } else if (category == "animal") {
        int estimated = parts[2].toInt();
        bool detected = parts[3].toInt() == 1;
        for (int i = 0; i < 30; i++) {
          if (animals[i].name == name) {
            animals[i].estimated = estimated;
            animals[i].detected = detected;
            animals[i].timestamp = timestamp;
            break;
          }
        }
      } else if (category == "event") {
        int estimated = parts[2].toInt();
        bool detected = parts[3].toInt() == 1;
        for (int i = 0; i < 10; i++) {
          if (events[i].name == name) {
            events[i].estimated = estimated;
            events[i].detected = detected;
            events[i].timestamp = timestamp;
            break;
          }
        }
      } else if (category == "env") {
        float value = parts[2].toFloat();
        bool alert = parts[3].toInt() == 1;
        for (int i = 0; i < 4; i++) {
          if (env_params[i].name == name) {
            env_params[i].value = value;
            env_params[i].alert = alert;
            env_params[i].timestamp = timestamp;
            break;
          }
        }
      }
    }
  }
};

void setup() {
  Serial.begin(115200);

  // Initialize WiFi AP
  WiFi.mode(WIFI_AP_STA); // Enable both AP and STA
  WiFi.softAP(ap_ssid, ap_password);
  Serial.print("AP IP address: ");
  Serial.println(WiFi.softAPIP());

  // Connect to STA if desired, replace with your credentials
  // WiFi.begin("your_ssid", "your_password");
  // while (WiFi.status() != WL_CONNECTED) { delay(500); }
  // Serial.print("STA IP address: ");
  // Serial.println(WiFi.localIP());

  // Initialize BLE
  BLEDevice::init("");
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(false);
  pBLEScan->start(0, false);  // Continuous scan

  // Initialize data
  String bird_names[30] = {
    "Eagle", "Owl", "Sparrow", "Robin", "Woodpecker", "Parrot", "Crow", "Pigeon", "Hawk", "Falcon",
    "Peacock", "Kingfisher", "Cuckoo", "Swallow", "Thrush", "Nightingale", "Lark", "Finch", "Canary", "Blue Jay",
    "Cardinal", "Hummingbird", "Toucan", "Macaw", "Vulture", "Buzzard", "Heron", "Duck", "Goose", "Swan"
  };
  for (int i = 0; i < 30; i++) {
    birds[i].name = bird_names[i];
    birds[i].estimated = 0;
    birds[i].timestamp = "N/A";
    birds[i].detected = false;
  }

  String animal_names[30] = {
    "Tiger", "Lion", "Elephant", "Deer", "Monkey", "Bear", "Wolf", "Fox", "Leopard", "Panther",
    "Giraffe", "Zebra", "Rhino", "Hippo", "Crocodile", "Snake", "Boar", "Buffalo", "Antelope", "Hyena",
    "Jackal", "Squirrel", "Rabbit", "Porcupine", "Badger", "Otter", "Beaver", "Moose", "Elk", "Reindeer"
  };
  for (int i = 0; i < 30; i++) {
    animals[i].name = animal_names[i];
    animals[i].estimated = 0;
    animals[i].timestamp = "N/A";
    animals[i].detected = false;
  }

  String event_names[10] = {
    "Tree Logging", "Human Interference", "Gunshot Detection", "Stream Sound", "Rain Sound",
    "Animal Fighting", "Birds Fighting Chirping", "Hunting Sound - Gunshot", "Hunting Sound - Bow Arrow", "Animal Cry"
  };
  for (int i = 0; i < 10; i++) {
    events[i].name = event_names[i];
    events[i].estimated = 0;
    events[i].timestamp = "N/A";
    events[i].detected = false;
  }

  // Env params
  env_params[0] = {"Humidity", 0.0, "%", "N/A", false, 60.0, 95.0};
  env_params[1] = {"Air

talk-to-elephant-wasm-v3.zip

JavaScript
No preview (download only).

ei-talk-to-elephant-arduino-1.0.2.zip

Arduino
No preview (download only).

Credits

μSense
5 projects • 1 follower
ButtonBoard: 3cm ESP32 boards with built-in IoT, AI cam, mesh net. Plug & play. No extra parts. Portable, powerful, ready to compete.

Comments