JuanVi
Created August 27, 2025 © GPL3+

MUUU!: Livestock smart collar 🐮

MUUU! is a smart livestock collar with LoRa, GPS, and vital sign sensors to track each animal’s location and health.

ExpertWork in progressOver 4 days67
MUUU!: Livestock smart collar 🐮

Things used in this project

Hardware components

PCBWay Custom PCB
PCBWay Custom PCB
×1
MAX30102 High-Sensitivity Pulse Oximeter and Heart-Rate Sensor for Wearable Health
Maxim Integrated MAX30102 High-Sensitivity Pulse Oximeter and Heart-Rate Sensor for Wearable Health
×1
MAX30205 Human Body Temperature Sensor
Maxim Integrated MAX30205 Human Body Temperature Sensor
×1
SHT40
Sensirion SHT40
×1
Solar Panel (5V/0.5W)
×1
LiPo 18650
×1
Wio Tracker L1 E-ink
Seeed Studio Wio Tracker L1 E-ink
×1

Software apps and online services

PlatformIO IDE
PlatformIO IDE
Arduino IDE
Arduino IDE
STM32CUBEPROG
STMicroelectronics STM32CUBEPROG

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free
3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Bottom_TX

Top_TX

Bottom_RX

Top_RX

Button_1_RX

Button_2_RX

Switch_RX

Schematics

Schematics Main Board

Main Board Layers

Code

main.ino

C/C++
#include "sensors.h"
#include "gps.h"  
#include "json_lora.h"

void setup() {
  Serial.begin(115200);
  while(!Serial);
  delay(100);

  sensors_setup();
  gps_setup();    
  lora_init();
  
  Serial.println("All systems initialized");
}

void loop() {
  sensors_update();
  gps_update();  

 

  float temp = (bodyTemp + ambientTemp) / 2.0;
  temp = roundf(temp * 10) / 10.0;
  
   if (gps.location.isValid()) {
        json_gen(spo2, temp, humidity, heartRate, latitude, longitude);
      
    }
    
    delay(5000); //TX every 5sg
  
}

sensors.h

C/C++
#ifndef SENSORS_H
#define SENSORS_H

#include <Wire.h>
#include "MAX30105.h"
#include "spo2_algorithm.h"
#include "Protocentral_MAX30205.h"
#include "Adafruit_SHT31.h"

// --- Sensors ---
extern MAX30105 particleSensor;    // Heart Rate + SpO2
extern MAX30205 tempSensor;        // Body temp
extern Adafruit_SHT31 sht31;       // ambient temp & hum

// --- Heart Rate vars ---
extern int32_t heartRate;
extern int8_t validHeartRate;
extern int32_t spo2;
extern int8_t validSPO2;

extern uint32_t irBuffer[100];
extern uint32_t redBuffer[100];

// --- Temp and Hum vars ---
extern float bodyTemp;
extern float ambientTemp;
extern int humidity;

// --- Functions ---
void sensors_setup();
void sensors_update();  // update HR, SpO2, temp and hum

#endif

sensors.cpp

C/C++
#include "sensors.h"

// --- Sensors ---
MAX30105 particleSensor;
MAX30205 tempSensor;
Adafruit_SHT31 sht31;

// --- HR vars---
int32_t heartRate;
int8_t validHeartRate;
int32_t spo2;
int8_t validSPO2;

uint32_t irBuffer[100];
uint32_t redBuffer[100];

// --- Temp and Hum vars ---
float bodyTemp;
float ambientTemp;
int humidity;

void sensors_setup() {
    Wire.begin();
    

    // --- MAX30105 ---
    if(!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
        Serial.println(F("MAX30105 no encontrado"));
        while(1);
    }
  particleSensor.setup();
    

    // --- MAX30205 ---
    while(!tempSensor.scanAvailableSensors()) {
        Serial.println(F("No se detecta MAX30205"));
        delay(30000);
    }
    tempSensor.begin();

    // --- SHT31 ---
    if(!sht31.begin(0x44)) {
        Serial.println(F("No se detecta SHT31/SHT30"));
    }
}

