RVR flashing red and blue leds

I’m using the Sphero EDU app, writing “text” programs.

Recently, from time-to-time, I’ve started to notice the RVR will start flashing red, then blue, repeatedly and Sphero EDU reports “robot disconnected.”

I looked around and couldn’t find an explanation for it…

What does it indicate when the RVR flashes all LEDs from red to blue and back, repeatedly?

1 Like

Hey @RVRTrainerJ!

Thanks so much for reaching out about what you’re seeing :relaxed: Have you noticed any patterns as to when this tends to happen?

It sounds like something might have gone wonky with one of your processors. I would recommend updating the firmware via the EDU app. If you click on your bot once it is connected to your app, there should be an “Update Robot Firmware” button at the top of the list :slight_smile:

Please let us know if you continue to see the police lights after do this or if you have any other questions! I can’t wait to see what you and your RVR are able to accomplish together!!

Kelsey

2 Likes

Thanks for the assistance, Kelsey! :pray:t2:

It didn’t occur to me that there might be a firmware update available; I updated the firmware of my RVR when I first got it. I’ll look for that option. :+1:t2:

So, I haven’t yet figured out a deterministic set of steps :footprints: that reproduces the symptoms :bug:. Generally speaking, it occurs only while I’m running this one program :smiling_imp:. And it seems more likely to occur after I’ve run it multiple times. :running_man:t2: I’ve included the gory details (collapsed below) for the intensely curious.

My current theory :nerd_face: is that the program somehow either overwhelmed the RVR (i.e. send commands at too great a pace) or timed-out (was too “busy” to properly service a response from the RVR).

I’ll report back what I find.

Q: Is there an official definition of what the “police lights” :rotating_light: means?

Thanks again for the support!

Gory Details

(Left here for the curious)

The program itself is almost 500 lines long…

