CMMotionActivityManager, NSOperationQueues and Async Behaviour

I’ve just deleted a couple of articles from the blog for the first time in five years because, having spent quite a few hours going over the example code I’d posted, I realised that it was rubbish.

There is a characteristic of some of the CMMotionActivityManager methods which gets you into some serious threading detail: they return results to an NSOperationsQueue asynchronously. What I was doing with the simpler of the two, queryStepCountStartingFrom, was to wrap a looping call to the inner results queue in an outer queue. So: I have an ‘inner’ results queue, which is being called as a parameter to the queryStepCountStartingFrom method, effectively adding results which return asynchronously. I then force this to be a prerequisite by making the entire loop the first add operation on an outer queue. When the loop is done I make the call to the Core Plot charting methods, because all of the async data will have returned before the charting operation is added. Right?

Wrong.

It’s pure chance that the async operations return in time. The queue will merrily accept new operations regardless of whether the previous tasks have completed.

These problems emerged when I tried to do the same thing with the slightly more complex queryActivityStartingFromDate method, which returns a richer data set, and so the async calls take significantly longer to return. I’ve been round the houses on this: the nested queues approach didn’t work, nor did a GCD async dispatch queue wrapped around the loop. I also tried messing around with adding dependencies between nested operations. No joy.

The fundamental problem [so far as I can see] is that the first port of call for identifying the end of some queue activity, waitUntilAllOperationsAreFinished, is a synchronous animal and therefore has no purview on the results coming back asynchronously on the queue.

What I really want to avoid is what I’ve done elsewhere when I’ve understood the problem less clearly, which is to mess around with timers. This is a pretty fragile approach.

Googling for this has led me to the conclusion that the ‘right’ way – i.e., to reliably detect the async results coming back – is to do something pretty low level with either threading or GCD.

As a compromise, what I’ve come up with is to set an observer on the queue, which I first serialise [maxConcurrentOperationCount = 1]. In the observeValueForKeyPath method, I can test for the queue being empty, and increment a counter. When the counter hits the threshold for the number of day’s worth of data / loops round the CMMotionActivityManager methods for [i.e., 7], I am done. It ain’t pretty but it works.
Today.

FWIW, here’s the code:

- (void) getDailyActivityTotals
{
    NSLog(@"Entering getDailyTotals");
    // activityQueue is declared as a property as we need to refer to it in the observer method:
    activityQueue.maxConcurrentOperationCount = 1;
    activityQueue.name =@ "activityQueue";
    [activityQueue addObserver:self forKeyPath:@"operations" options:0 context:NULL];
    
    NSLog(@"starting to loop in calActivityOpn");
    for (int dayOffset = 6; dayOffset >=0; dayOffset--)
    {
        NSMutableArray *dailyResultsTmpArray = [[NSMutableArray alloc] init];
        // startAndEndDates is a method which uses NSCalendar and NSDateComponents to
        // create some NSDate objects based on an offset: 24 hour periods - start and end dates:
        dailyResultsTmpArray = [self startAndEndDates:dayOffset];
        [motionActivity queryActivityStartingFromDate:dailyResultsTmpArray[1]
                                               toDate:dailyResultsTmpArray[0]
                                              toQueue:activityQueue
                                          withHandler:^(NSArray *activities, NSError *error)
        {
            NSInteger walkingCount = 0;
            NSInteger runningCount = 0;
            NSInteger carCount = 0;
            NSInteger staticCount = 0;
            NSInteger actionItemCount = 0;
            // This returns a lot of data, so we are just counting the number of events
            // per activity category:
            for (CMMotionActivity *actItem in activities)
            {
                if (actItem.walking == 1)
                    walkingCount++;
                if (actItem.running == 1)
                    runningCount++;
                if (actItem.automotive == 1)
                    carCount++;
                if (actItem.stationary == 1)
                    staticCount++;
                actionItemCount++;
            }
            //NSLog(@"walking: %i", walkingCount);
            DailyActivityObject *todaysResults = [[DailyActivityObject alloc] initWithNumWalkCount:walkingCount numRun:runningCount nummAuto:carCount numStatic:staticCount numAction:actionItemCount forDay:dailyResultsTmpArray[0]];
            [dailyResults addObject:todaysResults];
            NSLog(@"adding dailyResults");
                
        }];

    }
}

