We now have circuits that can work under a variety of lighting conditions. Now we need some code that can adapt as well. An example of sketch code that cannot adapt to change would be:
if(tLeft > 2500)… // Not good for navigation.
Maybe that statement would work well for turning away from shadows in one room, but take it to another with brighter lights, and it might never detect a shadow. Or, take it to a darker room, and it might think it’s seeing shadows all the time. For navigation, what matters is not an actual number reporting the light level over each sensor. What matters is the difference in how much light the two sensors detect, so the robot can turn toward the sensor seeing brighter light (or away from it, depending on what you want.)
The solution is simple. Just divide the right sensor measurement into the sum of both. Your 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:
For example, a normalized differential measurement of 0.25 would mean “the light is 1/2 as bright over the right sensor as it is over the left.” The actual values for tRight and tLeft might be small in a bright room or large in a dark room, but the answer will still be 0.25 if the light is 1/2 as bright over the right sensor. A measurement of 0.5 would mean that the tRight and tLeft values are equal. They could both be large, or both be small, but if the result is 0.5, it means the sensors are detecting the same level of brightness.
Here’s another trick: subtract 0.5 from the normalized differential shade measurement. That way, the results range from –0.5 to +0.5 instead of 0 to 1, and a measurement of 0 means equal brightness. The result is a zero-justified normalized differential shade measurement.
But why do it? The value range –0.5 to +0.5 is great for navigation sketches because the positive and negative values can be used to scale the wheels speeds. Here is how the zero-justified normalized differential shade equation appears in the next sketch:
float ndShade; // Normalized differential shade ndShade = tRight / (tLeft + tRight) - 0.5; // Calculate it and subtract 0.5
The final measurement will be stored in a floating point variable named ndShade, so that gets declared first. Then, the next line does the zero-justified normalized differential shade math. The result will be a value in the –0.5 to +0.5 range that represents the fraction of total shade that tRight detects, compared to tLeft. When ndShade is 0, it means tRight and tLeft are the same values, so the sensors are detecting equally bright light. The closer ndShade gets to –0.5, the darker the shade over the right sensor. The closer ndShade gets to 0.5 the darker the shade over the left sensor. This will be very useful for navigation. Let’s test it first with the Serial Monitor.
This screencapture shows a Serial Monitor example with the LightSensorValues sketch running. With shade over the right sensor, the ndShade value is about 0.4. With shade over the left sensor, it’s about –0.4.
/* * Robotics with the BOE Shield - LightSensorValues * Displays tLeft, ndShade and tRight in the Serial Monitor. */ void setup() // Built-in initialization block { tone(4, 3000, 1000); // Play tone for 1 second delay(1000); // Delay to finish tone Serial.begin(9600); // Set data rate to 9600 bps } void loop() // Main loop auto-repeats { float tLeft = float(rcTime(8)); // Get left light & make float float tRight = float(rcTime(6)); // Get right light & make float float ndShade; // Normalized differential shade ndShade = tRight / (tLeft + tRight) - 0.5; // Calculate it and subtract 0.5 // Display heading Serial.println("tLeft ndShade tRight"); Serial.print(tLeft); // Display tLeft value Serial.print(" "); // Display spaces Serial.print(ndShade); // Display ndShade value Serial.print(" "); // Display more spaces Serial.println(tRight); // Display tRight value Serial.println(' '); // Add an extra newline delay(1000); // 1 second delay } long rcTime(int pin) // rcTime measures decay at pin { pinMode(pin, OUTPUT); // Charge capacitor digitalWrite(pin, HIGH); // ..by setting pin ouput-high delay(5); // ..for 5 ms pinMode(pin, INPUT); // Set pin to input digitalWrite(pin, LOW); // ..with no pullup long time = micros(); // Mark the time while(digitalRead(pin)); // Wait for voltage < threshold time = micros() - time; // Calculate decay time return time; // Returns decay time }
The Serial Monitor screencapture below shows an example of a graphical display of the ndShade variable. The asterisk will be in the center of the -0.5 to +0.5 scale if the light or shade is the same over both sensors. If the shade is darker over the BOE Shield-Bot’s right sensor, the asterisk will position to the right in the scale. If it’s darker over the left, the asterisk will position toward the left. A larger shade/light contrast (like darker shade over one of the sensors) will result in the asterisk positioning further from the center.
/* * Robotics with the BOE Shield - LightSensorDisplay * Displays a scrolling graph of ndShade. The asterisk positions ranges * from 0 to 40 with 20 (middle of the display) indicating same light on * both sides. */ void setup() // Built-in initialization block { tone(4, 3000, 1000); // Play tone for 1 second delay(1000); // Delay to finish tone Serial.begin(9600); // Set data rate to 9600 bps } void loop() // Main loop auto-repeats { float tLeft = float(rcTime(8)); // Get left light & make float float tRight = float(rcTime(6)); // Get right light & make float float ndShade; // Normalized differential shade ndShade = tRight / (tLeft+tRight) - 0.5; // Calculate it and subtract 0.5 for(int i = 0; i<(ndShade * 40) + 20; i++) // Place asterisk in 0 to 40 { Serial.print(' '); // Pad (ndShade * 40) + 20 spaces } Serial.println('*'); // Print asterisk and newline delay(100); // 0.1 second delay } long rcTime(int pin) // rcTime measures decay at pin { pinMode(pin, OUTPUT); // Charge capacitor digitalWrite(pin, HIGH); // ..by setting pin ouput-high delay(5); // ..for 5 ms pinMode(pin, INPUT); // Set pin to input digitalWrite(pin, LOW); // ..with no pullup long time = micros(); // Mark the time while(digitalRead(pin)); // Wait for voltage < threshold time = micros() - time; // Calculate decay time return time; // Returns decay time }
The loop function starts by taking the two rcTime measurements for the left and right light sensors, and stores them in tLeft and tRight.
void loop() // Main loop auto-repeats { float tLeft = float(rcTime(8)); // Get left light & make float float tRight = float(rcTime(6)); // Get right light & make float
After declaring ndShade as a floating-point variable, tLeft and tRight are used in an expression to get that zero-justified normalized differential measurement. The result will be between –0.5 and +0.5, and gets stored in ndShade.
float ndShade; // Normalized differential shade ndShade = tRight / (tLeft+tRight) - 0.5; // Calculate it and subtract 0.5
Next, this for loop places the cursor in the right place for printing an asterisk. Take a close look at the for loop’s condition. It takes ndShade and multiples it by 40. It also has to add 20 to the value because if ndShade is –0.5, we want that to print with zero leading spaces. So (–0.5 × 40) + 20 = 0. Now, if ndShade is 0, we want it to print 20 spaces over: (0 × 40) + 20 = 20. If it’s +0.5 we want it to print 40 spaces over: (0.5 × 40) + 20 = 40. Of course, if it’s something in between, like 0.25, we have (0.25 × 40) + 20 = 30. So, it’ll print half way between center and far right.
for(int i = 0; i<(ndShade * 40) + 20; i++) // Place asterisk in 0 to 40 { Serial.print(' '); // Pad (ndShade * 40) + 20 spaces }
After printing the spaces, a single asterisk prints on the line. Recall that println prints and also adds a newline so that the next time through the loop, the asterisk will display on the next line.
Serial.println('*'); // Print asterisk and newline delay(100); // 0.1 second delay }