MaxSonar Range Finder for iPhone

At least in the first instance, we’re going to make use of the sensor in analog mode, and connect it to our Arduino board, which in turn will be connected to our iPhone via the Redpark cable.

Open Xcode, choose to create a new project, select a View-based application for the iPhone device family, and when prompted name it “MaxSonar” and save it to the Desktop.

Adding the Serial Library

Open your copy of the Redpark Serial SDK and grab the redparkSerial.h and rscMgr.h header files from the inc/ folder, and the libRscMgrUniv.a static libraries from lib/. Drag and drop them into your new MaxSonar project, remembering to tick the “Copy items into destination group’s folder (if needed)” checkbox when prompted.

We also need to add the External Accessories framework, so click on the project icon at the top of the Project pane in Xcode, and click on the MaxSonar Target and then on the Build Phases tab. Finally, click on the Link Binary with Libraries item to open up the list of linked frameworks, and click on the + symbol to add a new framework. Select the External Accessory framework from the drop-down list and click the Add button.

Finally, we need to declare support for the cable. Click on the SerialConsole-Info.plist file to open it in the editor. Right-click on the bottommost row of the list and select Add Row from the menu. An extra row will be added to the table, and you’ll be presented with a drop-down menu. Type UISupportedExternalAccessoryProtocols into the box. This will change to the human-readable text “Supported external accessory protocols.” Type the string com.redpark.hobdb9 into Item 0 to declare our support for the cable.

The CorePlot Library

Unsurprisingly perhaps when dealing with sensor data, the need to display a graph or a plot of some kind comes up fairly often when you’re developing applications. Unfortunately, there is no native support for graphs in the iOS SDK. The Core Plot library fills that hole (see Figure 4-3).

The CorePlot test application running in the iPhone Simulator

Figure 4-3. The CorePlot test application running in the iPhone Simulator

The Core Plot library is a third-party 2D plotting framework for iOS and Mac OS X, tightly integrated with Core Animation and under active development. At the time of this writing, the latest release was version 0.4. You can download it from http://code.google.com/p/core-plot/.

Note

If you want to live on the bleeding edge, you can check out the source code straight from Core Plot’s repository using Mercurial:

hg clone http://core-plot.googlecode.com/hg/ core-plot

Mercurial doesn’t ship with Xcode, so unless you’ve already installed it, your first step before doing that will probably be to install Mercurial itself. You can download the latest version of Mercurial from http://mercurial.selenic.com/wiki/.

Compiling the Core Plot library from source code

Download the CorePlot_0.4.zip file from the Core Plot website, or check the latest version out of the project’s Mercurial repository. Open up the CorePlot/framework/ folder and click on the CorePlot-CocoaTouch.xcodeproj project file to open it in Xcode.

Select “Universal Library | iOSDevice” from the Scheme drop-down menu and then Build (⌘B) from the Product menu to build a universal (fat) static library that we can use on both the device and in the iPhone Simulator.

After building, move one folder back up and go into the newly created build directory. Inside the build/Release-universal/ directory you’ll find the libCorePlot-CocoaTouch.a static library; however, in addition to this, we need the associated header files. The custom build script, which created the universal library, doesn’t copy these header files from the individual build directories. However you can find them in the build/Release-iphoneos/usr/local/include/ or build/Release-iphonesimulator/usr/local/include/ directories. Which set of header files you use in your project isn’t really relevant; both sets are identical.

Adding the Core Plot library to the project

Drag and drop the libCorePlot-CocoaTouch.a static library from the build/Release-universal/ folder into your project, remembering to tick the “Copy items into destination group’s folder (if needed)” checkbox when prompted. Then copy one of the two sets of header files, all of them, into your project. Again remember to tick the “Copy items into destination group’s folder (if needed)” checkbox.

Click on the MaxSonar project file at the top of the Project pane, select the MaxSonar Target, and click on the Build Settings tab. Find the Other Linker Flags setting (under the Linking section) and double-click to open the pop-up window and add the -ObjC -all_load flags to the target (see Figure 4-4).

