Behaviours Using a Photocell




Introduction
A photocell is a sensor that can detect light. It's a light-dependent resistor (LDR), so by using it as an analog input with an Arduino, we will be able to determine how much light there is.

A behaviour is a group of actions made by animals (or robots), in conjunction with their environment, as a response to a stimuli or input. (paraphrased from Wikipedia)


What behaviours are there in nature that react to light?
The change in light each day affects many living things. It's pretty much necessary in order to function properly, especially with the waking and sleeping patterns. Us as humans, we are diurnal. We wake up when it becomes light out, and go to sleep when it is dark. Owls are the reverse, as they are nocturnal. There are also some creatures, such as deer, that are crepuscular. They are awake throughout twilight, the time just before the sun sets or rises.

The length of daylight notifies animals of the changing seasons. Some animals will migrate to warmer places when there is less daylight, others will prepare for hibernation. Sometimes animals can react differently when there is a change of light levels. An example of this is a deer in headlights. When it is dark out and a deer sees headlights, it will freeze (stand still). By turning off the headlights, the deer will start to move again. This is an example of a photophobe behaviour. A photophile behaviour is the opposite, where the animal seeks light rather than fleeing from it.


Using a photocell with RoboBrrd
So as you can see, reacting to light is one of the most basic behaviours that many animals have in common. All of the words we italicized above are examples of some of the behaviours that we can make a robot do. It's only obvious that we should make RoboBrrd have light behaviours as well!

Prerequisites:
  • Electrical: You will need to have your photocells connected to RoboBrrd. Please refer to the kit instructions here if you have not done so already.
  • Programming: Start up the Arduino IDE and load your RoboBrrd's program. We will be using this as a starting point. See here if you need to get started on the example sketch.
  • Things you will need: You will need something to shine light onto your RoboBrrd (like a flashlight), as well as shade (like a piece of dark cloth). That's about it- other than your own creativity of course ;)

Overview
We will be going through several examples in this tutorial, and hopefully by the end of it, you will be able to add your own light-behaviours to your RoboBrrd. Although the actions are coded specifically for RoboBrrd, the gist of the examples can apply to any robot.

After calibrating our LDRs, we will dive in to a 'peek-a-boo' behaviour, followed by some light-controlled puppetry. The behaviours cover the two main aspects of robot behaviours- output based off of input thresholds, and mapping the input to an output. You can combine these aspects multiple times over to create complex behaviours.

We'll give some examples of complex behaviours, as well as propose some challenges for you to try making a behaviour for as well! Sounds exciting? Then let's get started!


Download demo sketch

Calibrating LDRs


In this step, we will be finding the low and high values for our LDRs. This will be done by shining light into the sensor, or covering it up and making it dark, while looking at the values displayed on the Serial Monitor. The lowest and highest values will be printed onto the screen, which we will then set as our initial variable values.

Variables
#define DEBUG

uint16_t ldrL_low = 0;
uint16_t ldrL_high = 1023;
uint16_t ldrR_low = 0;
uint16_t ldrR_high = 1023;

uint16_t ldrL_val = 0;
uint16_t ldrR_val = 0;
The first part here is to just list out the variables that we will need. We're using unsigned 16 bit integers since we need the range to be (at least) 0-1023, and the value will never be negative. 216 will give us a range of 0-65535.

The DEBUG define will be used to control if some verbose messages are printed.


Sensor Helper Functions
// -- SENSORS -- //

uint16_t getLDR_L() {
  analogRead(ldrL);
  delay(10);
  return analogRead(ldrL);
}

uint16_t getLDR_R() {
  analogRead(ldrR);
  delay(10);
  return analogRead(ldrR);
}

uint16_t getTMP() {
  analogRead(tmp);
  delay(10);
  return analogRead(tmp);
}
Here are some sensor helper functions that will come in handy. You can place this code anywhere- at the bottom of the sketch, or in a new tab, is good. We will be using these functions to read the values, rather than directly calling analogRead. The multiple reads and delay is used due to some sensors interfering with each other (more info here).


