Products for USB Sensing and Control
Products for USB Sensing and Control

Phidgets Smart Sprinkler Controller with iOS - Part 1

Creating a smart sprinker controller using Phidgets and an iOS app (written in Objective-C)


by Lucas

Source Code

Introduction

If you are interested in smart sprinkler controllers, there are a lot of options available to you. There is Rachio, RainMachine, and SkyDrop, just to name a few. All of these systems have a few things in common:

  • They access local weather forecasts to ensure they only water your lawn when it needs it.
  • They provide users access to the system via a mobile app so you can quickly check or adjust your watering schedule
  • They handle multiple watering zones

In this project, we will be creating a smart sprinkler system that has the features listed above, and more, using Phidgets.

Hardware

If you do not have a sprinkler system in place, you will also need to buy a solenoid valve. Something similar to this should do.

Software

Libraries and Drivers

This project assumes that you are somewhat familiar with the basic operation of Phidgets (i.e. attaching and opening devices, reading data, etc). If this is your first time building a Phidgets project with iOS or a Phidget SBC, please refer to the iOS page or SBC page for instructions on how to set up your environment and get started.

Overview

The general architecture of this project is:

  1. Phidget Network Server directly connected to Phidget relays via USB or VINT Hub depending on hardware
  2. iOS app connected to the Phidget Network Server, displaying/modifying information from a dictionary

The server is configured with a dictionary that is used in order to relay information between the iOS app and the Phidget SBC.

Step 1: Server Configuration

The server configuration was very basic, and added only a single dictionary channel. To create a new dictionary, open the SBC Web Interface and navigate to:

Phidgets->phidget22NetworkServer->Phidget Dictionaries

Create a dictionary like the one shown above

This dictionary will store some key-value pairs that will be used throughout the program. These are described below:

  • city is a string that records which city the forecast information is from. The iOS app will display this information.
  • id is an integer that corresponds to certain weather conditions. Will be discussed in more detail below.
  • forecast is a string that details current weather conditions. The iOS app will display this information.
  • temperature is a double that corresponds to the current temperature in the specified location. The iOS app will display this information.
  • startHour is an integer that corresponds to when the user wants to start watering. This is in 24 hour format and is configured from the iOS app.
  • startMinute - see startHour
  • isMinutes will be used in a future project, stay tuned.
  • duration is an integer that corresponds to the watering duration in minutes. This is configured from the iOS app.

Step 2: iOS app

Note: when opening the Xcode project included for this project, you will have to change some settings before it will work. These include:

  • Header search path
  • Other linker flags

There is a detailed guide on how to change these here.

Now that we have the server configured, we can create a simple iOS app that will allow users to interact with the sprinkler system. Below you can see the main view of the app.

Main app view showing current weather in Calgary. Note the status indicator at the top of the view indicated whether the app has a connection to the PhidgetDictionary. Stay tuned for the next project where water usage will be implemented!

In order to gain access to the PhidgetDictionary, we must create one and initalize it properly.
Note the use of PhidgetNet_enableServerDiscovery(PHIDGETSERVER_DEVICE). This allows us to connect to a Phidget remotely.


//In ViewController.h

PhidgetDictionaryHandle ch;

//In ViewController.c

