iPhone Development - Developers' Corner

Basics | File Structure | Frameworks | API Docs | Code

Treadmill Control Basics

When discussing digital data and audio files it is important to distinguish the STORAGE MEDIA from the STORAGE FORMAT from the TRANSMISSION FORMAT. STORAGE MEDIA for DIGITAL audio can be a hard drive, a SD memory card, an audio CD, internal computer memory, and more. STORAGE MEDIA for ANALOG audio include phonograph records and audio tape. Likewise, the TRANSMISSION FORMAT for audio can be either analog or digital. A simple earphone jack (like the one on your iPod) is ANALOG. Your HD TV may be connected to satellite speakers by a DIGITAL HDMI cable.

Chirp controlled treadmills require ANALOG audio. This means the digitally stored audio on your computer or iPod must be converted to analog. No problem since computers and iPods are made to do this. Just hook up the earphone jack to the treadmill with a suitable cable and you have the needed ANALOG connection in place.

It's interesting to note that the ANALOG audio chirp that goes to the treadmill actually contains DIGITAL information. That's right. A binary command is modulated into audio and sent to the treadmill where it is demodulated and executed.

In my iPhone app, the user creates a sequence of workout segments. Each segment consists of a speed, incline, and duration. I build short audio WAV files, each of which "command" the treadmill to the desired speed and incline. I "play" this WAV audio - then set a timer for the segment duration. When the timer expires I "play" the audio "command" for the next segment.

WAV audio is a relatively simple uncompressed DIGITAL audio format. I believe it was defined by Microsoft in the early 1990s. To keep my uncompressed digital audio files fairly small I create the files using monophonic audio and modest quality values of 8 bit samples with a 22.05 kHz sampling rate.

Treadmills controlled by SD cards are fundamentally very different. While the SD card workout may contain audio, that audio is NOT used in controlling the treadmill. That audio only exists to provide information, encouragement, and ...perhaps... some marketing. Actual control of the treadmill speed and incline is accomplished by a sequence of [very strange] binary commands.


The SD Card File Structure

Compatible treadmills only recognize specific directories and files as legal workouts. The minimum SD Card structure for a legal workout is:

    iFit
        Tread
            layout.fit
            S000000A.fit
            W000000A.fit
                        
Where:

layout.fit

This file specifies workouts available to the treadmill on this SD card. These workouts can be accessed sequentially using the up/down arrows on the treadmill console. The zip file emailed by my Treadmill Controller App includes a layout.fit file that always specifies the same workout and sound files.

The analytic individual may want to use a BINARY editor (there are hidden, non-printable characters in the file) to manually edit this file to specify multiple workouts instead of just one. For example, one might change this file from:

  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  ********,********,********,*
  ********,********,********,*
  ********,********,********,*
                        
To:
  W000000A,S000000A,********,*
  W000000B,S000000A,********,*
  W000000C,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  W000000A,S000000A,********,*
  ********,********,********,*
  ********,********,********,*
  ********,********,********,*
                        
Then, instead of renaming the emailed workout to W000000A.fit, your would rename one to W000000B.fit, another to W000000C.fit, and so on. Since the Treadmill Controller App does not include sound in the workouts, S000000A.fit is the only sound definition file needed or wanted.

Hint: If you edit the layout.fit file and the treadmill SD Card stops working, erase all the files from the card and restore the original zip file content to the SD Card.


S000000A.fit

In the file structure defined by the treadmill folks, 'S' files are the text files that specify the sound files played during a workout. Since the Treadmill Controller App uses no sound, this file is only an empty place holder. This same empty place holder can be used for every workout specified in the layout.fit file.


W000000A.fit

This is a binary file that tells the treadmill what to do. Each workout must have its own, distinct, file and treadmills like these files to be named W000000A.fit, W000000B.fit, W000000C.fit, ...


Experiment!

I stopped investigating the treadmill file structure when I learned enough to do what I wanted. Some of you out there may go far beyond me. If you learn anything cool, or have a comment, drop me an email. Be aware that whatever you share with me will be considered shared freely and I am under no responsibility to compensate you in any way.



iOS Frameworks

Overview

