Project showcase

Building a Sensor Network for an 18th Century Gristmill © GPL3+

Monitoring 100 year old factory processes are hard, but it gets easier, safer and more reliable with a network of nRF24L01 RF transmitters.

  • 2,759 views
  • 0 comments
  • 21 respects

Components and supplies

Necessary tools and machines

09507 01
Soldering iron (generic)
3drag
3D Printer (generic)
Hy gluegun
Hot glue gun (generic)

Apps and online services

About this project

The modern factory process relies heavily on digital technology to monitor different parts of the facility. With digital technology, factories have become safer, more productive and easier to operate. This technology is taken for granted which becomes evident when you visit older and older factories. I work at an 18th century grist mill where electricity is scarcely used. Flat belts and pulleys transmit power from line shafts to machinery, and the majority of our machinery is made of wood, cut by hand and assembled in the early 1900s. When I was asked to develop a network of sensors to monitor the different parts of my job's factory process, I jumped at the opportunity to bring my workplace into the 21st century.

The mill can be thought of as one big machine with many smaller processes happening simultaneously. Therefore, we needed a variety of sensors that can oversee these processes and alert us when something isn't working the way it should. The initial sensors included at the project's launch take measurements inside a grain bin to figure out how full it is, monitor the 0-10v dc output of an AC Tech SMVector controller, and take temperature and humidity readings of various parts of the building. Future sensors will include spout flow meters that measure the grain flowing through a spout, magnetic switches on 2 way valves that record which spouting the grain moves through, and temperature sensors on line shaft bearings that let us know how hot a bearing has become.

With the introduction of this network, we can save time by not walking around visually inspecting the processes throughout the four floors of the building and we can quantify parts of different processes to give us data for statistical purposes .

The network consists of the standard nodes, Arduino powered devices connected to sensors, the base node, the Arduino device that acts as the network hub, and a Raspberry Pi, the device that acts as the server and data interpreter.

The Hardware

The nRF24L01 (RF24) module is a radio frequency transmitter that is capable of sending and receiving data to other RF24 modules. The RF24 modules can be hooked up to an Arduino Nano by following the the wiring diagram below.

RF24 modules can be powered at 5 volts but its recommended that you power it at 3.3 volts. To ensure the RF24 module doesnt lose power sporadically, a decoupling capacitor is required. I used a 10 uF electrolytic capacitor as close to the RF module's power and ground pins (not shown in diagram). Without this capacitor, the RF module will perform poorly.

Once I had an understanding of the RF24 module, it was time to make a PCB that could be customized to allow for different sensors.

The PCB I designed consists of an Arduino Nano, an RF24 module, a 10 uF electrolytic capacitor, 2 indicator LEDs, two resistors for the LEDs, and a spot to put a micro usb female connector to power the node. When a node is put together, it looks like this...

Each node is then encased in a 3D-printed case that makes it easy to mount to walls and surfaces.

Due to range issues, I have modified the RF24 to add a longer antenna. The antenna modification boosts the range of a stock RF24 Module from a 5-10 foot range to 20 or 30 feet. To modify the RF24 antenna, I cut a piece of solid 18 gauge wire to about 7 inches and soldered it onto the end of the trace for the RF24 antenna. WARNING: Extending the RF24 antenna can pull up the pre-existing trace on the RF24 module if too much pressure is applied to the antenna.

I put a large glob of hot glue onto both sides of the new antenna because, during experimentation, I found that it helps stiffen the new antenna to the RF24 module.

To bring the system together, each network needs a base node where all the data is routed to. In my project, I use a node connected to a Raspberry Pi over serial cable. The node is used to send and receive network messages and the RPI is used as the central server for recording and interpreting data (the server program is explained later in this project).

Node and Base Programming

For this project, I exclusively used the RF24Network library (created by Tmrh20) to handle the RF24 messaging. The RF24Network library allows you to structure a network of nodes in a tree structure. Addresses are written out in octal format. Each RF24 module can branch off into no more than 5 nodes and the addresses for those sub nodes are followed by the parents address. Therefore, if we want to assign two nodes to be under Node 2, then we address one node as 012 (1st node that is a child to node 2) and the other node as 022 (2nd node that is a child to node 2).

