Orb Ambient Interface

1. Introduction

1.1. Ambient Interfaces

Ambient interfaces are a way of making information pervasive and ever-present. Instead of having to go to a computer and check share prices or check Teletext for the latest weather forecast the information can be communicated unobtrusively to the user without them having to explicitly request it.

1.2. The Orb

“The Orb” is my attempt at building an ambient interface. It consists of a number of high-brightness red, green and blue LEDs, a PIC microprocessor and a radio receiver for communication with a computer. The PIC uses PWM to control the brightness of the LEDs allowing the Orb to fade through all the colours of the rainbow. The radio link uses an easy-Radio module to provide a wireless (433MHz) RS232 serial link. This allows software on the computer to control the colour of the Orb based on any events you like, e.g. new email, msn contact coming online, share prices changing, etc.

The housing for the Orb is actually a Mathmos Aduki with its insides removed. It’s perhaps an expensive option for an enclosure but it does look rather nice. Alternatively you could use some other non-transparent plastic enclosure.

Orb 1Orb 2

2. Circuit

2.1. The Orb

The orb circuit consists of a PIC microcontroller, six high-brightness LEDs (two red, two green, two blue), three transistors and appropriate resisters to drive them, a radio receiver, and a small switch to put the orb into power-save mode in case it is running off batteries.

The circuit itself is fairly straightforward. Two LEDs of each colour are used to get a decent amount of light and because the PIC isn’t able to drive two LEDs from a single output a transistor is used to drive them. The easy-Radio receiver module takes care of receiving the data from the PC and provides serial data to the PIC. The RDY pin on the receiver module is used for handshaking so that the receiver will only send data to the PIC when the PIC is ready. I’ve also included a small microswitch which could be used to manually cycle the colour of the orb or put the orb into power-save mode when running off batteries.

Orb Circuit Diagram

Circuit diagram for the Orb (click to enlarge)

The parts list for the orb is shown below:

Table 1: Parts list for the orb
Description Supplier Order Code Price Quantity Total Price
Total Price £32.33
PIC16F84A-20/P Maplin RN49D £4.99 1 £4.99
Transistor – BC549C Maplin QQ15R £0.09 3 £0.27
Red LED – 5mm – 1.0cd – 30 degrees Maplin UK51F £0.37 2 £0.74
Green LED – 5mm – 1.0cd – 20 degrees Maplin N72AJ £0.50 2 £1.00
Blue LED – 5mm – 1.5cd – 30 degrees Maplin NR84F £2.99 2 £5.98
120 Ohm Resistor Maplin M12R £0.07 6 £0.42
2.7k Resistor Maplin M2K7 £0.07 3 £0.21
15k Resistor Maplin M15K £0.07 2 £0.14
Crystal 20MHz U4 Maplin RR90X £0.49 1 £0.49
15pF Ceramic Capacitor Maplin WX46A £0.05 2 £0.10
Easy-Radio ER400RS Farnell 5096522 £17.99 1 £17.99
Orb 3Orb 4Orb 5

2.2. Transmitter

The easy-Radio modules provide a wireless RS232 link. The receiver module is connected to the PIC in the Orb and a transmitter module needs to be connected to a serial port on the PC. The easy-Radio modules use signal levels of 0-5 volts, however the serial ports on PCs use -12 to +12 volts. If the transmitter module was connected directly to the PCs serial port the transmitter module probably wouldn’t survive. This is why we need to use a MAX232 converts the signals levels between the transmitter module and the transmitter chip.

Once this circuit is built it can be hooked up to the PC and any data that is sent to the serial port is transmitted over the radio link and picked up by any nearby receivers. The default serial port settings for the radio modules are 19200,8,N,1 although these can be changed by sending special commands to the modules.

Orb Transmitter Circuit Diagram

Circuit diagram for the Orb transmitter (click to enlarge)

The parts list for the transmitter is shown below:

Table 2: Parts list for the transmitter
Description Supplier Order Code Price Quantity Total Price
Total Price £20.95
D-Range 9 Way Skt Maplin RK61R £0.79 1 £0.79
MAX232CPE Maplin FD92A £1.40 1 £1.40
1uF Polyester Layer Capacitor Maplin WW53H £0.41 4 £1.64
100nF Polyester Layer Capacitor Maplin WW41U £0.17 2 £0.34
3mm Green LED Maplin CK35Q £0.09 1 £0.09
620 Ohm Resistor Maplin M620R £0.07 1 £0.07
Easy-Radio ER400TS Farnell 5096510 £11.90 1 £11.90
433MHz Helical Antenna (M4) Farnell 4317877 £4.72 1 £4.72