Finally, since the Core Plot library makes use of the QuartzCore framework, we need to add this to our project. Click on the Build Phases tab and then on the Link Binary with Libraries item to open up the list of linked frameworks, and click on the + symbol to add a new framework. Select the QuartzCore framework from the drop-down list and click the Add button.

Adding the other linker flags to your target

Figure 4-4. Adding the other linker flags to your target

Building the User Interface

Now that we’ve added the third-party infrastructure we’ll need to build the application, it’s time to build out the user interface. Click on the MaxSonarViewController.xib file to open it in Interface Builder and drag and drop a UINavigationBar, a UISwitch, a couple of UILabel elements, and finally a generic UIView into your View. Arrange them as shown in Figure 4-5. Change the text in the navigation bar to read “LV-MaxSonar-EZ1” and the text in the two labels to be “Distance:” and “0.00”.

Building the user interface

Figure 4-5. Building the user interface

Click on the generic UIView in your View and open up the Identity inspector in the Utilities pane on the right. We need to change the Class of our View from UIView to be a CPTGraphHostingView. See Figure 4-6.

Warning

Depending on which version of the Core Plot library you are working with, you may need to specify the custom view as a CPGraphHostingView instead.

This custom view is the one we’re going to use to plot our real-time graph of the sensor readings coming from the EZ1 sensor via the Arduino and the serial link to our iPhone.

Setting a custom view

Figure 4-6. Setting a custom view

After changing the view’s class, close the Utilities panel and open up the Assistant Editor. Make sure it’s in Automatic mode and Ctrl-click and drag from the CPTGraphHostingView to the MaxSonarViewController.h interface file to create a graph instance variable and property. Similarly, Ctrl-click and drag from the right-hand UILabel and UISwitch to create distance and toggle instance variables and properties. Finally, Ctrl-click and drag one more time from the UISwitch to a toggled method and IBAction, as in Figure 4-7.

Connecting the outlets and actions

Figure 4-7. Connecting the outlets and actions

Save your changes, switch back to the Standard Editor, and click on the MaxSonarViewController.h interface file to open it in the editor. You should notice that we’ve got an error next to the CPTGraphHostingView instance variable; that’s because Xcode doesn’t know what it is yet. Go ahead and add the following import statement at the top of your code:

#import "CorePlot-CocoaTouch.h"

If you select Build (⌘B) from the Product menu to build your project at this point, everything should compile. If that’s not the case, you should probably make sure you’ve added the External Accessory and QuartzCore frameworks to your project before you begin looking elsewhere.

Building the Backend

We’re done with the user interface. What we need to do now is connect our interface to the incoming measurements from our Arduino board. Obviously the first thing we need to do is integrate the Redpark library into our code. The code for this is going to look almost identical to the code we’ve seen in the last couple of chapters. However, we’re also going to have to establish some infrastructure for our plot. We’ll use a simple XY scatter plot for that task.

Open the MaxSonarViewController.h interface file and add the following code:

#import <UIKit/UIKit.h>
#import "CorePlot-CocoaTouch.h"
#import "RscMgr.h"

#define BUFFER_LEN 1024
#define MAX_POINTS 200

@interface MaxSonarViewController : UIViewController
                                    <CPTPlotDataSource, RscMgrDelegate> {

    CPTGraphHostingView *graph;

    CPTXYGraph *plot;
    NSMutableArray *dataForPlot;

    UILabel *distance;
    UISwitch *toggle;

    RscMgr *manager;
    UInt8 rxBuffer[BUFFER_LEN];
    UInt8 txBuffer[BUFFER_LEN];
}

@property (nonatomic, retain) IBOutlet CPTGraphHostingView *graph;
@property (nonatomic, retain) IBOutlet UILabel *distance;
@property (nonatomic, retain) IBOutlet UISwitch *toggle;
@property (nonatomic, retain) NSMutableArray *dataForPlot;

