Long range pulse water meter sensor using Moteino/LoRa RF95/MySensors/Domoticz/RasPi3

  • My first arduino project. I work at a winery where water meters are have to be checked daily, and are far apart/ hard to get to. This will make it so manual checks only need to be performed weekly. I would love for you guys to let me know how I could refine my code (It's a bit of a patchwork of MySensor examples and Felix's moteino examples, with some additions and tweaking to work with the LoRa radios). I'm also looking for input on the best way to add more nodes, as right now the reciever/gateway is only setup for sensor. Thanks for being an awesome community!


    // 915MHZ Wireless Pulse Water Meter Sensor
    // Stag's Leap Wine Cellars
    // Rev. 1.0 -- Max Kilpatrick
    #include <EEPROM.h>
    #include <TimerOne.h>
    #include <RH_RF95.h>
    #define NODEID       22  //network ID used for this unit
    #define NETWORKID    99  //the network ID we are on
    #define GATEWAYID     1  //the node ID we're sending to
    #define LED           9
    #define INPUTPIN      1  //INT1 = digital pin 3 (must be an interrupt pin!)
    #define RFM95_CS 10
    #define RFM95_RST 9
    #define RFM95_INT 2
    #define SERIAL_EN        //uncomment this line to enable serial IO (when you debug Moteino and need serial output)
    #define SERIAL_BAUD  57600
    #define RF95_FREQ 915.0
    RH_RF95 rf95(RFM95_CS, RFM95_INT); // Initialize radio
    int ledState = LOW;
    volatile unsigned long PulseCounterVolatile = 0; // use volatile for shared variables
    unsigned long PulseCounter = 0;
    float GAL;
    byte PulsesPerGAL = 1;
    volatile unsigned long NOW = 0;
    unsigned long LASTMINUTEMARK = 0;
    unsigned long PULSECOUNTLASTMINUTEMARK = 0; //keeps pulse count at the last minute mark
    unsigned long COUNTERADDRBASE = 8; //address in EEPROM that points to the first possible slot for a counter
    unsigned long COUNTERADDR = 0;     //address in EEPROM that points to the latest Counter in EEPROM
    byte secondCounter = 0;
    unsigned long TIMESTAMP_pulse_prev = 0;
    unsigned long TIMESTAMP_pulse_curr = 0;
    int GPMthreshold = 8000;         // GPM will reset after this many MS if no pulses are registered
    int XMIT_Interval = 5000;        // GPMthreshold should be less than 2*XMIT_Interval
    int pulseAVGInterval = 0;
    int pulsesPerXMITperiod = 0;
    float GPM=0;
    byte sendLen;
    long lastDebounce0 = 0;
    long debounceDelay = 1000;                        // Ensure accurate pulse count
    void setup() {
      pinMode(LED, OUTPUT);
      pinMode(RFM95_RST, OUTPUT);
      digitalWrite(RFM95_RST, HIGH);
      //initialize counter from EEPROM
     unsigned long savedCounter = EEPROM_Read_Counter(); // Unsigned long EEPROM_Read_Counter()
      if (savedCounter <=0) savedCounter = 1;            // Avoid division by 0
      PulseCounterVolatile = PulseCounter = PULSECOUNTLASTMINUTEMARK = savedCounter;
      attachInterrupt(INPUTPIN, pulseCounter, RISING);   // Interrupt when rising edge of pulse is sensed
      Timer1.initialize(XMIT_Interval * 1000L);
      digitalWrite(RFM95_RST, LOW);
      digitalWrite(RFM95_RST, HIGH);
        if (!rf95.setFrequency(RF95_FREQ)) {
        Serial.println("setFrequency failed");
        while (1);
      Serial.print("Set Freq to: "); Serial.println(RF95_FREQ);
      while (!rf95.init()) {
        Serial.println("LoRa radio init failed");
        while (1);
      Serial.println("LoRa radio init OK!");
      rf95.setTxPower(23);                    // Set power of radio
      #ifdef SERIAL_EN                        // Is serial output enabled?
        Serial.begin(SERIAL_BAUD);            // Then set baud rate
    void XMIT(void)
      PulseCounter = PulseCounterVolatile;
      if (millis() - TIMESTAMP_pulse_curr >= 5000) // Change LED state if over 5 seconds
      {  ledState = !ledState; digitalWrite(LED, ledState); }
      // Calculate Gallons counter 
      GAL = ((float)PulseCounter)/PulsesPerGAL;    
      // Print current pulse and gallon count to serial
      Serial.print("PulseCounter:");Serial.print(PulseCounter);Serial.print(", GAL: "); Serial.println(GAL); 
      String tempstr = String("");    // tempstr eventually gets sent through radio to reciever
      tempstr += (unsigned long)GAL;  // It contains the flow and volume date in the form of a string
      tempstr += '.';
      tempstr += ((unsigned long)(GAL * 100)) % 100;
      tempstr += ',';
      // Calculate & output GPM
      GPM = pulseAVGInterval > 0 ? 60.0 * 1000 * (1.0/PulsesPerGAL)/(pulseAVGInterval/pulsesPerXMITperiod)
                                 : 0;
      pulsesPerXMITperiod = 0;
      pulseAVGInterval = 0;
      tempstr += " ";
      tempstr += (int)GPM;
      tempstr += '.';
      tempstr += ((int)(GPM * 100)) % 100;
      tempstr += '!';
      secondCounter += XMIT_Interval/1000;
      // Once per minute, output a GallonsLastMinute count
      // This is not currently needed, keeping for later possible use
      if (secondCounter>=60)
        //tempstr += " GLM:";
        float GLM = ((float)(PulseCounter - PULSECOUNTLASTMINUTEMARK))/PulsesPerGAL;
    //    tempstr += (int)GLM;
    //    tempstr += '.';
    //    tempstr += ((int)(GLM * 100)) % 100;
      // Make char array from tempstr (Flow and Vol data)
      tempstr.toCharArray(sendBuf, RH_RF95_MAX_MESSAGE_LEN); 
      sendLen = tempstr.length();
      // Send data to reciever and print to serial
      rf95.send(sendBuf, sizeof(sendBuf));
    void loop() {}
    void pulseCounter(void)
      if( (millis() - lastDebounce0) > debounceDelay){ // Make sure we aren't double counting pulses
      ledState = !ledState;
      PulseCounterVolatile++;  // increase when LED turns on
      digitalWrite(LED, ledState);
      NOW = millis();
      //remember how long between pulses (sliding window)
      TIMESTAMP_pulse_prev = TIMESTAMP_pulse_curr;
      TIMESTAMP_pulse_curr = NOW;
      if (TIMESTAMP_pulse_curr - TIMESTAMP_pulse_prev > GPMthreshold)
        //more than 'GPMthreshold' seconds passed since last pulse... resetting GPM
        pulseAVGInterval += TIMESTAMP_pulse_curr - TIMESTAMP_pulse_prev;
        lastDebounce0 = millis();
    // EEPROM deals with writing to memory, keep in mind there are a limited amount of writes
    unsigned long EEPROM_Read_Counter()
      return EEPROM_Read_ULong(EEPROM_Read_ULong(COUNTERADDR));
    void EEPROM_Write_Counter(unsigned long counterNow)
      if (counterNow == EEPROM_Read_Counter()) 
        Serial.print("{EEPROM-SKIP(no changes)}");
        return; // Skip write if nothing changed, extends life of memory
      unsigned long CounterAddr = EEPROM_Read_ULong(COUNTERADDR);
        CounterAddr = COUNTERADDRBASE;
      else CounterAddr += 8;
      EEPROM_Write_ULong(CounterAddr, counterNow);
      EEPROM_Write_ULong(COUNTERADDR, CounterAddr);
    unsigned long EEPROM_Read_ULong(int address)
      unsigned long temp;
      for (byte i=0; i<8; i++)
        temp = (temp << 8) + EEPROM.read(address++);
      return temp;
    void EEPROM_Write_ULong(int address, unsigned long data)
      for (byte i=0; i<8; i++)
        EEPROM.write(address+7-i, data);
        data = data >> 8;
    ```// 915MHZ Wireless Sensor Reciever
    // Stag's Leap Wine Cellars
    // Rev. 1.0 -- Max Kilpatrick
    #define MY_GATEWAY_SERIAL                // MyMessage is sent through serial
    #define NODE_ID 1                         // Which node is this
    #include <MySensors.h>
    #include <SPI.h>
    #include <RH_RF95.h>
    MyMessage msg(NODE_ID, V_FLOW);           // Set's up mesaaging protocol to reciever, lets it know what variables are used
    MyMessage vmsg(NODE_ID,V_VOLUME);         // V_FLOW means that the data being sent is a flowrate, etc.
    RH_RF95 rf95;                             // Initialize radio
    String str;                               // Strings for parsing data
    String str1; 
    String str2;
    int led = 9;                              // Location of LED
    char array[20];                           // Array for parsing data
    float vol;                                // Volume data stored here
    float flow;                               // Flow rate data stored here
    void setup()                              // Setup serial output, etc.
        pinMode(led, OUTPUT);                 // Make LED blink
        Serial.begin(115200);                 // Sets baud rate
        while (!Serial) ;                     // Wait for serial port to be available
      if (!rf95.init())                       // Checks if radio is working
        Serial.println("init failed"); 
    void presentation()                       // Tell controller how to handle input
      sendSketchInfo("Water Meter", "1.1");   // Tells controller (domoticz) name of program
      present(NODE_ID, S_WATER);              // Tells controller which node data is coming from, as well as how to view the data
    void loop()
      String str1 = "";                       // Strings for parsing data from sensor
      String str2 = "";
      if (rf95.available())                   // When input from radio comes in
        // Should be a message for us now   
        uint8_t buf[RH_RF95_MAX_MESSAGE_LEN]; // Load incoming radio data to buffer
        uint8_t len = sizeof(buf);            // Determine size of buffer
        char array[len];                      // Array for parsing incoming data
        strcpy(array, (char*)buf);            // Copy buffer into array
        for(int i = 0; array[i] != 44; i++){  // Copy data from array into a string until a comma is reached
        str1 += array[i];                     // This string has the data for the total gallons used
        int str_len = str1.length() + 1;      // Determine size of first string (where to start reading next data)
        for(int i = str_len + 1; array[i] != 33; i++){ // Copy data from array into another string until end of flow data (denote end of data with !)
        str2 += array[i];                     // This string has data for current flow rate
        float vol = str1.toFloat();           // Converts data from string to floating point number
        send(vmsg.set(vol, 2));               // Send volume data to controller with 2 decimal points
        float flow = str2.toFloat();
        send(msg.set(flow, 2));
    //    Serial.println(flow);         <-----// Prints flow and vol to serial: uncomment for debugging purposes
    //    Serial.println(vol);
        if (rf95.recv(buf, &len))
         digitalWrite(led, HIGH);             // Turns on LED
    //      RH_RF95::printBuffer("request: ", buf, len); // Acknowledge request from reciever, not necessary, but leaving for futur use
    //      Serial.print("got request: ");
    //      Serial.println((char*)buf);       // Prints what is in buffer
    //      Serial.print("RSSI: ");           // Signal strength, uncomment for testing (along with line below)
    //      Serial.println(rf95.lastRssi(), DEC);
          // Send a reply
          uint8_t data[] = "Yo, I got the data."; //Sends reply to sensor, leaving for testing purposes
          rf95.send(data, sizeof(data));
          Serial.println("Sent a reply");
           digitalWrite(led, LOW);             // Turns off the LED
          Serial.println("recv failed");       // Lets us know if not recieving messages

  • Admin

    Welcome @maxk1236

    Try to inline the code in the post instead of adding it as an attachment. To messy for people to pick it up and read.

  • @hek Fixed, thanks. Forgot about the nice formatting here,

  • To refine your code:

    Try to use arduino IDEs "Tab"-Function. Add few tabs to organise your code.
    e.g. Tab "Network" with functions like init_rf() and just call this function from your setup() or before() method. Same with your actual sensors. Consider making an extra tab to put all the relevant functions there.

    I try to keep my "Main" tab as clean as possible.

    As I understand your code right your rf95 receiver acts as a gateway to mysensors

    MyMessage msg(NODE_ID, V_FLOW);           // Set's up mesaaging protocol to reciever, lets it know what variables are used
    MyMessage vmsg(NODE_ID,V_VOLUME);         // V_FLOW means that the data being sent is a flowrate, etc.

    All your Sensor-Nodes who send to your receiver-serial-gateway should have different IDs and your gateway should use these IDs to send the sensor date to your controller.

    So NODE_ID should be a variable depending on "from which node" you got your sensor date.

    Have a look at "MyMessage.h"

    	// Setters for building message "on the fly"
    	MyMessage& setType(uint8_t type);
    	MyMessage& setSensor(uint8_t sensor);
    	MyMessage& setDestination(uint8_t destination);

    You can change the message Sensor (your actual sensor no the gateway) dynamically on your gateway depending on the payload you get and from which sensor.

Log in to reply

Looks like your connection to MySensors Forum was lost, please wait while we try to reconnect.