const newColorSensorProcessor = function (getColorFn) {
// wire-in the built-in (i.e. defined in EDU) `getColor()` function.
    if (getColorFn === undefined) {
        getColorFn = getColor;
    }

    const config = {
        stability: 1,       // how many samples in a row must be equivalent to consider a new color read as "stable."
        sampleFrequency: 0  // how frequently (in Hz) to sample from RVR's color sensor. 0 = on-demand.
    };

    function configureSampling(newConfig) {
        config.stability = newConfig.stability !== undefined ? newConfig.stability : 20;
        config.sampleFrequency = newConfig.frequency !== undefined ? newConfig.frequency : 100;
        collectSamples();
    }

    function collectSamples() {
        if (config.sampleFrequency !== 0) {
            collectSample();
            setTimeout(collectSamples, 1000 / config.sampleFrequency);
        }
    }

    let rawColors = [];     // array of {r:, g:, b:}. a rolling log of colors sampled from the RVR's color sensor.
    let avgColors = [];     // array of {r:, g:, b:}. a rolling average over `rawColors`
    let latestStableColor = {r: 0, g: 0, b: 0};

    function collectSample() {
        rawColors.unshift(getColorFn());
        const currAvgColor = average(rawColors);
        avgColors.unshift(currAvgColor);

        // have we collected enough data points to even think about calculating stability?
        if (avgColors.length >= config.stability) {
            if (areStable(avgColors)) {
                // wait until the last possible moment to round values to minimize statistical error.
                const nextStableColor = round(currAvgColor);
                if (!isEqual(nextStableColor, latestStableColor)) {
                    latestStableColor = nextStableColor;
                    invokeHandlersMatching(latestStableColor);
                }
            }
            rawColors = rawColors.slice(0, config.stability - 1);
            avgColors = avgColors.slice(0, config.stability - 1);
        }
    }

    function areStable(colors) {
        const stdev = standardDeviation(colors);
        return stdev.r < 3.0 && stdev.g < 3.0 && stdev.b < 3.0;
    }

    const activeSpecs = new Map();  // from Spec to {handlers:[handlerFns...], hasBeenMatching: false}
    function registerHandler(spec, handler) {
        const state = activeSpecs.get(spec) || {
            handlers: [],
            hasBeenMatching: false
        };
        state.handlers.push({fn: handler, isRunning: false});
        activeSpecs.set(spec, state);
    }

    function invokeHandlersMatching(color) {
        for (const [spec, state] of activeSpecs) {
            if (spec.isMatch(color)) {
                if (!state.hasBeenMatching) {
                    for (let idx = 0; idx < state.handlers.length; idx++) {
                        const handler = state.handlers[idx];
                        if (!handler.isRunning) {
                            handler.isRunning = true;
                            handler.fn(function () {
                                handler.isRunning = false;
                            }, color, spec);
                        }
                    }
                    state.hasBeenMatching = true;
                }
            } else {
                state.hasBeenMatching = false;
            }
        }
    }

    function unregisterAllHandlers(spec) {
        activeSpecs.delete(spec);
    }


    function getStableColor() {
        if (config.sampleFrequency === 0) {
            collectSample();
        }
        return latestStableColor;
    }

    function startScan(scanFrequency) {
        return function (freq) {
            freq = freq || 10;
            let enabled = true;
            let count = 0;
            const values = {
                r: {min: 255, max: 0},
                g: {min: 255, max: 0},
                b: {min: 255, max: 0}
            };

            function scanForColor(freq) {
                if (enabled) {
                    const c = getColorFn();

                    // omit off; it's a start-up value and would result into artificially large tolerances in the
                    //   yielded color spec.
                    if (!(c.r === 0 && c.g === 0 && c.b === 0)) {
                        values.r.min = Math.min(values.r.min, c.r);
                        values.g.min = Math.min(values.g.min, c.g);
                        values.b.min = Math.min(values.b.min, c.b);
                        values.r.max = Math.max(values.r.max, c.r);
                        values.g.max = Math.max(values.g.max, c.g);
                        values.b.max = Math.max(values.b.max, c.b);
                        count++;
                    }
                    setTimeout(scanForColor, 1000 / freq, freq);
                }
            }

            function stop() {
                enabled = false;
            }

            function getColorSpec() {
                const avg = {
                    r: (values.r.max + values.r.min) / 2,
                    g: (values.g.max + values.g.min) / 2,
                    b: (values.b.max + values.b.min) / 2
                };
                return Spec.new({
                    r: {value: Math.round(avg.r), tolerance: Math.round(values.r.max - avg.r)},
                    g: {value: Math.round(avg.g), tolerance: Math.round(values.g.max - avg.g)},
                    b: {value: Math.round(avg.b), tolerance: Math.round(values.b.max - avg.b)}
                });
            }

            function getCount() {
                return count;
            }

            scanForColor(freq);
            return {
                stop: stop,
                getColorSpec: getColorSpec,
                getCount: getCount,
            }
        }(scanFrequency);
    }

    // calculates the average of a list of colors (for each channel).
    //   assumes there is at least one item in the list.
    function average(colors) {
        const avg = {r: 0, g: 0, b: 0};
        for (let idx = 0; idx < colors.length; idx++) {
            avg.r += colors[idx].r;
            avg.g += colors[idx].g;
            avg.b += colors[idx].b;
        }
        avg.r /= colors.length;
        avg.g /= colors.length;
        avg.b /= colors.length;

        return avg;
    }

    // calculates the standard deviation of a list of colors (for each channel).
    //   assumes there is at least one item in the list.
    //   (see also: https://www.mathsisfun.com/data/standard-deviation-formulas.html)
    function standardDeviation(colors) {
        const avg = average(colors);

        const sumOfDiffsSquared = {r: 0, g: 0, b: 0};
        for (let idx = 0; idx < colors.length; idx++) {
            sumOfDiffsSquared.r += (avg.r - colors[idx].r) * (avg.r - colors[idx].r);
            sumOfDiffsSquared.g += (avg.g - colors[idx].g) * (avg.g - colors[idx].g);
            sumOfDiffsSquared.b += (avg.b - colors[idx].b) * (avg.b - colors[idx].b);
        }
        return {
            r: Math.sqrt(sumOfDiffsSquared.r / colors.length),
            g: Math.sqrt(sumOfDiffsSquared.g / colors.length),
            b: Math.sqrt(sumOfDiffsSquared.b / colors.length)
        };
    }

    // rounds red, green, blue values of the given color.
    function round(color) {
        return {
            r: Math.round(color.r),
            g: Math.round(color.g),
            b: Math.round(color.b),
        }
    }

    function isEqual(colorA, colorB) {
        return colorA.r === colorB.r &&
            colorA.g === colorB.g &&
            colorA.b === colorB.b;
    }

    function deepCopyDataFrom(object) {
        return JSON.parse(JSON.stringify(object));
    }

    const Spec = {
        new: function (colorWithTolerances) {
            const newSpec = deepCopyDataFrom(colorWithTolerances);

            newSpec.isMatch = function (color) {
                const c = color || getStableColor();
                return c.r >= this.r.value - this.r.tolerance &&
                    c.r <= this.r.value + this.r.tolerance &&
                    c.g >= this.g.value - this.g.tolerance &&
                    c.g <= this.g.value + this.g.tolerance &&
                    c.b >= this.b.value - this.b.tolerance &&
                    c.b <= this.b.value + this.b.tolerance;
            };

            newSpec.whenMatches = function (handler) {
                if (typeof handler === "function") {
                    registerHandler(this, handler);
                } else {
                    unregisterAllHandlers(this);
                }
            };

            return newSpec;
        },
        not: function (spec) {
            const newSpec = Spec.new({op: "not", a: spec});
            newSpec.isMatch = function (color) {
                return !spec.isMatch(color);
            };
            return newSpec;
        },
        and: function (specA, specB) {
            const newSpec = Spec.new({op: "and", a: specA, b: specB});
            newSpec.isMatch = function (color) {
                return specA.isMatch(color) && specB.isMatch(color);
            };
            return newSpec;
        },
        or: function (specA, specB) {
            const newSpec = Spec.new({op: "or", a: specA, b: specB});
            newSpec.isMatch = function (color) {
                return specA.isMatch(color) || specB.isMatch(color);
            };
            return newSpec;
        }
    };

    return {
        configureSampling: configureSampling,
        getColor: getStableColor,
        startScan: startScan,
        Spec: Spec
    }
};

