Makerfabs Soil Moisture Sensor v3, LoRaWAN TTN v3 and HA Integration

resume from post #1

I2C AHT LIBRARY
Caveat: The below instructions assume you are using the Arduino IDE.

i2c_aht10.cpp
Create a new tab and call it I2C_AHT10.cpp. Paste the following in it.

/****************************************************************

 ***************************************************************/

#include "I2C_AHT10.h"

/*--------------------------- Device Status ------------------------------*/
bool AHT10::begin(TwoWire &wirePort)
{
    _i2cPort = &wirePort; //Grab the port the user wants to communicate on

    _deviceAddress = AHT10_DEFAULT_ADDRESS; //We had hoped the AHT10 would support two addresses but it doesn't seem to

    if (isConnected() == false)
        return false;

    //Wait 40 ms after power-on before reading temp or humidity. Datasheet pg 8
    delay(40);

    //Check if the calibrated bit is set. If not, init the sensor.
    if (isCalibrated() == false)
    {
        //Send 0xBE0800
        initialize();

        //Immediately trigger a measurement. Send 0xAC3300
        triggerMeasurement();

        delay(75); //Wait for measurement to complete

        uint8_t counter = 0;
        while (isBusy())
        {
            delay(1);
            if (counter++ > 100)
                return (false); //Give up after 100ms
        }

        //This calibration sequence is not completely proven. It's not clear how and when the cal bit clears
        //This seems to work but it's not easily testable
        if (isCalibrated() == false)
        {
            return (false);
        }
    }

    //Check that the cal bit has been set
    if (isCalibrated() == false)
        return false;

    //Mark all datums as fresh (not read before)
    sensorQueried.temperature = true;
    sensorQueried.humidity = true;

    return true;
}

//Ping the AHT10's I2C address
//If we get a response, we are correctly communicating with the AHT10
bool AHT10::isConnected()
{
    _i2cPort->beginTransmission(_deviceAddress);
    if (_i2cPort->endTransmission() == 0)
        return true;

    //If IC failed to respond, give it 20ms more for Power On Startup
    //Datasheet pg 7
    delay(20);

    _i2cPort->beginTransmission(_deviceAddress);
    if (_i2cPort->endTransmission() == 0)
        return true;

    return false;
}

/*------------------------ Measurement Helpers ---------------------------*/

uint8_t AHT10::getStatus()
{
    _i2cPort->requestFrom(_deviceAddress, (uint8_t)1);
    if (_i2cPort->available())
        return (_i2cPort->read());
    return (0);
}

//Returns the state of the cal bit in the status byte
bool AHT10::isCalibrated()
{
    return (getStatus() & (1 << 3));
}

//Returns the state of the busy bit in the status byte
bool AHT10::isBusy()
{
    return (getStatus() & (1 << 7));
}

bool AHT10::initialize()
{
    _i2cPort->beginTransmission(_deviceAddress);
    _i2cPort->write(sfe_aht10_reg_initialize);
    _i2cPort->write(0x80);
    _i2cPort->write(0x00);
    if (_i2cPort->endTransmission() == 0)
        return true;
    return false;
}

bool AHT10::triggerMeasurement()
{
    _i2cPort->beginTransmission(_deviceAddress);
    _i2cPort->write(sfe_aht10_reg_measure);
    _i2cPort->write(0x33);
    _i2cPort->write(0x00);
    if (_i2cPort->endTransmission() == 0)
        return true;
    return false;
}

