Papa Talla DioumThéotimeAyoub LadjiciNadir KaremIlyes
Published

B-NAHL Hive Monitor

This project aims to design a real-time hive monitoring system to improve bee health and optimize honey production.

IntermediateFull instructions providedOver 9 days216
B-NAHL Hive Monitor

Things used in this project

Hardware components

DHT22 Temperature Sensor
DHT22 Temperature Sensor
×1
Arduino MKR WAN 1310
Arduino MKR WAN 1310
×1
Arduino Nano 33 BLE Sense
Arduino Nano 33 BLE Sense
×1
Adafruit Waterproof DS18B20 Digital temperature sensor
Adafruit Waterproof DS18B20 Digital temperature sensor
×2
DFRobot SHT31 - Temperature and Humidity Sensor
×1
SparkFun Load Cell Amplifier - HX711
SparkFun Load Cell Amplifier - HX711
×1
Bosch Single point load cell
×1
LoRa Antenna
×1
Adafruit LiPo Battery 3,7V 2000 mAh
×1
Seeed Studio LiPo Rider Pro Board
×1
S7V8F3 Voltage Regulator
×1
Rohm BH1750 Light Sensor
×1
waterproof pvc box
×1
Resistor 100k ohm
Resistor 100k ohm
×1
Through Hole Resistor, 47 kohm
Through Hole Resistor, 47 kohm
×1

Software apps and online services

Ubidots
Ubidots
Arduino IDE
Arduino IDE
KiCad
KiCad
Edge Impulse Studio
Edge Impulse Studio
The Things Stack
The Things Industries The Things Stack
Beep

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Mastech MS8217 Autorange Digital Multimeter
Digilent Mastech MS8217 Autorange Digital Multimeter
Qoitech Otii
Solder Wire, Lead Free
Solder Wire, Lead Free
Wire Stripper & Cutter, 18-10 AWG / 0.75-4mm² Capacity Wires
Wire Stripper & Cutter, 18-10 AWG / 0.75-4mm² Capacity Wires
Mastech MS8217 Autorange Digital Multimeter
Digilent Mastech MS8217 Autorange Digital Multimeter
Breadboard, 830 Tie Points
Breadboard, 830 Tie Points
Soldering iron (generic)
Soldering iron (generic)
Brass Wool, Replacement
Brass Wool, Replacement
Hot glue gun (generic)
Hot glue gun (generic)
Premium Female/Male Extension Jumper Wires, 40 x 6" (150mm)
Premium Female/Male Extension Jumper Wires, 40 x 6" (150mm)

Story

Read more

Custom parts and enclosures

Tutorial for data transmission on TTN and visualization on BEEP

This tutorial explains how to connect an Arduino MKR WAN 1310 to The Things Network, send LoRa data, and visualize it in BEEP using a webhook and JavaScript decoder. Perfect for IoT beginners who want to build a connected beehive or environmental monitor.

Schematics

PCB files

You can find all the necessary files in the /tree/main/b_nahl_v1 directory.

Pinout diagram

This diagram shows the pin assignment for each sensor connected to the Arduino MKR WAN 1310

PCB

Schematic

Code

Beehive Monitor with Arduino MKR WAN 1310 and LoRaWAN

Arduino
This code collects environmental data inside and outside a beehive, including:

- Internal and external temperature & humidity
- Light intensity
- Hive weight
- Battery voltage
- Queen presence (from Nano BLE Sense)

It sends all data via LoRaWAN using the Arduino MKR WAN 1310. You can view the data on a cloud dashboard (e.g., Ubidots, TTN).

Downlink messages are also supported to adjust the sending interval remotely.
#include <MKRWAN.h>
#include <Arduino.h>
#include <Wire.h>
#include "Adafruit_SHT31.h"
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#include "HX711.h"
#include <BH1750.h>
#include "ArduinoLowPower.h"

// LoRa module configuration
LoRaModem modem;

// Message from the Nano
String percent_queen_present = "";

/* Sensor initialization */
// SHT31 sensor
Adafruit_SHT31 sht31 = Adafruit_SHT31();

// DS18B20 sensors
#define ONE_WIRE_BUS 0 // D0 connector
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature ds18b20(&oneWire);
DeviceAddress ds18b20_1, ds18b20_2;  // Addresses of the two probes

// DHT22 sensor
#define DHTPIN 2 // D2 connector
#define DHTTYPE DHT22 
DHT_Unified dht(DHTPIN, DHTTYPE);

// Weighing module (HX711)
#define LOADCELL_DOUT_PIN 3 // D3 connector
#define LOADCELL_SCK_PIN 4 // D4 connector
HX711 scale;
const float calibration_factor = -8860;
const long zero_factor = -2091146;

// Light sensor (BH1750)
BH1750 lightMeter;