- (IBAction)toggled:(id)sender;

- (NSNumber *) yValueForIndex:(NSUInteger)index;

@end

This makes our class both a Core Plot data source and a Redpark Serial Cable delegate object. Now open the corresponding MaxSonarViewController.m implementation file. We need to remember to synthesize our new property:

@synthesize dataForPlot;

and then release it in the dealloc method:

    [dataForPlot release];

and finally set it to nil in viewDidUnload method:

    [self setDataForPlot:nil];

We want to go ahead and add a viewDidLoad method. Here we’re going to set up the cable manager object as usual, but also our Core Plot plot object. Doing that setup involves a lot of boilerplate code, and I’m not really going to talk about it in detail here, as it’s all a bit tangential to what we’re actually doing:

- (void)viewDidLoad {
    [super viewDidLoad];

    // cable manager
    rscMgr = [[RscMgr alloc] init];
    [rscMgr setDelegate:self];

    // Create plot from theme
    plot = [[CPTXYGraph alloc] initWithFrame:CGRectZero];
    CPTTheme *theme = [CPTTheme themeNamed:kCPTPlainBlackTheme];
    [plot applyTheme:theme];
    graph.collapsesLayers = NO;
    graph.hostedGraph = plot;

    plot.paddingLeft = 10.0;
    plot.paddingTop = 10.0;
    plot.paddingRight = 10.0;
    plot.paddingBottom = 10.0;

    // Setup plot space
    CPTXYPlotSpace *plotSpace = (CPTXYPlotSpace *)plot.defaultPlotSpace;
    plotSpace.allowsUserInteraction = YES;
    plotSpace.xRange = [CPTPlotRange plotRangeWithLocation:CPTDecimalFromFloat(1.0) 
                                     length:CPTDecimalFromFloat(MAX_POINTS)];
    plotSpace.yRange = [CPTPlotRange plotRangeWithLocation:CPTDecimalFromFloat(1.0) 
                                     length:CPTDecimalFromFloat(MAX_POINTS)];

    // Axes
    CPTXYAxisSet *axisSet = (CPTXYAxisSet *)plot.axisSet;

    CPTXYAxis *x = axisSet.xAxis;
    x.majorIntervalLength = CPTDecimalFromString(@"100");
    x.minorTicksPerInterval = 4;
    x.minorTickLength = 5.0f;
    x.majorTickLength = 7.0f;

    CPTXYAxis *y = axisSet.yAxis;
    y.majorIntervalLength = CPTDecimalFromString(@"50");
    axisSet.yAxis.minorTicksPerInterval = 2;
    axisSet.yAxis.minorTickLength = 5.0f;
    axisSet.yAxis.majorTickLength = 7.0f;

    // Create a green plot area
    CPTScatterPlot *line = [[[CPTScatterPlot alloc] init] autorelease];
    CPTMutableLineStyle *lineStyle = [CPTMutableLineStyle lineStyle];
    lineStyle.miterLimit = 1.0f;
    lineStyle.lineWidth = 3.0f;
    lineStyle.lineColor = [CPTColor greenColor];
    line.dataLineStyle = lineStyle;
    line.identifier = @"Green Plot";
    line.dataSource = self;
    [plot addPlot:line];

    // Do a green gradient
    CPTColor *areaColor = [CPTColor colorWithComponentRed:0.3 
                                    green:1.0 blue:0.3 alpha:0.8];
    CPTGradient *areaGradient = [CPTGradient gradientWithBeginningColor:areaColor 
                                             endingColor:[CPTColor clearColor]];
    areaGradient.angle = −90.0f;
    CPTFill *areaGradientFill = [CPTFill fillWithGradient:areaGradient];
    line.areaFill = areaGradientFill;
    line.areaBaseValue = [[NSDecimalNumber zero] decimalValue];

    // Add plot symbols
    CPTMutableLineStyle *symbolLineStyle = [CPTMutableLineStyle lineStyle];
    symbolLineStyle.lineColor = [CPTColor blackColor];
    CPTPlotSymbol *plotSymbol = [CPTPlotSymbol ellipsePlotSymbol];
    plotSymbol.fill = [CPTFill fillWithColor:[CPTColor greenColor]];
    plotSymbol.lineStyle = symbolLineStyle;
    plotSymbol.size = CGSizeMake(10.0, 10.0);
    line.plotSymbol = plotSymbol;

    CABasicAnimation *fadeInAnimation = [CABasicAnimation 
                                         animationWithKeyPath:@"opacity"];
    fadeInAnimation.duration = 1.0f;
    fadeInAnimation.removedOnCompletion = NO;
    fadeInAnimation.fillMode = kCAFillModeForwards;
    fadeInAnimation.toValue = [NSNumber numberWithFloat:1.0];

    // Add some initial data
    NSMutableArray *contentArray = [NSMutableArray arrayWithCapacity:200];
    self.dataForPlot = contentArray;
}