So you can understand a little bit better, here is a basic layout of a few nodes connected in my network.

I use nodes 01, 011, 0111, and 01111 as repeater nodes, meaning they are mainly used for transmitting information from nodes further down the tree structure. Nodes 03, 0211 and 0311 are all sensor nodes, meaning they have sensors connected that generate data we need to send back to node 00.

Node and Sensor Program

The Node program runs on the node you are creating. This is the program that acts as an endpoint, where the data is generated from sensors attached to the node. I have provided a version of the Node code without any of my sensor modification (with comments to explain whats going on) but I am also including the program I've written (slightly different than the node code) for my project's network.

Base Program

The base program is the program you run on the base node (noted as node 00).

Something to note about the programs, when your creating a data structure for your message, the C struct needs to be identical in both your endpoint program and your base program.

Attaching Sensors to a Node

The network was launched with 3 sensor types, sensors to measure how full grain bins are, sensors to monitor the power output of certain motors, and sensors that give us temperature and humidity readings from around the building.

Grain Bin Sensing

To measure the depth of grain bins, I installed ultrasonic sensors on the top of grain bins so that the sensor is pointing into the bin. I then cabled 3 of the ultrasonic sensors into the pins I set up on the node's protoboard area. Each echo pin is wired to a separate Arduino pin but the trigger pin is shared for easier programming.

Temperature and Humidity Sensing

The DHT11 is used to measure temperature and humidity throughout the factory building. This is important information because when working with grain and flour, temperature and humidity fluctuations can affect how fine the flour is milled.

SMVector 3 Phase Controller Monitor

To mill wheat berries into flour, we have to grind the berry with a stone mill. The mill itself is run with a 3 phase motor connected to an AC Tech SMVector controller, which has the option of outputting a 0-10 volt analog signal that scales with how much current the motor is drawing. This is useful to monitor because a stone mill can open its feed mechanism, letting more grain into the mill, which makes it harder for the mill to grind the grain. Ultimately, this ends in the motor trying to compensate, drawing more current, and eventually overloading and seizing. The network allows us to monitor the current remotely so if we see this value steadily rising, we can close the feeding mechanism manually before the mill begins to overload. The circuit acts as a voltage divider, cutting the voltage in half and allowing the Arduino to measure the 0-10 volt dc in a 0-5 volt dc scale.

Python Server Program

After a message is sent from an RF24 node in the network, the message is routed to the base node (address 00) and is then sent, as a string, to a Raspberry Pi over a serial cable.

The program will receive a message, break it down into its component parts, interpret the data and then display it in the GUI. From the GUI, you can view node\sensor information and send messages to nodes with the entry box and "send command" button. After every 10 messages, the program will automatically save the nodes information into a text file that can be analyzed at a later date.

Final Thoughts

My goal for this project was to build an RF24 Network that could monitor different parts of our factory from one central place. Through the use of the RF24 module, Arduino Nano and various sensors I was able to create an inexpensive solution to complete my goal while building a robust framework that can be easily built and expanded upon. While the network isn't fully wireless (the nodes need power cabling unless the node is battery powered), implementing new nodes into the network is as easy as turning on the device.

Newer and Better Versions

If you would like to build upon the work ive done already, I designed a better PCB that is easier to make sensors for. I will include that board as a gerber file and, if you would like to build your own network based off of my work, I advise you to use Network Board version 2 instead of version 1.

Code

Network Base CodeArduino
Use this as the base code (address 00) for your RF24 Network project
//RF24 Network Base Code Template
//Created by totalJTM
//RF24Network library by TMRH20

#include <RF24Network.h>
#include <RF24Network_config.h>
#include <Sync.h>
#include <RF24.h>
#include <SPI.h>

const String versionStr = "~Version 1.0 9/11/2018";
const uint16_t this_node = 00;