Several years ago companies started making treadmills that could be controlled from an external source. In the early days control was accomplished by applying a series of audio "chirps" to a special input jack. More recently, treadmills were manufactured that could be commanded by the insertion of a specially prepared Secure Digital (SD) memory card. Files on the SD Card provided a complete predetermined workout.

Unfortunately, many of these "factory made" workouts, both chirp and SD Card, were costly and more like a media production than a workout. Version 4.0 of my programs Treadmill Controller and Treadmill Controller Free allow the user to economically take personal control of their treadmill exercise for both Chirp and SD Card treadmills. These Apps use the professionally produced and tested iOS Frameworks that are offered here. Using these Frameworks, iPhone developers may incorporate treadmill control in any Application.

Treadmills are managed very differently by the Chirp and SD Card technologies. The file(s) that constitute a workout must be created and written to an SD Card in an appropriate directory structure. The controlling file has a very particular format with checksums and anticipates a general structure and timing to workouts. Chirps, however, are externally generated and can be applied to the treadmill at any time.

Sample programs are provided that show experienced iPhone developers exactly how to use these Frameworks.


Chirp Control iOS Framework

This Framework is used to develop iPhone/iPad/iTouch Applications that can operate compatible treadmills with audio "Chirps".

SD Card iOS Framework

This Framework is used to develop iPhone/iPad/iTouch Applications that can operate compatible treadmills by inserting a specially prepared SD Card.

Treadmill Chirp Framework API Description

View or Download a PDF of the Chirp Framework API document.



Treadmill SD Card Framework API Description

View or Download a PDF of the SD Framework API document




Code

Sample code to call Chirp and SD Card Framework APIs.


Screen Shot of Sample App (running on the xcode simulator)


Screen Shot Here

chirpAPI.h

/*
 *  chirpAPI.h
 *  Treadmill Controller App
 *
 *  Created by Mike Ficco on 12/29/10.
 *  Copyright 2010-2015 Mike Ficco. All rights reserved.
 *
 * This file defines the EXTERNAL interface to the chirp iOS Framework
*/

/*
   Note that to play the audio (with AudioServicesPlaySystemSound and such) we need to:
      - include/import <AudioToolbox/AudioToolbox.h>
      - and add the AudioToolbox.framework.  Do this by
         > right click on Frameworks > Add > Existing Frameworks > AudioToolbox.framework
*/

#import <AudioToolbox/AudioToolbox.h>


#define ChirpERR_NONE          0x00000000     // No error detected
#define ChirpERR_LIBOPEN       0x80000001     // The Chirp Library is already open
#define ChirpERR_LIBCLOSED     0x80000002     // The Chirp Library has not yet been opened or is already closed
#define ChirpERR_MALLOCFAILURE 0x80000003     // Problem allocating memory
#define ChirpERR_BADSPEED      0x80000004     // The specified speed   value is out of range
#define ChirpERR_BADINCLINE    0x80000005     // The specified incline value is out of range

#define MAX_SPEED              12.0           // Treadmill speed   must be less than (or equal) 12 MPH
#define MAX_INCLINE            12.0           // Treadmill incline must be less than (or equal) 12 degrees


uint32_t getChirpLibraryVersion(void);

uint32_t openChirpLibrary(void);
uint32_t closeChirpLibrary(void);

uint32_t makeChirp(float speed, float incline, uint32_t* bufSize, uint8_t** chirpBuffer);
uint32_t makeStopChirp(uint32_t* bufSize, uint8_t** chirpBuffer);
                      


sdAPI.h

/*
 *  sdAPI.h
 *  Treadmill Controller App
 *
 *  Created by Mike Ficco on 01/10/11.
 *  Copyright 2011-2015 Mike Ficco. All rights reserved.
 *
 * This file defines the EXTERNAL interface to the SD iOS Framework
 */


