Guiding an autonomous toy car

The short version of my question: Will a set of PIR sensors mounted on a re-purposed RC car be able to pick up people and guide the car while it is in motion?

The long version: I’m in the process of building a project designed only to scare the crap out of unsuspecting victims. The base is a cheap RC car that has had the brain stripped out, and the relevant pinouts for the old IC redirected to an Arduino Uno to control the car’s functions. The car will have extra weight added to the chassis to offset some lightweight ornamentation designed to make it look like a creepy little kid ghost. I’d like to be able to arm this thing and leave it sitting in a darkened room some distance from the door, so that it will charge straight at the first person to enter (complete with LED eyes and shrill sound effects). My initial plan was to use a PIR sensor coupled with an ultrasonic sensor set to activate the car, determine range to target, and stop the car short of running into the person in front of it. If possible, I’d like to put 3 PIR sensors on top, angled to different arcs, to allow the car to detect when the target is trying to move to the side- which would trigger a steering event to bring them back to the center PIR. Not having worked with PIRs yet, I’m unsure of their ability to pick out a person against the background when the car (and sensor) is moving. Does anyone have any experience with this sort of thing that can shed some light and save me some trial and error time? Or possibly point me in the direction of a more suitable sensor setup that I’ve not considered?

A standard PIR will only work when stationary. As soon as the car moves, all of them will fire.