void sensors_update() {
    const int bufferLength = 100;

    // --- MAX30105 ---
    for(int i = 25; i < bufferLength; i++) {
        redBuffer[i-25] = redBuffer[i];
        irBuffer[i-25]  = irBuffer[i];
    }

    for(int i = 75; i < bufferLength; i++) {
        while(!particleSensor.available()) particleSensor.check();
        redBuffer[i] = particleSensor.getRed();
        irBuffer[i]  = particleSensor.getIR();
        particleSensor.nextSample();
    }

    maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer,
                                           &spo2, &validSPO2, &heartRate,                                                   &validHeartRate);

    // --- Temp and Hum---
    bodyTemp    = tempSensor.getTemperature();
    ambientTemp = sht31.readTemperature();
    humidity    = sht31.readHumidity();
}

json_lora.h

C/C++
#ifndef JSON_LORA_H
#define JSON_LORA_H

#include <Arduino.h>
#include <RadioLib.h>
#include <ArduinoJson.h>   // For json_gen()

// Init Lora Module (STM32WLx for LoRa-E5)
extern STM32WLx radio;

// Prototipos
void lora_init();
void json_gen(int32_t spo2, float temp, int humedad, int32_t hr, float lat, float lon);
void setFlag(void);

#endif // JSON_LORA_H

json_lora.cpp

C/C++
#include "json_lora.h"
#include <Adafruit_NeoPixel.h>
#include <STM32LowPower.h>
#include "gps.h"

// WS2812 setup
#define PIN_WS2812  PB4
#define NUMPIXELS   1
Adafruit_NeoPixel pixels(NUMPIXELS, PIN_WS2812, NEO_GRB + NEO_KHZ800);

STM32WLx radio = new STM32WLx_Module();

static const uint32_t rfswitch_pins[] = {PA4, PA5, RADIOLIB_NC};
static const Module::RfSwitchMode_t rfswitch_table[] = {
  {STM32WLx::MODE_IDLE,  {LOW,  LOW}},
  {STM32WLx::MODE_RX,    {HIGH, LOW}},
  {STM32WLx::MODE_TX_LP, {HIGH, HIGH}},
  END_OF_MODE_TABLE,
};

bool transmittedFlag = false;

// ************************************************************************************************
void lora_init() {
    
    
    
    Serial.println("**** LoRa Init ****");

    pixels.begin();
    pixels.show();

    radio.setRfSwitchTable(rfswitch_pins, rfswitch_table);

    int state = radio.begin(868, 62.5, 12, 5, 0x12, 14, 8, 1.7, 0);
    if (state == RADIOLIB_ERR_NONE) {
        Serial.println("radio.begin() success!");
    } else {
        Serial.print("radio.begin() failed, code ");
        Serial.println(state);
        while(true) {
            pixels.setPixelColor(0, pixels.Color(255, 0, 0));
            pixels.show();
            delay(100);
            pixels.setPixelColor(0, pixels.Color(0, 0, 0));
            pixels.show();
            delay(100); 
        }
    }

    radio.setDio1Action(setFlag);
    LowPower.begin();
}

// ************************************************************************************************
void setFlag(void) {
    uint16_t irqstatus = radio.getIrqStatus();
    if(irqstatus == RADIOLIB_SX126X_IRQ_TX_DONE) {
        transmittedFlag = true;
    }
}


void json_gen(int32_t spo2, float temp, int humedad, int32_t hr, float lat, float lon) {
    char payload[256]; // Buffer for JSON
    memset(payload, 0, sizeof(payload));
    // Create JSON object
    StaticJsonDocument<256> doc;

    // GPS hour and date to string
    String timeString = String(gps.time.hour() + 2) + ":" + String(gps.time.minute()) + ":" + String(gps.time.second());
    String dateString = String(gps.date.day()) + "/" + String(gps.date.month()) + "/" + String(gps.date.year());

    // Add fields to Json
    doc["time"] = timeString;
    doc["date"] = dateString;
    doc["lat"] = lat;
    doc["lon"] = lon;
    doc["temp"] = temp;
    doc["humedad"] = humedad;
    doc["hr"] = hr;
    doc["spo2"] = spo2;

    // Serializar el JSON al buffer
    size_t jsonLength = serializeJson(doc, payload, sizeof(payload));
    payload[jsonLength] = '\0'; // TERMINADOR NULO

    Serial.write((uint8_t*)payload, jsonLength);
    Serial.println();

   transmittedFlag = false;
    int state = radio.startTransmit((uint8_t*)payload, jsonLength);

    pixels.setPixelColor(0, pixels.Color(0, 255, 0));  // Led verde transmisin
    pixels.show();

    // Wait until TX is finished
    while(!transmittedFlag) {
        while(SubGhz.isBusy());
    }

    pixels.setPixelColor(0, pixels.Color(0, 0, 0));
    pixels.show();

    if(state == RADIOLIB_ERR_NONE) {
        Serial.println("JSON transmitted successfully!");
    } else {
        Serial.print("Transmission failed, code: ");
        Serial.println(state);
    }
}

