// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import android.os.Handler;
import java.util.HashMap;
import java.util.Map;
/**
* A component that provides a high-level interface to a color sensor on a LEGO
* MINDSTORMS NXT robot.
*
* @author lizlooney@google.com (Liz Looney)
*/
@DesignerComponent(version = YaVersion.NXT_COLORSENSOR_COMPONENT_VERSION,
description = "A component that provides a high-level interface to a color sensor on a " +
"LEGO MINDSTORMS NXT robot.",
category = ComponentCategory.LEGOMINDSTORMS,
nonVisible = true,
iconName = "images/legoMindstormsNxt.png")
@SimpleObject
public class NxtColorSensor extends LegoMindstormsNxtSensor implements Deleteable {
private enum State { UNKNOWN, BELOW_RANGE, WITHIN_RANGE, ABOVE_RANGE }
private static final String DEFAULT_SENSOR_PORT = "3";
private static final int DEFAULT_BOTTOM_OF_RANGE = 256;
private static final int DEFAULT_TOP_OF_RANGE = 767;
static final int SENSOR_TYPE_COLOR_FULL = 0x0D; // Color detector mode
static final int SENSOR_TYPE_COLOR_RED = 0x0E; // Light sensor mode with red light on
static final int SENSOR_TYPE_COLOR_GREEN = 0x0F; // Light sensor mode with green light on
static final int SENSOR_TYPE_COLOR_BLUE = 0x10; // Light sensor mode with blue light on
static final int SENSOR_TYPE_COLOR_NONE = 0x11; // Light sensor mode with no light
private static final Map<Integer, Integer> mapColorToSensorType;
private static final Map<Integer, Integer> mapSensorValueToColor;
static {
mapColorToSensorType = new HashMap<Integer, Integer>();
mapColorToSensorType.put(Component.COLOR_RED, SENSOR_TYPE_COLOR_RED);
mapColorToSensorType.put(Component.COLOR_GREEN, SENSOR_TYPE_COLOR_GREEN);
mapColorToSensorType.put(Component.COLOR_BLUE, SENSOR_TYPE_COLOR_BLUE);
mapColorToSensorType.put(Component.COLOR_NONE, SENSOR_TYPE_COLOR_NONE);
mapSensorValueToColor = new HashMap<Integer, Integer>();
mapSensorValueToColor.put(0x01, Component.COLOR_BLACK);
mapSensorValueToColor.put(0x02, Component.COLOR_BLUE);
mapSensorValueToColor.put(0x03, Component.COLOR_GREEN);
mapSensorValueToColor.put(0x04, Component.COLOR_YELLOW);
mapSensorValueToColor.put(0x05, Component.COLOR_RED);
mapSensorValueToColor.put(0x06, Component.COLOR_WHITE);
}
private boolean detectColor;
private Handler handler;
private final Runnable sensorReader;
// Fields related to detecting color
private int previousColor;
private boolean colorChangedEventEnabled;
// Fields related to detecting light
private State previousState;
private int bottomOfRange;
private int topOfRange;
private boolean belowRangeEventEnabled;
private boolean withinRangeEventEnabled;
private boolean aboveRangeEventEnabled;
private int generateColor;
/**
* Creates a new NxtColorSensor component.
*/
public NxtColorSensor(ComponentContainer container) {
super(container, "NxtColorSensor");
handler = new Handler();
previousState = State.UNKNOWN;
previousColor = Component.COLOR_NONE;
sensorReader = new Runnable() {
public void run() {
if (bluetooth != null && bluetooth.IsConnected()) {
if (detectColor) {
// Detecting color
SensorValue<Integer> sensorValue = getColorValue("");
if (sensorValue.valid) {
int currentColor = sensorValue.value;
if (currentColor != previousColor) {
ColorChanged(currentColor);
}
previousColor = currentColor;
}
} else {
// Detecting light
SensorValue<Integer> sensorValue = getLightValue("");
if (sensorValue.valid) {
State currentState;
if (sensorValue.value < bottomOfRange) {
currentState = State.BELOW_RANGE;
} else if (sensorValue.value > topOfRange) {
currentState = State.ABOVE_RANGE;
} else {
currentState = State.WITHIN_RANGE;
}
if (currentState != previousState) {
if (currentState == State.BELOW_RANGE && belowRangeEventEnabled) {
BelowRange();
}
if (currentState == State.WITHIN_RANGE && withinRangeEventEnabled) {
WithinRange();
}
if (currentState == State.ABOVE_RANGE && aboveRangeEventEnabled) {
AboveRange();
}
}
previousState = currentState;
}
}
}
if (isHandlerNeeded()) {
handler.post(sensorReader);
}
}
};
SensorPort(DEFAULT_SENSOR_PORT);
// Detecting color
DetectColor(true);
ColorChangedEventEnabled(false);
// Detecting light
BottomOfRange(DEFAULT_BOTTOM_OF_RANGE);
TopOfRange(DEFAULT_TOP_OF_RANGE);
BelowRangeEventEnabled(false);
WithinRangeEventEnabled(false);
AboveRangeEventEnabled(false);
GenerateColor(Component.COLOR_NONE);
}
@Override
protected void initializeSensor(String functionName) {
int sensorType = detectColor ? SENSOR_TYPE_COLOR_FULL : mapColorToSensorType.get(generateColor);
setInputMode(functionName, port, sensorType, SENSOR_MODE_RAWMODE);
resetInputScaledValue(functionName, port);
}
/**
* Specifies the sensor port that the sensor is connected to.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_LEGO_NXT_SENSOR_PORT,
defaultValue = DEFAULT_SENSOR_PORT)
@SimpleProperty(userVisible = false)
public void SensorPort(String sensorPortLetter) {
setSensorPort(sensorPortLetter);
}
/**
* Returns whether the sensor should detect color or light. True indicates that
* the sensor should detect color; False indicates that the sensor should
* detect light.
*
* The ColorChanged event will not occur if the DetectColor property is set
* to False.
* The BelowRange, WithinRange, and AboveRange events will not occur if the
* DetectColor property is set to True.
* The sensor will not generate color when the DetectColor property is set to
* True.
*/
@SimpleProperty(description = "Whether the sensor should detect color or light. " +
"True indicates that the sensor should detect color; False indicates that the sensor " +
"should detect light. " +
"If the DetectColor property is set to True, the BelowRange, WithinRange, and AboveRange " +
"events will not occur and the sensor will not generate color. " +
"If the DetectColor property is set to False, the ColorChanged event will not occur.",
category = PropertyCategory.BEHAVIOR)
public boolean DetectColor() {
return detectColor;
}
/**
* Specifies whether the sensor should detect color light. True indicates
* that the sensor should detect color; False indicates that the sensor
* should detect light.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN,
defaultValue = "True")
@SimpleProperty
public void DetectColor(boolean detectColor) {
boolean handlerWasNeeded = isHandlerNeeded();
this.detectColor = detectColor;
if (bluetooth != null && bluetooth.IsConnected()) {
initializeSensor("DetectColor");
}
boolean handlerIsNeeded = isHandlerNeeded();
if (handlerWasNeeded && !handlerIsNeeded) {
handler.removeCallbacks(sensorReader);
}
previousColor = Component.COLOR_NONE;
previousState = State.UNKNOWN;
if (!handlerWasNeeded && handlerIsNeeded) {
handler.post(sensorReader);
}
}
// Methods for detecting color
@SimpleFunction(description = "Returns the current detected color, or the color None if the " +
"color can not be read or if the DetectColor property is set to False.")
public int GetColor() {
String functionName = "GetColor";
if (!checkBluetooth(functionName)) {
return Component.COLOR_NONE;
}
if (!detectColor) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_NXT_CANNOT_DETECT_COLOR);
return Component.COLOR_NONE;
}
SensorValue<Integer> sensorValue = getColorValue(functionName);
if (sensorValue.valid) {
return sensorValue.value;
}
// invalid response
return Component.COLOR_NONE;
}
private SensorValue<Integer> getColorValue(String functionName) {
byte[] returnPackage = getInputValues(functionName, port);
if (returnPackage != null) {
boolean valid = getBooleanValueFromBytes(returnPackage, 4);
if (valid) {
int scaledValue = getSWORDValueFromBytes(returnPackage, 12);
if (mapSensorValueToColor.containsKey(scaledValue)) {
int color = mapSensorValueToColor.get(scaledValue);
return new SensorValue<Integer>(true, color);
}
}
}
// invalid response
return new SensorValue<Integer>(false, null);
}
/**
* Returns whether the ColorChanged event should fire when the DetectColor
* property is set to True and the detected color changes.
*/
@SimpleProperty(description = "Whether the ColorChanged event should fire when the DetectColor" +
" property is set to True and the detected color changes.",
category = PropertyCategory.BEHAVIOR)
public boolean ColorChangedEventEnabled() {
return colorChangedEventEnabled;
}
/**
* Specifies whether the ColorChanged event should fire when the DetectColor
* property is set to True and the detected color changes
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "False")
@SimpleProperty
public void ColorChangedEventEnabled(boolean enabled) {
boolean handlerWasNeeded = isHandlerNeeded();
colorChangedEventEnabled = enabled;
boolean handlerIsNeeded = isHandlerNeeded();
if (handlerWasNeeded && !handlerIsNeeded) {
handler.removeCallbacks(sensorReader);
}
if (!handlerWasNeeded && handlerIsNeeded) {
previousColor = Component.COLOR_NONE;
handler.post(sensorReader);
}
}
@SimpleEvent(description = "Detected color has changed. " +
"The ColorChanged event will not occur if the DetectColor property is set to False or if " +
"the ColorChangedEventEnabled property is set to False.")
public void ColorChanged(int color) {
EventDispatcher.dispatchEvent(this, "ColorChanged", color);
}
// Methods for detecting light
@SimpleFunction(description = "Returns the current light level as a value between 0 and 1023, " +
"or -1 if the light level can not be read or if the DetectColor property is set to True.")
public int GetLightLevel() {
String functionName = "GetLightLevel";
if (!checkBluetooth(functionName)) {
return -1;
}
if (detectColor) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_NXT_CANNOT_DETECT_LIGHT);
return -1;
}
SensorValue<Integer> sensorValue = getLightValue(functionName);
if (sensorValue.valid) {
return sensorValue.value;
}
// invalid response
return -1;
}
private SensorValue<Integer> getLightValue(String functionName) {
byte[] returnPackage = getInputValues(functionName, port);
if (returnPackage != null) {
boolean valid = getBooleanValueFromBytes(returnPackage, 4);
if (valid) {
int normalizedValue = getUWORDValueFromBytes(returnPackage, 10);
return new SensorValue<Integer>(true, normalizedValue);
}
}
// invalid response
return new SensorValue<Integer>(false, null);
}
/**
* Returns the bottom of the range used for the BelowRange, WithinRange,
* and AboveRange events.
*/
@SimpleProperty(description = "The bottom of the range used for the BelowRange, WithinRange," +
" and AboveRange events.",
category = PropertyCategory.BEHAVIOR)
public int BottomOfRange() {
return bottomOfRange;
}
/**
* Specifies the bottom of the range used for the BelowRange, WithinRange,
* and AboveRange events.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER,
defaultValue = "" + DEFAULT_BOTTOM_OF_RANGE)
@SimpleProperty
public void BottomOfRange(int bottomOfRange) {
this.bottomOfRange = bottomOfRange;
previousState = State.UNKNOWN;
}
/**
* Returns the top of the range used for the BelowRange, WithinRange, and
* AboveRange events.
*/
@SimpleProperty(description = "The top of the range used for the BelowRange, WithinRange, and" +
" AboveRange events.",
category = PropertyCategory.BEHAVIOR)
public int TopOfRange() {
return topOfRange;
}
/**
* Specifies the top of the range used for the BelowRange, WithinRange, and
* AboveRange events.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER,
defaultValue = "" + DEFAULT_TOP_OF_RANGE)
@SimpleProperty
public void TopOfRange(int topOfRange) {
this.topOfRange = topOfRange;
previousState = State.UNKNOWN;
}
/**
* Returns whether the BelowRange event should fire when the DetectColor
* property is set to False and the light level goes below the BottomOfRange.
*/
@SimpleProperty(description = "Whether the BelowRange event should fire when the DetectColor" +
" property is set to False and the light level goes below the BottomOfRange.",
category = PropertyCategory.BEHAVIOR)
public boolean BelowRangeEventEnabled() {
return belowRangeEventEnabled;
}
/**
* Specifies whether the BelowRange event should fire when the DetectColor
* property is set to False and the light level goes below the BottomOfRange.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "False")
@SimpleProperty
public void BelowRangeEventEnabled(boolean enabled) {
boolean handlerWasNeeded = isHandlerNeeded();
belowRangeEventEnabled = enabled;
boolean handlerIsNeeded = isHandlerNeeded();
if (handlerWasNeeded && !handlerIsNeeded) {
handler.removeCallbacks(sensorReader);
}
if (!handlerWasNeeded && handlerIsNeeded) {
previousState = State.UNKNOWN;
handler.post(sensorReader);
}
}
@SimpleEvent(description = "Light level has gone below the range. " +
"The BelowRange event will not occur if the DetectColor property is set to True or if " +
"the BelowRangeEventEnabled property is set to False.")
public void BelowRange() {
EventDispatcher.dispatchEvent(this, "BelowRange");
}
/**
* Returns whether the WithinRange event should fire when the DetectColor
* property is set to False and the light level goes between the
* BottomOfRange and the TopOfRange.
*/
@SimpleProperty(description = "Whether the WithinRange event should fire when the DetectColor" +
" property is set to False and the light level goes between the BottomOfRange and the " +
"TopOfRange.",
category = PropertyCategory.BEHAVIOR)
public boolean WithinRangeEventEnabled() {
return withinRangeEventEnabled;
}
/**
* Specifies whether the WithinRange event should fire when the DetectColor
* property is set to False and the light level goes between the
* BottomOfRange and the TopOfRange.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "False")
@SimpleProperty
public void WithinRangeEventEnabled(boolean enabled) {
boolean handlerWasNeeded = isHandlerNeeded();
withinRangeEventEnabled = enabled;
boolean handlerIsNeeded = isHandlerNeeded();
if (handlerWasNeeded && !handlerIsNeeded) {
handler.removeCallbacks(sensorReader);
}
if (!handlerWasNeeded && handlerIsNeeded) {
previousState = State.UNKNOWN;
handler.post(sensorReader);
}
}
@SimpleEvent(description = "Light level has gone within the range. " +
"The WithinRange event will not occur if the DetectColor property is set to True or if " +
"the WithinRangeEventEnabled property is set to False.")
public void WithinRange() {
EventDispatcher.dispatchEvent(this, "WithinRange");
}
/**
* Returns whether the AboveRange event should fire when the DetectColor
* property is set to False and the light level goes above the TopOfRange.
*/
@SimpleProperty(description = "Whether the AboveRange event should fire when the DetectColor" +
" property is set to False and the light level goes above the TopOfRange.",
category = PropertyCategory.BEHAVIOR)
public boolean AboveRangeEventEnabled() {
return aboveRangeEventEnabled;
}
/**
* Specifies whether the AboveRange event should fire when the DetectColor
* property is set to False and the light level goes above the TopOfRange.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "False")
@SimpleProperty
public void AboveRangeEventEnabled(boolean enabled) {
boolean handlerWasNeeded = isHandlerNeeded();
aboveRangeEventEnabled = enabled;
boolean handlerIsNeeded = isHandlerNeeded();
if (handlerWasNeeded && !handlerIsNeeded) {
handler.removeCallbacks(sensorReader);
}
if (!handlerWasNeeded && handlerIsNeeded) {
previousState = State.UNKNOWN;
handler.post(sensorReader);
}
}
@SimpleEvent(description = "Light level has gone above the range. " +
"The AboveRange event will not occur if the DetectColor property is set to True or if " +
"the AboveRangeEventEnabled property is set to False.")
public void AboveRange() {
EventDispatcher.dispatchEvent(this, "AboveRange");
}
/**
* Returns the color that should generated by the sensor.
* Only None, Red, Green, or Blue are valid values.
* The sensor will not generate color when the DetectColor property is set to
* True.
*/
@SimpleProperty(description = "The color that should generated by the sensor. " +
"Only None, Red, Green, or Blue are valid values. " +
"The sensor will not generate color when the DetectColor property is set to True.",
category = PropertyCategory.BEHAVIOR)
public int GenerateColor() {
return generateColor;
}
/**
* Specifies the color that should generated by the sensor.
* Only None, Red, Green, or Blue are valid values.
* The sensor will not generate color when the DetectColor property is set to
* True.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_LEGO_NXT_GENERATED_COLOR,
defaultValue = Component.DEFAULT_VALUE_COLOR_NONE)
@SimpleProperty
public void GenerateColor(int generateColor) {
String functionName = "GenerateColor";
if (mapColorToSensorType.containsKey(generateColor)) {
this.generateColor = generateColor;
if (bluetooth != null && bluetooth.IsConnected()) {
initializeSensor(functionName);
}
} else {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_NXT_INVALID_GENERATE_COLOR);
}
}
private boolean isHandlerNeeded() {
if (detectColor) {
return colorChangedEventEnabled;
} else {
return belowRangeEventEnabled || withinRangeEventEnabled || aboveRangeEventEnabled;
}
}
// Deleteable implementation
@Override
public void onDelete() {
handler.removeCallbacks(sensorReader);
super.onDelete();
}
}