var colorProc = newColorSensorProcessor(getColor);
var count;
var cleanFloorSpec;

async function speakParagraphs(paragraphs) {
	for(const paragraph of paragraphs) {
		await speak(paragraph);
		await delay(1);
	}
}

async function startProgram() {
	resetAim();
	await speakIntroduction();
	await speakScanPreamble();
	await scanCleanFloor();
	await speakSpecPreamble();
	await speakSpecValuesFromScan(cleanFloorSpec, count);
	await analyzeSpec(cleanFloorSpec);
	await scanDirtyFloor(cleanFloorSpec);
	await report();
}

async function speakIntroduction() {
	let paragraphs = [
		"Hello.  This is a demonstration of the Color Sensor Processor written by John Ryan, a programmer in Los Angeles.",
		"For this demonstration, I will learn how to detect rubbish on this floor.",
		"I will do this by first learning the range of colors naturally present in the clean floor.",
		"Then, I'll invite you to put items on the floor that are of a different color from the floor.",
		"Those items should be about the same size as the color cards that were bundled with your rover.",
		"I'll talk you through the demonstration.",
		"Okay.  Here we go."
		];
	
	await speakParagraphs(paragraphs);
}

async function speakScanPreamble() {
	let paragraphs = [
		"To begin, we need a space of about 4 feet by 4 feet.",
		"The floor, here, needs to be roughly of a solid single color.  This demonstration won't work on a floor with patterns or a variety of colors.",
		"",
		"This space also needs to be free of clutter for this first part of the demonstration.  If there is anything on the floor, please remove it now.",
		"",
		"",
		"",
		"Place the rover in the bottom left-hand corner of the space.",
		"It will move around in the 4 by 4 square, scanning the colors in the floor.",
		"",
		"If you have not already placed the rover in the bottom left-hand corner of the space, do so now."
	];
	await speakParagraphs(paragraphs);
}

async function scanCleanFloor() {
	var countDown = 5;
	await speak("Scanning will begin in " + countDown + " seconds.");
	await delay(1);
	for(var secondsLeft = countDown; secondsLeft > 0; secondsLeft--) {
		await speak("" + secondsLeft);
		await delay(1);
	}
	await speak("Scanning...");
	var cleanFloorScan = colorProc.startScan();
	await roll(0, 50, 5);
	await roll(90, 50, 5);
	await roll(180, 50, 5);
	await roll(270, 50, 5);
	cleanFloorScan.stop();
	await speak("Scan complete.");
	cleanFloorSpec = cleanFloorScan.getColorSpec();
	count = cleanFloorScan.getCount();
	cleanFloorSpec.r.tolerance += 10;
	cleanFloorSpec.g.tolerance += 10;
	cleanFloorSpec.b.tolerance += 10;
}