3. PIC Microprocessor

3.1. The Code

The code running on the PIC is responsible for performing PWM to control the brightness of the LEDs. This needs to be performed fast enough to reduce flicker. The PIC also needs to monitor the data from radio receiver and decode any messages sent by the PC. I’ve also included a switch to put the PIC into power-save mode so this needs to be monitored and de-bounced.

The PIC I used for building my Orb is the PIC16F84 which does not have a hardware UART on it. This means that I need a software library to decode the serial data from the radio module, i.e. check for the start and stop bits and handle the bit timings, etc. For this I used mikeslib.c which is a software RS232 library written by Mike Pearce and is available from http://www.microchipc.com/sourcecode/#PulseMon. I modified this library to make the getch() non-blocking. If a character isn’t received with a small time interval the method simply returns 0. This allows me to regularly poll the radio receiver and fade the LEDs using PWM from the main() method.

/**
 * [ Add Descriotion Here! ]
 *
 * @author uk_dave
 * @version 1.0 30-Sept-2004
 */

#include "pic.h"

#define processor = 16F84
__CONFIG(0x3ff2);

//**********************************************************************
// ORB ID - CHANGE THIS!!!!!
//**********************************************************************

#define OrbIDhi     '0'
#define OrbIDlo     '1'

//**********************************************************************
//**********************************************************************

//**********************************************************************
// IO PIN DEFINITIONS
//**********************************************************************

#define RadioPwr        RA3
#define RadioRXData     RA1
#define RadioBusy       RA2

#define PwrSwitch       RB0

#define RedLed          RB1
#define GreenLed        RB2
#define BlueLed         RB3

#define DebugLed        RB7

//**********************************************************************
//**********************************************************************

//**********************************************************************
// SERIAL PORT CONFIG
//**********************************************************************

// http://www.microchipc.com/sourcecode/
/* Serial Port Settings - must be before MikesLib */
#define	XTAL   21000000        // Crystal frequency in Hz (4MHz = 4.3MHz, 20MHz = 21MHz)
#define SERIALSOFTWARE         // Indicate type of serial output
#define TxPort PORTA           // Pin on Port a
#define RxPort PORTA
#define TxBit  0               // Pin Number to use
#define RxBit  1
#define TxTris TRISA           // Tris Register for Pin
#define RxTris TRISA
#define BRATE  19200           // Baud rate required (Easy-Radio's use 19200)
#include "mikeslib.c"

//**********************************************************************
//**********************************************************************

//**********************************************************************
// DEFINITIONS
//**********************************************************************

void init();
void main();
void pollSerialPort();
void powerOff();
void updateLEDs();
void fade();

typedef struct {
  unsigned char r;
  unsigned char g;
  unsigned char b;
} colour;

colour state;
colour target[2];

unsigned char delay = 2;
unsigned char pwm = 0;
unsigned char target_index = 0;
unsigned char targetReached = 0;

unsigned char packet[25];

//**********************************************************************
//**********************************************************************

/**
 * Initialises the PIC. Sets all the IO pins to their required mode,
 * ensures the LEDs are off, turns the radio on, and initialises the
 * serial input.
 */
void init() {
	INTCON = 0;                 // Disable all interrupts, clear all flags.
	TRISA = 0b00000010;         // All output except for RA1 which is for received radio data
	TRISB = 0b00000001;         // All output except for RB0 which is for soft power switch

	RedLed = 0;                 // Turn red LED off
	GreenLed = 0;               // Turn green LED off
	BlueLed = 0;                // Turn blue LED off
	DebugLed = 0;               // Turn debug LED off

	state.r = 0;
	state.g = 0;
	state.b = 0;
	target[0].r = 0;
	target[0].g = 0;
	target[0].b = 255;
	target[1].r = 0;
	target[1].g = 0;
	target[1].b = 255;

	RadioPwr = 1;               // Turn radio receiver chip on
	RadioBusy = 1;              // Not ready to receive radio data

	InitSerial();               // Set up soft serial
}