const int redLed = 2, greenLed = 4;

RF24 radio(7, 8);                   // nRF24L01(+) radio attached using 7 as CE and 8 as CSN

RF24Network network(radio);

struct n_message {                  //n_message structure
  float voltageReading;             //add more variables or change the datatype to whatever suits your project
  float data1;
  //bool data2;                     //example of another variable, just remove the "//"
};
struct b_message {                  //b_message structure
  char message[8];               //this array is here so we can have multiple types of formatted messages
};

void setup() {
  Serial.begin(115200);
  Serial.println(versionStr);

  SPI.begin();
  radio.begin();
  network.begin(89, this_node);
}

unsigned long lastSent = 0;
void loop() {
  network.update();

  while (network.available()) {
    RF24NetworkHeader fromheader;        // If so, grab it and print it out
    network.peek(fromheader);
    if (fromheader.type == 'n') {
      n_message receivedMessage;
      network.read(fromheader, &receivedMessage, sizeof(receivedMessage));
      String msg = "";
      msg += fromheader.from_node;
      msg += ":";
      msg += receivedMessage.voltageReading;
      msg += ":";
      msg += receivedMessage.data1;
      Serial.println(msg);
    }
  }
  if (Serial.available()) {
    //node id|message
    String input = Serial.readString();
    RF24NetworkHeader toheader;
    b_message messageToSend;
    String toRaw = "";
    bool messageB = false;
    byte messageStart = 0;
    char messageRaw[8];
    for (int i = 0; i < input.length(); i++) {
      if (input.charAt(i) == '|') {
        messageB = true;
        messageStart = i + 1;
      }
      else if (messageB == true)
        messageRaw[i - messageStart] = input.charAt(i);
      else
        toRaw += input.charAt(i);
    }
    for (int i = 0; i < 8; i++)
      messageToSend.message[i] = messageRaw[i];
    toheader.to_node = toRaw.toInt();
    toheader.type = 'b';

    if (network.write(toheader, &messageToSend, sizeof(messageToSend))) {             //try to send message
      Serial.println("message sent");                                                 //if message succeeds to send, print "message sent" and turn on green led on circuit board
      digitalWrite(greenLed, HIGH);
      digitalWrite(redLed, LOW);
    } else {
      Serial.println("message failed");                                               //if message fails to send, print "message failed" and turn on red led on circuit board
      digitalWrite(greenLed, LOW);
      digitalWrite(redLed, HIGH);
    }
  }
}
Network Node CodeArduino
This is the code you would run on an RF24 Node
//RF24 Network Node Code Template
//Created by totalJTM
//RF24Network library by TMRH20

#include <RF24Network.h>
#include <RF24Network_config.h>
#include <Sync.h>
#include <RF24.h>
#include <SPI.h>
#include <EEPROM.h>

const String versionStr = "Version 1.0 9/11/2018";
/****************** User Config ***************************/
uint16_t this_node = 01; //this is the address of this node CHANGE FOR EACH NODE
float data_delay = 5.0; //this is the delay interval between when messages are sent
/**********************************************************/

const int redLed = 2, greenLed = 4;

RF24 radio(7, 8);                   // nRF24L01(+) radio attached using Getting Started board

RF24Network network(radio);         //Declaring rfnetwork as network

struct n_message {                  //n_message structure
  float voltageReading;             //add more variables or change the datatype to whatever suits your project
  float data1;
  //bool data2;                     //example of another variable, just remove the "//"
};
struct b_message {                  //b_message structure
  char message[8];               //this array is here so we can have multiple types of formatted messages
};

//void(* resetArduino) (void) = 0; //function to reset arduino

void onReceiveMessage(char message[8]) {   //when a message is received, this function is called.

  if (message[0] == 'e' and message[1] == 'g') {                      //a message starting with "eg" triggers this statement
    //action for receiving this message
  }
}

n_message sendMessageActions(n_message nmessage) {
  nmessage.data1 = 50.0;
  return nmessage;
}