//Loads the
void AHT10::readData()
{
    //Clear previous data
    sensorData.temperature = 0;
    sensorData.humidity = 0;

    if (_i2cPort->requestFrom(_deviceAddress, (uint8_t)6) > 0)
    {
        uint8_t state = _i2cPort->read();

        uint32_t incoming = 0;
        incoming |= (uint32_t)_i2cPort->read() << (8 * 2);
        incoming |= (uint32_t)_i2cPort->read() << (8 * 1);
        uint8_t midByte = _i2cPort->read();

        incoming |= midByte;
        sensorData.humidity = incoming >> 4;

        sensorData.temperature = (uint32_t)midByte << (8 * 2);
        sensorData.temperature |= (uint32_t)_i2cPort->read() << (8 * 1);
        sensorData.temperature |= (uint32_t)_i2cPort->read() << (8 * 0);

        //Need to get rid of data in bits > 20
        sensorData.temperature = sensorData.temperature & ~(0xFFF00000);

        //Mark data as fresh
        sensorQueried.temperature = false;
        sensorQueried.humidity = false;
    }
}

//Triggers a measurement if one has not been previously started, then returns false
//If measurement has been started, checks to see if complete.
//If not complete, returns false
//If complete, readData(), mark measurement as not started, return true
bool AHT10::available()
{
    if (measurementStarted == false)
    {
        triggerMeasurement();
        measurementStarted = true;
        return (false);
    }

    if (isBusy() == true)
    {
        return (false);
    }

    readData();
    measurementStarted = false;
    return (true);
}

bool AHT10::softReset()
{
    _i2cPort->beginTransmission(_deviceAddress);
    _i2cPort->write(sfe_aht10_reg_reset);
    if (_i2cPort->endTransmission() == 0)
        return true;
    return false;
}

/*------------------------- Make Measurements ----------------------------*/

float AHT10::getTemperature()
{
    if (sensorQueried.temperature == true)
    {
        //We've got old data so trigger new measurement
        triggerMeasurement();

        delay(75); //Wait for measurement to complete

        uint8_t counter = 0;
        while (isBusy())
        {
            delay(1);
            if (counter++ > 100)
                return (false); //Give up after 100ms
        }

        readData();
    }

    //From datasheet pg 8
    float tempCelsius = ((float)sensorData.temperature / 1048576) * 200 - 50;

    //Mark data as old
    sensorQueried.temperature = true;

    return tempCelsius;
}

float AHT10::getHumidity()
{
    if (sensorQueried.humidity == true)
    {
        //We've got old data so trigger new measurement
        triggerMeasurement();

        delay(75); //Wait for measurement to complete

        uint8_t counter = 0;
        while (isBusy())
        {
            delay(1);
            if (counter++ > 100)
                return (false); //Give up after 100ms
        }

        readData();
    }

    //From datasheet pg 8
    float relHumidity = ((float)sensorData.humidity / 1048576) * 100;

    //Mark data as old
    sensorQueried.humidity = true;

    return relHumidity;
}

i2c_AHT10.h
Create a new tab and call it I2C_AHT10.h. Paste the following in it.

/****************************************************************
 * 
 ******************************************************************/

#ifndef __I2C_AHT10_H__
#define __I2C_AHT10_H__

#include <Arduino.h>
#include <Wire.h>

#define AHT10_DEFAULT_ADDRESS 0x38

enum registers
{
    sfe_aht10_reg_reset = 0xBA,
    sfe_aht10_reg_initialize = 0xBE,
    sfe_aht10_reg_measure = 0xAC,
};

class AHT10
{
private:
    TwoWire *_i2cPort; //The generic connection to user's chosen I2C hardware
    uint8_t _deviceAddress;
    bool measurementStarted = false;

    struct
    {
        uint32_t humidity;
        uint32_t temperature;
    } sensorData;

    struct
    {
        uint8_t temperature : 1;
        uint8_t humidity : 1;
    } sensorQueried;

public:
    //Device status
    bool begin(TwoWire &wirePort = Wire); //Sets the address of the device and opens the I2C bus
    bool isConnected();                   //Checks if the AHT10 is connected to the I2C bus
    bool available();                     //Returns true if new data is available

