Saturday, 5 November 2016

More messing about with IR

We've been using IR quite a bit lately, for a project that involves an object which moves, and tells a "base" object that it's resting in about it's current state. The idea is that the base object lights up to tell the user what state the first object is in.

Because the object can spin, rotate and generally move around, we had to implement a wireless method of sending information between the base and the object. Radio seemed a bit overkill (and, since the object is battery powered, we wanted to reduce the current draw as much as possible). IR seemed like the perfect solution.

In fact, we had a working solution a few weeks back, using tiny reflective sensors. But instead of sending a signal and reading the reflected response, both the object and the base had a QRE1113 sensor inside them - so the base didn't read it's own reflected signal, it received a signal sent from the object.

Everything seemed fine. Except the distance between the sensors was critical. At this distance, everything worked great.


But at this distance...


... even after ensuring that the sensors are perfectly in line, the signal often didn't make it from one to the other. After inspecting the IR LED in the sensor (using a phone camera) the amount of light emitted appears as a tiny, tiny dot. Basically, these reflective sensors are good up to about 8-10mm. Anything more than that and the signal gets lost - or occasionally corrupted.

We wanted to increase the distance between our sensors so needed something with a bit more oomph. A simple IR LED and IR phototransistor seemed like a good idea.


Except these were very sensitive to the angle of two being absolutely accurate. Even just a couple of degrees off and the phototransistor stopped responding to the IR LED.

So Steve suggested using the Vishay 38khz IR receivers, most commonly found in TV units (to decode remote control button presses). These are able to receive IR bounced around from all over the place!


Not only do they work if you point an LED directly at a sensor....


They also work at greater distances, and even at 90 degrees or more! Until we put our LED/sensor combo inside a container, they could receive IR from the LED no matter which angle they were facing. But just being able to consistently receive IR from the object was only half the problem - we also wanted to ensure that both the base and the object were in sync (something that wasn't always possible with just single-way communication)

Using a send and receive pair in both the object and the base, we modified the firmware slightly, to use a call-and-response method of keeping the two in sync.

Originally we just had the object report it's status and the base would update to indicate to the user the current state of the object. But sometimes the object would change state and the base failed to receive the updated status. The two would often go out of sync.

With this new hardware,  the messaging process is going to be:
a) the object detects a change
b) the object sends a message to the base
c) the base updates its' display (LEDs)
d) the base returns the message to the object (up to three times)
e) the object updates its status when it receives any one of these messages.

This method of send and receive ensures that if the message from the object doesn't get to the base (for whatever reason) the two remain in sync. It's almost like the object is asking "can I go to state x" and the base confirms "yes, you can now move to state x". Only when the response message is received does does the object update its status. This should ensure that the display on the base matches the status of the object that initiated the change.

Of course it's not 100% foolproof - there's a chance that the message from the base to the object gets lost and they go out of sync again. But it's less likely to happen because the response message is sent almost instantly (and is repeated up to three times). So as soon as the object sends its "request" message, the base responds (within about 50ms). As long as the object hasn't moved too much within this tiny time frame, it should be able to receive at least one of the three response messages.

Here's how we set up our two Arduinos:



And the firmware (the same firmware is uploaded to both and the state of the button pin checked on boot up to decide the "role" of the device- either base or object).


#include <IRLib.h>

//Create a receiver object to listen on pin 10
IRrecv My_Receiver(10);
IRsend My_Sender;
IRdecode My_Decoder;

#define IR_RECEIVE_ALL_THE_TIME 0
#define IR_SEND_THEN_RECEIVE      1
#define IR_RECEIVE_FOR_1_SEC      2
#define IR_SEND_RELAY                3

int button_pin=12;
bool is_sender = true;

int ir_state = IR_RECEIVE_ALL_THE_TIME;
long ir_value = 0xf578;
long ir_last_value = 0x0000;

long time_start_listening;
long time_now;
long time_diff;


void sendIRValue(){   
   Serial.print(F("Sending: "));
   Serial.println(ir_value, HEX);
   My_Sender.send(RC5, ir_value, 13);
   delay(10);
   My_Sender.send(RC5, ir_value, 13);
}


void parseMessage(){
   // now what to do with the received IR value?
   Serial.print(F("Parsing: "));
   Serial.println(ir_value, HEX);
   
   // for testing the base, just send this message back out
   // (for the sender, don't do anything)
   if(is_sender==false){
      delay(2);
      ir_state = IR_SEND_RELAY;   
   }else{
      if(ir_value == ir_last_value){
         Serial.println(F("Nothing to do, repeat value received"));
      }else{
         Serial.println(F("Change language"));
      }
   }

   ir_last_value = ir_value;
}


void setup() {
   Serial.begin(19200);
   pinMode(button_pin, INPUT_PULLUP);
   My_Receiver.enableIRIn(); // Start the receiver   

   // this is just for testing
   int b = digitalRead(button_pin);
   if(b == LOW){
      is_sender=false;
      Serial.println(F("I am the base"));
   }else{
      Serial.println(F("I am the sender"));
   }
}


void loop() {
   int b;
   time_now = millis();

   // this is just for testing
   b = digitalRead(button_pin);
   if(b==LOW && is_sender==true){
      // debounce the button press
      delay(10);
      b = digitalRead(button_pin);
      if(b==LOW){
         // wait for release
         while(b==LOW){ b = digitalRead(button_pin); }
         // now send a blast of IR
         ir_state = IR_SEND_THEN_RECEIVE;
      }
   }

   // deal with any incoming IR (if necessary)
   switch(ir_state){

   //-----------------------------
      case IR_RECEIVE_ALL_THE_TIME:
   //-----------------------------
      if (My_Receiver.GetResults(&My_Decoder)) {
         My_Decoder.decode();      //Decode the data
         b = My_Decoder.value & 0x1fff;   // our data is only 13 bits long
         if(b > 0){
            ir_value = b;
            parseMessage();
         }
         My_Receiver.resume();       //Restart the receiver
      }
      break;

   //-----------------------------
      case IR_SEND_THEN_RECEIVE:
   //-----------------------------
      delay(20);
      sendIRValue();

      // now we listen for a response for a second or two
      time_start_listening = millis();
            
      // re-enable the receiver
      My_Receiver.enableIRIn();
      ir_state = IR_RECEIVE_FOR_1_SEC;
      break;

   //-----------------------------
      case IR_SEND_RELAY:
   //-----------------------------
      delay(50);
      sendIRValue();

      // send the response message another couple of
      // times in case one of them burps.
      delay(50);
      sendIRValue();
      delay(50);
      sendIRValue();

      // re-enable the receiver
      My_Receiver.enableIRIn();
      ir_state = IR_RECEIVE_ALL_THE_TIME;
      break;

   //-----------------------------------
      case IR_RECEIVE_FOR_1_SEC:
   //-----------------------------------
      if (My_Receiver.GetResults(&My_Decoder)) {
         My_Decoder.decode();      //Decode the data
         b = My_Decoder.value & 0x1fff;   // our data is only 13 bits long
         if(b > 0){
            ir_value = b;
            parseMessage();
       
            // now leave this state
            ir_state = 99;
         }
         My_Receiver.resume();       //Restart the receiver
      }

      time_diff = time_now - time_start_listening;
      if(time_diff > 500){
         // leave this state
         Serial.println(F("Leaving receive state"));
         ir_state = 99;
      }
      break;

   }
}