void setup() {
  Serial.begin(115200);
  Serial.println(versionStr);

  SPI.begin();                      //Starting SPI
  radio.begin();                    //Start Radio
  network.begin(89, this_node);     //start network (default channel 89, you can adjust it to whatever but a node can only communicate with nodes on its channel)
}
unsigned long lastSent = 0;
void loop() {
  network.update();                 //update node with network traffic

  while (network.available()) {     //if there is a message,
    RF24NetworkHeader fromheader;
    network.peek(fromheader);       //peek at the message's header
    if (fromheader.type == 'b') {   //if it is a b_message (b messages are of type b)
      b_message receivedMessage;
      network.read(fromheader, &receivedMessage, sizeof(receivedMessage));            //read message
      String printMessage = ""; printMessage += "~Message Received: "; printMessage += receivedMessage.message;
      printMessage += " -From: "; printMessage += fromheader.from_node;
      Serial.println(printMessage);  //print message to arduino serial
      onReceiveMessage(receivedMessage.message);                                              //call onReceiveMessage function to do user functions
    }
  }

  if ((millis() - lastSent) > (data_delay * 1000))                                    //countdown to next message transmission
  {
    Serial.println("sending");
    RF24NetworkHeader toheader;                                                       //create a new message header
    n_message messageToSend;                                                          //create a new message
    messageToSend = sendMessageActions(messageToSend);                                //rewrites message with user defined sensor code (can be edited in sendMessageActions function)
    toheader.to_node = 00;                                                            //message told to be directed at node 00 as a final destination (base node)
    messageToSend.voltageReading = readBatteryVoltage();                              //read battery voltage
    Serial.println(messageToSend.voltageReading);
    toheader.type = 'n';                                                              //set message type to an n_message

    if (network.write(toheader, &messageToSend, sizeof(messageToSend))) {             //try to send message
      Serial.println("message sent");                                                 //if message succeeds to send, print "message sent" and turn on green led on circuit board
      digitalWrite(greenLed, HIGH);
      digitalWrite(redLed, LOW);
    } else {
      Serial.println("message failed");                                               //if message fails to send, print "message failed" and turn on red led on circuit board
      digitalWrite(greenLed, LOW);
      digitalWrite(redLed, HIGH);
    }
    lastSent = millis();            //update lastSent variable with current time to start over the countdown
  }
  //if(millis() < 86400000)           //reset arduino after 24 hours
  //resetArduino();
}


float readBatteryVoltage() {        //code to read the voltage of the power source to the node
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
  ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
  ADMUX = _BV(MUX3) | _BV(MUX2);
#else
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif
  delay(2);
  ADCSRA |= _BV(ADSC);
  while (bit_is_set(ADCSRA, ADSC));
  uint8_t low  = ADCL;
  uint8_t high = ADCH;
  long result = (high << 8) | low;
  result = 1125300L / result;
  return (((float)(result)) / 1000.0);
}
Personal Network Sensor CodeArduino
This is the code I wrote and uploaded to all of my network nodes. Use this code as an example but build upon the other arduino code Ive provided.
//CVMill Network Program
//Created by totalJTM
//RF24Network library by TMRH20

#include <dht.h>
#include <RF24Network.h>
#include <RF24Network_config.h>
#include <Sync.h>
#include <RF24.h>
#include <SPI.h>

const String versionStr = "Version 1.1 8/20/2018";
/****************** User Config ***************************/
const uint16_t this_node = 03;
float data_delay = 2.0;

const int sensorType[3] = {0, 0, 0}; //1-Ultra,2-Temp,3-Pressure,4-Valve,5-DHT Humidity,6-DHT Temp, 7-mill power
const int sensorPin[3] = {A0, A1, A2}; //A0,A1,A2 or 14,15,16
/**********************************************************/

int messagesSent = 0, fail = 0, success = 0;
const int redLed = 2, greenLed = 4;
float dhtTempHold = 0.0;

RF24 radio(7, 8);                   // nRF24L01(+) radio attached using Getting Started board