Loop
  ldrL_val = getLDR_L();
  ldrR_val = getLDR_R();
  
  if(ldrL_low == 0) ldrL_low = ldrL_val;
  if(ldrL_high == 1023) ldrL_high = ldrL_val;
  if(ldrR_low == 0) ldrR_low = ldrR_val;
  if(ldrR_high == 1023) ldrR_high = ldrR_val;
  
  if(ldrL_val < ldrL_low) ldrL_low = ldrL_val;
  if(ldrL_val > ldrL_high) ldrL_high = ldrL_val;
  if(ldrR_val < ldrR_low) ldrR_low = ldrR_val;
  if(ldrR_val > ldrR_high) ldrR_high = ldrR_val;
  
  #ifdef DEBUG
  printLDR();
  #endif
This code goes inside of the loop function. First, we are getting each of the LDR values. If our low and high values are set to be the default ones, then we set these to be the current value.

When the next iteration of the loop comes, then we compare the latest value to the saved low and high values. Whenever the latest value is less/greater than the lowest/highest value, then we set it as the latest value.

Finally, we'll print these out to the screen every 100ms.


Print
void printLDR() {
 
 if(millis()% 100 == 0) {
    Serial << "L: " << ldrL_val << " R: " << ldrR_val << "\t\t";
    Serial << "L low: " << ldrL_low << " L high: " << ldrL_high << "\t\t";
    Serial << "R low: " << ldrR_low << " R high: " << ldrR_high;
    Serial << endl;
  }
  
}
This is just a helper function that we can call whenever we want to print out the LDR information. As seen above, we call this function if we are in debug mode (if DEBUG is defined).


Finding the lowest & highest values
Upload the modified code to your Arduino, and open the Serial Monitor. Our goal now is to get the lowest and highest values for each of the LDRs. For the lowest, cover up the LDRs with something dark. For the highest, shine a bright light into the LDR.

Now look at the numbers in the Serial Monitor and take note of the highest and lowest values. Set these numbers to the variables (up at the top of the sketch).

Function
After finding the highest and lowest values above, copy all of the code inside of loop() and create a new function called ldrCalibration(). Add all of the copied code to that function. Save your sketch, and delete everything from the loop() section. This helps keep the code neat for the next task!

---

That's it for calibrating, now on to the fun parts!

'Peek-a-boo' Behaviour


Threshold-Triggered Action

One of the ways you can play 'peek-a-boo' with RoboBrrd, is by covering up the light sensors, and then uncovering them. What happens here is a change from light to dark, then dark to light. The change from dark to light in both sensors is what we are looking for here.

In order to do this, we will need to create a threshold for when we can say that RoboBrrd senses darkness. The current LDR values get compared to this threshold and a state is set. By comparing the current state with the state of the previous iteration, we can detect the change from dark to light.

Variables
uint8_t ldr_add = 20;

uint16_t ldrL_thresh = ldrL_low+ldr_add;
uint16_t ldrR_thresh = ldrR_low+ldr_add;
Here we are setting the threshold values. The thresholds are set to be a small amount above the lowest LDR reading. This small amount is controlled by the ldr_add variable.

When you set ldr_add close to 0, then it will become trickier for it to be triggered. Increasing it by a large amount will make it easier to trigger- so much easier that it might be triggered all the time. Sometimes this variable does require tinkering to get just right.

Of course, it all depends on your sensors. If you want, you can also set the thresholds to some arbitrary value.

These variable declarations go at the top of your sketch, along with the other variable declarations.


Loop
  ldrL_val = map(getLDR_L(), ldrL_low, ldrL_high, 0, 1023);
  ldrR_val = map(getLDR_R(), ldrR_low, ldrR_high, 0, 1023);
  
  if(ldrL_val <= ldrL_thresh && ldrR_val <= ldrR_thresh) {
    Serial << "dark" << endl;
  } else if(ldrL_val <= ldrL_thresh && ldrR_val > ldrR_thresh) {
    Serial << "left" << endl;
  } else if(ldrL_val > ldrL_thresh && ldrR_val <= ldrR_thresh) {
    Serial << "right" << endl;
  } else {
    Serial << "bright!" << endl;
  }
Now it is time to get the LDR values. As we are getting the value, we are also mapping it from the calibrated low and high values to 0-1023. This will make it easier in the next lines of code to compare the value to the threshold.

