Architecture

Smart Greenhouse is a greenhouse with sensors and actuators. The sensors and actuators are connected to Intel Edison based micro controller. The micro controller send data and receive commands from a control center hosted in AWS cloud. Users can interact with Smart Greenhouse through a dashboard or a tablet application. Users can also issue voice commands to the greenhouse.

Architecture of Smart Greenhouse can be broken down into three major components

Connected greenhouse with Sensors and Actuators

Control Center

User Interaction devices

Lets look at each one of these in more detail.

Connected greenhouse with Sensors and Actuators

The connected devices inside the Smart Greenhouse communicate with AWS IOT service using Intel Edison micro-controller boards that are housed in the spine of the greenhouse. There are two micro-controller hubs, a sensor hub for gathering all the sensor data and an actuator hub controlling actuators associated with devices. Each of the hub micro-controller boards have a NodeJS based application running and talking to connected devices on one end and AWS IOT service on the other end. The NodeJS application running on micro-controller board talks to

Sensor node is an Intel Edison microcontroller board with a NodeJS application running on it. The NodeJS application reads data from all the connected sensors every second and publishes the readings to AWS IOT service over a secured channel using MQTT protocol.

General data flow for the sensor node is as follows:

1. Read from connected sensors every second

2. Convert the readings into appropriate units

3. Envelope the readings into json based payload

4. Send the json payload as MQTT message to AWS IOT

Actuator Node

Actuator node is another Intel Edison microcontroller board with NodeJS application running on it. The actuator NodeJS application subscribes to a set of MQTT topics and waits for messages to arrive on these topics. The actuator MQTT topics convey commands for the actuators connected to the greenhouse. Data flow for actuator node is as follows:

1. Subscribe to actuator MQTT topics and listen for messages on these topics

2. When the message arrives, parse the message into command and determine the actuator associated with the message

3. Execute the command by sending appropriate signals to the actuator specified by the message

4. After a preconfigured delay, read the status of actuator and send the reading to AWS IOT message topic

Controller Web Application is a set of RESTful APIs used to control the Smart Greenhouse. This is a NodeJS application utilizing ‘expressesjs' for request response handling and ‘passportjs' for authentication. This application accepts REST PUT commands to carry out various IOT Greenhouse tasks such as opening a window vent or turning on a sprinkler.

Streaming Web Server

Streaming Web Server is used to communicate status of Smart Greenhouse to the interested parties in real time. This is a WebSockets streaming server based on NodeJS. It listens for MQTT topic updates from IOT Greenhouse and publishes these updates on a streaming WebSockets channel. Front-end application running in a browser can get real-time updates from this channel about the conditions in greenhouse such as current temperature, humidity and luminosity. Actuator state updates are also communicated on this channel.

Monitoring Lambda Functions

Conditions in greenhouse such as, temperature and humidity are monitored continuously by triggering AWS lambda functions through IOT rules engine. Lambda function then evaluates the readings for conditions such as ‘temperature is greater than 75 degrees Fahrenheit for 5 seconds and for at least 3 readings’. If condition is satisfied, a set of preconfigured actions such as opening a vent, turning on fan and switching off humidifier are fired.

User Interaction devices

Users can interact with the Smart Greenhouse using a dashboard, through voice prompts or using a tablet app

Dashboard

The web-based dashboard shows real-time graph of current temperature, humidity, soil moisture and luminosity. Greenhouse fan, ventilation windows, overhead lamp, led light and humidifier can also be controlled by the web-based dashboard application.

Voice prompts

Simple commands such as ‘Alexa turn on the fan’ can be used with Amazon Echo device to activate the fan. The Alexa commands currently supported for the smart greenhouse are:

1. Alexa what is Internet of Things

2. Alexa turn on/off fan

3. Alexa turn on/off vents

4. Alexa turn on/off overhead lamp

Tablet app

Accenture developed an android app, which has similar functionality as that of the web based dashboard. This app can also be used to interact with the Smart Greenhouse.

Code

Actuator NodeJavaScript