#define SdERR_NONE            0x00000000      // No error detected
#define SdERR_LIBOPEN         0x80000001      // The SD Library is already open
#define SdERR_LIBCLOSED       0x80000002      // The SD Library has not yet been opened or is already closed
#define SdERR_MALLOCFAILURE   0x80000003      // Problem allocating memory
#define SdERR_BADSPEED        0x80000004      // The specified speed   value is out of range
#define SdERR_BADINCLINE      0x80000005      // The specified incline value is out of range
#define SdERR_BADTIME         0x80000006      // The specified time is illegal, not specified, or already used
#define SdERR_BADINDEX        0x80000007      // The specified index could not be found
#define SdERR_BADPOINTER      0x80000008      // The specified memory pointer is invalid
#define SdERR_BADSTARTTIME    0x80000009      // The workout does not start at 0 seconds
#define SdERR_WORKOUTINVALID  0x8000000A      // No workout has been started
#define SdERR_WORKOUTACTIVE   0x8000000B      // A workout is already in progress
#define SdERR_WORKOUTEMPTY    0x8000000C      // A workout has been started, but there are no segments in it
#define SdERR_WORKOUTTOOBIG   0x8000000D      // The number of workout segments has gotten too large

#define MAX_TIME              5400            // This is 90 minutes in seconds
#define MAX_SPEED             12.0            // Treadmill speed   must be less than (or equal) 12 MPH
#define MAX_INCLINE           12.0            // Treadmill incline must be less than (or equal) 12 degrees


typedef struct
{
   uint16_t   time;
   float      speed;
   float      incline;
} WoSegment;

uint32_t getSdLibraryVersion(void);

uint32_t openSdLibrary(void);
uint32_t closeSdLibrary(void);

uint32_t startSdWorkout(void);
uint32_t addSegmentToSdWorkout(uint16_t time, float speed, float incline);
uint32_t finishSdWorkout(uint32_t* fileSize, uint8_t** fileBuf);

uint32_t countSegmentsInSdWorkout(int* count);
uint32_t listSegmentsInSdWorkout(int maxSegments, int* count, WoSegment* segmentBuf);

uint32_t deleteSegmentByIndexFromSdWorkout(int segmentIndex);
uint32_t deleteSegmentByTimeFromSdWorkout(uint16_t time);
                      


sample.h

/*
 *  sample.h
 *  Reference implementation for using the Chirp and SD treadmill controller iOS Frameworks
 *
 *  Created by Mike Ficco on 2011 05-29.
 *  Copyright 2011-2015 Mike Ficco. All rights reserved.
 *
 * This file contains information used in the implementation of an example Treadmill Controller App.
 * It is independent of specific chirp or SD functions
 *
 * Version 1.00
 *
 */

#import "ChirpAPI.h"
#import "sdAPI.h"


#define   ENDSAMP             0xFFFF
#define   SEGMENTS_IN_SAMPLE       9


typedef   struct
{
   uint32_t       duration;
   float          speed;
   float          incline;
   SystemSoundID  soundID;
} WOSEGMENT;


extern   void   copySampleWorkout(WOSEGMENT* sampleWorkout);
                      


sample.m

/*
 *  sample.m
 *  Reference implementation for using the Chirp and SD treadmill controller iOS Frameworks
 *
 *  Created by Mike Ficco on 2011 05-29.
 *  Copyright 2011-2015 Mike Ficco. All rights reserved.
 *
 * This file contains information used in the implementation of an example Treadmill Controller App.
 * It is independent of specific chirp or SD functions
 *
 * Version 1.00
 *
 */

#import "sample.h"


WOSEGMENT   runThis[SEGMENTS_IN_SAMPLE] =
{
   {  60,      3.0,   3.0 },      // Segment [ 0]
   {  60,      4.0,   4.0 },      // Segment [ 1]
   {  60,      4.0,   5.0 },      // Segment [ 2]
   {  60,      4.5,   5.0 },      // Segment [ 3]
   {  60,      5.0,   5.0 },      // Segment [ 4]
   {  60,      4.0,   5.0 },      // Segment [ 5]
   {  60,      4.0,   4.0 },      // Segment [ 6]
   {  60,      3.0,   3.0 },      // Segment [ 7]
   { ENDSAMP,  0.0,   0.0 }
};


void   copySampleWorkout(WOSEGMENT* sampleWorkout)
{
   memcpy(sampleWorkout, runThis, sizeof(runThis));
}
                      


ReferenceAppAppDelegate.h

//
//  ReferenceAppAppDelegate.h
//  ReferenceApp
//
//  Created by Mike Ficco on 5/29/11.
//  Copyright Mike Ficco 2011-2015. All rights reserved.
//

#import <UIKit/UIKit.h>

@class ReferenceAppViewController;