RF24Network network(radio);

struct s_message {
  float voltageReading;
  float data1;
  float data2;
  float data3;
};
struct BasePacket
{
  uint8_t message[8];
};
struct stat_message {
  float voltageReading;
  int messagesSent;
  int fail;
  int success;
};

s_message myPacket;

void onReceiveMessage(char message[8]) {   //when a message is received, this function is called.

  if (message[0] == 'c' and message[1] == 'a' and message[2] == 'd') { //a message has to start with "cad" to change this nodes address
    String newAddress = "";
    for (int i = 3; i < 8; i++)               //parses through message (excluding spaces)
      if (!message[i] == ' ')
        newAddress += message[i];
    Serial.println(newAddress);
    //this_node = newAddress.toInt();              //updates this arduinos rfnetwork address
    //Serial.println(this_node);
    //EEPROM.write(1, this_node);               //saves new address to eeprom
    //resetArduino();                           //resets arduino so its address is changed to new address (not needed in most situations)
    Serial.println("Address Changed");
  }

  if (message[0] == 's' and message[1] == 't') {
    Serial.println("sending");
    RF24NetworkHeader toheader;                                                       //create a new message header
    stat_message messageToSend;                                                          //create a new message
    messageToSend.messagesSent = messagesSent;                               //rewrites message with user defined sensor code (can be edited in sendMessageActions function)
    messageToSend.fail = fail;
    messageToSend.success = success;
    toheader.to_node = 00;                                                            //message told to be directed at node 00 as a final destination (base node)
    messageToSend.voltageReading = readBatteryVoltage();                              //read battery voltage
    Serial.println(messageToSend.voltageReading);
    toheader.type = 't';                                                              //set message type to an n_message
    messagesSent++;
    if (network.write(toheader, &messageToSend, sizeof(messageToSend))) {             //try to send message
      Serial.println("message sent");                                                 //if message succeeds to send, print "message sent" and turn on green led on circuit board
      digitalWrite(greenLed, HIGH);
      digitalWrite(redLed, LOW);
      success++;
    } else {
      Serial.println("message failed");                                               //if message fails to send, print "message failed" and turn on red led on circuit board
      digitalWrite(greenLed, LOW);
      digitalWrite(redLed, HIGH);
      fail++;
    }
  }

  if (message[0] == 'e' and message[1] == 'g') {                      //a message starting with "eg" triggers this statement
    //action for receiving this message
  }
}

float sensorCommand(int st, int pin) {
  if (st == 1) { //ultrasonic
    const int trigPin = 5;
    float combined = 0;
    pinMode(trigPin, OUTPUT);
    pinMode(pin, INPUT);
    int j = 0;
    for (int i = 0; i < 3; i++) {
      long duration;
      float distance;
      digitalWrite(trigPin, LOW);  // Added this line
      delayMicroseconds(5); // Added this line
      digitalWrite(trigPin, HIGH);
      delayMicroseconds(10); // Added this line
      digitalWrite(trigPin, LOW);
      duration = pulseIn(pin, HIGH);
      distance = ((float)duration / 2) / 74;
      if(distance <= 200 && distance >= 0)
        combined += distance;
      else{
        i -= 1;
        j += 1;
      }
      if(j > 4)
        return -99.0;
      Serial.println(distance);
      delay(100);
    }
    Serial.print("_");
    Serial.println(combined / 3);
    return (combined / 3);
  }
  if (st == 2) { //temp
    pinMode(pin, INPUT);
  }
  if (st == 3) { //Pressure
    pinMode(pin, INPUT);

  }
  if (st == 4) { //Valve
    pinMode(pin, INPUT);
  }
  if (st == 5) {//DHTS Humidity
    pinMode(pin, INPUT);
    dht DHT;
    int chk = DHT.read11(pin);
    float d = float(DHT.humidity);
    dhtTempHold = DHT.temperature;
    Serial.println(d);
    return d;
  }
  if (st == 6) {//DHTS Temperature
    float d = dhtTempHold;
    Serial.println((1.8 * d) + 32.0);
    return (1.8 * d) + 32.0;
  }
  if (st == 7) {//mill power
    pinMode(pin, INPUT);
    int mVoltage = analogRead(pin);
    Serial.println(mVoltage);
    float voltage = (mVoltage * (5.0/1024.0));
    Serial.println(voltage);
    return voltage;
  }
  if (st == 0) {//Standard
    return 0.00;
  }
}