Code that runs on micro-controller board connected to all the actuators. This code listens for commands on an MQTT channel connected to AWS IOT service and executes this command by interpreting the message and carrying out actuator actions.

Sensor NodeJavaScript

Code that runs on micro-controller board connected to all the sensors. Sensor reading is collected from each connected sensor every second and is posted to AWS IOT service over an MQTT channel.

/* IOT Greenhouse Sensor Node Code*/varmraa=require('mraa');//require mraaconsole.log('MRAA Version: '+mraa.getVersion());//write the mraa version to the Intel XDK consolevarmqtt=require('mqtt');varfs=require('fs');varmyOnboardLed=newmraa.Gpio(13);//LED hooked up to digital pin 13 (or built in pin on Intel Galileo Gen2 as well as Intel Edison)myOnboardLed.dir(mraa.DIR_OUT);varLightSensorPin=newmraa.Aio(0);varHumiditySensorPin=newmraa.Aio(1);varSoilSensorPin=newmraa.Aio(2);varTempSensorPin=newmraa.Aio(3);varB=3975;varLoopInterval=null;varCollectingData=false;// ssl cert configvaricebreakerCirtsDir=__dirname+'/certs/icebreaker';varoptions={key:fs.readFileSync(icebreakerCirtsDir+'/iot-greenhouse-private.crt'),cert:fs.readFileSync(icebreakerCirtsDir+'/iot-greenhouse.pem'),ca:[fs.readFileSync(icebreakerCirtsDir+'/rootCA.pem')],requestCert:true,rejectUnauthorized:true,port:8883,cleanSession:true,//reconnectPeriod: 2000,//connectTimeout: 2000,protocolId:'MQIsdp',protocolVersion:3};varclient=mqtt.connect('mqtts://data.iot.us-east-1.amazonaws.com',options);console.log("Connecting to AWS MQTT server...");client.on('connect',functiononConnect(data){console.log("Connected: "+data);//only restart the sensor watch if it is not running. This has to do with the setinterval not//being cleared correctly on errorif(CollectingData==false){startSensorWatch();}});//Emitted only when the client can't connect on startupclient.on('error',function(err){console.log('Error connecting to IceBreaker '+err);});//Emitted after a disconnectionclient.on('offline',function(){console.log('IceBreaker connection lost or WiFi down.');clearInterval(LoopInterval);});//Emitted after a closed connectionclient.on('close',function(){console.log('IceBreaker connection closed.');clearInterval(LoopInterval);});//Emitted after a reconnectionclient.on('reconnect',function(){console.log('IceBreaker connection restored.');startSensorWatch();});/*Function: startSensorWatch(client)Parameters: client - mqtt client communication channelDescription: Read Sensor Data on timer event and send it to AWS IOT*///function startSensorWatch(client) {functionstartSensorWatch(){'use strict';varSensor1Success=false;varSensor2Success=false;varSensor3Success=false;varSensor4Success=false;LoopInterval=setInterval(function(){myOnboardLed.write(1);CollectingData=true;varLightSensorValue=LightSensorPin.read();varHumiditySensorValue=HumiditySensorPin.read();varHumiditySensorPercentage=SensorMap(HumiditySensorValue,200,700,0,100);varTempSensorValue=TempSensorPin.read();varTempSensorMilliVolts=(TempSensorValue*(5000/1024));//for 5v AVREFvarCentigradeTemp=(TempSensorMilliVolts-500)/10;varfahrenheit_temperature=(((CentigradeTemp*9)/5)+32).toFixed(2);varmessage={reading_timestamp:newDate().getTime(),luminosity:LightSensorValue};client.publish('IOTGreenhouse/Sensors/Luminance/Sensor1',JSON.stringify(message),{},function(){//console.log("luminosity: " + LightSensorValue); Sensor1Success=true;});varmessage={reading_timestamp:newDate().getTime(),humidity:HumiditySensorPercentage};client.publish('IOTGreenhouse/Sensors/Humidity/Air/Sensor1',JSON.stringify(message),{},function(){console.log("humidity: "+HumiditySensorPercentage);Sensor2Success=true;});varmessage={reading_timestamp:newDate().getTime(),temperature:fahrenheit_temperature};client.publish('IOTGreenhouse/Sensors/Temperature/Sensor1',JSON.stringify(message),{},function(){Sensor3Success=true;});//Only flash the heartbeat indicator if all sensors read correctly//if not, leave it on solidif(Sensor1Success&&Sensor2Success&&Sensor3Success){//if (Sensor4Success) {myOnboardLed.write(0);varSensor1Success=false;varSensor2Success=false;varSensor3Success=false;varSensor4Success=false;}},1000);}functionSensorMap(x,in_min,in_max,out_min,out_max){returnMath.round((x-in_min)*(out_max-out_min)/(in_max-in_min)+out_min);}process.on('SIGINT',function(){console.log("Received Interrupt, Exiting...");clearInterval(LoopInterval);client.end();process.exit(0);});