/**
 * Main method. Initialises the PIC and serial input. We then loop
 * forever fading the LEDs between the target values and listening
 * to the radio for new target values. We also monitor the soft-power
 * switch to put the PIC into sleep mode.
 */
void main() {
	unsigned int tmr=0;

	init();

	while(1) {
		updateLEDs();
		fade();

		// Has the the soft-power switch been pressed?
		if (PwrSwitch==0) {
			powerOff();
		}

		// Check serial port once every second or so
		if (++tmr==1000) {
			tmr=0;
			updateLEDs();
			pollSerialPort();
		}
	}
}

/**
 * Reads data from the Easy-Radio.
 * If two !! are found then a valid packet has been detected and we read the rest of it in.
 * a packet looks like either:
 *   !!ID-RRRGGGBBB-TT
 *   !!ID-RRRGGGBBB-TT-RRRGGGBBB
 * The ID is the ID of the orb for which the packet is destined. If the ID doesn't match the ID of this ORB
 * then the packet is ignored. Note that FF is a broadcast address and all orbs will respond to the packet.
 * The first packet fades the LEDs to the the newly speficied colour over duration TT.
 * The second packet fades the LEDs back and forth between the two newly specified colours over duration TT.
 */
void pollSerialPort() {
	unsigned char i;
	DebugLed=1;
	RadioBusy=0;			// Tell the radio we're ready to receive data
	// See if we have valid start bytes
	if (getch()=='!' && getch()=='!') {
		// Read entire packet in/
		for (i=0; i<25; i++) {
			packet[i] = getch();
		}
		// Does the ID match ours the the broadcast ID?
		if ((packet[0]==OrbIDhi && packet[1]==OrbIDlo) || (packet[0]=='F' && packet[1]=='F')) {
			target[target_index].r = ((packet[3]-48)*100)+((packet[4]-48)*10)+(packet[5]-48);
			target[target_index].g = ((packet[6]-48)*100)+((packet[7]-48)*10)+(packet[8]-48);
			target[target_index].b = ((packet[9]-48)*100)+((packet[10]-48)*10)+(packet[11]-48);
			// Has a second colour been specified
			if (packet[15]=='-') {
				// Quickly fade to start colour
				targetReached=0;
				delay=1;
				while (targetReached==0) {
					updateLEDs();
					fade();
				}
				// Set end colour
				target[target_index].r = ((packet[16]-48)*100)+((packet[17]-48)*10)+(packet[18]-48);
				target[target_index].g = ((packet[19]-48)*100)+((packet[20]-48)*10)+(packet[21]-48);
				target[target_index].b = ((packet[22]-48)*100)+((packet[23]-48)*10)+(packet[24]-48);
				// Set delay
				delay = ((packet[13]-48)*10)+(packet[14]-48);
			} else {
				target[1-target_index].r = ((packet[3]-48)*100)+((packet[4]-48)*10)+(packet[5]-48);
				target[1-target_index].g = ((packet[6]-48)*100)+((packet[7]-48)*10)+(packet[8]-48);
				target[1-target_index].b = ((packet[9]-48)*100)+((packet[10]-48)*10)+(packet[11]-48);
				delay = ((packet[13]-48)*10)+(packet[14]-48);
			}
		}
	}
	RadioBusy=1;			// Tell the radio to hang on to data until we're ready again
	DebugLed=0;
}

/**
 * Called when the soft-power switch is pressed.
 * The state of the LEDs is saved, then the LEDs are turned off along with the radio
 * and the PIC is put into sleep mode. Pushing the switch again will wake the PIC up,
 * restore the LED states, turn the radio back on and carry on doing what it was
 * originally doing.
 */