and the KVO method:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                         change:(NSDictionary *)change context:(void *)context
{
    if (object == activityQueue && [keyPath isEqualToString:@"operations"]) 
    {
        // if the queue is empty - remember we have serialised:
        if (activityQueue.operationCount == 0)
        {
            oberserverCount ++;
            NSLog(@"queue has completed %i iterations", oberserverCount);
            if (oberserverCount == 7)
            {
                // we have all the charting data so....
                [activityQueue addOperationWithBlock:^{
                    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                        [self plotCompoundChart:dailyResults];
                    }];
                }];
            }
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object
                               change:change context:context];
    }
}

Adding a Speaker

This was a piece of cake: I realised that just scrolling ‘Incoming!’ isn’t particularly attention grabbing, so I ripped a speaker out of a redundant toy [freebie phone extension speakers from a conference I think], and wired it up to one of the digital outputs. When the code identifies the termination character in the message body, it plays the series of tones. This is straight out of the tutorial.

Someone pointed out that the reason that the POP library was occasionally having problems parsing emails being sent by different mail user agents was possibly down to the send format – plain text versus HTML or RTF.

Email to LCD By Way of Arduino

There are plenty of tutorials out there for this, but I’ve taken a particular tack to pull the data down on the Raspberry Pi which, because it’s fundamentally torturous, I’ve not seen anyone else take. So here goes…

The idea is that I use Tasker on my phone to set GeoFences, and then using the email plugin for it, send a message to a spare account [one that I originally set up for the motion detection]. I then haul down the email, parse out the body, and then send it down over the serial port to the LCD connected to the Arduino. All of which is so that I can “impress” my wife to [I can almost hear her eyes rolling] with a fundamentally daft piece of technology while I’m out of the house :). Let the “I’m in the pub!” messages ensue….

The easiest way to do the serial communication is using the pySerial library. Unfortunately, I don’t know Python at all, and so reverted to Perl as my scripting glue of choice. Getting this combination working was one of the trickiest steps of the whole process, as the Debian version of Perl is a little strange to navigate. I installed cpanimus, and then tried to install Inline-Python. This failed, complaining about a missing library, so I then installed python-dev via apt-get [which, in Raspberry Pi life-on-an-SD-card terms, is massive – 43mb]. I repeated the Inline-Python install which then worked fine, only to discover that there aren’t a plethora of examples explaining basics like passing in variables to inline methods.

Next step was which POP3 library to go with. I tried about 4, before settling on POP3Client [installed via sudo cpanm]. BTW, I’m not using TLS, because I’ve found it to be a little bit hit and miss with my ISP, and there are enough things that can go wrong with this project already. Needless to say you should use it wherever possible. You probably want an account dedicated for this anyway.

Before getting on to talk about the serial port comms, it’s worth taking a minute over the connecting of the LCD. I can’t solder for toffee, and managed to break the first LCD I tried [not sure how]. FWIW, this is just a random 16×2 LCD which supports HD44780, and which I ordered up from a crew called HobbyTronics [great service]. The dodgy carpentry was just a way of stabilising the display. What I ended up doing was putting a blob of solder on the end of each pin, and then pull it through from the back. There’s actually a piece of cardboard underneath to firm up the contacts – not exactly production quality, but hey, it works :).

Serial port communication is fundamentally unreliable. I experimented for quite a while with this: the termination of the data is a given, but I also tried delimiting the start of the message as well. Regardless, the repeated padding prequel data worked for me. I can imagine that this is going to be very display-dependent.

A couple more gotchas. The POP client appends what I initially thought was binary data to the string containing the body of the email message, which was appearing as a corrupt character on the LCD. It’s actually a control character, which I globally substitute out.

Having done all my sketch development and testing on the Mac, I then hit the hardy perennial of power consumption problems with the Raspberry Pi, because I have both USB ports occupied [WiFi and the Arduino]. At the standard 150 milliseconds that the scrolling method example on the Arduino website suggests, the text was borderline unreadable, so I tuned this down to 350 milliseconds. Finally, I found that the scrolling text wrapped on me, if I just offset by the length of the display string [again, as per the example]. I was running out of steam by this point, so I just subtracted the width of the display from the length of the string.

On to the code. First the POP3 to serial port client, that I run every minute as a cron job on the Raspberry Pi:

#!/usr/bin/perl
use Mail::POP3Client;
$pop = new Mail::POP3Client( USER     => "your\@email.com",
                               PASSWORD => "yourPassword",
                               HOST     => "your.email.server.com" ) || die "failed: $!\n";
$messageCount = $pop->Count();
#print "$messageCount messages\n";

# just confirming the message counter starts at 1.
# The newest message is at the bottom so we will set the body array to that value:
$textForLED = $pop->Body($messageCount);
# appended character removal:
$textForLED =~ s/(\r\n)//g;