@interface ReferenceAppAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    ReferenceAppViewController *viewController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet ReferenceAppViewController *viewController;

@end
                      


ReferenceAppViewController.h

//
//  ReferenceAppViewController.h
//  ReferenceApp
//
//  Created by Mike Ficco on 5/29/11.
//  Copyright Mike Ficco 2011-2015. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface ReferenceAppViewController : UIViewController
{
               NSInteger       nowPlaying;
               NSTimer*        chirpTimer;
   IBOutlet    UITextField*    libVersion;
   IBOutlet    UITextField*    information;
}


@property (nonatomic)                     NSInteger      nowPlaying;
@property (nonatomic, retain)             NSTimer*       chirpTimer;
@property (nonatomic, retain)  IBOutlet   UITextField*   libVersion;
@property (nonatomic, retain)  IBOutlet   UITextField*   information;


- (IBAction) makeChirpWorkout:   (id) sender;
- (IBAction) makeSdWorkout:      (id) sender;

@end
                      


ReferenceAppAppDelegate.m

//
//  ReferenceAppAppDelegate.m
//  ReferenceApp
//
//  Created by Mike Ficco on 5/29/11.
//  Copyright Mike Ficco 2011-2015. All rights reserved.
//

#import "ReferenceAppAppDelegate.h"
#import "ReferenceAppViewController.h"

@implementation ReferenceAppAppDelegate

@synthesize window;
@synthesize viewController;


- (void)applicationDidFinishLaunching:(UIApplication *)application {    
    
    // Override point for customization after app launch    
    [window addSubview:viewController.view];
    [window makeKeyAndVisible];
}


- (void)dealloc {
    [viewController release];
    [window release];
    [super dealloc];
}

@end
                      


ReferenceAppViewController.m

/*
 *  ReferenceAppViewController.m
 *  Reference implementation for using the Chirp and SD treadmill controller iOS Frameworks
 *
 *  Created by Mike Ficco on 2011 05-29.
 *  Copyright 2011-2015 Mike Ficco. All rights reserved.
 *
 * This file contains information used in the implementation of an example Treadmill Controller App.
 *
 * Version 1.00
 *
 */


#import "sample.h"
#import "ReferenceAppViewController.h"

@implementation ReferenceAppViewController

@synthesize nowPlaying;
@synthesize chirpTimer;
@synthesize libVersion;
@synthesize information;


/*
// The designated initializer. Override to perform setup that is required before the view is loaded.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        // Custom initialization
    }
    return self;
}
*/

/*
// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView {
}
*/


/*
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
    [super viewDidLoad];
}
*/


/*
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
*/

- (void)didReceiveMemoryWarning {
   // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
   
   // Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
   // Release any retained subviews of the main view.
   // e.g. self.myOutlet = nil;
}


- (void)dealloc {
    [super dealloc];
}


/*
   Treadmill Sample Code Follows
*/

WOSEGMENT   sampleWorkout[SEGMENTS_IN_SAMPLE];


- (void) playChirp: (NSTimer*) timer
{
   /* Because of the way the workout is built for this sample app, the last workout
      segment is the stop chirp, but the duration is still ENDSAMP
   */
   self.nowPlaying++;
   if ( self.nowPlaying < SEGMENTS_IN_SAMPLE )
   {
      AudioServicesPlaySystemSound(sampleWorkout[self.nowPlaying].soundID);
      if ( (self.nowPlaying+1) < SEGMENTS_IN_SAMPLE )
      {
         /* This was NOT the stop chirp.  The duration is legal and more chirps
            remain to be played for this workout
         */
         self.chirpTimer = [NSTimer scheduledTimerWithTimeInterval:  sampleWorkout[self.nowPlaying].duration
                                                target:   self
                                              selector:   @selector (playChirp:)
                                              userInfo:   nil
                                               repeats:   NO];

         [self.information setText: [NSString stringWithFormat: @"Just played chirp %2d of %2d. Delaying %d sec", self.nowPlaying+1, SEGMENTS_IN_SAMPLE, sampleWorkout[self.nowPlaying].duration]];
      }
      else
      {
         // We just played the last chirp
         self.chirpTimer = nil;
         [self.information setText: [NSString stringWithFormat: @"Finished playing all %2d chirps", SEGMENTS_IN_SAMPLE]];
      }
   }
   else
   {
      UIAlertView*   alert = [[UIAlertView alloc] initWithTitle: @"Internal Error" message: @"We have a very confused timer" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil];
      [alert show];
      [alert release];
   }   
}