For comparing the value to the threshold, we need to do it for both sides. This means there are four cases to look at, two where both the left and right are either less/greater than the threshold, and two where the left and right differ from being less/greater than the threshold.

As you can see, we are printing out the phrase to the Serial Monitor. Now would be a good time to upload this code and test it out with your RoboBrrd. This way, you'll be able to see if the threshold is set properly, and make any adjustments. For example, if it is always sending 'bright', then your threshold is too low, and you need to add more to it. If it is always sending 'dark/left/right', then your threshold is too high, and you need to subtract some from it. The thresholds may differ from each side.


Variables
uint8_t peekState = 3;
boolean covered = false;
Time to add some more variables. These two are for the peek state, one is the current state and the other is the previous state.


Loop
  if(ldrL_val <= ldrL_thresh && ldrR_val <= ldrR_thresh) {
    peekState = 0;
  } else if(ldrL_val <= ldrL_thresh && ldrR_val > ldrR_thresh) {
    peekState = 1;
  } else if(ldrL_val > ldrL_thresh && ldrR_val <= ldrR_thresh) {
    peekState = 2;
  } else {
    peekState = 3;
  }
  
  if(peekState == 3 && covered == true) {
    Serial << "PEEK A BOO!" << endl;
    covered = false;
  } else if(peekState == 0) {
    Serial << "BOO!" << endl;
    covered = true;
  }
The first few lines of code are the same as the ones we had previously, but we removed the Serial prints and added setting the peekState variable.

Whenever we see the peekState as being 0, then we set covered to be true. Then, once the LDRs are uncovered, peekState is 3 AND covered is true, then it is a legitimate 'peek a boo', and covered is set back to false.

One of the flaws of this approach is that if one of the LDRs is uncovered, followed by the other being uncovered, it would still be classified as a 'peek-a-boo'. This flaw can be fixed by checking if peekState is 1 or 2, and setting covered to false.

This is pretty boring just printing out text to the screen. Let's add some animation!

Animation
  if(peekState == 3 && covered == true) {
    Serial << "PEEK A BOO!" << endl;
    beak.attach(bpin);
    lwing.write(l_upper);
    rwing.write(r_upper);
    beakOpen(250);
    eyes(100,255,0);
    delay(100);
    lwing.write(l_middle);
    rwing.write(r_middle);
    beakClose(250);
    eyes(ledVals[0], ledVals[1], ledVals[2]);
    beak.detach();
    covered = false;
  } else if(peekState == 0) {
    Serial << "BOO!" << endl;
    //eyes(50,50,50);
    beak.attach(bpin);
    lwing.write(l_lower);
    rwing.write(r_lower);
    beakClose(250);
    beak.detach();
    covered = true;
  }
Here is the same if statement as above, but now with all the calls to animate RoboBrrd's movements. We tell RoboBrrd to basically look excited when peekState is 3, as a nice treat to the human who has been playing along. Since RoboBrrd doesn't enjoy its eyes being covered, it acts 'scared' whenever peekState is 0.

You might notice when peekState is 0, that we commented out an eye update. This was because it accidentally triggered peekState to be 3. This is most likely caused by the LDRs being in close proximity of the LEDs. So, we just leave RoboBrrd's eyes as the same colour for now.

You can customize the animations as much as you want. Just remember that the more times you command a servo to move, you should delay for the servo to move to its end position. With too many delays, a lag time will become evident when playing with RoboBrrd. It's all a balance between the animation and lag time. ;]

Testing it out
When you are done modifying the code, upload it to your RoboBrrd, open the Serial Monitor, and let's try it out! With your hands not covering RoboBrrd, it should not be printing anything out by default. If you cover one of the LDRs, it should print 'BOO!'. When you uncover the LDR, then cover both of them up at the same time, then it should print out the phrase, 'PEEK A BOO!'.

By changing the threshold value, there will be slight differences in the way the behaviour works. Another thing that you can add is to compare the current peekState to a previous one. An example of using this in action could be with a 'pattern game', where you would have to cover/uncover the LDRs in a specific pattern. Almost like a secret door-knock, but with LDRs.