/* Battery */
#define VBAT_PIN A0  // Pin used to measure the voltage from the divider  
#define DIVISEUR_RATIO 1.47 // Conversion factor based on 100kΩ / 47kΩ divider
// LiPo battery voltage range (3.7V)
#define VBAT_MAX 4.2 // Fully charged
#define VBAT_MIN 3.0 // Discharged

#define REGULATEUR_PIN 6  // D6 pin to control the regulator

// LoRa OTAA Keys
String AppEUI = "213D57ED00000000";
String AppKEY = "FA813BD835A67CBE3EF4298A06FAB916"; 

bool connected;
int err_count;
short con;
int delayBetweenSends = 30; // Default delay = 30 seconds

/* Read external temperature and humidity using SHT31 */
void readSHT31(float &temperature, float &humidity) {
    temperature = sht31.readTemperature();
    humidity = sht31.readHumidity();
    if (isnan(temperature)) temperature = 0;
    if (isnan(humidity)) humidity = 0;
}

/* Read internal temperature in two different spots using DS18B20 */
void readDS18B20(float &temp1, float &temp2) {
    ds18b20.requestTemperatures();
    temp1 = ds18b20.getTempC(ds18b20_1);
    temp2 = ds18b20.getTempC(ds18b20_2);
    if (temp1 == -127) temp1 = 0;
    if (temp2 == -127) temp2 = 0;
}

/* Read internal middle temperature and humidity using DHT22 */
void readDHT22(float &temperature, float &humidity) {
    sensors_event_t event;
    dht.temperature().getEvent(&event);
    temperature = event.temperature;
    dht.humidity().getEvent(&event);
    humidity = event.relative_humidity;
    if (isnan(temperature)) temperature = 0;
    if (isnan(humidity)) humidity = 0;
}

/* Read the weight of the beehive */
float readWeight() {
    float weight = scale.get_units(10) / 2.2046; // Convert lbs to kg
    return (weight < 0) ? 0 : weight;  // Correct negative values
}

/* Read ambient light level using BH1750 */
float readLuminosity() {
  lightMeter.begin();
  delay(200);  // Stabilization delay
  float lux = lightMeter.readLightLevel();
  lightMeter.powerDown();
  return isnan(lux) ? 0 : lux;
}

/* Read battery voltage */
float readBatteryLevel() {
  analogReadResolution(12);
  float vBat = ((analogRead(VBAT_PIN) / 4095.0) * 3.3) * DIVISEUR_RATIO;
  return vBat;
  // Uncomment below line if you prefer to return a percentage instead
  // return (vBat >= VBAT_MAX) ? 100 : (vBat <= VBAT_MIN) ? 0 : (int)(((vBat - VBAT_MIN) / (VBAT_MAX - VBAT_MIN)) * 100);
}

/* Read and apply downlink message to update delay */
void checkDownlink() {
  delay(1000);  // RX1 delay
  if (modem.available() >= 2) {
    int high = modem.read();
    int low  = modem.read();
    int value = (high << 8) | low;
    delayBetweenSends = value;
  }
}

void setup() {
  Serial.begin(115200);
  Serial1.begin(9600); // Serial terminal to read Nano values

  modem.begin(EU868);
  delay(1000);
  connected = false;
  err_count = 0;
  con = 0;

  // Enable 3.3V regulator (if used)
  // pinMode(REGULATEUR_PIN, OUTPUT);
  // digitalWrite(REGULATEUR_PIN, HIGH);  

  Wire.begin();
  
  if (!sht31.begin(0x44)) {
    // Serial.println("Error: Unable to detect SHT31!");
  }

  ds18b20.begin();
  dht.begin();
  lightMeter.begin();

  // Check presence of both DS18B20 probes
  if (ds18b20.getAddress(ds18b20_1, 0)) {
    ds18b20.setResolution(ds18b20_1, 12);
  }
  if (ds18b20.getAddress(ds18b20_2, 1)) {
    ds18b20.setResolution(ds18b20_2, 12);
  }

  // Initialize the HX711 scale
  scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
  scale.set_scale(calibration_factor);
  scale.set_offset(zero_factor); // No auto tare

  digitalWrite(LED_BUILTIN, HIGH);
  delay(500);
  digitalWrite(LED_BUILTIN, LOW);
}