- (IBAction) makeChirpWorkout: (id) sender
{
   int            loop;
   uint32_t       result;
   uint32_t       bufSize;
   CFURLRef       mySoundURL;
   CFStringRef    fileCFStringRef;
   FILE*          chirpFile;
   uint8_t*       chirpBuf;
   NSString*      pathNSString;
   NSString*      fileNSString;
   NSString*      cacheDirectory;


   // Grab access to the cacheDirectory (where we store the chirp files)
   NSArray*   filePaths   = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
   cacheDirectory         = [filePaths objectAtIndex: 0];

   copySampleWorkout(sampleWorkout);    // Get our predefined sample workout

   result = openChirpLibrary();         // ...and start using it
   if ( result == ChirpERR_NONE )
   {
      [self.libVersion setBorderStyle: UITextBorderStyleNone];
      [self.libVersion setBackgroundColor: [UIColor yellowColor]];
      [self.libVersion setTextAlignment: UITextAlignmentCenter];
      [self.libVersion setText: [NSString stringWithFormat:@"Using Chirp Library Version %8X", getChirpLibraryVersion()]];

      for ( loop = 0; loop < SEGMENTS_IN_SAMPLE; loop++ )
      {
         // Build any needed audio chirp files for this workout
         fileNSString    = [NSString stringWithFormat:@"segment%2d.wav", loop];
         pathNSString    = [cacheDirectory stringByAppendingPathComponent: fileNSString];
         fileCFStringRef = (CFStringRef) pathNSString;

         // Create the file URL for the chirp audio WAV file
         mySoundURL = CFURLCreateWithFileSystemPath(NULL, fileCFStringRef, kCFURLPOSIXPathStyle, false);
         if ( mySoundURL )
         {
            // Create the controlling chirp files
            if ( sampleWorkout[loop].duration == ENDSAMP )
            {
               // This is the last segment.  Stop the treadmill
               makeStopChirp(&bufSize, &chirpBuf);
            }
            else
               makeChirp(sampleWorkout[loop].speed, sampleWorkout[loop].incline, &bufSize, &chirpBuf);

            // Write the file to program cache
            chirpFile = fopen([pathNSString cStringUsingEncoding: 1], "wb");
            if ( chirpFile )
            {
               fwrite(chirpBuf, sizeof(uint8_t), bufSize, chirpFile);
               fclose(chirpFile);
            }

            // Create system sound objects representing the sound files
            AudioServicesCreateSystemSoundID(mySoundURL, &sampleWorkout[loop].soundID);
            CFRelease(mySoundURL);
         }
         else
         {
            UIAlertView*   alert = [[UIAlertView alloc] initWithTitle: @"Internal Error" message: @"Unable To Create File URL" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil];
            [alert show];
            [alert release];
         }
      }

      closeChirpLibrary();

      /* ALERT!!
         The Chirp Workout Audio Files are now ready for use...

         This sample presents the basic usage of my Chirp iOS Framework.
         You'll want to improve error checking and optimize this for the
         specifics of your application.

         Use the chirp files by creating a timer and periodically playing them
      */
      self.nowPlaying = 0;
      AudioServicesPlaySystemSound(sampleWorkout[self.nowPlaying].soundID);
      self.chirpTimer = [NSTimer scheduledTimerWithTimeInterval:   sampleWorkout[self.nowPlaying].duration
                                             target:   self
                                           selector:   @selector (playChirp:)
                                           userInfo:   nil
                                            repeats:   NO];

      [self.information setBorderStyle: UITextBorderStyleNone];
      [self.information setBackgroundColor: [UIColor lightGrayColor]];
      [self.information setTextAlignment: UITextAlignmentCenter];
      [self.information setText: [NSString stringWithFormat: @"Just played chirp %2d of %2d. Delaying %d sec", self.nowPlaying+1, SEGMENTS_IN_SAMPLE, sampleWorkout[self.nowPlaying].duration]];
   }
   else
   {
      UIAlertView*   alert = [[UIAlertView alloc] initWithTitle: @"Error opening Chirp Framework" message: @"openChirpLibrary() returned an error" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil];
      [alert show];
      [alert release];            
   }
   
}   