async function speakSpecPreamble() {
	let paragraphs = [
		"From that scan, I can generate what we call a color specification or color spec, for short.",
		"Here are the values from this latest scan..."
	];
	
	await speakParagraphs(paragraphs);
}

async function speakSpecValuesFromScan(spec, count) {
	setMainLed({r: 255, g: 255, b: 255});
	await speak("From " + count + " samples.");

	setMainLed({r: spec.r.value, g: 0, b: 0});
	await speak("red: " + spec.r.value + "; delta " + spec.r.tolerance + "..");

	setMainLed({r: 0, g: spec.g.value, b: 0});
	await speak("green: " + spec.g.value + "; delta " + spec.g.tolerance + "..");

	setMainLed({r: 0, g: 0, b: spec.b.value});
	await speak("blue: " + spec.b.value + "; delta " + spec.b.tolerance + "..");

	setMainLed({r: 0, g: 0, b: 0});
	await delay(5);
}


async function analyzeSpec(spec) {
	var channelsWithHighTolerance = [];
	var channelsWithMidTolerance = [];
	
	if(spec.r.tolerance > 42) {
		channelsWithHighTolerance.push("red");
	}
	if(spec.g.tolerance > 42) {
		channelsWithHighTolerance.push("green");
	}
	if(spec.b.tolerance > 42) {
		channelsWithHighTolerance.push("blue");
	}
	
	if(spec.r.tolerance > 25) {
		channelsWithMidTolerance.push("red");
	}
	if(spec.g.tolerance > 25) {
		channelsWithMidTolerance.push("green");
	}
	if(spec.b.tolerance > 25) {
		channelsWithMidTolerance.push("blue");
	}
	
	if (channelsWithHighTolerance.length > 0) {
		await speak("The color range for " + channelsWithHighTolerance.join(" and ") + " have a notably high tolerance.");
		await speak("This means that the color spec we just generated is so wide, I will likely mistake some rubbish for the floor.");
		await speak("Either this floor has a wide range of color, or the rover was not on the floor during the entire scan.");
		await speak("If you believe that this floor is of one color, you might want to stop the program here and try again.");
		await delay(10);
	} else if (channelsWithMidTolerance.length > 0) {
		await speak("The color range for " + channelsWithMidTolerance.join(" and ") + " is somewhat wide.");
		await speak("This means that the color spec we just generated is wide enough, I might mistake a few pieces of rubbish for the floor.");
		await speak("This is probably okay, but don't be surprised if I miss a couple of similarly colored pieces of rubbish.");
		await delay(2);
	}
	
	if (spec.r.tolerance < 5 && spec.g.tolerance < 5 && spec.b.tolerance < 5) {
		await speak("Wow, this is a rather tight color spec!");
		await speak("This means that I might mistake a part of the floor for some rubbish.");
		await speak("Either this floor is particularly uniform in color, or the rover's color sensor was scanning the same spot the whole time.");
		await speak("If you suspect there was an anomoly with the scan, you might want to stop the program here and try again.");
		await delay(10);
	}
	
	await speak("Okay.  We'll proceed.");
}

async function scanDirtyFloor(cleanFloorSpec) {
	var dirtSpec = colorProc.Spec.not(cleanFloorSpec);
	
	dirtSpec.whenMatches(callOutDirt);
	cleanFloorSpec.whenMatches(goGreen);
	colorProc.configureSampling({stability: 50, frequency: 100});

	await roll(0, 50, 5);
	await roll(90, 50, 5);
	await roll(180, 50, 5);
	await roll(270, 50, 5);
	
	colorProc.configureSampling({frequency: 0});
}

var piecesOfDirtFound = 0;
var exclaimations = [
	"Looks like we've got something to clean-up, here.",
	"Is this rubbish?!",
	"This doesn't belong here!",
	"tisk, tisk.  Someone has failed to clean-up after themselves.",
	"huh.  This doesn't at all look like clean floor territory.",
	"Clean-up on isle 7!",
	"I'm pretty sure HERE is not the home for THIS item."
];