as for how to do it. Ultrasonic range finder is the easiest way. If you can get quite a directional one and mount it on a servo/motor you could scan it to get a ‘range map’ to work with. (Just try and keep the hump in the centre and stop when it reaches a certain size.

Another option is no contact IR. You can get sensors that measure temperature in front of it.
Just mount 3-5 on the front.
Wait for the centre one to have the highest temperature. (Target in sight).
Start running forward.
Turn towards sensor of highest temp.
As for stopping…Bump sensor?

Word of advice though. Make it tough! I’d almost guarantee, someone will kick it at some point!

I'm just a beginner, but here my 5 cent: use a IR led if we charge for some millis the Negative pin of a IR led, then it will de-charge in function of response IR. sorry for bad english, but essentialy: use 2 led IR, one emitter and one receiver. turn on emitter, read the de-charge time, turn of emitter, read the de-charge time, the difference is in function of distance. In italian forum gbm has get a distance of 2 meters using 2 remote led... this is the basic code I've written:

#define irEmitter 13            // polo positivo emettitore
#define irReceiverN 2          // polo negativo ricevitore
#define irReceiverP 5          // polo positivo ricevitore
volatile unsigned long distance0 = 0;
volatile unsigned long distance1 = 0;
volatile unsigned long distance2 = 0;
volatile unsigned long lightTime = 0;
volatile boolean nuovaLettura = false;
volatile unsigned long lostRead = 0;

unsigned long lastRead = 0;

void setup() {
  Serial.begin(9600);             // inizializza seriale a 9600 baud
  pinMode(irEmitter, OUTPUT);      // il led emettitore è un output
  pinMode(irReceiverP, OUTPUT);    // il polo positivo del let ricevitore è un output
  digitalWrite(irReceiverP, LOW);  // e deve essere LOW
  
  //start led read
  init(false);
  attachInterrupt(0, one, FALLING);
  lastRead=millis();
}

void one() {
  detachInterrupt(0);
  distance0 = micros() - lightTime;
  init(true);
  attachInterrupt(0, two, FALLING);
}

void two(){
 detachInterrupt(0);
 distance1 = micros() - lightTime;
 distance2 = distance0 - distance1;
 distance1 = 0; 
 distance0 = 0;
 if (nuovaLettura){
   lostRead++;
 }else{
   nuovaLettura=true;
   lostRead=0;
 }

 init(false);
 attachInterrupt(0, one, FALLING);
}

void init(boolean emitterOn) {
 if(emitterOn)
   digitalWrite(irEmitter, HIGH);
 else
   digitalWrite(irEmitter, LOW);
   
 lightTime = micros();
 pinMode(irReceiverN, OUTPUT);
 digitalWrite(irReceiverN, HIGH); //carico ricevitore di induttanza
 pinMode(irReceiverN, INPUT);
 digitalWrite(irReceiverN, LOW);
}

int tempLostRead;
unsigned long time_out_millis = 1000;//1 secondo
void loop() {
  if (nuovaLettura){
    nuovaLettura=false;
    tempLostRead=lostRead;//questo perchè le serial sono lente, e se non si azzera lostRead subito potremmo avere una lettura inconsistente dopo
    Serial.print("D: "); //disatnza
    Serial.println(distance2);
    Serial.print("L: "); //letture perse
    Serial.println(tempLostRead);
    Serial.println(); //lascia una riga vuota
    
    lastRead=millis();
  }
  
  if (millis()-time_out_millis > lastRead){
    Serial.println("ERROR! Time Out or millis() Overflow! restarting interrupt:");
    detachInterrupt(0);
    init(false);
    attachInterrupt(0, one, FALLING);
    lastRead=millis();
  }
}

and here is the "evolution", but still untested (unfortunately actually I don't have any IR led)

ERER.h

#ifndef ERER_h
#define ERER_h

#include "WProgram.h"

class ERER {
  public:
    ERER(byte, byte, byte);
    long getRawDistance();
  private:
    byte getBCDpin(byte);
    void setStatus(byte, byte, boolean);
  };

#endif

LITTLE problem: if you look at 2m the response time will be in order of 1 or 2 seconds.. So you can use a timer interrupt like every 200 or 500millis that will reset the count... you won't see far, but you'll gain "reflex" if the class is right, you can have 1 sensor every 2 pin (receiverP can be simply put to GND), and you can easily* build an array system. *easily if the class works. I can give support, but you'll have to give many feedback

ehm, here the second piece of the class (due post length limit)
ERER.cpp

#import "ERER.h"

/*
  ERER.cpp - Library for ERER sensors.
  (use 2 infrared LEDs to build a distance sensor)
  Rewritten by Mauro Mombelli 03/04/2011
  Based on code of Giovanni Blu Mitolo, March 11, 2011.
  Released with Creative Commons Attribution licence  
*/

volatile byte _oldbit;
byte _changed, _newbit;
volatile boolean _hasChanged=false;
volatile long rawDistance=0;
volatile byte BCDreceiverN, BCDreceiverP, BCDemitterP;
volatile byte MASKreceiverN, MASKemitterP, MASKreceiverP;
volatile boolean emitterStatus;
unsigned long _time;
unsigned long _startIn[2];
unsigned long _rawIn[2];

ERER::ERER(byte PCINTemitterP, byte PCINTreceiverP, byte PCINTreceiverN){

  //set emitterP pin mode
  BCDemitterP = getBCDpin(PCINTemitterP); //find BCD (see PortManipulation)
  MASKemitterP = (1<< (PCINTemitterP - (8*BCDemitterP) ) ); //find MASK for esy port manipulation
  //set as OUTPUT and HIGH
  switch (BCDemitterP){
    case 0:
      DDRB |= MASKemitterP;
      PORTB |= MASKemitterP;
      break;
    case 1:
      DDRC |= MASKemitterP;
      PORTC |= MASKemitterP;
      break;
    case 2:
      DDRD |= MASKemitterP;
      PORTD |= MASKemitterP;
      break;
  }
  emitterStatus=true;
  
  //set receiverP pin mode
  BCDreceiverP = getBCDpin(PCINTreceiverP); //find BCD (see PortManipulation)
  MASKreceiverP = (1<< (PCINTreceiverP - (8*BCDreceiverP) ) ); //find MASK for esy port manipulation
  //set as OUTPUT and LOW
  switch (BCDreceiverP){
    case 0:
      DDRB |= MASKreceiverP;
      PORTB &= ~MASKemitterP;
      break;
    case 1:
      DDRC |= MASKreceiverP;
      PORTC &= ~MASKemitterP;
      break;
    case 2:
      DDRD |= MASKreceiverP;
      PORTD &= ~MASKemitterP;
      break;
  }
  
  //set PCINTreceiverN as INPUT AND attach Interrupt
  /*
  remember:
  PORTB maps to Arduino digital pins 8 to 13 (PCIE0) form PCINT0 to PCINT7
  PORTC maps to Arduino analog pins 0 to 5 (PCIE1) form PCINT8 to PCINT14
  PORTD maps to Arduino digital pins 0 to 7 (PCIE2) form PCINT16 to PCINT23
  */
  BCDreceiverN = getBCDpin(PCINTreceiverN);
  MASKreceiverN = (1<< (PCINTreceiverN - (8*BCDreceiverN) ) ); //find MASK for esy port manipulation
  
  switch (BCDreceiverN){
    case 0:
      PORTB &= ~MASKreceiverN;//set as INPUT
      PCICR |= (1 << PCIE0); //INTERRUPT ON PCIE0 CHANGE ACTIVATED
      PCMSK0 |= MASKreceiverN; //UMASKING INTERRUPT FOR PIN
      _oldbit = PINB;//To understand witch pin has changed
      break;
    case 1:
      PORTC &= ~MASKreceiverN;//set as INPUT
      PCICR |= (1 << PCIE1); //INTERRUPT ON PCIE0 CHANGE ACTIVATED
      PCMSK1 |= MASKreceiverN; //UMASKING INTERRUPT FOR PIN
      _oldbit = PINC;//To understand witch pin has changed
      break;
    case 2:
      PORTD &= ~MASKreceiverN;//set as INPUT
      PCICR |= (1 << PCIE2); //INTERRUPT ON PCIE0 CHANGE ACTIVATED
      PCMSK2 |= MASKreceiverN; //UMASKING INTERRUPT FOR PIN
      _oldbit = PIND;//To understand witch pin has changed
      break;
  }
  
  //charge ReceiverN using internal pull-up
  setStatus(BCDreceiverN, MASKreceiverN, true);
  //stop charge ReceiverP
  setStatus(BCDreceiverN, MASKreceiverN, false);
}

long ERER::getRawDistance(){
  return rawDistance;
}

byte ERER::getBCDpin(byte PCINTpin){
  if (PCINTpin>=0 && PCINTpin<=7){
    return 0;
  }
  
  if (PCINTpin>=8 && PCINTpin<=14){
    return 1;
  }
  
  if (PCINTpin>=16 && PCINTpin<=23){
    return 2;
  }
  return 3;  
}

void ERER::setStatus(byte BCD, byte MASK, boolean state){
  if (state){
    switch (BCD){
      case 0:
      PORTB |= MASK;
      break;
    case 1:
      PORTC |= MASK;
      break;
    case 2:
      PORTD |= MASK;
      break;
    }
  }else{
    switch (BCD){
      case 0:
      PORTB &= ~MASK;
      break;
    case 1:
      PORTC &= ~MASK;
      break;
    case 2:
      PORTD &= ~MASK;
      break;
    }
  }
}

ISR(PCINT2_vect) {
  if (BCDreceiverN == 2){
    _newbit=PIND;
    _changed=_newbit^_oldbit;
  
    if (_changed & MASKreceiverN){//if PCINTreceiverN has changed
      _time=micros();
      _hasChanged=true;
      
      if (_newbit & MASKreceiverN) { //if PCINTreceiverN now is high
        _startIn[emitterStatus]=_time;
      }else{
        _rawIn[emitterStatus]=_time-_startIn[emitterStatus];
      
        if (!emitterStatus){ //if we read with emitter HIGH and then LOW we have a distace
          rawDistance = _rawIn[0]-_rawIn[1]; //because dark read teorically is lower than light read
        }
      
        //start a new reading changing emitter status
        emitterStatus!=emitterStatus;
        if (emitterStatus){
          switch (BCDemitterP){
            case 0:
              PORTB |= MASKemitterP;
              break;
            case 1:
              PORTC |= MASKemitterP;
              break;
            case 2:
              PORTD |= MASKemitterP;
              break;
          }
        }else{
          switch (BCDemitterP){
            case 0:
              PORTB &= ~MASKemitterP;
              break;
            case 1:
              PORTC &= ~MASKemitterP;
              break;
            case 2:
              PORTD &= ~MASKemitterP;
              break;
          }
        }
    
        //charge ReceiverN using internal pull-up
        PORTD |= MASKreceiverN; //we alreadi know it's PORTD becuse we are in this interrupt :-)
        
        //stop charge ReceiverN
        PORTD &= ~MASKreceiverN;
      }
    }
  
    _oldbit=_newbit;
  }
}

ISR(PCINT1_vect) {
  if (BCDreceiverN == 1){
    _newbit=PINC;
    _changed=_newbit^_oldbit;
  
    if (_changed & MASKreceiverN){//if PCINTreceiverN has changed
      _time=micros();
      _hasChanged=true;
      
      if (_newbit & MASKreceiverN) { //if PCINTreceiverN now is high
        _startIn[emitterStatus]=_time;
      }else{
        _rawIn[emitterStatus]=_time-_startIn[emitterStatus];
      
        if (!emitterStatus){ //if we read with emitter HIGH and then LOW we have a distace
          rawDistance = _rawIn[0]-_rawIn[1]; //because dark read teorically is lower than light read
        }
      
        //start a new reading changing emitter status
        emitterStatus!=emitterStatus;
        if (emitterStatus){
          switch (BCDemitterP){
            case 0:
              PORTB |= MASKemitterP;
              break;
            case 1:
              PORTC |= MASKemitterP;
              break;
            case 2:
              PORTD |= MASKemitterP;
              break;
          }
        }else{
          switch (BCDemitterP){
            case 0:
              PORTB &= ~MASKemitterP;
              break;
            case 1:
              PORTC &= ~MASKemitterP;
              break;
            case 2:
              PORTD &= ~MASKemitterP;
              break;
          }
        }
    
        //charge ReceiverN using internal pull-up
        PORTC |= MASKreceiverN; //we alreadi know it's PORTC becuse we are in this interrupt :-)
        
        //stop charge ReceiverN
        PORTC &= ~MASKreceiverN;
      }
    }
  
    _oldbit=_newbit;
  }
}

ISR(PCINT0_vect) {
  if (BCDreceiverN == 0){
    _newbit=PINB;
    _changed=_newbit^_oldbit;
  
    if (_changed & MASKreceiverN){//if PCINTreceiverN has changed
      _time=micros();
      _hasChanged=true;
      
      if (_newbit & MASKreceiverN) { //if PCINTreceiverN now is high
        _startIn[emitterStatus]=_time;
      }else{
        _rawIn[emitterStatus]=_time-_startIn[emitterStatus];
      
        if (!emitterStatus){ //if we read with emitter HIGH and then LOW we have a distace
          rawDistance = _rawIn[0]-_rawIn[1]; //because dark read teorically is lower than light read
        }
      
        //start a new reading changing emitter status
        emitterStatus!=emitterStatus;
        if (emitterStatus){
          switch (BCDemitterP){
            case 0:
              PORTB |= MASKemitterP;
              break;
            case 1:
              PORTC |= MASKemitterP;
              break;
            case 2:
              PORTD |= MASKemitterP;
              break;
          }
        }else{
          switch (BCDemitterP){
            case 0:
              PORTB &= ~MASKemitterP;
              break;
            case 1:
              PORTC &= ~MASKemitterP;
              break;
            case 2:
              PORTD &= ~MASKemitterP;
              break;
          }
        }
    
        //charge ReceiverN using internal pull-up
        PORTB |= MASKreceiverN; //we alreadi know it's PORTB becuse we are in this interrupt :-)
        
        //stop charge ReceiverN
        PORTB &= ~MASKreceiverN;
      }
    }
  
    _oldbit=_newbit;
  }
}

Thanks for the ideas so far. I hadn't thought of trying to use an active IR system, lesto, but I'll need this thing to initially activate at almost 6 meters, so they may not have the response time necessary to do any real steering at that distance. They aren't that expensive at a local shop, though, so I'll pick up a pair to try with different numbers. Much appreciation for the coding work- it'll save a good amount of time in the testing.

Cynar, your post has gotten me curious about the possibilities of going all ultrasonic. I hadn't considered a sweeping ultrasonic sensor at all. I'll have to dig out a servo and see if I can fashion something that will give it adequate blinders. And yes, I expect it to get abused at least once or twice. All the expensive stuff is in a polycarbonate housing that should hold up against a field goal kick or two. Not much I can do for the RC car itself, but it was only $20 off the shelf, anyway.

ultrasonic is good for long distance: http://www.robot-italy.com/product_info.php?cPath=15_48_143&products_id=1483