- (void)viewDidLoad {
    [super viewDidLoad];
    [self initPhidgetDictionary];
    
}
-(void)initPhidgetDictionary{
    PhidgetReturnCode result;
    result = PhidgetDictionary_create(&ch);
    if(result != EPHIDGET_OK){
        const char* errorMessage;
        Phidget_getErrorDescription(result, &errorMessage);
        [self outputError:@"Failed to create channel" message:[NSString stringWithUTF8String:errorMessage]];
    }
    
    result = Phidget_setDeviceSerialNumber((PhidgetHandle)ch, 2000);
    if(result != EPHIDGET_OK){
        const char* errorMessage;
        Phidget_getErrorDescription(result, &errorMessage);
        [self outputError:@"Failed to set serial number" message:[NSString stringWithUTF8String:errorMessage]];
    }
    
    result = Phidget_setOnAttachHandler((PhidgetHandle)ch, gotAttach, (__bridge void*)self);
    if(result != EPHIDGET_OK){
        const char* errorMessage;
        Phidget_getErrorDescription(result, &errorMessage);
        [self outputError:@"Failed to set on attach handler" message:[NSString stringWithUTF8String:errorMessage]];
    }
    
    result = Phidget_setOnDetachHandler((PhidgetHandle)ch, gotDetach, (__bridge void*)self);
    if(result != EPHIDGET_OK){
        const char* errorMessage;
        Phidget_getErrorDescription(result, &errorMessage);
        [self outputError:@"Failed to set on attach handler" message:[NSString stringWithUTF8String:errorMessage]];
    }
    
    result = PhidgetNet_enableServerDiscovery(PHIDGETSERVER_DEVICE);
    if(result != EPHIDGET_OK){
        const char* errorMessage;
        Phidget_getErrorDescription(result, &errorMessage);
        [self outputError:@"Failed to enable server discovery" message:[NSString stringWithUTF8String:errorMessage]];
    }
    
    result = Phidget_open((PhidgetHandle)ch);
    if(result != EPHIDGET_OK){
        const char* errorMessage;
        Phidget_getErrorDescription(result, &errorMessage);
        [self outputError:@"Failed to open channel" message:[NSString stringWithUTF8String:errorMessage]];
    }
    
}

Below is the attach handler for the PhidgetDictionary where we access the city, forecast, and temperature that the PhidgetSBC will provide. In this attach handler we will get all relevant values and display them to the main view as shown in the images above.

				
-(void)onAttachHandler{
    char temp[100];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"DictionaryAttach" object:nil]; //used for status indicator at top of view
    PhidgetDictionary_get(ch, "city", temp, 100);
    [cityLabel setText:[NSString stringWithUTF8String:temp]];
    PhidgetDictionary_get(ch, "forecast",temp,100);
    [forecastLabel setText:[NSString stringWithUTF8String:temp]];
    PhidgetDictionary_get(ch,"temperature",temp,100);
    [temperatureLabel setText:[NSString stringWithFormat:@"%@ ℃",[NSString stringWithUTF8String:temp]]];
}

So, now that we have information from the Phidget SBC, it is time to send some data from the app.
As you can see below, users can select the following:

  • When to start watering
  • How long to water for in minutes
  • How long to water for in litres (i.e. only use X litres to water lawn). Note: this will be implemented in the next project
  • Sync value with system (i.e. update PhidgetDictionary)

Settings for sprinkler system that users can configure.

When the user selects the "Sync to System" button, the following code will be executed:

	
-(void)syncSettings:(NSNotification *)notification{
    PhidgetReturnCode result = 0;
	//get properties that the user has set and that have been stored in NSUserDefaults
    NSString  *duration = [[NSUserDefaults standardUserDefaults] objectForKey:@"duration"];
    NSString  *startHour = [[NSUserDefaults standardUserDefaults] objectForKey:@"startHour"];
    NSString  *startMinute = [[NSUserDefaults standardUserDefaults] objectForKey:@"startMinute"];
    NSString  *isMinutes = [[NSUserDefaults standardUserDefaults] boolForKey:@"isMinutes"] == true ? @"1" : @"0";

	//Apply new settings to the PhidgetDictionary, let user know if something has gone wrong
    result = PhidgetDictionary_set(ch, "duration", [duration cStringUsingEncoding:NSASCIIStringEncoding]);
    if(result != EPHIDGET_OK){
        [[NSNotificationCenter defaultCenter] postNotificationName:@"syncUnsuccessful" object:nil];
    }
    result = PhidgetDictionary_set(ch, "startHour", [startHour cStringUsingEncoding:NSASCIIStringEncoding]);
    if(result != EPHIDGET_OK){
        [[NSNotificationCenter defaultCenter] postNotificationName:@"syncUnsuccessful" object:nil];
    }
    result = PhidgetDictionary_set(ch, "startMinute", [startMinute cStringUsingEncoding:NSASCIIStringEncoding]);
    if(result != EPHIDGET_OK){
        [[NSNotificationCenter defaultCenter] postNotificationName:@"syncUnsuccessful" object:nil];
    }
    result = PhidgetDictionary_set(ch, "isMinutes", [isMinutes cStringUsingEncoding:NSASCIIStringEncoding]);
    if(result != EPHIDGET_OK){
        [[NSNotificationCenter defaultCenter] postNotificationName:@"syncUnsuccessful" object:nil];
    }
    [[NSNotificationCenter defaultCenter] postNotificationName:@"syncSuccessful" object:nil];
}

