EdOliverVictor Altamirano
Published © MIT

AggroFox: Large-Scale and Urban Agriculture IoT Solution

IoT sensing, notifications, and analytics platform for urban and large-scale agriculture with automated irrigation, using Sigfox technology.

AdvancedFull instructions provided13 hours22,037

Things used in this project

Hardware components

Pycom FiPy
×1
Pycom expansion board
×1
Pycom Antenna Kit
×1
Jumper wires (generic)
Jumper wires (generic)
×1
Arduino Pro Mini 328 - 5V/16MHz
SparkFun Arduino Pro Mini 328 - 5V/16MHz
×1
DHT11 Temperature & Humidity Sensor (4 pins)
DHT11 Temperature & Humidity Sensor (4 pins)
×1
Modulo Temperature Probe
Modulo Temperature Probe
×1
SparkFun Soil Moisture Sensor (with Screw Terminals)
SparkFun Soil Moisture Sensor (with Screw Terminals)
×1
Photon
Particle Photon
×1
Adafruit Solenoid Valve
×1
plumbing adapters
×1
Darlington High Power Transistor
Darlington High Power Transistor
×1
Resistor 2.21k ohm
Resistor 2.21k ohm
×1
Resistor 1k ohm
Resistor 1k ohm
×3
Resistor 4.75k ohm
Resistor 4.75k ohm
×1
1N4007 – High Voltage, High Current Rated Diode
1N4007 – High Voltage, High Current Rated Diode
×1
12V/29A Power Supply
OpenBuilds 12V/29A Power Supply
×1
Seeed Studio Lipo rider Pro
×1
Li-Ion Battery 1000mAh
Li-Ion Battery 1000mAh
×1
Seeed Studio solar panel
×1
Solderless Breadboard Half Size
Solderless Breadboard Half Size
×1
Hose
×1

Software apps and online services

BlueMix
IBM BlueMix
Watson
IBM Watson
Openweathermap
ThingSpeak API
ThingSpeak API
Sigfox
Sigfox
Pymakr Plugin
Pycom Pymakr Plugin

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Screwdrivers
Soldering Tools

Story

Read more

Schematics

Sensor and Sigfox connectivity Diagram

These are the schematics for the Sensor and Sigfox communication portion of the system.

Automated irrigation system.

These are the schematics for the irrigation part of the system.

Code

Node-RED flow