gps.h

C/C++
#ifndef GPS_H
#define GPS_H

#include <TinyGPS++.h>
#include <HardwareSerial.h>

// Declaracion de la interfaz UART para GPS
extern HardwareSerial GPS_LPUART;

// Objeto GPS global
extern TinyGPSPlus gps;
extern double latitude;
extern double longitude;

// Inicializacin del GPS
void gps_setup();

// Actualizacin y lectura de datos GPS
void gps_update();

#endif // GPS_H

gps.cpp

C/C++
#include "gps.h"

// Inicializar LPUART en PC0 (TX) y PC1 (RX) para GPS
HardwareSerial GPS_LPUART{PC0, PC1};
TinyGPSPlus gps;

double latitude = 0.0;
double longitude = 0.0;

void gps_setup()
{
  
  Serial.println("Iniciando sistema GPS...");

  GPS_LPUART.begin(9600); // Inicializar UART del GPS
  
  Serial.print("Esperando seal GPS...");
 

  while (latitude == 0.0 && longitude == 0.0 && gps.satellites.value() == 0)
  {
    while (GPS_LPUART.available())
    {
      if (gps.encode(GPS_LPUART.read()))
      {
        latitude = gps.location.lat();
        longitude = gps.location.lng();
        Serial.print(".");
        //Serial.print("Latitud: "); Serial.println(latitude, 6);
        //Serial.print("Longitud: "); Serial.println(longitude, 6);
      }
    }
  }
  Serial.print("\nGPS Inicializado Coordenadas: ");
  Serial.print("Latitud: "); Serial.println(latitude, 6);
  Serial.print("Longitud: "); Serial.println(longitude, 6);


}
void gps_update()
{
    // Esperar hasta que haya datos disponibles y vlidos
    while (true)
    {
        while (GPS_LPUART.available())
        {
            if (gps.encode(GPS_LPUART.read()))
            {
                // Actualizar latitud y longitud solo si son vlidas
                if (gps.location.isUpdated() && gps.time.isUpdated())
                {
                    latitude = gps.location.lat();
                    longitude = gps.location.lng();
            
                    return; // Datos vlidos, salir de la funcin
                }
            }
        }
        delay(10); // Evitar bloqueo total y darle tiempo al GPS
    }
}

LORA_RX.ino

C/C++
RX Code, using a Wio E5 Mini plan to use the Wio Tracker L1 E-Ink
// BSP : STM32 boards groups by STMicroelectronics 2.7.1
// Board select : STM Boards groups / LoRa boards
// Board part number : LoRa-E5 mini

#include <RadioLib.h>           // 6.4.2  https://github.com/jgromes/RadioLib
#include <ArduinoJson.h>        // https://arduinojson.org/

// RM0461 Reference manual Rev 8  37.1.4 IEEE 64-bit unique device ID register (UID64)
#define MASTER_UID  *((uint32_t*)0x1FFF7580)    // use uid64 for Master device UID
#define LED_RED          PB5

STM32WLx radio = new STM32WLx_Module();

static const uint32_t rfswitch_pins[] = {PA4, PA5, RADIOLIB_NC};
static const Module::RfSwitchMode_t rfswitch_table[] = {
  {STM32WLx::MODE_IDLE,  {LOW,  LOW}},
  {STM32WLx::MODE_RX,    {HIGH, LOW}},
  {STM32WLx::MODE_TX_HP, {LOW, HIGH}},  // for LoRa-E5 mini
  END_OF_MODE_TABLE,
};