After successfully sending new values to the PhidgetDictionary

Step 3: Program on SBC

For this project, the code that runs on the Phidget SBC is written in C, but can easily be ported to your preferred language.

Note: you should create a project on the SBC that looks like this:

Your project on the SBC should look like this



Note: After some minor changes to main.c (serial numbers), and after downloading curl, you can compile the program like so:

How to compile the included C code for your SBC

Accessing weather data

The first step to creating a smart sprinkler system is accessing weather data. Fortunately, there are many resources available for obtaining weather forecasts. We ended up using OpenWeatherMap which provides free access to weather forecasts in over 200,000 cities. Weather data is available in multiple formats including:

  • JSON
  • XML
  • HTML

For this project, we used JSON along with the jsmn JSON parser for C. We made a request using curl with the following URL:

http://api.openweathermap.org/data/2.5/weather?q=Calgary&APPID=YOUR_APP_ID&units=metric

In order to get an APPID you must sign up for a free account at OpenWeatherMap.

You can see an example of the returned JSON below.

				
{"coord":{"lon":-114.09,"lat":51.05},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"base":"stations","main":{"temp":5,"pressure":1015,"humidity":44,"temp_min":5,"temp_max":5},"visibility":64372,"wind":{"speed":7.2,"deg":150},"clouds":{"all":90},"dt":1489788000,"sys":{"type":1,"id":3145,"message":0.005,"country":"CA","sunrise":1489758162,"sunset":1489801605},"id":5913490,"name":"Calgary","cod":200}

Creating PhidgetDictionary and PhidgetDigitalOutput

We want to be able to access the PhidgetDictionary that is running on our server, so we need to create and configure a PhidgetDictionary in our C program. We also need to be able to control Phidget relays, therefore we will create one or more (depending on the number of zones) PhidgetDigitalOutputs.
Note the use of PhidgetDictionary_setOnUpdateHandler(dict, onDictionaryUpdate, NULL). This will allow our program to be notified when the user changes a setting via the iOS app.

	
PhidgetDictionaryHandle dict;
PhidgetDigitalOutputHandle digout;
PhidgetReturnCode result;
...		
//Create and init dictionary
result = PhidgetDictionary_create(&dict);
if(result != EPHIDGET_OK){
	Phidget_log(PHIDGET_LOG_ERROR,"failed to create dictionary\n");
	return 1;
}
result = Phidget_setDeviceSerialNumber((PhidgetHandle)dict, DICTIONARY_SERIAL_NUM);
if(result != EPHIDGET_OK){
	Phidget_log(PHIDGET_LOG_ERROR,"failed to set dictionary serial number\n");
	return 1;
}
result = PhidgetDictionary_setOnUpdateHandler(dict, onDictionaryUpdate, NULL);
if (result != EPHIDGET_OK) {
	Phidget_log(PHIDGET_LOG_ERROR,"failed to set dictionary update handler\n");
	return 1;
}

//Create and init digital output
result = PhidgetDigitalOutput_create(&digout);
if (result != EPHIDGET_OK) {
	Phidget_log(PHIDGET_LOG_ERROR,"failed to create digital output\n");
	return 1;
}

result = Phidget_setDeviceSerialNumber((PhidgetHandle)digout, DIGITALOUTPUT_SERIAL_NUM);
if (result != EPHIDGET_OK) {
	Phidget_log(PHIDGET_LOG_ERROR,"failed to set digital output serial number\n");
	return 1;
}

result = Phidget_setHubPort((PhidgetHandle)digout, 0);
if (result != EPHIDGET_OK) {
	Phidget_log(PHIDGET_LOG_ERROR,"failed to set digital output hub port\n");
	return 1;
}
result = Phidget_setIsHubPortDevice((PhidgetHandle)digout, 1);
if (result != EPHIDGET_OK) {
	Phidget_log(PHIDGET_LOG_ERROR,"failed to set digital output hub port device\n");
	return 1;
}