void powerOff() {
	unsigned char origDelay;
	colour origTarget[2];

	// Turn radio off
	RadioBusy=0;
	RadioPwr=0;

	// Backup current led state and targets
	origDelay = delay;
	origTarget[0].r = target[0].r;
	origTarget[0].g = target[0].g;
	origTarget[0].b = target[0].b;
	origTarget[1].r = target[1].r;
	origTarget[1].g = target[1].g;
	origTarget[1].b = target[1].b;

	// Fade the LEDs to off
	target[0].r=0;
	target[0].g=0;
	target[0].b=0;
	target[1].r=0;
	target[1].g=0;
	target[1].b=0;
	targetReached=0;
	delay=1;
	while (targetReached==0) {
		updateLEDs();
		fade();
	}
	RedLed=0;
	GreenLed=0;
	BlueLed=0;

	while (PwrSwitch==0) {}         // wait for switch to be released

	// Enable interrupt so switch can wake the PIC up again
	INTF=0;
	INTE=1;                         // Enable interrupt for pwrSwitch so the PIC can be woken from its sleep
	GIE=0;                          // Enable GIE bit so that we will branch to ISR when we wake up
	SLEEP();                        // Go to sleep

	// restore led state and targets
	target[0].r = origTarget[0].r;
	target[0].g = origTarget[0].g;
	target[0].b = origTarget[0].b;
	target[1].r = origTarget[1].r;
	target[1].g = origTarget[1].g;
	target[1].b = origTarget[1].b;
	delay=origDelay;

	// Turn radio back on
	RadioPwr=1;
	RadioBusy=1;
}

/**
 * Uses PWM to vary the brightness of the LEDs.
 * Call this method regularly!!
 */
void updateLEDs() {
	unsigned char i = 0;

	for(i = 0; i<255; i++) {
		if (i >= state.r) {
			RedLed=0;
		} else {
			RedLed=1;
		}

		if (i >= state.g) {
			GreenLed=0;
		} else {
			GreenLed=1;
		}

		if (i >= state.b) {
			BlueLed=0;
		} else {
			BlueLed=1;
		}
	}
}

/**
 * Updates the brightness of the LEDs to fade them towards the target value.
 * When the LEDs have reached the target value the target is swapped to the other
 * target value in the target array (i.e. the LEDs 'bounce' between the two target
 * colours).
 */
void fade() {
	if (!(pwm % delay)) {
		// update red
		if (state.r > target[target_index].r) state.r--;
		else if (state.r < target[target_index].r) state.r++;

		// update green
		if (state.g > target[target_index].g) state.g--;
		else if (state.g < target[target_index].g) state.g++;

		// update blue
		if (state.b > target[target_index].b) state.b--;
		else if (state.b < target[target_index].b) state.b++;

		// have we reached target?
		if ((state.r == target[target_index].r) && (state.g == target[target_index].g) && (state.b == target[target_index].b)) {
			target_index = 1 - target_index;
			targetReached=1;
		}
	}
	pwm++;
}

Listing 1: orb.c

//-----------------------------------------------------------------
//                       mikeslib.c
//
// Mikes header file with handy definitions and typedefs in it
//
//-----------------------------------------------------------------