#define dataNum   256            // data bytes
union unionData {                   // for data type conversion
  uint32_t  dataBuff32[dataNum/4];
  float     dataBuff32F[dataNum/4];
  int16_t   dataBuff16[dataNum/2];
  uint8_t   dataBuff8[dataNum];
};
union unionData ud;

String rxdata = "";          // received data packet string
bool receivedFlag = false;   // flag that a packet was received

// *******************************************************************************************************
void setup() {
  Serial.begin(115200);
  while(!Serial);
  delay(2000);

  pinMode(LED_RED, OUTPUT);         // built-in LED
  digitalWrite(LED_RED, HIGH);

  // STM32WL initialization
  radio.setRfSwitchTable(rfswitch_pins, rfswitch_table);

  // LoRa begin(frequency, bandwidth, spreading factor, coding rate, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO)
  int state =  radio.begin(868, 62.5, 12, 5, 0x12, 14, 8, 1.7, 0);

  if (state == RADIOLIB_ERR_NONE) {
    Serial.println("radio.begin() success!");
  } else {
    Serial.print("failed, code ");
    Serial.println(state);
    while(true) {
      digitalWrite(LED_RED, LOW);
      delay(100);
      digitalWrite(LED_RED, HIGH);
      delay(100);      
    }
  }

  // callback function when received or transmitted
  radio.setDio1Action(setFlag);
}

// callback function when received or transmitted
void setFlag(void) 
{
  uint16_t irqstatus = radio.getIrqStatus();
  if(irqstatus == RADIOLIB_SX126X_IRQ_RX_DONE) {
    receivedFlag = true;
  } else {
    receivedFlag = false;
  }
}

// ***********************************************************************************************************
void loop() 
{
  Serial.println("Waiting for incoming Slave transmitting packet ... ");

  // start listening for LoRa packets
  int state = radio.startReceive();
  if (state != RADIOLIB_ERR_NONE) {
    Serial.print("failed, code ");
    Serial.println(state);
    while (true) {
      digitalWrite(LED_RED, LOW);
      delay(100);
      digitalWrite(LED_RED, HIGH);
      delay(100);    
    }
  }

  // wait until packet is received
  while(!receivedFlag) {
    while(SubGhz.isBusy());
  }
  receivedFlag = false;

  // read a packet
  state = radio.readData(ud.dataBuff8, dataNum);

  if (state == RADIOLIB_ERR_NONE) {
    // convert received bytes to String (stop at null terminator)
    rxdata = "";
    for(int i = 0; i < dataNum; i++) {
      if(ud.dataBuff8[i] == 0) break;
      rxdata += (char)ud.dataBuff8[i];
    }

    int rssi = radio.getRSSI();
    int snr  = radio.getSNR();

    Serial.println("Received raw JSON:");
    Serial.println(rxdata);

    // parse JSON
    StaticJsonDocument<256> doc;  // ajustar tamao segn JSON
    DeserializationError error = deserializeJson(doc, rxdata);

    if (error) {
      Serial.print("JSON parse failed: ");
      Serial.println(error.c_str());
    } else {
      // leer campos del JSON
      const char* time   = doc["time"];
      const char* date   = doc["date"];
      float lat          = doc["lat"] | 0.0;
      float lon          = doc["lon"] | 0.0;
      float temp         = doc["temp"] | 0.0;
      int humedad        = doc["humedad"] | 0;
      int hr             = doc["hr"] | 0;
      int spo2           = doc["spo2"] | 0;

      // imprimir valores
      Serial.println("Parsed JSON data:");
      Serial.print("Time: "); Serial.println(time);
      Serial.print("Date: "); Serial.println(date);
      Serial.print("Latitude: "); Serial.println(lat, 8);
      Serial.print("Longitude: "); Serial.println(lon, 8);
      Serial.print("Temperature: "); Serial.println(temp);
      Serial.print("Humedad: "); Serial.println(humedad);
      Serial.print("Heart Rate: "); Serial.println(hr);
      Serial.print("SpO2: "); Serial.println(spo2);
      Serial.printf("RSSI: %d dBm, SNR: %d dB\n", rssi, snr);
    }
  } else {
    Serial.print("failed to read packet, code ");
    Serial.println(state);
  }

  delay(100);
}

Credits

JuanVi
4 projects • 6 followers

Comments