void getSensorData() {
  for (int i = 0; i < 3; i++) {
    if (i == 0)
      myPacket.data1 = sensorCommand(sensorType[0], sensorPin[0]);
    if (i == 1)
      myPacket.data2 = sensorCommand(sensorType[1], sensorPin[1]);
    if (i == 2)
      myPacket.data3 = sensorCommand(sensorType[2], sensorPin[2]);
  }
}

float readBatteryVoltage() {
  // Read 1.1V reference against AVcc
  // set the reference to Vcc and the measurement to the internal 1.1V reference
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
  ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
  ADMUX = _BV(MUX3) | _BV(MUX2);
#else
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif

  delay(2); // Wait for Vref to settle
  ADCSRA |= _BV(ADSC); // Start conversion
  while (bit_is_set(ADCSRA, ADSC)); // measuring

  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
  uint8_t high = ADCH; // unlocks both

  long result = (high << 8) | low;

  result = 1125300L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000
  return ((float)(result)/1000.0); // Vcc in millivolts
}

void setup() {
  Serial.begin(115200);
  Serial.println(versionStr);

  SPI.begin();
  radio.begin();
  network.begin(89, this_node);
  getSensorData();

}
unsigned long lastSent = 0;
void loop() {

  network.update();

  while (network.available()) {
    RF24NetworkHeader header;        // If so, grab it and print it out
    network.peek(header);
    if (header.type == 'b') {
      BasePacket bMessage;
      network.read(header, &bMessage, sizeof(bMessage));
      onReceiveMessage(bMessage.message);
    }
  }

  if ((millis() - lastSent) > (data_delay * 1000))
  {
    Serial.println("sending");
    RF24NetworkHeader header;
    header.to_node = 00;
    myPacket.voltageReading = readBatteryVoltage();
    Serial.println(myPacket.voltageReading);
    getSensorData();
    header.type = 's';
    
    messagesSent++;
    if (network.write(header, &myPacket, sizeof(myPacket))) {
      Serial.println("message sent");
      digitalWrite(greenLed, HIGH);
      digitalWrite(redLed, LOW);
      success++;
    } else {
      Serial.println("message failed");
      digitalWrite(greenLed, LOW);
      digitalWrite(redLed, HIGH);
      fail++;
    }
    lastSent = millis();
  }
}
Python Server ProgramPython
This is the program to run on the RPI as a server for all the network messages
No preview (download only).

Custom parts and enclosures

RF24 Node Case
This is the case for my RF24 Network board
RF24 Node Lid
This is the lid for my RF24 Network board

Schematics

Network Board Version 1
This is the gerber file for my RF24 Network node
rf24_network_board_v1_eYckHVyELq.zip
Network Board Version 2 (not the board the project is based on)
This is the gerber file for version 2 of the network board. This is not the board the project is based on, but it is the better of the two versions.
rf24_network_board_v2_APrbbg2MZG.zip

Comments

Similar projects you might like

Arduino Bluetooth Basic Tutorial

by Mayoogh Girish

  • 454,629 views
  • 42 comments
  • 236 respects

Home Automation Using Raspberry Pi 2 And Windows 10 IoT

Project tutorial by Anurag S. Vasanwala

  • 285,502 views
  • 95 comments
  • 671 respects

Security Access Using RFID Reader

by Aritro Mukherjee

  • 229,318 views
  • 38 comments
  • 236 respects

OpenCat

Project in progress by Team Petoi

  • 195,895 views
  • 154 comments
  • 1,361 respects
Add projectSign up / Login