JavaScript
Node-RED flow that uses the openweathermap API, and the sensor readings to do data analytics and proceed to act, and in fact joins the whole platform.
[{"id":"d71476d5.e80198","type":"ui_gauge","z":"db15ed1.447301","name":"Tempamb","group":"1140416a.87e79f","order":0,"width":0,"height":0,"gtype":"gage","title":"","label":"°C","format":"{{value}}","min":0,"max":"55","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":818.888916015625,"y":61.111106872558594,"wires":[]},{"id":"741a342.e69decc","type":"inject","z":"db15ed1.447301","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":215.888916015625,"y":165.6111068725586,"wires":[["dd05b948.077978","31701415.b696cc"]]},{"id":"dd05b948.077978","type":"http request","z":"db15ed1.447301","name":"letempambchannel","method":"GET","ret":"obj","url":"","tls":"","x":399.888916015625,"y":193.9111099243164,"wires":[["322f8c31.bd4414","12ed7168.d0c4df","c172e706.5a17b8","72ffade5.3f5ab4","308cd76f.f79048"]]},{"id":"322f8c31.bd4414","type":"function","z":"db15ed1.447301","name":"Field1","func":"msg.payload = msg.payload.field1\nmsg.topic= msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":597.888916015625,"y":174.9111099243164,"wires":[["d71476d5.e80198","4f88b838.66f648"]]},{"id":"12ed7168.d0c4df","type":"function","z":"db15ed1.447301","name":"Field 2","func":"msg.payload = msg.payload.field2\nmsg.topic = msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":598.888916015625,"y":238.91109466552734,"wires":[["c0d96316.47c43","593ff4f5.3adbdc","c62f10b4.e10b7"]]},{"id":"c172e706.5a17b8","type":"function","z":"db15ed1.447301","name":"Field 3","func":"msg.payload = msg.payload.field3\nmsg.topic = 'Soil Temperature';\nreturn msg;","outputs":1,"noerr":0,"x":600.888916015625,"y":305.91109466552734,"wires":[["2406b943.7f83e6","51e17274.c84d4c","1498d2e9.247bfd"]]},{"id":"72ffade5.3f5ab4","type":"function","z":"db15ed1.447301","name":"Field 4","func":"msg.payload = msg.payload.field4\nmsg.topic = 'Soil Temperature';\nreturn msg;","outputs":1,"noerr":0,"x":739.888916015625,"y":498.9110641479492,"wires":[["8de7ad32.016bb","edb70d8.11d02f"]]},{"id":"308cd76f.f79048","type":"function","z":"db15ed1.447301","name":"Field 5","func":"msg.payload = msg.payload.field5\nmsg.topic = 'Soil Temperature';\nreturn msg;","outputs":1,"noerr":0,"x":693.888916015625,"y":596.9110336303711,"wires":[["faa295a5.21e348","573b06ef.515c48"]]},{"id":"4f88b838.66f648","type":"ui_chart","z":"db15ed1.447301","name":"","group":"1140416a.87e79f","order":0,"width":0,"height":0,"label":"","chartType":"line","legend":"false","xformat":"dd HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"604800","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"x":837.888916015625,"y":189.1111068725586,"wires":[[],[]]},{"id":"c0d96316.47c43","type":"ui_gauge","z":"db15ed1.447301","name":"","group":"347821f3.6d19be","order":0,"width":0,"height":0,"gtype":"gage","title":"","label":"%","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":852.888916015625,"y":256.1111068725586,"wires":[]},{"id":"593ff4f5.3adbdc","type":"ui_chart","z":"db15ed1.447301","name":"","group":"347821f3.6d19be","order":0,"width":0,"height":0,"label":"","chartType":"line","legend":"false","xformat":"dd HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0f","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"x":848.888916015625,"y":296.1111068725586,"wires":[[],[]]},{"id":"2406b943.7f83e6","type":"ui_text","z":"db15ed1.447301","group":"cea5e94f.0954a8","order":0,"width":0,"height":0,"name":"","label":"32.2 °C is optimal Heat index","format":"{{msg.payload}}","layout":"row-spread","x":918.888916015625,"y":349.1111068725586,"wires":[]},{"id":"51e17274.c84d4c","type":"ui_chart","z":"db15ed1.447301","name":"","group":"cea5e94f.0954a8","order":0,"width":0,"height":0,"label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9367bc","#c5b0d5"],"useOldStyle":false,"x":839.888916015625,"y":392.1111068725586,"wires":[[],[]]},{"id":"1498d2e9.247bfd","type":"ui_gauge","z":"db15ed1.447301","name":"","group":"cea5e94f.0954a8","order":0,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"°C","format":"{{value}}","min":"15","max":"44.4","colors":["#fff647","#0fe600","#ca3838"],"seg1":"","seg2":"","x":847.888916015625,"y":436.1111068725586,"wires":[]},{"id":"8de7ad32.016bb","type":"ui_gauge","z":"db15ed1.447301","name":"","group":"c9ffe2c3.b2bee","order":0,"width":0,"height":0,"gtype":"gage","title":"","label":"°C","format":"{{value}}","min":0,"max":"55","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":922.888916015625,"y":487.11104583740234,"wires":[]},{"id":"edb70d8.11d02f","type":"ui_chart","z":"db15ed1.447301","name":"","group":"c9ffe2c3.b2bee","order":0,"width":0,"height":0,"label":"","chartType":"line","legend":"false","xformat":"dd HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#99df8b","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"x":919.888916015625,"y":527.1110458374023,"wires":[[],[]]},{"id":"faa295a5.21e348","type":"ui_gauge","z":"db15ed1.447301","name":"","group":"9304bf1b.ac483","order":0,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"%","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":895.888916015625,"y":576.1110458374023,"wires":[]},{"id":"573b06ef.515c48","type":"ui_chart","z":"db15ed1.447301","name":"","group":"9304bf1b.ac483","order":0,"width":0,"height":0,"label":"","chartType":"line","legend":"false","xformat":"dd HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9367bc","#c5b0d5"],"useOldStyle":false,"x":899.888916015625,"y":614.1110458374023,"wires":[[],[]]},{"id":"31701415.b696cc","type":"openweathermap","z":"db15ed1.447301","name":"mexa weather","wtype":"forecast","lon":"","lat":"","city":"your city","country":"your country","language":"en","x":273.888916015625,"y":338.66109466552734,"wires":[["5f4900a3.6190e","e7a5f1c8.5a3e6"]]},{"id":"5f4900a3.6190e","type":"debug","z":"db15ed1.447301","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":395.888916015625,"y":257.76110076904297,"wires":[]},{"id":"e7a5f1c8.5a3e6","type":"function","z":"db15ed1.447301","name":"","func":"msg.payload= msg.payload[0].weather[0].main\nmsg.count = msg.payload\nreturn msg;","outputs":1,"noerr":0,"x":401.888916015625,"y":396.91109466552734,"wires":[["fc38b1e6.25317","f91a9562.a44228","e6fddeb2.80055","25fab1ba.80622e"]]},{"id":"fc38b1e6.25317","type":"debug","z":"db15ed1.447301","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":567.888916015625,"y":515.761100769043,"wires":[]},{"id":"f91a9562.a44228","type":"ui_text","z":"db15ed1.447301","group":"3f5cb968.f8d436","order":0,"width":0,"height":0,"name":"","label":"Weather forecast for the next 5 hours:","format":"{{msg.payload}}","layout":"row-spread","x":416.888916015625,"y":579.1110763549805,"wires":[]},{"id":"f6ffaae2.ceb3b8","type":"ParticleFunc out","z":"db15ed1.447301","baseurl":"","devid":"your device name","fname":"led","param":"","once":false,"repeat":0,"consolelog":false,"x":699.888916015625,"y":688.531120300293,"wires":[["a89561ff.e04d2"]]},{"id":"3256e5e6.f45a0a","type":"inject","z":"db15ed1.447301","name":"","topic":"","payload":"on","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":246.888916015625,"y":646.6110763549805,"wires":[["f6ffaae2.ceb3b8"]]},{"id":"a89561ff.e04d2","type":"debug","z":"db15ed1.447301","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1030.888916015625,"y":737.761100769043,"wires":[]},{"id":"6dfced87.1b7e54","type":"inject","z":"db15ed1.447301","name":"","topic":"","payload":"off","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":215.888916015625,"y":692.6110763549805,"wires":[["f6ffaae2.ceb3b8"]]},{"id":"c62f10b4.e10b7","type":"link out","z":"db15ed1.447301","name":"","links":["17e4713e.55e1bf"],"x":835.888916015625,"y":124.76111602783203,"wires":[]},{"id":"17e4713e.55e1bf","type":"link in","z":"db15ed1.447301","name":"","links":["c62f10b4.e10b7"],"x":133.888916015625,"y":761.6110763549805,"wires":[["25fab1ba.80622e"]]},{"id":"25fab1ba.80622e","type":"function","z":"db15ed1.447301","name":"","func":"if (msg.topic<=10 && msg.count != 'Rain' && msg.count !=null){\n    msg.payload= 'on'\n}\nelse{\n    msg.payload= 'off'\n}\nreturn msg;","outputs":1,"noerr":0,"x":476.888916015625,"y":722.9111251831055,"wires":[["f6ffaae2.ceb3b8"]]},{"id":"e6fddeb2.80055","type":"debug","z":"db15ed1.447301","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"count","x":503.888916015625,"y":805.761100769043,"wires":[]},{"id":"9ec9146.d9b39e8","type":"inject","z":"db15ed1.447301","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":167.888916015625,"y":426.6111068725586,"wires":[["b7785f17.e9d8a"]]},{"id":"b7785f17.e9d8a","type":"function","z":"db15ed1.447301","name":"","func":"msg.count = 'Arid'\nreturn msg;","outputs":1,"noerr":0,"x":196.888916015625,"y":480.91109466552734,"wires":[["25fab1ba.80622e"]]},{"id":"abb64b9d.401e58","type":"inject","z":"db15ed1.447301","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":286.888916015625,"y":849.6110763549805,"wires":[["25fab1ba.80622e"]]},{"id":"1140416a.87e79f","type":"ui_group","z":"","name":"Soil Temperature","tab":"dbbcdb89.f7c1c8","disp":true,"width":"6","collapse":false},{"id":"347821f3.6d19be","type":"ui_group","z":"","name":"Soil Moisture","tab":"dbbcdb89.f7c1c8","disp":true,"width":"6","collapse":false},{"id":"cea5e94f.0954a8","type":"ui_group","z":"","name":"Heat Index","tab":"dbbcdb89.f7c1c8","disp":true,"width":"6","collapse":false},{"id":"c9ffe2c3.b2bee","type":"ui_group","z":"","name":"Temperature (ambient)","tab":"dbbcdb89.f7c1c8","disp":true,"width":"6","collapse":false},{"id":"9304bf1b.ac483","type":"ui_group","z":"","name":"Humidity","tab":"dbbcdb89.f7c1c8","disp":true,"width":"6","collapse":false},{"id":"3f5cb968.f8d436","type":"ui_group","z":"","name":"Weather Forecast","tab":"dbbcdb89.f7c1c8","disp":true,"width":"6","collapse":false},{"id":"dbbcdb89.f7c1c8","type":"ui_tab","z":"","name":"AggroFox","icon":"dashboard"}]

Pycom FiPy Sigfox code.

MicroPython
Pycom FiPy Sigfox code.....please, please, please always use the antenna. The code is to receive the sensor data from the arduino and then publish it in the Sigfox Backend.
from network import Sigfox
import socket
from machine import UART
# this uses the UART_1 default pins for TXD and RXD (``P3`` and ``P4``)
uart = UART(1, baudrate=9600)
dato="mensaje"
i=0

while i==0:
    dato = uart.read(12) # read up to 5 bytes}
    if dato == None:
        i=0
    else:
        print(dato.decode())
        i=1
print ("Recibidos")

datos=dato.decode()
tempe = int(datos[0:-10])
hume = int(datos[2:-8])
inde = int(datos[4:-6])
tempa = int(datos[6:-4])
huma = int(datos[8:-2])
inda = int(datos[10:])

# init Sigfox for RCZ1 (Europe)
sigfox = Sigfox(mode=Sigfox.SIGFOX, rcz=Sigfox.RCZ2)

# create a Sigfox socket
s = socket.socket(socket.AF_SIGFOX, socket.SOCK_RAW)

# make the socket blocking
s.setblocking(True)

# configure it as uplink only
s.setsockopt(socket.SOL_SIGFOX, socket.SO_RX, False)

# send some bytes
# s.send(bytes([temp,hume,inde,tempa,huma,inda]))
s.send(bytes([tempe,hume,inde,tempa,huma,inda]))

print("Datos Enviados")

Arduino Code

C/C++
Code to send the sensor data to the pycom board via serial @ 9600 bauds.
// Include the libraries we need
#include <OneWire.h>
#include <DallasTemperature.h>
#include "DHT.h"
#include <SoftwareSerial.h>

// All define components
#define ONE_WIRE_BUS A1
#define humedad A3
#define temp    A2
#define DHTTYPE DHT11
#define minu 60000

// Setup libraries variables
OneWire oneWire(ONE_WIRE_BUS);
DHT dht(temp, DHTTYPE);
SoftwareSerial mySerial(13, 10); // RX(DONT CARE) , TX (Pycom Serial Communication)

// DallasTemperature sensors definition
DallasTemperature sensors(&oneWire);

/*
 * The setup function. We only start all sensors here
 */
void setup(void)
{ 
  mySerial.begin(9600);
  Serial.begin(9600);
  sensors.begin();
  dht.begin();
}

/*
 * Main function, get all the sensor data
 */
void loop(void)
{
  sensors.requestTemperatures();
  int hume = analogRead(humedad);
  int hum  = map(hume, 0, 1023, 0, 950);
  int humes = -(hum*100)/950;
  int h = int(dht.readHumidity());
  int t = int(dht.readTemperature());
  int hica = int(dht.computeHeatIndex(t, h,false));
  int te = int(sensors.getTempCByIndex(0)); // Send the command to get temperatures
  int hice = int(dht.computeHeatIndex(sensors.getTempCByIndex(0), ((hum*100)/950),false));

// Serial Strings Declaration

  String dte;
  String dhume;
  String dhe;
  String dta;
  String dhuma;
  String dha;


  // These conditional forces the serial string to have the format of two digits for each value

  if (te < 10)
  {
    dte=String(0)+String(te);
  }
  else
  {
    dte=String(te);
  }
  if (humes < 10)
  {
    dhume=String(0)+String(humes);
  }
  else
  {
    dhume=String(humes);
  }
  if (hice < 10)
  {
    dhe=String(0)+String(hice);
  }
  else
  {
    dhe=String(hice);
  }
    if (t < 10)
  {
    dta=String(0)+String(t);
  }
  else
  {
    dta=String(t);
  }
    if (h < 10)
  {
    dhuma=String(0)+String(h);
  }
  else
  {
    dhuma=String(h);
  }
    if (hica < 10)
  {
    dha=String(0)+String(hica);
  }
  else
  {
    dha=String(hica);
  }
  
  mySerial.print(dte+dhume+dhe+dta+dhuma+dha);  // We send sensor data by serial to pycom
  Serial.println(dte+dhume+dhe+dta+dhuma+dha);  // This serial string is only for debugging
  delay(6*minu); // Update Frecuency Sensor Values, in this case 6 minutes, 120 data by day
}

Particle Photon Code

C/C++
This code is to activate the irrigation system whenever the conditions of the Node-RED flow are met. In fact whenever the platform detects that there will be no rain and the sensors indicate that the soil is dry, it will send a signal to the Valve portion and it will activate.
// -----------------------------------
// Controlling Aggrofox's irrigation over the Internet
// -----------------------------------

// First, let's create our "shorthand" for the pins
// Same as in the Blink an LED example:
// led1 is D0, led2 is D7

int led1 = D0;
int led2 = D7;

// Last time, we only needed to declare pins in the setup function.
// This time, we are also going to register our Particle function

void setup()
{

   // Here's the pin configuration, same as last time
   pinMode(led1, OUTPUT);
   pinMode(led2, OUTPUT);

   // We are also going to declare a Particle.function so that we can turn the LED on and off from the cloud.
   Particle.function("led",ledToggle);
   // This is saying that when we ask the cloud for the function "led", it will employ the function ledToggle() from this app.

   // For good measure, let's also make sure both LEDs are off when we start:
   digitalWrite(led1, LOW);
   digitalWrite(led2, LOW);

}


// Last time, we wanted to continously blink the LED on and off
// Since we're waiting for input through the cloud this time,
// we don't actually need to put anything in the loop

void loop()
{
   // Nothing to do here
}

// We're going to have a super cool function now that gets called when a matching API request is sent
// This is the ledToggle function we registered to the "led" Particle.function earlier.


int ledToggle(String command) {
    /* Particle.functions always take a string as an argument and return an integer.
    Since we can pass a string, it means that we can give the program commands on how the function should be used.
    In this case, telling the function "on" will turn the LED on and telling it "off" will turn the LED off.
    Then, the function returns a value to us to let us know what happened.
    In this case, it will return 1 for the LEDs turning on, 0 for the LEDs turning off,
    and -1 if we received a totally bogus command that didn't do anything to the LEDs.
    */

    if (command=="on") {
        
        digitalWrite(led1,HIGH);
        digitalWrite(led2,HIGH);
        delay(30000);
        digitalWrite(led1,LOW);
        digitalWrite(led2,LOW);
        return 1;
        
    }
    /*Well we are going to irrigate them plants for 30 seconds each time we need
    */
    else if (command=="off") {
        digitalWrite(led1,LOW);
        digitalWrite(led2,LOW);
        return 0;
    }
    else {
        return -1;
    }
}

Credits

EdOliver

EdOliver

37 projects • 71 followers
Engineer, Scientist, Maker. Entrepreneur and Futurist.
Victor Altamirano

Victor Altamirano

25 projects • 79 followers
I am a Biomedical engineer who likes to develop hardware and software solutions.

Comments