    //Measurement helper functions
    uint8_t getStatus();       //Returns the status byte
    bool isCalibrated();       //Returns true if the cal bit is set, false otherwise
    bool isBusy();             //Returns true if the busy bit is set, false otherwise
    bool initialize();         //Initialize for taking measurement
    bool triggerMeasurement(); //Trigger the AHT10 to take a measurement
    void readData();           //Read and parse the 6 bytes of data into raw humidity and temp
    bool softReset();          //Restart the sensor system without turning power off and on

    //Make measurements
    float getTemperature(); //Goes through the measurement sequence and returns temperature in degrees celcius
    float getHumidity();    //Goes through the measurement sequence and returns humidity in % RH
};
#endif

WORTHY OF NOTE

  1. If you order a soil moisture LoRa sensor from Makerfabs, I highly recommend you ordering theirs CP2104 USB to Serial Converter Arduino Programmer (CP2104 USB to Serial Converter Arduino Programmer | Makerfabs). This is unless you have a CH340G or similar at hand. Just note that it may not have the DTR pin/signal. In that case you need to reset the device once the Arduino IDE goes into uploading mode after compilation.
    You also might need to cross TX/RX connection between devices that’s, RX on the sensor to TX on the USB device and TX on the sensor to RX on the USB device.
  2. Follow the sketch uploading instructions found on Makerfabs wiki (https://www.makerfabs.com/wiki/index.php?title=Lora_Soil_Moisture_Sensor_V3)
  3. Advantage of using a water tank:
    a. Limit possible water damage to the content of the tank.
    b. The water can be enriched with fertilizers.
  4. Water tank with water valve: I experienced water pressure problems at the end of the watering line caused by diverse circumstances. The lack of water pressure was not observed if no valve was used that is, having the watering hose connected directly to the water tank therefore, a water pump is used instead.

FAQ

  1. Why don’t you post the sensor code in GitHub?
    That’s a good question… next question.

  2. Why don’t you explain how you build the LoRaWAN gateway?
    Reference: Building a gateway with Raspberry Pi and IC880A | The Things Stack for LoRaWAN

  3. Would you recommend Makerfabs and/or this LoRa Soil Moisture product?
    Based on my personal experience, yes.

  4. Would you suggest any improvement to this Makerfabs LoRa Soil Moisture product?
    I don’t want to spin out on this topic because the product is very affordable and perfectly fit for purpose for aficionados and hobbyists however, I wonder if there is a possible “no cost” improvement of the PCB capacitance of the sensor by using the design published by acolomitchi here: https://www.instructables.com/Automatic-Watering-System-With-Capacitive-Probe-an/
    Update Jan-2023: The circuit shows it would not make sense changing the design of the sensor as it attends to voltage (analog DC voltage), not to changes in frequency. IMG_0312
    D9: 2 MHz input
    A2: Analog output

APPENDIX #1: Measuring bandgap voltage for accurate battery voltage measurement
DISCLAIMER
• This step is not strictly necessary and, if not properly done, can worsen the results.
• The usage of a well calibrated multimeter is a must otherwise worsen results are to be provided.

BACKGROUND
The ATmega328 bandgap reference voltage is expected to be 1.1v however unexpected small variations may happen from chip to chip. The guide here provides instructions to find the bandgap reference value of your chip and so to configure the sketch accordingly.

Bandgap Voltage Sketch. Directions
1 Load the following sketch in the sensor
2 Make use of a multimeter and measure voltage on pin 20 of the ATmega328 processor with reference to ground.
3 Take note of value and multiply it by 1000, e.g. 1.101v would be 1101
4 Set this value in variable InternalReferenceVoltage in the calibration sketch and upload the sketch to the sensor.
5 Check “BAT VOL” value accuracy.
6 If satisfied, set this same value in the same variable found in the main sketch.

void setup( void ) {
  Serial.begin(9600);
  Serial.println("\r\n\r\n");
  analogReference(INTERNAL);
}

void loop( void ) {
  Serial.println(analogRead(0));
  delay(1000);
}

APPENDIX #2: Testing another probe on Makerfabs sensor
Even when the resolution of the original probe provides a downscaling of 3:1 I wondered how a change (improvement?) of its design could affect the accuracy of the device. For this, I made use of the design provided by acolomitchi. I cut the original probe off the sensor and glued and solder the new one:

I found out the downscaling improved to 4:1.

My conclusion based on my own observations is:

  • This design could be useful if high resolution is needed.
  • I observe the values are more stable (less fluctuation) than with the other sensors (probe).
  • The current Makerfabs probe design is just fine.

APPENDIX #3: Recover a bricked sensor
DISCLAIMER

  • The information herein is provided as-is that means “I got it to work following the below directions and so no comprehensive documentation about drivers, versions, etc. are provided”.
  • These directions shall be read as “a possible way to solve a situation” rather than “the way to solve the problem I have”.
  • The below procedure makes use of an USBasp programmer.

ACKNOWLEDGMENT
I want to thank Makerfab for providing me with the bootloader file as well as the fuse configuration. Without Makerfabs help this appendix would not be possible as well as I would have an inoperative sensor at home.

DIRECTIONS
0) Solder a six pin connector to the sensor as shown in the picture and plug the USBasp device to it.
Connector:


USBasp:

  1. Install avrdude
  2. Download file bootloader.rar from here: Dropbox
  3. Open the command prompt and execute the following commands:

Configure Low fuse

avrdude -c usbasp-clone -p m328p -U lfuse:w:0xFF:m

Configure High fuse

avrdude -c usbasp-clone -p m328p -U hfuse:w:0xDA:m

Configure Extended fuse

avrdude -c usbasp-clone -p m328p -U efuse:w:0xFE:m

Flash bootloader

avrdude -c usbasp -p m328p -U flash:w:m328p.hex:i  

m328p.hex is the name of the bootloader file that should be on same folder as avrdude.exe

Check status

avrdude -c usbasp -p m328p -b 19200 -v

BRICKED. HOW IT HAPPENED
Under the Worthy of Note section above I wrote: I highly recommend you ordering theirs CP2104 USB to Serial Converter Arduino Programmer (CP2104 USB to Serial Converter Arduino Programmer | Makerfabs 8). This is unless you have a CH340G […] In that case you need to reset the device once the Arduino IDE goes into uploading mode after compilation… well, I make use of a CH340G. I got it bricked because I pressed the reset button one too many times at the wrong moment, just during initial negotiation period. Moral of the story: either to press&hold the reset button before compiling and then release it when Uploading... shows up or even better, buy Makerfabs CP2104 USB to Serial Converter Arduino Programmer, then you don’t need to hassle with the reset button procedure.

APPENDIX #4: Battery & Voltage Note
The ATmega328p operating voltage goes from 2.7v to 5.5v. This means the following:

  1. AAA NiMH (1.2v) batteries should not be used. The device would work but the soil humidity value would go up around 17 percent points (too low voltage).
  2. Alkaline batteries should be replaced when the supplied voltage (both batteries) is 2.7v. This means, replacing the batteries when the cell voltage is about 1.35v that is, circa 30% of the battery capacity has been used.
  3. Using three AAA alkaline batteries, as in the “red” LoRaWAN model, would lead to replacing the batteries when the cell voltage is 0.9v (0.9 x 3 = 2.7v). An alkaline cell is considered to be depleted when its voltage is 1.0v.

APPENDIX #5: V2 Better Battery Life Hack
Caveat: I have not implemented this hack myself although it is in my to-do list.

The ATmega328P has the following operating voltages: [email protected], [email protected], [email protected]. The V2 operates at 8MHz therefore the minimum operating voltage is 2.7v. At the same time, the RFM98 LoRa transceiver has an operating range from 1.8v to 3.7v.

The hack would consist of:

  1. replacing the 8MHz cristal by a 4MHz cristal.
  2. change the ATmega328P bootloader accordingly to operate it at 4MHz.

Please enlighten me if my above suppositions are wrong or incomplete.