async function callOutDirt(done) {
	var speed = getSpeed();
	setSpeed(0);	
	await speak(exclamations[Math.random()*exclamations.length], false);
	await strobe({r: 255, g: 0, b: 0}, (3/5) * 0.5, 5);
	piecesOfDirtFound++;
	setSpeed(speed);
	done();
}

async function goGreen(done) {
	setMainLed({r: 0, g: 0, b: 255});
	done();
}

async function report() {
	if(piecesOfDirtFound < 3) {
		await speak("We that wasn't very productive, now, was it?");
		await speak("I mean, I only foumd " + piecesOfDirtFound + " pieces of rubbish.");
		await speak("So, either the space was rather decluttered to start with, or my clean floor spec was too permissive.");
		await speak("Either way, at least you got to see a demonstration of me identifying rubbish.");
	} else if(piecesOfDirtFound > 10) {
		await speak("WOW!  Okay, so either I falsely identified a bunch of open space as rubbish, or we just cleaned-up quiet the mess!");
		await speak("I count... like... one, two, three, four, ..." + piecesOfDirtFound + ", " + piecesOfDirtFound + " pieces of debris!");
		await speak("Either way, at least you got to see a demonstration of me identifying rubbish.");		
	} else {
		await speak("Okay.  Looks like we did a reasonable job cleaning things up.");
		await speak("I remember identifying " + piecesOfDirtFound + " bits of trash.  All in a day's work!");
	}
}


… but the part where these symptoms occur is the part driven by this function:

418: async function scanDirtyFloor(cleanFloorSpec) {
419:	var dirtSpec = colorProc.Spec.not(cleanFloorSpec);
420:	
421:	dirtSpec.whenMatches(callOutDirt);
422:	cleanFloorSpec.whenMatches(goGreen);
423:
424:	colorProc.configureSampling({stability: 50, frequency: 100});
425:	await roll(0, 50, 5);
426:	await roll(90, 50, 5);
427:	await roll(180, 50, 5);
428:	await roll(270, 50, 5);	
429:	colorProc.configureSampling({frequency: 0});
430: }

where…

  • this function is invoked with what I’ve referred to as a “color spec”. It looks something like this

    cleanFloorSpec = {
      r: {value: 59, tolerance: 15},
      g: {value: 33, tolerance: 10},
      b: {value: 10, tolerance: 6},
    }
    

    where it is describing a range of colors.

  • on line 419, we create another “color spec” which is the inverse of the one passed in. So, here, whatever is not “clean”, we’ll consider “dirty”.

  • on line 421, we register an event handler, callOutDirt() that gets invoked when the dirtSpec first matches the current color from the RVR’s RGB sensor.

    async function callOutDirt(done) {
        var speed = getSpeed();
        setSpeed(0);	
        await speak(exclamations[Math.random()*exclamations.length], false);
        await strobe({r: 255, g: 0, b: 0}, (3/5) * 0.5, 5);
        piecesOfDirtFound++;
        setSpeed(speed);
        done();
    }
    

    (note: I’m not using the built-in EventHandler, but wrote one specifically for identifying events occurring from the RGB sensor)

  • on line 422, same thing except the handler is called goGreen()

    async function goGreen(done) {
        setMainLed({r: 0, g: 0, b: 255});
        done();
    }
    
  • Line 424 starts up an event loop, running at 100Hz (i.e every 10ms). That loop is what is sampling the RVR’s color sensor, iterating over the registered “color specs” and invoking their handlers if the color sensor “stably” reports a color within the tolerances of each spec.

  • lines 425 through 428 tell RVR to travel in a square; meanwhile the event loop is collecting samples and triggering handlers whenever their respective color spec matches. This is when the “police lights” symptom occurs.

    • I noticed that despite the code in callOutDirt(), the EDU fails to speak the exclamation. However, when I replace this code with a simple setMainLed(), that works…
1 Like

Hey @RVRTrainerJ!

Thank you for providing so much detail; it is immensely helpful!!

I’ve reached out to the team about your situation and will let you know what they say :relaxed:

To answer your question, we’ll soon have a page about the LED patterns on sdk.sphero.com, but “Police Lights” indicate a processor error (which could be an element of the program you’re running overwhelming it (but also could not be)).

I’ll let you know when I hear more and hope this isn’t hindering your having fun with your RVR too much!

Kelsey

1 Like