Function
Similar to what we did with the calibrate code, copy all of this code from loop(). Put it into a function named peekabooBehaviour(). Be sure to save your sketch, and delete the code from loop now.

---

Ready for the next behaviour? Let's go!

'Puppeteer' Behaviour


Direct Control Action

You can actually puppeteer RoboBrrd's wings using the light sensors. What is happening is that as we change the light level, then it will map the reading to the wing's upper and lower values. We can just write the reading to the servo, and it will move. It's quite straightforward, but makes for a nice trick.

One of the extra things that we will do, so that we can also control the beak, is by checking to see if the current value will be within a threshold range. This threshold range is defined by the lower servo values, plus or minus some constant number. The constant number serves as a way to increase the triggering range. It's similar to what we did in the previous peek a boo behaviour.

Variables
boolean beakState = false;
Only one variable this time- a boolean for the beak state. It will only ever be set to open or closed in this example.


Setup
  lwing.attach(lpin);
  rwing.attach(rpin);
We need to make sure that we attach the wing servos, so add this to the end of your setup code.


Loop
  ldrL_val = map(getLDR_L(), ldrL_low, ldrL_high, l_lower, l_upper);
  ldrR_val = map(getLDR_R(), ldrR_low, ldrR_high, r_lower, r_upper);
  
  lwing.write(ldrL_val);
  rwing.write(ldrR_val);
  
  if(ldrL_val <= ldrL_thresh && ldrR_val <= ldrR_thresh) {
    
    beak.attach(bpin);
    if(beakState) {
      beakOpen(250);
    } else {
      beakClose(250);
    }
    beak.detach();
    
    beakState = !beakState;
    
  }
As always, we start out by retrieving the current value of the LDRs. You might notice something different this time, that the value is being mapped to the lower and upper servo values. This way we can write it directly to the servo.

The next block of code is where we are checking to see if both wings are in some area of being down (or lowered). If they are, then we attach the beak servo, open/close it, detach the servo, and change the beak state. The next iteration of the loop, if the beak was open- then now it will be closed (or vice versa).

Testing it out
Upload the code to your RoboBrrd, and try it out. It should be a pretty fun effect, where by using your hands to bring shade on the light sensors, you get to control the wing servos.

If the movement seems jittery to you, one of the modifications you can make is by adding a delay(100); to the end of the loop section. It will slow down the delay time a little, but also not make the servos as crazy.

There are some better ways other than just adding a delay that we can use to smooth the light readings. Can you think of some? We will be diving into this in part 2 of the tutorial. Function
Take the code from loop, and add it to a function named puppeteerBehaviour(). Be sure to save your sketch!

Summary


Now you should have two working behaviours for RoboBrrd using its light dependent resistors. Plus, a routine to follow to calibrate the light sensor values in case you are using RoboBrrd in a different light scene. From here, you can go on to make more interesting behaviours with the photocells! Possibly combining the two in an interesting way... or something!

The lighting in the room can have a substantial effect on whether or not the behaviours work. It is obviously hard to make it work in a completely pitch dark room, but even when testing with a dimly-lit room, if we moved a few cm, then we would have to re-calibrate the LDRs. It works well when there is a decent amount of ambient light in the room. If your lower/upper values do change, it will effect the mapping of the ldrValue. If you ever see a larger number than 1023, that is probably what happened.

So, what did we learn here?
  • How the light sensors behave in light and darkness
  • Making RoboBrrd do different actions based off of a threshold
  • Mapping the light values to move RoboBrrd's wing
  • What else did you learn?

What remaining questions do we have?
  • How to make the movement less jittery?
  • Can we combine the two behaviours in some way?
  • How do we control sounds from the light sensors?
  • What other questions do you have?

If you are stuck on this tutorial, feel free to ask any questions on the forum.

Next: Photocell Behaviours Part 2


Now that we have the basics of photocell behaviours under our belt, let's try out some more for different RoboBrrd interactivity!

PART 2 COMING SOON!



"I learned all about basic behaviours using a photocell with RoboBrrd! #RoboBrrd"
RoboBrrd Behaviours I learned all about basic behaviours using a photocell with RoboBrrd! #RoboBrrd