Now we need to add the CorePlot CPTPlotDataSource data source methods:

#pragma mark - CPTPlotDataSource Methods

-(void)pointReceived:(NSNumber *)point{

    CPTPlot *thisPlot = [plot plotWithIdentifier:@"Green Plot"];
    [self.dataForPlot addObject:point];
    [thisPlot insertDataAtIndex:self.dataForPlot.count-1 numberOfRecords:1];
    self.distance.text = [NSString stringWithFormat:@"%@", point];

}

-(NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot {
    return [dataForPlot count];
}

-(NSNumber *)numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index {

    if (fieldEnum == CPTScatterPlotFieldX) {
        return [NSNumber numberWithInteger:index];
    } else {
        return [self yValueForIndex:index];
    }
}

Along with our yValueForIndex: convenience method:

- (NSNumber *) yValueForIndex:(NSUInteger)index {
     return [self.dataForPlot objectAtIndex:index];
}

Finally add the Redpark Serial delegate methods:

#pragma mark - RscMgrDelegate methods

- (void) cableConnected:(NSString *)protocol {
    [rscMgr setBaud:9600];
    [rscMgr open];

}

- (void) cableDisconnected {
}


- (void) portStatusChanged {
}

- (void) readBytesAvailable:(UInt32)numBytes {
    int bytesRead = [rscMgr read:rxBuffer Length:numBytes];

    NSString *string = nil;
    for(int i = 0;i < numBytes;++i) {
        if ( string ) {
            string =  [NSString stringWithFormat:@"%@%c", string, rxBuffer[i]];
        } else {
            string =  [NSString stringWithFormat:@"%c", rxBuffer[i]];
        }
    }
    [self pointReceived:[NSNumber numberWithInt:[string intValue]]];

}

- (BOOL) rscMessageReceived:(UInt8 *)msg TotalLength:(int)len {
    return FALSE;
}

- (void) didReceivePortConfig {
}

Check that everything builds and runs by running the app in the simulator. You should see something like Figure 4-8. If that looks good, switch to the device and build and deploy the application onto your iPhone.

The MaxSonar app running in the iPhone Simulator

Figure 4-8. The MaxSonar app running in the iPhone Simulator

If everything has gone well, at this point we need to put the iPhone application, along with Xcode itself, to one side for now. It’s time to look at the Arduino end of our build.

Writing the Arduino Sketch

Open up the Arduino development environment and enter the following code and upload it to your Arduino board. We’ll be using analog pin 0, normally marked as A0 on most boards, for the analog input from the EZ1 sensor:

int statusLed = 13;
int ez1Analog = 0;

void setup() {
   pinMode(statusLed,OUTPUT);
   pinMode(ez1Analog,INPUT);
   Serial.begin(9600);
}

void loop() {
   int val = analogRead(ez1Analog);
   if (val > 0) {
      val = val / 2;
      float cm = float(val)*2.54;
      Serial.println( int(cm) );
   }
   blinkLed( statusLed, 100 );1

}

void blinkLed(int pin, int ms) {
   digitalWrite(pin,LOW);
   digitalWrite(pin,HIGH);
   delay(ms);
   digitalWrite(pin,LOW);
   delay(ms);
}
1

We’ll use a convenience method here to blink the board LED on pin 13 for 100 ms. First, so we know something is actually happening, but also to space out the readings being sent to the phone.

Putting It All Together

Nominally the EZ1 requires 5 V; however, according to the datasheet, we can operate it at 3.3 V, albeit with somewhat reduced performance. This simplifies connecting all of our components to the Arduino board. Plug the +VE wire from the EZ1 into the 3.3V pin on the Arduino board, and the GND wire into one of the available GND pins, then connect the AN wire to analog pin 0 (A0). Finally connect your RS-232 to TTL serial adaptor to the 5V, GND, RX, and TX pins as normal (see Figure 4-9).

Connecting all the components

Figure 4-9. Connecting all the components

If you do want to connect the EZ1 to the +5V pin rather than the 3.3V pin, probably the easiest thing to do would be to use a breadboard and some jumper wires.

Once you’re satisfied that everything is connected correctly, power on the Arduino board and start up the application. Point the EZ1 toward the ceiling to get a decent distance measurement, and then place your hand over the sensor. You should see the measured distance drop dramatically. Slowly raise your hand, holding it above the sensor to see the reading change. Hopefully you’ll see something that looks a lot like Figure 4-10 at this point.

The MaxSonar application running on the iPhone

Figure 4-10. The MaxSonar application running on the iPhone

Turning Things On and Off

You’ve probably noticed that we haven’t done anything with the toggle switch. Let’s go ahead and make that usable now. First of all, we’ll need to change our Arduino sketch:

int statusLed = 13;
int powerLed = 12;
int ez1Analog = 0;

int aByte;
int flag = 0;

void setup() {
   pinMode(statusLed,OUTPUT);
   pinMode(ez1Analog,INPUT);
   Serial.begin(9600);

   pinMode(powerLed,OUTPUT);
   blinkLed( statusLed, 500 );
}

void loop() {

   if (Serial.available() > 0) {
          aByte = Serial.read();
          if ( flag == 0 ) {
            flag = 1;
            digitalWrite(powerLed, HIGH);
          } else {
            flag = 0;
            digitalWrite(powerLed, LOW);
          }
    }

   if ( flag == 1 ) {
     int val = analogRead(ez1Analog);

     if ( val > 0 ) {
         val = val / 2;
         float cm = float(val)*2.54;
         Serial.println( int(cm) ); // cm
         blinkLed( statusLed, 100);
      }
   }

}

void blinkLed(int pin, int ms) {
   digitalWrite(pin,LOW);
   digitalWrite(pin,HIGH);
   delay(ms);
   digitalWrite(pin,LOW);
   delay(ms);
}

Here we’ve rewritten our loop( ) so that the Arduino only sends data to the iPhone if it has been toggled on, by receiving a byte from the phone, and then stops transmitting data if it receives another byte. I’m also using a second LED in pin 12 to indicate that data is being sent, but that’s purely optional, and you can omit it if you can’t be bothered to wire another LED up to your Arduino board.

You’ll notice that we’re assuming an initial state of “off,” but that currently our user interface has an initial state of “on.” We need to fix that, so open up the MaxSonarViewController.xib file in Interface Build, click on the switch in the view, and in the Attributes inspector in the Utilities panel, change the initial switch state to “off.”

Finally, to glue everything together, we add some code to the toggled callback:

- (IBAction)toggled:(id)sender {
    txBuffer[0] = 1;
    int bytesWritten = [rscMgr write:txBuffer Length:1];

}

Now if you rebuild and then redeploy the MaxSonar application to the iPhone, you should be able to stop and start the data coming from the phone using the switch.

Get iOS Sensor Apps with Arduino now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.