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
- SBC3003 - Phidget SBC4
- Phidget relays - depending on the number of zones you want to connect
- Power supply to turn on solenoids
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:
- Phidget Network Server directly connected to Phidget relays via USB or VINT Hub depending on hardware
- 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
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.
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)
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];
}
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:
Note: After some minor changes to main.c (serial numbers), and after downloading curl, you can compile the program
like so:
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!