- (IBAction) makeSdWorkout: (id) sender
{
   int            loop;
   int            count;
   int            fResult = 0;   // File operation results
   int            runningTime;   // Time for next segment in seconds
   uint32_t       res;           // Result (error code) of function called
   uint32_t       fileSize;
   uint8_t*       sdBuf;
   FILE*          sdFile;
   NSString*      sdName;
   NSString*      sdPath;
   NSString*      cacheDirectory;


   // Don't allow a chirp workout to continue running...
   if ( self.chirpTimer )
   {
      [self.chirpTimer invalidate];
      self.chirpTimer = nil;
   }

   // Grab access to the cacheDirectory (where we store the SD file)
   NSArray*   filePaths   = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
   cacheDirectory         = [filePaths objectAtIndex: 0];

   copySampleWorkout(sampleWorkout);    // Get our predefined sample workout

   res = openSdLibrary();               // ...and start using it
   if ( res == SdERR_NONE )
   {
      [self.libVersion setBorderStyle: UITextBorderStyleNone];
      [self.libVersion setBackgroundColor: [UIColor yellowColor]];
      [self.libVersion setTextAlignment: UITextAlignmentCenter];
      [self.libVersion setText: [NSString stringWithFormat:@"Using SD Framework Version %8X", getSdLibraryVersion()]];

      res         = startSdWorkout();
      loop        = 0;
      runningTime = 0;
      while ( (res == SdERR_NONE) && (loop < SEGMENTS_IN_SAMPLE) )
      {
         if ( sampleWorkout[loop].duration == ENDSAMP )
         {
            // This is the last segment.  Stop the treadmill
            res = addSegmentToSdWorkout((uint16_t) runningTime, 0.0, 0.0);
         }
         else
            res = addSegmentToSdWorkout((uint16_t) runningTime, sampleWorkout[loop].speed, sampleWorkout[loop].incline);

         runningTime += sampleWorkout[loop].duration;
         loop++;
      }

      if ( res == SdERR_NONE )
      {
         res = finishSdWorkout(&fileSize, &sdBuf);
         if ( res == SdERR_NONE )
         {
            // Write the SD workout file to program cache
            sdName   = [NSString stringWithFormat:@"%@", @"sampleworkout"];
            sdPath   = [cacheDirectory stringByAppendingPathComponent: sdName];
            sdFile   = fopen([sdPath cStringUsingEncoding: 1], "wb");
            if ( sdFile )
            {
               count = fwrite(sdBuf, sizeof(uint8_t), fileSize, sdFile);
               fclose(sdFile);
               
               if ( count != fileSize )
                  fResult = 1;      // ...anything non-zero
            }
            else
               fResult = 2;         // ...anything non-zero

            if ( fResult == 0 )
            {
               /* The SD Workout File, whose path is 'sdPath', is ready for use
                  Do something with it, like emailing it...
               */
               [self.information setBorderStyle: UITextBorderStyleNone];
               [self.information setBackgroundColor: [UIColor lightGrayColor]];
               [self.information setTextAlignment: UITextAlignmentCenter];
               [self.information setText: [NSString stringWithFormat: @"SD file created. E-Mail it (or whatever)"]];
            }
            else
            {
               UIAlertView*   alert = [[UIAlertView alloc] initWithTitle: @"Error:\nA problem was encountered while writing a file" message: @"" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil];
               [alert show];
               [alert release];
            }
         }
         else
         {
            UIAlertView*   alert = [[UIAlertView alloc] initWithTitle: @"Error:\nA problem was encountered writing this workout" message: @"" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil];
            [alert show];
            [alert release];
         }
      }
      else
      {
         UIAlertView*   alert = [[UIAlertView alloc] initWithTitle: @"ERROR:\nA problem was encountered creating this workout" message: @"" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil];
         [alert show];
         [alert release];            
      }
      
      closeSdLibrary();      // File written, free() memory...
   }
   else
   {
      UIAlertView*   alert = [[UIAlertView alloc] initWithTitle: @"Error opening SD Framework" message: @"openSdLibrary() returned an error" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil];
      [alert show];
      [alert release];            
   }
}   


@end