Nevenka PavlovskaAna Jenkolo7909Luka Mali
Published

BoarMap - Detect & Alert Boar Intrusions

BoarMap detects boar sounds and alerts users, helping farmers protect their land and stay notified in real time.

IntermediateFull instructions provided246
BoarMap - Detect & Alert Boar Intrusions

Things used in this project

Hardware components

Seeed Studio XIAO nRF52840 Sense (XIAO BLE Sense)
Seeed Studio XIAO nRF52840 Sense (XIAO BLE Sense)
×1
Nano 33 BLE Sense
Arduino Nano 33 BLE Sense
×1
Android device
Android device
×1

Software apps and online services

Edge Impulse Studio
Edge Impulse Studio
Arduino IDE
Arduino IDE
Android Studio
Android Studio
The Things Industries The Things Network

Story

Read more

Schematics

BoarMap process diagram

A graphical representation of how this project was created and all its features implemented to get the final product.

Code

Detecting wild boar sounds with a microphone and sending alerts over a LoRa network

C/C++
This code uses a PDM microphone to record audio and classify sounds using an Edge Impulse model. If boar sounds are detected consistently, it sends an alert via LoRa; otherwise, it sends periodic no-boar and battery updates.
/* Edge Impulse ingestion SDK
 * Copyright (c) 2022 EdgeImpulse Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */

// If your target is limited in memory remove this to save 10K RAM
#define EIDSP_QUANTIZE_FILTERBANK   1

#define EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW 4
#define EI_CLASSIFIER_FREQUENCY               16000

#include <Arduino.h>
#include <Adafruit_TinyUSB.h>
#include <PDM.h>
#include <MIS_Projekt_inferencing.h>
#include <stdlib.h>
#include <string.h>

// LoRa credentials (MSB-order hex strings from TTN console)
const char* DEVEUI = "70B3D57ED0070695";
const char* APPEUI = "2CF7F1206310299A";
const char* APPKEY = "D336DA7D41E50C50F661D51AEB152134";

// Timing for no boar & battery alerts
static unsigned long lastNoBoarAlertTime  = 0;
static unsigned long lastBatteryAlertTime = 0;

// Boar-history smoothing
#define BOAR_HISTORY_SIZE 20
static bool boarHistory[BOAR_HISTORY_SIZE] = {0};
static int  boarHistoryIndex               = 0;

// Inference buffers & state
typedef struct {
    signed short *buffers[2];
    unsigned char buf_select;
    unsigned char buf_ready;
    unsigned int  buf_count;
    unsigned int  n_samples;
} inference_t;
static inference_t inference;
static bool record_ready = false;
static signed short *sampleBuffer;

// Forward prototypes (no empty bodies!)
static void pdm_data_ready_inference_callback(void);
static bool microphone_inference_start(uint32_t n_samples);
static bool microphone_inference_record(void);
static int  microphone_audio_signal_get_data(size_t offset, size_t length, float *out_ptr);
static void microphone_inference_end(void);

//------------------------------------------------------------------------------
// Payload = 1 => boar detected
void sendBoarAlert() {
    Serial.println("ALERT: Boar sound detected (divja_svinja)!");
    Serial1.print("AT+MSG=\"1\"\r\n");
}

// Payload = 0 => no boar
void sendNoBoarAlert() {
    Serial.println("Regular alert: No boar detected.");
    Serial1.print("AT+MSG=\"0\"\r\n");
}

void triggerSound() {
    Serial.println("Sound triggered: Beep beep beep!");
}

void sendBatteryLevel() {
    Serial.println("Battery level: 75%");
}

//------------------------------------------------------------------------------
// Helper: send a simple AT command
void sendAT(const char *cmd) {
    Serial.print(">> "); Serial.println(cmd);
    Serial1.print(cmd);
    Serial1.write("\r\n");
    delay(200);
    // echo any reply
    while (Serial1.available()) {
        Serial.write(Serial1.read());
    }
}

