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

Phidgets Smart Sprinker Controller with iOS - Part 2

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


by Lucas

Part 2 Preview

Completed iOS app with new settings and water usage.

Introduction

At the end of the first project, we left some goals for future projects:

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

In part 2 of this project, we will be tackling goals one and two. We will implement a flow meter in order to track water usage, and we will incorporate new settings to the iOS app which will allow users to set the days of the week they would like to water.

Hardware

The hardware remains the same from the last project, however, we do need to incorporate a flow meter and a Phidget FrequencyCounter to interface the flow meter with.

Software

Libraries and Drivers

Overview

The general architecture of this project remains the same:

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

Step 1: Server Configuration

We will be adding a few new key-value pairs to our dictionary that we created last project.

Overview of new properties:

  • waterMonday, waterTuesday, etc. - a true/false value that will let our C program know whether we should water on a specific day.

Step 2: iOS app

We need to make the following modifications to our iOS app:

  • Allow user to view water usage data from the flow meter that has been stored on the Phidget SBC
  • Implement a way for users to select which days they would like to water on

iOS app after modifications.

Accessing water usage data

Example of what water usage data looks like. This file lives on the Phidget SBC

Water usage data was stored in a file on the Phidget SBC (more detail below). In order to access the data, the following code was implemented in the viewDidLoad method of our main view controller.


//In ViewController.h
NSMutableArray *volumeArray;
NSMutableArray *dateArray;

//In ViewController.c viewDidLoad method
if(volumeArray == nil){
    volumeArray = [[NSMutableArray alloc] init];
}
if(dateArray == nil){
    dateArray =[[NSMutableArray alloc] init];
}
NSURL *url = [NSURL URLWithString: @"http://sprinklersbc4.local:8080/data/weatherdata.txt"];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    formatter.dateFormat = @"EEE MMM dd HH:mm:ss yyyy";
    NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@"\n"];
    NSArray *arr = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] componentsSeparatedByCharactersInSet:set];
    for(int i = 0; i < arr.count; i++){
        if(i % 2 == 0){
            [dateArray addObject:[formatter dateFromString:(NSString *)arr[i]]];
        }
        else{
            [volumeArray addObject:[NSNumber numberWithDouble:[(NSString *)arr[i] doubleValue]]];
        }
    }
    if(volumeArray.count > 0){
        [self graphInit];
        [self updateGraph:volumeArray:dateArray];
        [totalWaterUsage setText:[NSString stringWithFormat:@"Total: %.2f Litres",[[volumeArray valueForKeyPath:@"@sum.self"] doubleValue]]];
    }
}];
[task resume];

After parsing the data we simply have to graph it.

Graphing

For this project, the charts library by Daniel Cohen Gindi was used in order to graph the water usage.

Watering Days

As we can see above, there were a number of key-value pairs added to the dictionary in order to allow the user to configure the watering days. The user can now simply toggle a switch on the iOS app if they want to water on a specific day. The value is stored in NSUserDefaults as before, and then the PhidgetDictionary is modified when the user selects the "Sync to System" button.
The new code for this can be seen below.


-(void)syncSettings:(NSNotification *)notification{
    PhidgetReturnCode result = 0;
    const char *weekdays[] = {"waterSunday","waterMonday","waterTuesday","waterWednesday","waterThursday","waterFriday","waterSaturday"};
    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";


    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];
    }
    for(int i = 0; i < 7; i ++){
        NSString *key = [NSString stringWithFormat:@"weekday%dIsEnabled", i];
        NSString *value = [[NSUserDefaults standardUserDefaults] boolForKey:key] == true ? @"1" : @"0";
        result = PhidgetDictionary_set(ch, weekdays[i], [value cStringUsingEncoding:NSASCIIStringEncoding]);
        if(result != EPHIDGET_OK){
            [[NSNotificationCenter defaultCenter] postNotificationName:@"syncUnsuccessful" object:nil];
        }
    }
    
    
    [[NSNotificationCenter defaultCenter] postNotificationName:@"syncSuccessful" object:nil];
}

Program on SBC

The next step is modifying the C code that is running on the Phidget SBC. The following needs to be done:

  • Create a PhidgetFrequencyCounter and subscribe to its data events.
  • Log data from the Phidget SBC to a file that the iOS app can access.
  • Modify existing code so that user can set a duration based on water consumption instead of only time.
  • Modify existing code so that lawn is only watered on specific days.

Creating PhidgetFrequencyCounter

In order to easily interface with a flow meter, we will include a PhidgetFrequencyCounter such as the 1054 - Phidget FrequencyCounter or the Versatile Input Phidget into our project. We can subscribe to the PhidgetFrequencyCounter_setOnCountChangeHandler() function which will give us a count change. We can use this count change and the milimeters/pulse value from the flow meter data sheet in order to calcluate the amount of water flowing through the meter.
The code below shows how to create and intialize a PhidgetFrequencyCounter in C.

        
PhidgetFrequencyCounterHandle freq;
...
//Create and init frequency counter
result = PhidgetFrequencyCounter_create(&freq);
if (result != EPHIDGET_OK) {
    PhidgetLog_log(PHIDGET_LOG_ERROR,"failed to create frequency counter\n");
    return 1;
}

result = Phidget_setDeviceSerialNumber((PhidgetHandle)freq, FREQUENCYCOUNTER_SERIAL_NUM);
if (result != EPHIDGET_OK) {
    PhidgetLog_log(PHIDGET_LOG_ERROR,"failed to set frequency counter serial number\n");
    return 1;
}

