Light sensors are used in all kinds of robotics, industrial, and commercial applications. Next, let's teach your ActivityBot to navigate by visible light. Using a pair of small light sensors, called phototransistors, your ActivityBot can measure and compare the light levels on its right and its left. It can then turn toward the brighter side as it navigates. This way, the ActivityBot can be programmed to follow a flashlight, or find its way out of a dark room to a bright doorway.
Phototransistors are a common type of analog light sensor – the kind that might help a porch light automatically turn on at night. Think of the phototransistor as a light-controlled current valve. When light strikes the base of the device (B), more current will conduct into its collector lead (C) and flow out of its emitter lead (E).
Now, it's time to build and test two phototransistor circuits to give your ActivityBot phototransistor "eyes." Leave the piezo speaker circuit in place.
Build the light sensor circuits shown below.
*P14/P15 resistors are only needed for ActivityBot kits using External Encoders (#32500 [2]).
This test will display raw light sensor readings in the Terminal window. You will need a flashlight or lamp that is brighter than the overall light level in the room.
The phototransistor circuits are designed to work well indoors, with fluorescent or incandescent lighting. Make sure to avoid direct sunlight and direct halogen lights; they flood the phototransistors with too much infrared light.
The values shown above were taken in a room with overhead fluorescent lighting. Notice how covering the left phototransistor causes the lightLeft measurement to be larger.
Mismatched Measurements
It is unlikely that your two phototransistor measurements will match exactly. Each sensor will vary slightly as a natural part of the manufacturing process, and the brightness levels are rarely precisely even in any environment. However, if one gives a measurement about 100 times larger than the other when exposed to the same level of light, that phototransistor is probably plugged in backward. In this case, check your circuit and try again.
Take a peek back at the circuit schematic, for P9. Think of the 0.01 µF capacitor in the circuit as a tiny battery, and the phototransistor as a light-controlled current valve.
The first block in the repeat forever loop is make PIN 9 high. This block supplies current to the capacitor battery, which only takes a pause 1 (ms) to charge. Immediately after that, the block RC discharge PIN 9 changes I/O pin P9 from output-high to input. As an input, P9 will switch from sensing a high signal to a low signal as the capacitor battery drains its current through the phototransistor valve. The Propeller is measuring how long it takes for this logic transition to happen, and the RC block stores this discharge time in the lightLeft variable block.
The next three blocks in the repeat forever loop go through the same process with the P5 phototransistor circuit and storing another discharge time measurement in the lightRight variable.
When bright light is shining on a phototransistor, its valve is wide open and current drains through it very quickly, so the measured discharge time is small. When the phototransistor is covered up, its valve opens only a little, and the current will drain more slowly, resulting in a higher discharge time value. The remaining blocks in the loop display the discharge time of both variables in the Terminal. The Terminal screen capture above was taken when the left phototransistor was covered up, so the value of lightLeft is larger than the value of lightRight.
QT is short for Charge Transfer: In this activity, each photoresistor is being used in a charge transfer or QT circuit. This circuit lets the robot sense a much wider range of light levels than an analog-to-digital conversion circuit you might typically see with this sensor. This QT circuit is a simple option for using a variety of analog sensors with a digital I/O pin.
Visible and Invisible Light: Light travels in waves so small that the distance between adjacent peaks is measured in nanometers (nm), which are billionths of meters. The figure below shows the wavelengths for colors of light we are familiar with, along with some the human eye cannot detect, such as ultraviolet and infrared. The phototransistor in your ActivityBot kit detects visible light but is most sensitive to 850 nm wavelengths, which is in the infrared range.
Just for fun, let's see if the phototransistor can detect different colors.
From your observation, do you think a clear phototransistor alone, like this one, could be used as a color sensor? Why or why not?
For ActivityBot navigation, the raw sensor values aren't directly useful. What matters is the difference between the light levels detected by each sensor so that the ActivityBot can turn to the side of the one sensing brighter light. It is also nice to have values that will fall into a predictable range that can add or subtract them from the speeds sent to drive the servo motors.
Accomplishing this takes just a few steps - after building the equation you need, you'll then figure out how best to use blocks that follow this equation.
The concept is simple - of all of the light detected by both sensors, which portion of it is coming from the right sensor (and if you need to, you can tell which portion is coming from the right sensor). First, divide one sensor measurement into the sum of both. The result will always be in the 0 to 1 range. This technique is an example of a normalized differential measurement. Here’s what it looks like as an equation:
A range from 0 to 1 still isn't useful - especially in Blockly, since we can't use decimal or fractional numbers. The goal is to have a range from -100 to +100 (a range with 200 possible values). Multiplying the normalized differential measurement by 200 will give a result between 0 and 200. Subtracting 100 from that result will provide a range from -100 to +100. If the phototransistors sense the same level of light, regardless of how bright or dim, the final whole number will be zero. This is an example of a zero-justified normalized differential measurement, let's call it "ndiff" for short. Now our equation looks like this:
We have to be careful in BlocklyProp. Because all math is done with integers, it is possible to create an equation that always results in zero.
Preserving values in integer math
A good habit is to make your value bigger before you make it smaller. If you can use decimals or fractional values, it doesn't matter if you divide then multiply or multiply then divide. When doing integer math, it DOES matter. If we do the multiplication first, we can preserve the value of our result:
To turn this into blocks, you'll create a few different quantities. The top part of the fraction (the numerator) becomes:
The bottom part of the fraction (the denominator) becomes:
Which means nDiff is:
The example code below will display the lightRight and lightLeft measurements, and also show the value of nDiff as an asterisk. This makes it easier to visualize how the ActivityBot navigation program uses the sensor values to aim for a new trajectory towards the brighter light.
When the Terminal opens, you should see something like this:
The graphical display was generated using Terminal print number and character value blocks.
This program builds on Test Light Sensors from the previous page. Two more variables are declared in addition to lightLeft and lightRight: nDiff and position. nDiff holds the result of the fancy-named equation, and position is used to print a graphical display that represents the value of nDiff.
After calculating nDiff, its result needs to be turned into a position in the terminal. nDiff can be any number from -100 to +100, and we need positions from 0 to 50 (that's about how many characters wide the Terminal window is).
To get from (-100 to +100) to (0 to 50), we first add 100 (bringing it to 0 to 200), then divide by 4 (bringing it to 0 to 50).
Then, using a repeat x times block, we use the Terminal print number and character value blocks to print enough spaces to push out the asterisk (*) we are planning to print it where it needs to be on the Terminal screen.
Integer math, nesting, and order of operations all matter. Just like with normal mathematical operations in real life, multiplication and division operations are performed before addition and subtraction. However, you may have already learned that Blockly evaluates each block from "inside-out," and you have to plan accordingly to get the correct result.
Dealing with integer math can be tricky, too. Most modern microcontrollers can do floating-point math, including the Propeller Multicore microcontroller. BlocklyProp doesn't support it, however, because the blocks themselves are not able to tell what kind of number - integer or floating-point - your variable might be. In addition, floating-point operations are slower and take up more memory than integer operations.
Once you know a few tricks, it's easy to overcome this limitation.
There may be times where you want to turn the value of a sensor into a percentage. To change a sensor's value to a percentage, we have to know what it's minimum and maximum values are. The difference in those values is the sensor's range. Then, you can use the following formula to convert the sensor's value to a percentage:
In real life, because multiplication and division have the same operator precedence, either of these calculation steps will get you the same result:
To see what difference your order of operations can have on the result in integer math, try the following example:
Here's why this happens:
What percentage of 200 is 150? To find out, you might take 150 divided by 200 and get 0.75, then multiply that by 100 to get 75.
But what is 0.75 as an integer? Here's a hint: it's not 1. In integer math, the decimal is truncated. This means that the number is cut off at the decimal, leaving only 0. If you're trying to convert to a percentage by multiplying by 100, 100 times 0 is still 0.
If we use the same numbers, but this time multiply first by 100 before dividing by 200, we get 150 times 100 = 15000, then 15000 divided by 200 = 75.
Now, let's make the ActivityBot follow light! The following code should allow your robot to follow a flashlight beam, or navigate out of a dark room through a brightly lit doorway.
This program uses the Robot blocks for the ActivityBot. After initializing the ActivityBot, the program consists of a repeat forever loop which receives values from the phototransistors and uses them to adjust the servos' speeds to navigate toward whichever side detects brighter light.
First, the now familiar variables lightLeft, lightRight, and nDiff are setup for taking and using the sensor measurements. Then, the variables speedLeft and speedRight are set up for use with the Robot drive speed block later on.
The first two parts inside of the repeat forever loop are the same as our previous test program. They take the light sensor measurements and store the values in lightLeft and lightRight.
The next line is the same nDiff equation used in Test Light Sensor Graphical project. It calculates nDiff, which will have a value in the range of -100 and 100. Positive nDiff values mean the left photoresistor is sensing brighter light, and negative nDiff values mean the right photoresistor is sensing brighter light. The farther the value is from zero, the greater the difference in the amount of light that the two phototransistors are sensing.
The variables speedLeft and speedRight are initialized to 64. Keep this in mind when looking at the if...else block that comes next.
The first condition translates to "if nDiff is greater than or equal to zero (brighter light on the the left), make speedLeft equal to 64 minus nDiff × 4." The new value for speedLeft would be something less than 64, while speedRight remains 64. These variables are then used in the Robot drive speed block after the if...else block. For example, if nDiff = 32, this statement is the same as speedLeft = 64 - (32 × 4), which equals -64. The result is Robot drive speed (left = -64, right = 64) which makes the ActivityBot rotate to the left toward the light.
However, if nDiff is NOT positive, the code drops to the else part of the if...else block, making speedRight equal to (nDiff × 4). This translates to "make speedRight equal to 64 plus nDiff × 4." This is still a number smaller than 64; remember that nDiff is negative here. For example, if nDiff = -16, this statement is the same as speedRight equals 64 + (-16 × 4) so speedRight ends up equal to zero, while speedLeft is still 64. This gives the Robot drive speed (left = 64, right = 0), causing the ActivityBot to pivot right towards the brighter light.
Phototaxis — This is the term that describes an organism moving its whole body in direct response to light stimulus. Moving towards brighter light is called positive phototaxis, and moving away from brighter light would then be negative phototaxis. While the Navigate by Light program makes the ActivityBot mimic positive phototaxis, in nature, it seems this behavior is usually found in microscopic organisms. Moths are attracted to light, but their varied and circling flight paths around a streetlamp demonstrate that their navigation systems are far more complex.
Would you like to make your ActivityBot mimic negative phototaxis? It can be done by replacing one single variable in the Navigate by Light project.
What other behaviors can you give your ActivityBot with light sensors?
Links
[1] https://www.parallax.com/product/350-00029
[2] https://www.parallax.com/product/32500