AWS Lambda funtion to Monitor Humidity in the GreenhouseJavaScript

This AWS Lambda function gets triggered on every humidity reading from the greenhouse. When pre-set conditions are met, actions specified in configuration file are executed in order to monitor humidity in the greenhouse

/** * Created by atulbar on 9/25/15. */console.log('Loading function');varnconf=require('nconf');varAWS=require('aws-sdk');//Setup nconf to use (in-order):// 1. Command-line arguments,// 2. Environment variables// 3. A file located at 'path/to/config.json'nconf.argv().env().file({file:__dirname+'/air-humidity-monitor-config.json'});// nconf.argv().env().file({ file: __dirname + '/temperature-monitor-config.json' });// create IceBreaker Mqtt Client and connect it to the servervaricebreakerMqttClient=require('./icebreaker-mqtt-client');icebreakerMqttClient.connect();vardocClient=newAWS.DynamoDB.DocumentClient({region:nconf.get('aws_region')});console.log('Settings '+JSON.stringify(nconf.get(),null,2));varMEASUREMENT=nconf.get('measurement');varMEASUREMENT_HIGH_THRESHOLD=nconf.get('measurement_high_threshold');// measurement high thresholdvarMEASUREMENT_LOW_THRESHOLD=nconf.get('measurement_low_threshold');// measurement low thresholdvarEVALUATION_DURATION=nconf.get('evaluation_duration');// duration for evaluation of trigger in secondsvarMEASUREMENT_SENSOR=nconf.get('measurement_sensor');varHIGH_THRESHOLD_ALARM_ACTIONS=nconf.get('high_threshold_alarm_actions');varLOW_THRESHOLD_ALARM_ACTIONS=nconf.get('low_threshold_alarm_actions');varALARM_TABLE_NAME=nconf.get('alarm_table_name')||'IOTGreenhouse_alarms';vardataSource=nconf.get('mode');varsensorTopicPrefix=dataSource+'/Sensors/';varsensors=nconf.get('sensors');varactuators=nconf.get('actuators');varMEASUREMENT_SENSOR_ID;if(sensors){MEASUREMENT_SENSOR_ID=sensorTopicPrefix+sensors[MEASUREMENT_SENSOR];}exports.handler=function(event,context){// console.log('Received event:', JSON.stringify(event, null, 2));if(!MEASUREMENT_SENSOR_ID)failWithMessage('No Sensor is configured');if(!event[MEASUREMENT])failWithMessage('Incorect event received');readCurrentAlarmState(function(err,alarmState){if(err)failWithMessage('Failed to read alarm state due to error '+err);if(!alarmState.processing){// alarm is offif(crossedHighThreshold(event)){// crossed threshold for first timeinitializeAlarmProcessing(true,function(err){if(err)failWithMessage('Failed to initialize alarm processing due to error '+err);context.succeed();});}elseif(crossedLowThreshold(event)){// crossed threshold for first timeinitializeAlarmProcessing(false,function(err){if(err)failWithMessage('Failed to initialize alarm processing due to error '+err);context.succeed();});}else{context.succeed();// threshold not reached}}else{// alarm was processingif(!(alarmState.highThreshold)&&crossedHighThreshold(event)){// crossed other thresholdinitializeAlarmProcessing(true,function(err){if(err)failWithMessage('Failed to initialize alarm processing due to error '+err);context.succeed();});}elseif(alarmState.highThreshold&&crossedLowThreshold(event)){// crossed threshold for first timeinitializeAlarmProcessing(false,function(err){if(err)failWithMessage('Failed to initialize alarm processing due to error '+err);context.succeed();});}elseif(droppedBackWithinTolerance(event,alarmState)){// we dropped back within toleranceconsole.log(MEASUREMENT+' back within tolerance, clearing alarm processing');clearAlarmProcessing(function(err){if(err)failWithMessage('Failed to clear alarm state due to error '+err);context.succeed();});}else{// we continue to be above thresholdalarmState.readingCount++;updateAlarmProcessing(alarmState,function(err){if(err)failWithMessage('Failed to update alarm state due to error '+err);console.log('alarm reading count '+alarmState.readingCount);if(alarmState.readingCount>2){// at least 3 readingsvartimeElapsedInSeconds=(newDate().getTime()-alarmState.firstReadingTime)/1000;if(timeElapsedInSeconds>EVALUATION_DURATION){fireAlarm(alarmState,function(err){if(err)console.log('Failed to fire alarm state due to error '+err);// do not fail here, we need to clear alarm stateclearAlarmProcessing(function(errDb){if(errDb)failWithMessage('Failed to clear alarm state due to error '+errDb);context.succeed();});});}else{context.succeed();// within evaluation duration}}else{context.succeed();// not enough readings}});}}});functioncrossedHighThreshold(event){returnevent[MEASUREMENT]>MEASUREMENT_HIGH_THRESHOLD;}functioncrossedLowThreshold(event){returnevent[MEASUREMENT]<MEASUREMENT_LOW_THRESHOLD;}functiondroppedBackWithinTolerance(event,alarmState){if(alarmState.highThreshold){returnevent[MEASUREMENT]<=MEASUREMENT_HIGH_THRESHOLD;}else{returnevent[MEASUREMENT]>=MEASUREMENT_LOW_THRESHOLD;}}functionfailWithMessage(message){console.log(message);context.fail(message);}functionreadCurrentAlarmState(callback){varparams={TableName:ALARM_TABLE_NAME,Key:{sensor_id:MEASUREMENT_SENSOR_ID},ConsistentRead:true};docClient.get(params,function(err,data){if(err){callback(err);}else{varalarmState;if(!data.Item||!data.Item.alarmState){alarmState={processing:false};}else{alarmState=data.Item.alarmState;}callback(null,alarmState);}});}functionupdateAlarmProcessing(alarmState,callback){varparams={TableName:ALARM_TABLE_NAME,Item:{sensor_id:MEASUREMENT_SENSOR_ID}};params.Item.alarmState=alarmState;docClient.put(params,callback);}functioninitializeAlarmProcessing(highThreshold,callback){varalarmState={processing:true,highThreshold:highThreshold,firstReadingTime:newDate().getTime(),readingCount:1};console.log('Initializing alarm processing state '+JSON.stringify(alarmState));updateAlarmProcessing(alarmState,callback);}functionclearAlarmProcessing(callback){varalarmState={processing:false};console.log('clearing alarm processing ');updateAlarmProcessing(alarmState,callback);}functionfireAlarm(alarmState,callback){varalarmActions=alarmState.highThreshold?HIGH_THRESHOLD_ALARM_ACTIONS:LOW_THRESHOLD_ALARM_ACTIONS;console.log('Firing alarm with state '+JSON.stringify(alarmState,null,2)+' mqtt messages '+JSON.stringify(alarmActions,null,2));varpublishPending=alarmActions.length;alarmActions.map(function(alarmAction){icebreakerMqttClient.sendCommandToActuator(alarmAction.actuator,alarmAction.message,function(err){if(--publishPending==0){callback(err);}});});}};