# Only read the newest message; housekeeping is delete everything else that might be there:
for( $i = 1; $i Delete($i);
}
$pop->Close();

if ($messageCount == 0)
{
   # This is vestigial: as I run the script every minute, I don't want to display
   # any filler content if there is no email.
   #print "message count is zero\n";
   # http://numbersapi.com/number/type
   #$randNum = int(rand(5000));
   #$textForLED = `curl -s http://numbersapi.com/$randNum/trivia`;
   #print $textForLED;
}

else
{
   # this was for debug purposes:
   open (LOGFILE, ">>/home/pi/temp_sensor/logdata.txt");
   print LOGFILE "Email content: $textForLED, \n";
   close (LOGFILE);
   # Add the character to terminate:

   $textForLED = $textForLED . "^";

   for ($repeatCount = 0; $repeatCount<= 1; $repeatCount++)
   {
      serialPipe($textForLED);
   }

# This indentation can't be changed for inline code:
use Inline Python => q{

def serialPipe(textIn):
   #print 'Hi, %s.' % textIn
   import serial
   import time
   ser = serial.Serial('/dev/ttyACM0', 115200)
   for x in range(0, 5):
      ser.write('*************************************')
      time.sleep(1)
   ser.write(textIn)
   #ser.write('test message^');
};
}

and now the sketch:

#include 

// Connections:
// rs (LCD pin 4) to Arduino pin 12
// rw (LCD pin 5) to Arduino pin 11
// enable (LCD pin 6) to Arduino pin 10
// LCD pin 15 to Arduino pin 13
// LCD pins d4, d5, d6, d7 to Arduino pins 5, 4, 3, 2
LiquidCrystal lcd(12, 11, 10, 5, 4, 3, 2);

//int backLight = 13;    // pin 13 will control the backlight

void setup()
{
  //pinMode(backLight, OUTPUT);
  //digitalWrite(backLight, HIGH); // turn backlight on. Replace 'HIGH' with 'LOW' to turn it off.
  lcd.begin(16,2);              // columns, rows.  use 16,2 for a 16x2 LCD, etc.
  lcd.clear();                  // start with a blank screen
  lcd.setCursor(0,0);           // set cursor to column 0, row 0 (the first row)
  //lcd.print("About Bleeding");    // change this text to whatever you like. keep it clean.
  lcd.setCursor(0,1);           // set cursor to column 0, row 1
  //lcd.print("time!");
  delay(3000);
  Serial.begin(115200);
  
  // if you have a 4 row LCD, uncomment these lines to write to the bottom rows
  // and change the lcd.begin() statement above.
  //lcd.setCursor(0,2);         // set cursor to column 0, row 2
  //lcd.print("Row 3");
  //lcd.setCursor(0,3);         // set cursor to column 0, row 3
  //lcd.print("Row 4");
}

void loop()
{
   lcd.clear(); 
   String ledDisplay;  
   boolean endOfText = false;
   while ((Serial.available() > 0) && (endOfText == false))
   {
      //Serial.println('looping');
      char inByte = Serial.read();
      if (inByte != '^')
      {
          ledDisplay = ledDisplay + inByte;
      }
      else
      {
         for (int repeatCount = 0; repeatCount <=3; repeatCount++)
         {
            lcd.setCursor(0,0);
            lcd.print("***INCOMING!!!!***");
            delay(2000);
            for (int positionCounter = 0; positionCounter < 18; positionCounter++)
            {
              lcd.scrollDisplayLeft();
              delay(350);
            }
            delay(1000);
            lcd.clear();
         }       
         for (int repeatCount = 0; repeatCount <=4; repeatCount++)
         {
            lcd.setCursor(0,0);
            lcd.print(ledDisplay);
            delay(1000);
            for (int positionCounter = 0; positionCounter < (ledDisplay.length() -16); positionCounter++)
            {
              lcd.scrollDisplayLeft();
              delay(350);
            }
            delay(2000);
            lcd.clear();
            delay(1000);
         }
         for (int repeatCount = 0; repeatCount <=3; repeatCount++)
         {
            lcd.setCursor(0,0);
            lcd.print("***THAT IS ALL***");
            delay(2000);
            for (int positionCounter = 0; positionCounter < 17; positionCounter++)
            {
              lcd.scrollDisplayLeft();
              delay(350);
            }
            delay(1000);
            lcd.clear();
         }  
         endOfText = true;
         ledDisplay = "";
         delay(5000);
         lcd.clear();
      }
   }
   delay(1000);
}