result = PhidgetFrequencyCounter_setOnCountChangeHandler(freq, onCountChange, NULL);
if(result != EPHIDGET_OK){
    PhidgetLog_log(PHIDGET_LOG_ERROR,"failed to set on count change handler\n");
    return 1;
}    

result = Phidget_openWaitForAttachment((PhidgetHandle)freq, 2000);
if (result != EPHIDGET_OK) {
    PhidgetLog_log(PHIDGET_LOG_ERROR,"Exiting program, frequency counter did not connect\n");
    return 1;
}  

We can use the count change event in order to calculate the total volume of water used. This is shown below:

  
double totalVolume = 0.0;
...
void
onCountChange(PhidgetFrequencyCounterHandle ch, void *ctx, uint64_t counts, double timeChange) {
	totalVolume += (double)counts * 0.00225; //adafruit flow meter specifies 2.25ml per pulse
}

Modifying Main Loop

Now that we are tracking the amount of water being used, we can incorporate this into our main loop. We can also modify the main loop so that the lawn is only watered on user-specified days.

     
//Main loop
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();
        updateDictionary(dict);
    }

    if (dictionaryUpdate) {
        PhidgetLog_log(PHIDGET_LOG_INFO, "Updating dictionary");
        dictionaryUpdate = 0;
        PhidgetDictionary_get(dict, "startHour", temp, STRING_LENGTH);
        systemSettings.startHour = strtol(temp, NULL, 0);
        PhidgetDictionary_get(dict, "startMinute", temp, STRING_LENGTH);
        systemSettings.startMinute = strtol(temp, NULL, 0);
        PhidgetDictionary_get(dict, "duration", temp, STRING_LENGTH);
        systemSettings.duration = strtol(temp, NULL, 0);
        PhidgetDictionary_get(dict, "isMinutes", temp, STRING_LENGTH);
        systemSettings.isMinutes = strtol(temp, NULL, 0);
        for(i = 0; i < 7; i ++){
            PhidgetDictionary_get(dict, weekdays[i],temp,STRING_LENGTH);
            systemSettings.waterToday[i] = strtol(temp,NULL,0);
        }
    }

    time(&startTime);
    
    if(systemSettings.waterToday[timeinfo->tm_wday]){
        if (timeinfo->tm_hour == systemSettings.startHour && timeinfo->tm_min == systemSettings.startMinute) {
            if(isBadWeather(weather.id) == 0){
            time_t currentTime;
            PhidgetLog_log(PHIDGET_LOG_INFO, "Setting total volume to 0");
            totalVolume = 0;
            PhidgetLog_log(PHIDGET_LOG_INFO, "Water turned on");
            PhidgetDigitalOutput_setState(digout, 1); //turn on water
            do {
                sleep(5);
                time(& currentTime);
            } while(continueWatering(systemSettings, startTime, currentTime));
            PhidgetLog_log(PHIDGET_LOG_INFO, "Water turned off");
            PhidgetLog_log(PHIDGET_LOG_INFO, "Logging total volume used");
            logData();
            PhidgetDigitalOutput_setState(digout, 0); //turn off water
            }
            else{
                PhidgetLog_log(PHIDGET_LOG_INFO, "Bad weather, not going to water");
            }
        }
    }
    else{
        PhidgetLog_log(PHIDGET_LOG_INFO, "Today is not on watering schedule");
    }	

    sleep(5);
}

The code for the continueWatering() function is shown below:

   
int continueWatering(struct IrrigationSettings settings, time_t startTime, time_t currentTime){
    if(settings.isMinutes){
        if(difftime(currentTime,startTime) < (settings.duration * 60.0))
            return 1;
        else
            return 0;
    }
    else{
        if(totalVolume < settings.duration)
            return 1;
        else
            return 0;
    }
}              

Logging Data

As shown in the code above, the logData() function is called after the lawn has been watered for the specified amount of time. This functions simply creates a file and logs the time and amount of water used as shown below:

 
void logData(void) {
    time_t rawtime;
    struct tm * timeinfo;
    time(&rawtime);
    timeinfo = localtime(&rawtime);
    FILE* log = fopen("data/weatherdata.txt", "a");
    if(log == NULL){
    PhidgetLog_log(PHIDGET_LOG_ERROR,"failed to open weatherdata.txt\n");
        return;
    }
    fprintf(log,"\n%s%f", asctime(timeinfo), totalVolume);
    fclose(log);
}

Now that we have stored the data, we should allow our iOS app access to it, so that it can be graphed and shown to users. In order to do that, we must have a Phidget Webserver enabled with access to the file our program will log to.

Make sure your Webserver is enabled!

Now simply create a link to the data/weatherdata.txt file at your docroot location.

Use a program like WinSCP to create a link to your data folder.

As shown above in the iOS code, you can now easily access this file by specifying the URL:
NSURL *url = [NSURL URLWithString: @"http://sprinklersbc4.local:8080/data/weatherdata.txt"];

Conclusion

Upon completion of part 2, we have a sprinkler system that does the following:

  • Allows users to enter a specific watering time (e.g. 5:20PM)
  • Allows users to enter a specific watering duration. Users can water for X minute or X litres.
  • Allows users to specify which days of the week to water
  • System checks local weather forecasts and adjusts watering schedule accordingly
  • System tracks water usage.

We still have the following goals to accomplish:

  1. Allow user to have unique settings for each zone.
  2. Make C program smarter. Take future/previous forecasts into account, not just current weather.
  3. Improve iOS app look by including themes for different weather forecasts.
  4. Installation

Stay tuned for the installation project coming soon!