//------------------------------------------------------------------------------
// setup(): USB console, LoRa AT init, Edge Impulse init
void setup() {
    // USB serial
    Serial.begin(115200);
    while (!Serial);

    Serial.println("Edge Impulse Inferencing + LoRa Demo");

    // Power on LoRa module (if you wire VCC to D5)
    pinMode(5, OUTPUT);
    digitalWrite(5, HIGH);

    // ATlink
    Serial1.begin(9600);
    delay(100);

    // 1) Sanity
    sendAT("AT");

    // 2) IDs
    Serial1.print("AT+ID=DevEui,\""); Serial1.print(DEVEUI); Serial1.print("\"\r\n");
    delay(200); while (Serial1.available()) Serial.write(Serial1.read());
    Serial1.print("AT+ID=AppEui,\""); Serial1.print(APPEUI); Serial1.print("\"\r\n");
    delay(200); while (Serial1.available()) Serial.write(Serial1.read());
    Serial1.print("AT+KEY=AppKey,\"");Serial1.print(APPKEY); Serial1.print("\"\r\n");
    delay(200); while (Serial1.available()) Serial.write(Serial1.read());

    // 3) OTAA + join
    sendAT("AT+MODE=LWOTAA");
    sendAT("AT+JOIN");
    delay(3000); // allow join

    //  Edge Impulse inferencing init 
    ei_printf("Inferencing settings:\n");
    ei_printf("\tInterval: %.2f ms.\n", (float)EI_CLASSIFIER_INTERVAL_MS);
    ei_printf("\tFrame size: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
    ei_printf("\tSample length: %d ms.\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT/16);
    ei_printf("\tNo. of classes: %d\n",
              sizeof(ei_classifier_inferencing_categories)/
              sizeof(ei_classifier_inferencing_categories[0]));

    run_classifier_init();
    if (!microphone_inference_start(EI_CLASSIFIER_SLICE_SIZE)) {
        ei_printf("ERR: Could not allocate audio buffer\n");
        while (1);
    }
}

//------------------------------------------------------------------------------
// loop(): record, classify, and send alerts over LoRa
void loop() {
    if (!microphone_inference_record()) {
        ei_printf("ERR: Failed to record audio\n");
        return;
    }

    signal_t signal;
    signal.total_length = EI_CLASSIFIER_SLICE_SIZE;
    signal.get_data     = &microphone_audio_signal_get_data;
    ei_impulse_result_t result = {0};

    if (run_classifier_continuous(&signal, &result, false) != EI_IMPULSE_OK) {
        ei_printf("ERR: Classifier error\n");
        return;
    }

    // Print out each detected class and its confidence:
    for (size_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
        Serial.printf("%s: %.5f\n",
                      result.classification[i].label,
                      result.classification[i].value);
    }

    // If you also want the anomaly score (when enabled):
    #if EI_CLASSIFIER_HAS_ANOMALY == 1
    Serial.printf("anomaly score: %.5f\n", result.anomaly);
    #endif

    // extract Boar score
    float boarScore = 0;
    for (size_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
        if (!strcmp(result.classification[i].label, "Boar")) {
            boarScore = result.classification[i].value;
            break;
        }
    }
    bool currentBoar = (boarScore > 0.5f);

    // update history
    boarHistory[boarHistoryIndex] = currentBoar;
    boarHistoryIndex = (boarHistoryIndex + 1) % BOAR_HISTORY_SIZE;

    // count positives
    int count = 0;
    for (int i = 0; i < BOAR_HISTORY_SIZE; i++) {
        if (boarHistory[i]) count++;
    }

    // if 15/20  boar alert
    if (count >= 15) {
        sendBoarAlert();
        triggerSound();
    }
    else if (millis() - lastNoBoarAlertTime >= 30000) {
        sendNoBoarAlert();
        lastNoBoarAlertTime = millis();
    }

    // battery update every 2s
    if (millis() - lastBatteryAlertTime >= 2000) {
        sendBatteryLevel();
        lastBatteryAlertTime = millis();
    }

    // echo any leftover module replies
    while (Serial1.available()) {
        Serial.write(Serial1.read());
    }
    delay(5000);
}

//------------------------------------------------------------------------------
// PDM Buffer Full Callback
static void pdm_data_ready_inference_callback(void) {
    int bytesAvailable = PDM.available();
    int bytesRead      = PDM.read((char *)&sampleBuffer[0], bytesAvailable);

    if (record_ready) {
        for (int i = 0; i < (bytesRead >> 1); i++) {
            inference.buffers[inference.buf_select][inference.buf_count++] = sampleBuffer[i];
            if (inference.buf_count >= inference.n_samples) {
                inference.buf_select ^= 1;
                inference.buf_count  = 0;
                inference.buf_ready  = 1;
            }
        }
    }
}

//------------------------------------------------------------------------------
// Allocate buffers & start PDM
static bool microphone_inference_start(uint32_t n_samples) {
    inference.buffers[0] = (signed short*)malloc(n_samples * sizeof(signed short));
    if (!inference.buffers[0]) return false;
    inference.buffers[1] = (signed short*)malloc(n_samples * sizeof(signed short));
    if (!inference.buffers[1]) { free(inference.buffers[0]); return false; }
    sampleBuffer = (signed short*)malloc((n_samples >> 1) * sizeof(signed short));
    if (!sampleBuffer) { free(inference.buffers[0]); free(inference.buffers[1]); return false; }

    inference.buf_select = 0;
    inference.buf_count  = 0;
    inference.n_samples  = n_samples;
    inference.buf_ready  = 0;

    PDM.onReceive(&pdm_data_ready_inference_callback);
    PDM.setBufferSize((n_samples >> 1) * sizeof(int16_t));
    if (!PDM.begin(1, EI_CLASSIFIER_FREQUENCY)) {
        ei_printf("Failed to start PDM!\n");
    }
    PDM.setGain(127);
    record_ready = true;
    return true;
}

//------------------------------------------------------------------------------
// Wait for next filled buffer
static bool microphone_inference_record(void) {
    bool ret = true;
    if (inference.buf_ready == 1) {
        ei_printf("Error sample buffer overrun. Decrease slices per window\n");
        ret = false;
    }
    while (inference.buf_ready == 0) {
        delay(1);
    }
    inference.buf_ready = 0;
    return ret;
}

//------------------------------------------------------------------------------
// Copy raw int16  float
static int microphone_audio_signal_get_data(size_t offset,
                                            size_t length,
                                            float *out_ptr) {
    numpy::int16_to_float(&inference.buffers[inference.buf_select ^ 1][offset],
                          out_ptr, length);
    return 0;
}

//------------------------------------------------------------------------------
// Tear down PDM & free buffers
static void microphone_inference_end(void) {
    PDM.end();
    free(inference.buffers[0]);
    free(inference.buffers[1]);
    free(sampleBuffer);
}

Recordings generator

Python
This Python script splits a long .wav file into smaller audio segments. It reads the audio, divides it by time, and saves each part as a new .wav file in a folder.
import wave
import numpy as np
import os

def split_wav(input_file, output_dir, segment_duration):
    # Open the input file and extract parameters
    with wave.open(input_file, 'rb') as wf:
        framerate = wf.getframerate()
        n_channels = wf.getnchannels()
        sampwidth = wf.getsampwidth()
        n_frames = wf.getnframes()
        audio_data = wf.readframes(n_frames)
   
    # Determine the NumPy data type based on sample width
    if sampwidth == 2:
        dtype = np.int16
    elif sampwidth == 1:
        dtype = np.uint8
    else:
        raise ValueError("Unsupported sample width: {}".format(sampwidth))
   
    # Convert binary data to a NumPy array and reshape if necessary
    audio_array = np.frombuffer(audio_data, dtype=dtype)
    if n_channels > 1:
        audio_array = audio_array.reshape(-1, n_channels)
   
    # Calculate the total duration of the audio in seconds
    total_duration = n_frames / framerate
    print(f"Total duration: {total_duration:.2f} seconds")
   
    # Create the output directory if it doesn't exist
    os.makedirs(output_dir, exist_ok=True)
   
    # Calculate the number of segments needed
    segment_duration_sec = segment_duration
    n_segments = int(np.ceil(total_duration / segment_duration_sec))
   
    for i in range(n_segments):
        start_time = i * segment_duration_sec
        end_time = min((i + 1) * segment_duration_sec, total_duration)
       
        start_sample = int(start_time * framerate)
        end_sample = int(end_time * framerate)
       
        segment_array = audio_array[start_sample:end_sample]
        segment_bytes = segment_array.tobytes()
       
        # Build output filename in the format gozd_[n].wav within the output directory
        output_file = os.path.join(output_dir, f"gozd_{i+1}.wav")
        with wave.open(output_file, 'wb') as wf_out:
            wf_out.setnchannels(n_channels)
            wf_out.setsampwidth(sampwidth)
            wf_out.setframerate(framerate)
            wf_out.writeframes(segment_bytes)
       
        print(f"Segment {i+1} saved: {output_file}")

if __name__ == "__main__":
    # Update these values as needed
    input_file = r"FOREST DUSK  2H Live-Recorded European Forest Ambience  No Loop [9-c1QNq-D7E].wav"
    output_dir = "gozd"  # Subfolder for segments
    segment_duration = 600  # 10 minutes = 600 seconds

    split_wav(input_file, output_dir, segment_duration)

Credits

Nevenka Pavlovska
1 project • 0 followers
Ana Jenko
0 projects • 0 followers
lo7909
0 projects • 0 followers
Luka Mali
20 projects • 24 followers
Maker Pro, prototyping enthusiast, head of MakerLab, a lecturer at the University of Ljubljana, founder.

Comments