void loop() {
  if (!connected) {
    modem.begin(EU868); // Reinitialize modem after deep sleep
    int ret = modem.joinOTAA(AppEUI, AppKEY);
    if (ret) {
      connected = true;
      modem.minPollInterval(60);
      modem.dataRate(5);
      delay(100);
      err_count = 0;
    }
  }

  if (connected) {
    /* Read sensors */
    float temp_sht31, hum_sht31;
    float temp_ds18b20_1, temp_ds18b20_2;
    float temp_dht22, hum_dht22;
    float weight, lux, batteryLevel;

    readSHT31(temp_sht31, hum_sht31);
    readDS18B20(temp_ds18b20_1, temp_ds18b20_2);
    readDHT22(temp_dht22, hum_dht22);
    weight = readWeight();
    lux = readLuminosity();
    batteryLevel = readBatteryLevel();

    if (Serial1.available()) {
      percent_queen_present = Serial1.readStringUntil('\n');
    }

    // Convert to short integers (2 decimal precision)
    short temp_sht31_int = (short)(temp_sht31 * 100);
    short hum_sht31_int = (short)(hum_sht31 * 100);
    short temp_ds18b20_1_int = (short)(temp_ds18b20_1 * 100);
    short temp_ds18b20_2_int = (short)(temp_ds18b20_2 * 100);
    short temp_dht22_int = (short)(temp_dht22 * 100);
    short hum_dht22_int = (short)(hum_dht22 * 100);
    short weight_int = (short)(weight * 100);
    short battery_int = (short)(batteryLevel * 100);
    float percent_queen = percent_queen_present.toFloat();
    short percent_queen_int = (short)(percent_queen * 100);

    // Cap lux to uint16 to avoid overflow
    uint16_t lux_int = (uint16_t)(lux);

    // Send via LoRa
    int err = 0;
    modem.beginPacket();
    modem.write((uint8_t*)&temp_sht31_int, sizeof(temp_sht31_int));
    modem.write((uint8_t*)&hum_sht31_int, sizeof(hum_sht31_int));
    modem.write((uint8_t*)&temp_ds18b20_1_int, sizeof(temp_ds18b20_1_int));
    modem.write((uint8_t*)&temp_ds18b20_2_int, sizeof(temp_ds18b20_2_int));
    modem.write((uint8_t*)&temp_dht22_int, sizeof(temp_dht22_int));
    modem.write((uint8_t*)&hum_dht22_int, sizeof(hum_dht22_int));
    modem.write((uint8_t*)&weight_int, sizeof(weight_int));
    modem.write((uint8_t*)&lux_int, sizeof(lux_int));
    modem.write((uint8_t*)&battery_int, sizeof(battery_int));
    modem.write((uint8_t*)&percent_queen_int, sizeof(percent_queen_int));
    err = modem.endPacket();

    if (err > 0) {
      err_count = 0;
    } else {
      err_count++;
      if (err_count > 50) {
        connected = false;
        err_count = 0;
      }
      // Wait for 2 minutes if SF12 used (duty cycle constraint)
      for (int i = 0 ; i < 120 ; i++ ) {
        delay(1000);
      }
    }

    // Check for downlink to update delay
    delay(5000); // Wait for RX1 window
    checkDownlink();

    /* Power optimization */
    modem.sleep();
    scale.power_down();
    LowPower.deepSleep(delayBetweenSends * 1000);
    scale.power_up();
  }
}

LoRa Uplink Decoder

JavaScript
JavaScript function used to decode uplink payloads received via LoRaWAN.
Each sensor value is extracted from the payload and scaled appropriately.

This decoder is used in platforms like The Things Network (TTN) to convert raw payload bytes into meaningful data fields.
function decodeUplink(input) {
  var data = {};
  data.key = "sqwvhtcqsnlaq27y"; // Your API key or device identifier

  data.t = (input.bytes[1] << 8 | (input.bytes[0])) / 100;        // temp_sht31
  data.h = (input.bytes[3] << 8 | (input.bytes[2])) / 100;        // hum_sht31

  data.t_0 = (input.bytes[5] << 8 | (input.bytes[4])) / 100;      // temp_ds18b20_1
  data.t_1 = (input.bytes[7] << 8 | (input.bytes[6])) / 100;      // temp_ds18b20_2

  data.t_2 = (input.bytes[9] << 8 | input.bytes[8]) / 100;        // temp_dht22

  data.t_3 = (input.bytes[11] << 8 | input.bytes[10]) / 100;      // hum_dht22 — was named h_i, now stored as t_3 due to variable issue

  data.weight_kg = (input.bytes[13] << 8 | (input.bytes[12])) / 100; // Hive weight in kg

  data.l = (input.bytes[15] << 8 | (input.bytes[14]));            // Light level (lux)

  data.bv = (input.bytes[17] << 8 | (input.bytes[16])) / 100;     // Battery voltage

  data.t_9 = (input.bytes[19] << 8 | (input.bytes[18])) / 100; // Queen presence percentage

  return {
    data: data,
    warnings: [],
    errors: []
  };
}

AI-based Queen Detection with Edge Impulse & Arduino Nano 33 BLE Sense

