# Tuesday, 29 June 2010
There are really only 2 tech blogs that I read; TechCrunch.com and ReadWriteWeb.com (RWW). They both provide balanced coverage of the consumer and enterprise markets while remaining objective. You don't get the sense that site sponsors and advertisers are driving the stories. I admire their integrity.

I've been particularly impressed with RWW's coverage on the cloud and the Internet of Things, so I was absolutely thrilled when Alex Williams ran with a story yesterday about Chatter Bot titled "How to Connect an Office Building to an Activity Stream". Check it out! (A hint on the title of this blog :-) )

Tuesday, 29 June 2010 11:32:15 (Pacific Daylight Time, UTC-07:00)
# Thursday, 24 June 2010

Chatter Developer Challenge / Hackathon 2010 Roundup

The Chatter Developer Challenge sponsored by Salesforce encouraged Developers to create a wide variety of applications that demonstrate the new Salesforce Chatter API.

The challenge culminated in a Hackthon event on June 22nd 2010 at the San Jose Convention Center where prizes were awarded for various applications.

My entry, Chatter Bot, demonstrated the use of Chatter within a Facility Management application that captured physical world events and moved them to the cloud to produce Chatter feed posts.

Chatter Bot is a system comprised of 4 major components:

  • Arduino board with motion and light sensors (C/C++)
  • Proxy Service (Java Processing.org Environment)
  • Salesforce Sites HTTP Listener (Visualforce/Apex)
  • Facility Management App (Force.com database and web forms)
(Source code to all components available at the bottom of this post)

I was elated to learn a few days before the hackathon that Chatter Bot had been selected as a finalist and I was strongly encouraged to attend. So I packed up Chatter Bot to take the 2 hour flight from Portland to San Jose.

It wasn't until I arrived at the airport that it suddenly dawned on me how much Chatter Bot bares a striking resemblance to a poorly assembled explosive device. Apparently the TSA agent handling the X-Ray machine thought so too and I was taken aside for the full bomb sniffing and search routine.

It crossed my mind to add a bit levity to the situation by making some kind of remark, but I quickly assessed that I was probably one misinterpreted comment away from being whisked off in handcuffs to some TSA lockup room. Ironically, I had no problem with security in San Jose coming back. They must be accustomed to these types of devices in Silicon Valley.

Upon arriving in San Jose, I setup Chatter Bot and configured the San Jose Convention Center (SJCC) as a Building Facility (Custom object) to be monitored.

Several assets were created to represent some rooms within the SJCC.

Finally, the Chatter Bot was associated with a particular room (Asset) through an intersection object called AssetSensors that relates a device ID (usually a MAC address) and an Asset.

Within minutes the motion and light sensors were communicating to the cloud via my laptop and reporting on activity in the Hackathon room.

Given the high quality and functionality of fellow competitors apps, such as the very cool Chatter for Android app by Jeff Douglas, and observations from the public voting, I thought Chatter Bot might be a little too "out of the box" to take a prize. It was a genuinely surreal and surprising moment when I learned Chatter Bot received the grand prize.

Thank you Salesforce for hosting such a great event and thank you to the coop-etition for the encouraging exchange of ideas and feedback during the challenge!

Arduino Sensor

/////////////////////////////
//VARS
//the time when the sensor outputs a low impulse
long unsigned int lowIn;

//the amount of milliseconds the sensor has to be low 
//before we assume all motion has stopped
long unsigned int pause = 5000;

boolean lockLow = true;
boolean takeLowTime;  

int LDR_PIN = 2;    // the analog pin for reading the LDR (Light Dependent Resistor)
int PIR_PIN = 3;    // the digital pin connected to the PIR sensor's output
int LED_PIN = 13;

byte LIGHT_ON    = 1;
byte LIGHT_OFF   = 0;
byte previousLightState  = LIGHT_ON;
int lightLastChangeTimestamp = 0;
unsigned int LIGHT_ON_MINIMUM_THRESHOLD = 1015;
unsigned long lastListStateChange = 0; //Used to de-bounce borderline transitions.