#ifndef MikesLibrary
 #define MikesLibrary

 //#include	<conio.h>

 //--- Short cuts for unsigned int and unsigned char ---
 #ifndef uint
  typedef unsigned int  uint;
 #endif
 #ifndef uchar
  typedef unsigned char uchar;
 #endif
 #ifndef ulong
  typedef unsigned long ulong;
 #endif

 #ifndef BITS
  //--- Structure defining 8 individual bits ---
  typedef struct {
	  uint a : 1;
	  uint b : 1;
	  uint c : 1;
	  uint d : 1;
	  uint e : 1;
	  uint f : 1;
	  uint g : 1;
	  uint h : 1;
  }BITS;
 #endif

 //--- Union for converting a BYTE to individual Bits ---
 #ifndef BITBYTE
  typedef union {
  	uchar B;
  	BITS  b;
  }BITBYTE;
 #endif

 //--- For defining port bits ------
 #ifndef PORTBIT
  #define PORTBIT(adr, bit)       ((unsigned)(&adr)*8+(bit))
 #endif

 //*************************************************************************
 // Following is the Software Driven Serial driver written but HI-TECH
 //
 // To Use this routine use
 //       #define SERIALSOFTWARE
 //
 // For Interrupt driven routine (not yet avaliable)
 //       #define SERIALINTERRUPT
 //
 // For Hardware driven routines (Not yet avaliable)
 //       #define SERIALHARDWARE
 //
 //
 //  Here is an example setup for the serial
 // #define	XTAL   20000000        // Crystal frequency in Hz
 // #define SERIALSOFTWARE         // Indicate type of serial output
 // #define TxPort PORTB           // Pin on Port B
 // #define RxPort PORTB
 // #define TxBit  2               // Pin Number to use
 // #define RxBit  1
 // #define TxTris TRISB           // Tris Register for Pin
 // #define RxTris TRISB
 // #define BRATE 9600             // Baud rate required
 //
 //*************************************************************************
 #ifdef SERIALSOFTWARE
  /*
  *	Serial port driver for 16Cxx chips
  *	using software delays.
  *
  *	Copyright (C)1996 HI-TECH Software.
  *	Freely distubutable.
  */

  /*
   *	Tunable parameters
  */
  /*	Transmit and Receive port bits */
  #ifndef TxPort
   #define TxTris TRISA
   #define TxPort PORTA    //-- PORT A Bit 1 for Tx
   #define TxBit  1
  #endif

  #ifndef RxPort
   #define RxTris TRISA
   #define RxPort PORTA    //-- PORT A Bit 0 for Rx
   #define RxBit  0
  #endif

  /*	Xtal frequency */

  #ifndef XTAL
   #define	XTAL	4000000
  #endif

  /*	Baud rate	*/
  #ifndef BRATE
   #define	BRATE	9600
  #endif

  /*	Don't change anything else */

  #define	DLY		3		/* cycles per null loop */
  #define	TX_OHEAD	13		/* overhead cycles per loop */
  #define	RX_OHEAD	12		/* receiver overhead per loop */

  #define	RSDELAY(ohead)	(((XTAL/4/BRATE)-(ohead))/DLY)

  //******************************************************************
  // Serial Initialisation Routine
  // Michael O. Pearce
  // Sets up the ports etc for the following routines
  //******************************************************************
  static bit   TxData @ PORTBIT(TxPort,TxBit);   //-- TXD Pin
  static bit   RxData @ PORTBIT(RxPort,RxBit);   //-- RXD Pin
  static bank1 bit TxTRIS @ PORTBIT(TxTris,TxBit);
  static bank1 bit RxTRIS @ PORTBIT(RxTris,RxBit);
  bit   TxRxInit = 0;
  void InitSerial (void) {
   TxData=1;          //-- set pin high to start with

   TxTRIS = 0;
   RxTRIS = 1;

   TxRxInit=1;
  }
  //******************************************************************
  //  Software Putch Routine
  //  HI-TECH
  //  Requires definition of TxData for output pin
  //******************************************************************
  void putch(char c) {
   unsigned char dly, bitno;

   CLRWDT();

   bitno = 11;

   if(TxRxInit==0) {
    InitSerial();
   }

   TxData = 0;			/* start bit */
   bitno = 12;
   do {
    dly = RSDELAY(TX_OHEAD);	/* wait one bit time */
    do {
     /* nix */ ;
    }while(--dly);
    if(c & 1) TxData = 1;
    if(!(c & 1)) TxData = 0;
    c = (c >> 1) | 0x80;
   } while(--bitno);
  }

  //******************************************************************
  //  Software Getch Routine
  //  HI-TECH
  //  Requires Definition of RxData for port pin
  //  >>> Modified by uk_dave to make non-blocking <<<
  //******************************************************************
  char getch(void) {
    unsigned char c, bitno, dly;
    unsigned char nonBlockCounter=128;					// added by uk_dave
    if (TxRxInit==0) {
      InitSerial();
    }
    for (;;) {
      while(RxData) { /* wait for start bit */
        CLRWDT();
        if (--nonBlockCounter==0) return 0;		// added by uk_dave
        continue;
      }
      dly = RSDELAY(3)/2;
      do {
       /* nix */ ;
      } while(--dly);
      if(RxData) {
        continue;	/* twas just noise */
      }
      bitno = 8;
      c = 0;
      do {
        dly = RSDELAY(RX_OHEAD);
        do/* nix */;
        while(--dly);
        c = (c >> 1) | (RxData << 7);
      }
      while(--bitno);
      return c;
    }
  }

 #endif               //-- End of Serial Software routines
 //--------------------------------------------------------------------------
#endif

Listing 2: mikeslib.c

3.2. Communications Protocol

There are two types of messages that can be sent to the Orbs. One simply makes the Orb change to a newly specified colour, while the other instructs the Orb to fade back and forth between two specified colours.