Arduino
This Arduino sketch is used to detect the presence of a queen bee inside the hive using sound analysis. It leverages a machine learning model trained with Edge Impulse and runs on the Arduino Nano 33 BLE Sense, equipped with a microphone.
The board listens to hive sounds, performs real-time inference using an MFCC audio model, lights up an LED when the queen is detected, and sends the confidence value to the MKR WAN 1310 via UART to be transmitted via LoRaWAN.
/* Edge Impulse – Queen detection inside the beehive */
#include <PDM.h>  // Microphone
#include "Projet_ruche_3.0_inferencing.h"  // AI model generated by Edge Impulse
#define LED_PIN 13  // Indication LED

typedef struct {
    int16_t *buffer;
    uint8_t buf_ready;
    uint32_t buf_count;
    uint32_t n_samples;
} inference_t;

static inference_t inference;
static signed short sampleBuffer[2048];
static bool debug_nn = false;  // Debug features

void setup() {
    Serial.begin(115200);       // Serial monitor
    Serial1.begin(9600);        // UART link with MKR WAN 1310
    pinMode(LED_PIN, OUTPUT);

    if (!microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT)) {
        // Failed to allocate audio buffer
    }
}

void loop() {
    if (!microphone_inference_record()) {
        // Failed to record audio
    }

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

    EI_IMPULSE_ERROR r = run_classifier(&signal, &result, debug_nn);
    if (r != EI_IMPULSE_OK) {
        // Inference error
    }

    float threshold = 0.75;  // Confidence threshold
    String resultLabel = result.classification[1].label;
    float confidence = result.classification[1].value;

    if (confidence > threshold && resultLabel == "queen_present") {
        digitalWrite(LED_PIN, HIGH);  // Queen detected
    } else {
        digitalWrite(LED_PIN, LOW);   // Queen not detected
    }

    String messageToSend = String(confidence, 2);
    Serial1.println(messageToSend);   // Send confidence to MKR
    Serial.println(messageToSend);    // Optional debug
}

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

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

/* Microphone setup */
static bool microphone_inference_start(uint32_t n_samples) {
    inference.buffer = (int16_t *)malloc(n_samples * sizeof(int16_t));
    if(inference.buffer == NULL) return false;

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

    PDM.onReceive(&pdm_data_ready_inference_callback);
    PDM.setBufferSize(4096);
    if (!PDM.begin(1, EI_CLASSIFIER_FREQUENCY)) {
        microphone_inference_end();
        return false;
    }

    PDM.setGain(127);
    return true;
}

/* Recording audio */
static bool microphone_inference_record(void) {
    inference.buf_ready = 0;
    inference.buf_count = 0;
    while(inference.buf_ready == 0) delay(10);
    return true;
}

/* Convert audio buffer to float */
static int microphone_audio_signal_get_data(size_t offset, size_t length, float *out_ptr) {
    numpy::int16_to_float(&inference.buffer[offset], out_ptr, length);
    return 0;
}

/* Stop microphone */
static void microphone_inference_end(void) {
    PDM.end();
    free(inference.buffer);
}

#if !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_MICROPHONE
#error "Incompatible model: requires microphone input."
#endif

LoRa Downlink Encoder and Decoder

JavaScript
Basic structure for handling downlink messages with The Things Network (TTN).

- `encodeDownlink(input)` prepares a downlink payload to send to the device.
- `decodeDownlink(input)` interprets the downlink payload received by the device.

You can edit the `bytes` array in `encodeDownlink` to change device behavior, such as update the sending interval or trigger an action.
// Function to encode a downlink payload
// You can customize the payload by adding data inside `bytes`
function encodeDownlink(input) {
  return {
    bytes: [],        // Payload to send (array of bytes). Fill this if needed.
    fPort: 1,         // Port number (usually 1 for application data)
    warnings: [],     // Optional warning messages
    errors: []        // Optional error messages
  };
}

// Function to decode a downlink payload
// Useful for debugging and testing the downlink content
function decodeDownlink(input) {
  return {
    data: {
      bytes: input.bytes // Returns the raw bytes as-is
    },
    warnings: [],         // Optional warning messages
    errors: []            // Optional error messages
  };
}

ei-projet_ruche_3.0-arduino-1.0.3.zip

How to install this library: 1. Download the ZIP file using the button above 2. Open the Arduino IDE 3. Go to Sketch > Include Library > Add .ZIP Library... 4. Select the downloaded file 5. The library will now be available in your Arduino > libraries folder and ready to use in your project This library contains the AI model generated with Edge Impulse for queen detection in the hive. It is required to compile the code running on the Nano 33 BLE Sense.

Credits

Papa Talla Dioum
1 project • 3 followers
Contact
Théotime
1 project • 2 followers
Contact
Ayoub Ladjici
0 projects • 2 followers
Contact
Nadir Karem
0 projects • 3 followers
Contact
Ilyes
0 projects • 2 followers
Contact

Comments

Please log in or sign up to comment.