// Messages
int SENSOR_MOTION = 1;
int SENSOR_LIGHT  = 2;

/////////////////////////////
//SETUP
void setup(){  
  //PIR initialization
  pinMode(PIR_PIN, INPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(PIR_PIN, LOW);
  
  Serial.begin(9600);
  
  InitializeLED();
  InitializeLightSensor();
  InitializeMotionSensor();
}

////////////////////////////
//LOOP
void loop(){
  
  if(digitalRead(PIR_PIN) == HIGH){
    digitalWrite(LED_PIN, HIGH);   //the led visualizes the sensors output pin state
    if(lockLow){
      //makes sure we wait for a transition to LOW before any further output is made:
      lockLow = false;
      writeMeasure(SENSOR_MOTION, HIGH);
      delay(50);
      digitalWrite(LED_PIN, LOW);   //the led visualizes the sensors output pin state
    }
    takeLowTime = true;
  }

  if(digitalRead(PIR_PIN) == LOW){
    digitalWrite(LED_PIN, LOW);  //the led visualizes the sensors output pin state
    if(takeLowTime){
      lowIn = millis();          //save the time of the transition from high to LOW
      takeLowTime = false;       //make sure this is only done at the start of a LOW phase
    }
    
    //if the sensor is low for more than the given pause, 
    //we assume that no more motion is going to happen
    if(!lockLow && millis() - lowIn > pause){
      //makes sure this block of code is only executed again after 
      //a new motion sequence has been detected
      lockLow = true;
      writeMeasure(SENSOR_MOTION, LOW);
      delay(50);
    }
  }
  
  ProcessLightSensor();
}

boolean InitializeLED(){
  Serial.println("INIT: Initializing LED (should see 3 blinks)... ");
  for(int i=0; i < 3; i++){
    digitalWrite(LED_PIN, HIGH);
    delay(500);
    digitalWrite(LED_PIN, LOW);
    delay(500);
  }
}

//the time we give the motion sensor to calibrate (10-60 secs according to the datasheet)
int calibrationTime = 10;

boolean InitializeMotionSensor(){
  //give the sensor some time to calibrate
  Serial.print("INIT: Calibrating motion sensor (this takes about ");
  Serial.print(calibrationTime);
  Serial.print(" seconds) ");
  for(int i = 0; i < calibrationTime; i++){
    Serial.print(".");
    delay(1000);
  }
  Serial.println(" done");
  Serial.println("INIT: SENSOR ACTIVE");
  delay(50);
}

boolean InitializeLightSensor(){
  Serial.print("INIT: Initializing light sensor. Light on threashold set to ");
  Serial.println(LIGHT_ON_MINIMUM_THRESHOLD);
  Serial.println("INIT: 20 samples follow...");
  for(int i = 0; i < 20; i++){
    int lightLevelValue = analogRead(LDR_PIN);
    Serial.print("INIT: ");
    Serial.println(lightLevelValue);
  }
}

boolean ProcessLightSensor(){
  byte currentState = previousLightState;
  int lightLevelValue = analogRead(LDR_PIN);  // returns value 0-1023. 0=max light. 1,023 means no light detected.
  
  if(lightLevelValue < LIGHT_ON_MINIMUM_THRESHOLD){
     currentState = LIGHT_ON;
  }
  else{
     currentState = LIGHT_OFF;
  }
  
  if(LightStateHasChanged(currentState) && !LightStateIsBouncing() ){
    previousLightState = currentState; 
    
    if(currentState == LIGHT_ON){
      writeMeasure(SENSOR_LIGHT, HIGH);
    }
    else{
      writeMeasure(SENSOR_LIGHT, LOW);
    }
    
    delay(2000);
    lightLastChangeTimestamp = millis();
    
    return true;
  }
  else{
    return false; 
  }
}

boolean LightStateHasChanged(byte currentState){
   return currentState != previousLightState; 
}

//De-bounce LDR readings in case light switch is being quickly turned on/off
unsigned int MIN_TIME_BETWEEN_LIGHT_CHANGES = 5000;
boolean LightStateIsBouncing(){
   if(millis() - lightLastChangeTimestamp < MIN_TIME_BETWEEN_LIGHT_CHANGES){
      return true; 
   }
   else{
      return false; 
   }
}

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; 
char deviceID[ ] = "007DEADBEEF0";
//Format MEASURE|version|DeviceID|Sensor Type|State (on/off)
void writeMeasure(int sensorType, int state){
  Serial.print("MEASURE|v1|");
  
  Serial.print(deviceID);
  Serial.print("|");
  
  if(sensorType == SENSOR_MOTION)
    Serial.print("motion|");
  else if(sensorType == SENSOR_LIGHT)
    Serial.print("light|");
  else
    Serial.print("unknown|");
  
  if(state == HIGH)
    Serial.print("on");
  else if(state == LOW)
    Serial.print("off");
  else
    Serial.print("unknown");
  
  Serial.println("");
}

Chatter Bot Proxy (Processing.org Environment)

import processing.serial.*;

Serial port;
String buffer = "";

void setup()
{
    size(255,255);
    println(Serial.list());
    port = new Serial(this, "COM7", 9600);
}

void draw()
{
  if(port.available() > 0){
    int inByte = port.read();
    print( char(inByte) );
    if(inByte != 10){ //check newline
      buffer = buffer + char(inByte);
    }
    else{
       if(buffer.length() > 1){
          if(IsMeasurement(buffer)){
              postToForce(buffer);
          }
          buffer = "";
          port.clear();
       }
    }
  }
}

boolean IsMeasurement(String message){
  return message.indexOf("MEASURE") > -1;
}

void postToForce(String message){
  String[] results = null;
  try
  {
    URL url= new URL("http://listener-developer-edition.na7.force.com/api/measure?data=" + message);
    URLConnection connection = url.openConnection();

    connection.setRequestProperty("User-Agent",  "Mozilla/5.0 (Processing)" );
    connection.setRequestProperty("Accept",  "text/plain,text/html,application/xhtml+xml,application/xml" );
    connection.setRequestProperty("Accept-Language",  "en-us,en" );
    connection.setRequestProperty("Accept-Charset",  "utf-8" );
    connection.setRequestProperty("Keep-Alive",  "300" );
    connection.setRequestProperty("Connection",  "keep-alive" );
    
    results = loadStrings(connection.getInputStream());  
  }
  catch (Exception e) // MalformedURL, IO
  {
    e.printStackTrace();
  }

  if (results != null)
  {
    for(int i=0; i < results.length; i++){
      println( results[i] );
    }
  }
}

Visualforce Site Chatter Listener

<apex:page controller="measureController" action="{!processRequest}" 
contentType="text/plain; charset=utf-8" showHeader="false" 
standardStylesheets="false" sidebar="false">
{!Result}
</apex:page>

Controller

public with sharing class measureController {
	public void processRequest(){
    	if(Data != null){
    		system.debug('data= ' + Data);
    	}
    	
    	CreateFeedPosts();
    }
    
    private void CreateFeedPosts(){
    	if(AssetDeviceBindings.size() == 0)
    		return;
    	
    	for(AssetSensor__c binding : AssetDeviceBindings){
	    	FeedPost newFeedPost = new FeedPost();
	    	newFeedPost.parentId = binding.Asset__c;
			newFeedPost.Type = 'TextPost';
	        newFeedPost.Body = FeedPostMessage();
	        insert newFeedPost;
    	}
    }
    
    private string FeedPostMessage(){
    	if(AssetDeviceBindings.size() == 0)
    		return '';
    	
    	if(SensorType == 'motion'){
    		if(State == 'on')
    			return 'Motion detected';
    		else
    			return 'Motion stopped';
    	}
    	else if(SensorType == 'light'){
    		return 'Lights turned ' + State;
    	}
    	else
    		return 'Unknown sensor event';
    }
    
    private List<AssetSensor__c> m_assetSensor = null;
    public List<AssetSensor__c> AssetDeviceBindings{
    	get{
    		if(m_assetSensor == null){
    			m_assetSensor = new List<AssetSensor__c>();
    			if(DeviceID != null){
    				m_assetSensor = [select Id, Name, Asset__c, DeviceID__c from AssetSensor__c where DeviceID__c=:DeviceID limit 500];
    			}
    		}
    		return m_assetSensor;
    	}
    }
    
    private integer EXPECTED_MESSAGE_PARTS = 5;
    private integer DATA_MESSAGE_TYPE = 0;
    private integer DATA_VERSION	= 1;
    private integer DATA_DEVICEID	= 2;
    private integer DATA_SENSOR_TYPE= 3;
    private integer DATA_STATE		= 4;
    
    private List<string> m_dataParts = null;
    public List<string> DataParts{
    	get{
    		if(m_dataParts == null && Data != null){
    			m_dataParts = Data.split('\\|');
    		}
    		return m_dataParts;
    	}
    }
    
    public string Version{
    	get{
    		if(Data != null && DataParts.size() >= EXPECTED_MESSAGE_PARTS){
    			return DataParts[DATA_VERSION];
    		}
    		else
    			return null;
    	}
    }
    
    public string DeviceID{
    	get{
    		if(Data != null && DataParts.size() >= EXPECTED_MESSAGE_PARTS){
    			return DataParts[DATA_DEVICEID];
    		}
    		else
    			return null;
    	}
    }
    
    public string SensorType{
    	get{
    		if(Data != null && DataParts.size() >= EXPECTED_MESSAGE_PARTS){
    			return DataParts[DATA_SENSOR_TYPE];
    		}
    		else
    			return null;
    	}
    }
    
    public string State{
    	get{
    		if(Data != null && DataParts.size() >= EXPECTED_MESSAGE_PARTS){
    			return DataParts[DATA_STATE];
    		}
    		else
    			return null;
    	}
    } 
    
    private string m_data = null;
    public string Data{
    	get{
    		if(m_data == null && ApexPages.currentPage().getParameters().get('data') != null){
    			m_data = ApexPages.currentPage().getParameters().get('data');
    		}
    		return m_data;
    	}
    }
    
    private string m_result = '';
    public String Result{
    	get{
    		return 'ok';
    	}
    }
    
    public static testMethod void tests(){
    	Asset testAsset = new Asset();
    	testAsset.Name = 'Test Asset';
    	testAsset.AccountID = [select Id from Account order by CreatedDate desc limit 1].Id;
    	insert testAsset;
    	
    	AssetSensor__c binding = new AssetSensor__c();
    	binding.Name = 'Test Binding';
    	binding.DeviceID__c = '007DEADBEEF9';
    	binding.Asset__c = testAsset.Id;
    	insert binding;
    	
    	measureController controller = new measureController();
    	controller.processRequest();
    	system.assert(controller.Data == null);
    	system.assert(controller.DataParts == null);
    	system.assert(controller.Version == null);
    	system.assert(controller.DeviceID == null);
    	system.assert(controller.SensorType == null);
    	system.assert(controller.State == null);
    	
    	string TEST_MEASURE = 'MEASURE|v1|007DEADBEEF9|motion|on';
    	ApexPages.currentPage().getParameters().put('data', TEST_MEASURE);
    	controller = new measureController();
    	controller.processRequest();
    	system.assert(controller.Data == TEST_MEASURE);
    	system.assert(controller.DataParts != null);
    	system.assert(controller.DataParts.size() == 5);
    	system.assert(controller.Version == 'v1');
    	system.assert(controller.DeviceID == '007DEADBEEF9');
    	system.assert(controller.SensorType == 'motion');
    	system.assert(controller.State == 'on');
    	
    	system.assert(controller.AssetDeviceBindings != null);
    	system.assert(controller.AssetDeviceBindings.size() == 1);
    	system.assertEquals('007DEADBEEF9', controller.AssetDeviceBindings[0].DeviceID__c);
    	system.assertEquals(testAsset.Id, controller.AssetDeviceBindings[0].Asset__c);
    	
    	system.assert(controller.Result == 'ok');
    }
}
Thursday, 24 June 2010 18:03:10 (Pacific Daylight Time, UTC-07:00)
# Monday, 21 June 2010
I'm very happy to announce that Cool Sites was released this weekend. Cool Sites provides a gallery of pre-built page templates, plugins, and sites built on Salesforce Sites.

Basic web content management tools and workflows for creating navigation menus and web pages are included. Check it out! http://www.getcoolsites.com


Monday, 21 June 2010 15:43:39 (Pacific Daylight Time, UTC-07:00)
# Sunday, 13 June 2010

Jazz - A Software Development Methodology


What
Jazz is a software development methodology for the ongoing creation and improvisation around several small apps distributed on open Internet platforms (the "cloud").


Why
Contemporary tools, languages, and distribution platforms of the 21st century make it feasible for software to be created, delivered, and performed similarly to Jazz music of the 20th century.

Software, like music, may be consumed repeatedly throughout the day by audiences.

Software has become ubiquitous and essential to people's everyday lives.

Audiences increasingly are demanding to know more about the personalities and teams behind their favorite software applications.


What (Apps)
Jazz combos seek to build a repertoire of several small apps, as opposed to developing and supporting one large app. Jazz is most suitable for Developers distributing applications through online stores (such as the Apple App Store, Google Marketplace, or Salesforce AppExchange).

The repertoire should consist of at least 10 apps.

Not all apps are for commercial distribution (such as contributions to open source frameworks and proof of concept apps used for marketing purposes).

Only 1 in 10 apps within the repertoire will achieve any significant amount of notoriety or market adoption.


Who (The Combo)
Apps are initially composed by a single person, which are then rehearsed and performed by combos of up to 5-7 people. Apps are occasionally composed by 2 collaborators.

Apps are consumed by audiences (customers).


Where (Spaces)
Jazz is composed, rehearsed, and performed in 3 distinct "spaces".

Composition and practice are done in solitude (home, coffee shop, the beach) but always connected (anywhere with Internet access)

Rehearsals are done with a group about once per week in public meeting places

Performances are ongoing. They occur both virtually and physically. All audience interactions are performances (this includes app store listings, webinars, support, and customizations)


When
Jazz is not a 9 to 5 job. However, ongoing performances are typically supported by someone during working hours.

Jazz Developers dedicate at least 3 days per week to uninterrupted work (composition and improvisation) and at least 1 day for group rehearsals (meetings) and audience interactions (support and review of customer feedback).

Compositions may take 1 day or 1 year to write. It should take no more than 2 years to accumulate a repertoire of 10 apps.

Team members are often given "the floor" during performances to lead and improvise within a composition. Improvisations are short creative cycles within the overall structure of a composition.


How
Each member of a Jazz combo owns and maintains their own instruments (tools) that best support their function within the combo.

Academic mastery of instrument and language are assumed before confidence in composition and improvisation can be achieved.

Jazz combos are unified in language presented to audiences, but individually use languages specific to their instrument or function.

Jazz encourages the sharing, remixing, and reuse of ideas through use of creative commons licenses.

All individuals are given public recognition for their contribution to an app.

Performers may practice up to 10 hours for a 1 hour performance (10:1 ratio of effort, unit tests behind released products).

The best performances are achieved through mastery of instrument, confidence in skills, and relaxation of mind.

The result is more important than the means. Don't over think the process to achieving a good performance.

Improvisations often rely on audience feedback to determine the path of a performance.

The Jazz combo may dynamically adjust composition structure during a performance through subtle communication to support an improvisation that may lead to new discoveries or increased levels of audience feedback and participation.
Sunday, 13 June 2010 13:04:29 (Pacific Daylight Time, UTC-07:00)
# Monday, 07 June 2010

Here's a fun video put together for my Salesforce Chatter Developer Challenge entry.

Monday, 07 June 2010 09:57:05 (Pacific Daylight Time, UTC-07:00)