//Open Phidgets
result = Phidget_openWaitForAttachment((PhidgetHandle)dict, 2000);
if(result != EPHIDGET_OK){
	Phidget_log(PHIDGET_LOG_ERROR,"Exiting program, dictionary did not connect\n");
	return 1;
}
result = Phidget_openWaitForAttachment((PhidgetHandle)digout, 2000);
if (result != EPHIDGET_OK) {
	Phidget_log(PHIDGET_LOG_ERROR,"Exiting program, digital output did not connect\n");
	return 1;
}

Watering your Lawn

Now that everything is in place, we should create the main loop in the C program that will decide whether or not to turn on the water.

	
struct Weather {
	int id;
	char name[100];
	char main[100];
	char description[100];
	double temp;
};

struct UserSettings {
	int startHour;
	int startMinute;
	int duration;
};
...
int counter = 180; //get weather and update dictionary immediately
char temp[6];
struct UserSettings userSettings;
struct Weather weather;
time_t rawtime;
time_t startTime;
struct tm * timeinfo;	
...	
while (1) {
	time(&rawtime);
	timeinfo = localtime(&rawtime);

	if (counter++ == 180) { //every 15 minutes (except when watering)
		PhidgetLog_log(PHIDGET_LOG_INFO, "Getting weather information");
		counter = 0;
		updateWeather(); //gets new JSON packet, parses, and fills updates Weather struct
		updateDictionary(dict); //updates PhidgetDictionary with new id, forecast, and temperature
	}

	//dictionaryUpdate is set in the DictionaryUpdate event handler, lets us know if the user has changed anything
	if (dictionaryUpdate) {
		PhidgetLog_log(PHIDGET_LOG_INFO, "Updating dictionary");
		dictionaryUpdate = 0;
		PhidgetDictionary_get(dict, "startHour", temp, 6);
		userSettings.startHour = strtol(temp, NULL, 0);
		PhidgetDictionary_get(dict, "startMinute", temp, 6);
		userSettings.startMinute = strtol(temp, NULL, 0);
		PhidgetDictionary_get(dict, "duration", temp, 6);
		userSettings.duration = strtol(temp, NULL, 0);
	}

	time(&startTime);
	if (timeinfo->tm_hour == userSettings.startHour && timeinfo->tm_min == userSettings.startMinute) {
		if (isBadWeather(weather.id) == 0){
			time_t currentTime;
			PhidgetLog_log(PHIDGET_LOG_INFO, "Water turned on");
			PhidgetDigitalOutput_setState(digout, 1); //turn on water
			do {
				sleep(5);
				time(¤tTime);
			} while(difftime(currentTime,startTime) < (userSettings.duration * 60.0));
			PhidgetLog_log(PHIDGET_LOG_INFO, "Water turned off");
			PhidgetDigitalOutput_setState(digout, 0); //turn off water
		}
		else{
			PhidgetLog_log(PHIDGET_LOG_INFO, "Not watering due to poor weather conditions");
		}
	}
	else {
		sleep(5);
	}
}	

The code above is still quite simple and is not yet as 'smart' as it should be, however, it is a good start.
Here is a small breakdown of the loop:

  • If it has been 15 minutes, update weather
  • If dictionary has been updated, update userSettings struct
  • If current time is equal to set watering time, begin watering
  • Check if the weather is "bad". Weather codes
  • If weather is good, set state of PhidgetDigitalOutput to 1 and water for selected duration.

Checking the Weather codes is an easy way to decide whether to water or not. The weather id is divided into multiple groups. For example, id values of 2xx indicate a thunderstorm, this would qualify as "bad weather" and prevent watering.

Conclusion

So far we have a system that will turn on the water when you want, for as long as you want, and it will also check for weather. Overall, we have accomplished the goal of creating a smart sprinkler system, however, a lot can be done to improve this. Here are some ideas for next time:

  • Incorporate flow meter to track water usage/cost. This can be stored on the SBC and brought out to users via the iOS app.
  • Don't water every day. Let user decide which days of the week to water on.
  • Allow user to have unique settings for each zone.
  • Make C program smarter. Take future/previous forecasts into account, not just current weather.
  • Improve iOS app look by including themes for different weather forecasts.

These are just some ideas, if you have any you would like to share, let us know!