The format of the packets are as follows:

!!ID-RRRGGGBBB-TT
!!ID-RRRGGGBBB-TT-RRRGGGBBB

You’ll notice that I have included an ID field in the message. This is so that multiple Orbs can be individually controlled. The messages are transmitted over the radio so any Orb within range will receive the message, so unless we want to control all the Orbs a method of addressing individual Orbs is needed. In this case the ID of an Orb is set by using a #define at the top of the PIC code.

All packets must begin with !!. The next two characters are the ID of the Orb for which the packet is destined. If the ID doesn’t match the ID of Orb then the packet is ignored. Note that FF is a broadcast address and all Orbs will respond to the packet. After a dash, an RGB colour value is specified. This is followed by a two digit decimal number specifying how quickly to fade to the new colour. A second RGB colour value may be specified next which will instruct the orb to costantly fade between the two colours over the duration specified by TT.

Admittedly this message format isn’t very compact and could be made smaller by using binary values for the colour codes and Orb ID. However this message format makes it very easy to send messages using HyperTerminal (on Windows) or echo’ing them to the serial port device (on Linux).

Example 1:
The packet:

!!01-255000255-01

instructs the Orb with ID 01 to quickly change colour to purple.

Example 2:
The packet:

!!FF-000255000-99-128000255

instructs all Orbs to very slowly fade between from green to purple.

4. PC Software

Now that the Orb is built and the PIC programmed we need to send it some commands to make it do stuff. The message format used for the orb was given in the previous section. These commands can be sent using something like HyperTerminal, but it would be much better if we could have a program that did it for us. Ideally what we need is a program that will check our email or the weather forecast or monitor our MSN contact list, for example, and relay this information via the Orb.

Before we jump in at the deep end, here’s a simple Java program that instructs all Orbs to change to red, then to green and then to blue. The colour changes every 6 seconds and the program runs forever. Note that you will need to have installed JavaComm to use this program.

import java.io.*;
import javax.comm.*;

public class OrbCycle {
	public static void main(String[] args) throws Exception {
		// Open the serial port
		SerialPort sp = (SerialPort)CommPortIdentifier.getPortIdentifier("COM5").open("Java Orb Controller", 5000);

		// Configure the serial port
		sp.setSerialPortParams(19200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
		sp.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);

		// Get I/O streams
		PrintWriter sout = new PrintWriter(sp.getOutputStream());

		// Continually cycle the colours through Red, Green and Blue
		while (true) {
			sout.println("!!FF-255000000-01");
			sout.flush();
			Thread.sleep(6000);
			sout.println("!!FF-000255000-01");
			sout.flush();
			Thread.sleep(6000);
			sout.println("!!FF-000000255-01");
			sout.flush();
			Thread.sleep(6000);
		}
	}
}

Listing 3: Simple Java program to fade the Orb through red, green and blue

For Linux command line junkies, here’s a very simple Bash shell script that will accept a colour as an argument and send the corresponding command to the Orb. The script can be run by typing ./orb.sh red. This command can then be appended to long compile tasks or other scripts. Chat programs such as XChat and Gaim have the ability to execute external programs when an event occurs such as someone sending you a message or typing our name. You can then link the event to this script to provide a visual alert, and because the Orb is wireless it doesn’t have to be near the PC!

#!/bin/sh

SERIAL_PORT="/dev/ttyUSB0"
ORB_ID="01"
DELAY="01"

COL_RED="255000000"
COL_GREEN="000255000"
COL_BLUE="000000255"
COL_BLACK="000000000"

ARGS=$@
for ARG in ${ARGS[@]}
do
    case "$ARG" in
        ("red")
            echo "!!$ORB_ID-$COL_RED-$DELAY" > $SERIAL_PORT
            ;;
        ("green")
            echo "!!$ORB_ID-$COL_GREEN-$DELAY" > $SERIAL_PORT
            ;;
        ("blue")
            echo "!!$ORB_ID-$COL_BLUE-$DELAY" > $SERIAL_PORT
            ;;
        ("black")
            echo "!!$ORB_ID-$COL_BLACK-$DELAY" > $SERIAL_PORT
            ;;
    esac
    sleep $DELAY
done

Listing 4: Simple Bash shell script for Linux