/*
* Copyright (C) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package interactivespaces.hardware.driver.proximity.sensacell;
import com.google.common.collect.Lists;
import interactivespaces.InteractiveSpacesException;
import interactivespaces.hardware.driver.DriverSupport;
import interactivespaces.service.comm.serial.SerialCommunicationEndpoint;
import interactivespaces.service.comm.serial.SerialCommunicationEndpoint.Parity;
import interactivespaces.service.comm.serial.SerialCommunicationEndpointService;
import interactivespaces.util.InteractiveSpacesUtilities;
import java.awt.Rectangle;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* Driver for a Sensacell.
*
* <p>
* This class is not thread safe.
*
* @author Keith M. Hughes
*/
public class SensacellDriver extends DriverSupport {
/**
* Configuration property giving which serial port to use for the sensacell.
*/
public static final String CONFIGURATION_SENSACELL_PORT = "space.hardware.driver.sensacell.port";
/**
* The default sensacell update rate value for reading the sensacell. In
* updates per second.
*/
public static final double DEFAULT_SENSACELL_UPDATE = 30.0;
/**
* The percentage of pixels in a rectangle which have to be active for the
* rectangle to be considered active.
*/
public static final double ACTIVE_RECTANGLE_PERCENTAGE = .5;
/**
* ASCII character value for a carriage return.
*/
public static final byte CARRIAGE_RETURN = 0x0D;
/**
* The default threshold value for an element to be considered active.
*/
public static final int SENSACELL_DEFAULT_THRESHOLD = 100;
/**
* The length of time for the sensacell to reset, in milliseconds.
*/
public static final int SENSACELL_RESET_PERIOD = 8000;
/**
* The length of time for reseting a sensor, in milliseconds.
*/
public static final int SENSACELL_SENSOR_RESET_PERIOD = 1000;
/**
* The ID for referring to the master sensacell module in an array of modules.
*/
public static final byte SENSACELL_MASTER_MODULE = 0x01;
/**
* The read mode setting for the sensor to be read in digital mode.
*/
public static final int SENSACELL_MODE_READ_DIGITAL = 0;
/**
* The read mode setting for the sensor to be read in proportional mode.
*/
public static final int SENSACELL_MODE_READ_PROPORTIONAL = 1;
/**
* The setting for the sensor to update at a 10 Hz rate.
*/
public static final int SENSACELL_SPEED_10HZ = 0;
/**
* The setting for the sensor to update at a 20 Hz rate.
*/
public static final int SENSACELL_SPEED_20HZ = 1;
/**
* A normal read for the sensacell.
*/
public static final int SENSACELL_NORMAL_READ = 0;
/**
* A latched read for the sensacell.
*/
public static final int SENSACELL_LATCHED_READ = 1;
/**
* A mapping of digits to their associated hex character.
*/
public static final byte[] NUMBER_TO_HEX = { (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'A',
(byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F' };
/**
* How often the SensacellDriver should be updated. Is updates per second.
*/
private double sensorUpdateRate = DEFAULT_SENSACELL_UPDATE;
/**
* Name of the serial port to talk to.
*/
private String portName;
/**
* Serial communication endpoint to the SensacellDriver.
*/
private SerialCommunicationEndpoint cellEndpoint;
/**
* {@code true} if the cell has completed setup.
*/
private boolean setupComplete;
/**
* ID of the cell being read.
*/
private int id;
/**
* {@code true} if the system is resetting.
*/
private boolean systemResetting;
/**
* The system time when a system reset started.
*/
private long systemResetStartTime;
/**
* {@code true} if the sensor is resetting.
*/
private boolean sensorResetting;
/**
* The system time when a sensor reset started.
*/
private long sensorResetStartTime;
/**
* {@code true} if a mode change happened.
*/
private boolean modeChanged;
/**
* The sensor data from the cell.
*/
private int[] sensorData = new int[16];
/**
* The threshold at which a sensor is considered to be active.
*/
private int sensorThreshold;
/**
* The system time that the last read was done, in milliseconds.
*/
private long lastTimeRead;
/**
* The read mode for the cell.
*/
private int readMode;
/**
* The latch mode for the cell.
*/
private int latchMode;
/**
* The speed at which the sensor hardware is scanning.
*/
private int sensorSpeed;
/**
* The rate at which the cell hardware is updating, and so valid to be read,
* in ms.
*/
private long updateRate;
/**
* Buffer for storing outgoing commands to the sensacell.
*/
private byte[] command = new byte[8];
/**
* Buffer for doing a digital data read.
*
* <p>
* This data is packed, 1 bit for each of 16 sensors in 4 bytes.
*/
private byte[] digitalReadBuffer = new byte[5];
/**
* Buffer for doing a proportional data read.
*
* <p>
* This is a byte for each of 16 sensors terminated with a carriage return.
*/
private byte[] proportionalReadBuffer = new byte[17];
/**
* The listeners for the cell.
*/
private List<SensacellListener> listeners = Lists.newArrayList();
/**
* The update loop for this app.
*/
private Future<?> updater;
/**
* Construct a driver.
*/
public SensacellDriver() {
setupComplete = false;
systemResetting = false;
sensorResetting = false;
modeChanged = false;
id = SENSACELL_MASTER_MODULE;
readMode = SENSACELL_MODE_READ_DIGITAL;
latchMode = SENSACELL_NORMAL_READ;
sensorSpeed = SENSACELL_SPEED_10HZ;
updateRate = 100;
setSensorThreshold(SENSACELL_DEFAULT_THRESHOLD);
Arrays.fill(sensorData, 255);
lastTimeRead = 0;
}
/**
* @return the sensorUpdateRate
*/
public double getSensorUpdateRate() {
return sensorUpdateRate;
}
/**
* @param sensorUpdateRate
* the sensorUpdateRate to set
*/
public void setSensorUpdateRate(double sensorUpdateRate) {
this.sensorUpdateRate = sensorUpdateRate;
}
@Override
public void startup() {
SerialCommunicationEndpointService communicationEndpointService =
spaceEnvironment.getServiceRegistry().getRequiredService(
SerialCommunicationEndpointService.SERVICE_NAME);
try {
portName = configuration.getRequiredPropertyString(CONFIGURATION_SENSACELL_PORT);
cellEndpoint = communicationEndpointService.newSerialEndpoint(portName);
cellEndpoint.setBaud(230400).setDataBits(8).setStopBits(1).setParity(Parity.NONE);
cellEndpoint.startup();
setReadMode(SensacellDriver.SENSACELL_MODE_READ_PROPORTIONAL);
setSensorSpeed(SensacellDriver.SENSACELL_SPEED_20HZ);
log.info("Sensacell driver started");
setupComplete = true;
updater = spaceEnvironment.getExecutorService().scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
update();
}
}, 0, (long) (1000.0 / sensorUpdateRate), TimeUnit.MILLISECONDS);
} catch (Exception e) {
throw new InteractiveSpacesException(String.format("Cannot set up port %s for sensacell",
portName), e);
}
}
@Override
public void shutdown() {
log.info("Sensacell driver shutting down");
if (updater != null) {
updater.cancel(true);
cellEndpoint.shutdown();
cellEndpoint = null;
}
}
/**
* Add a new listener to the cell.
*
* @param listener
* the listener to add
*/
public void addListener(SensacellListener listener) {
listeners.add(listener);
}
/**
* Update the sensacell's state.
*/
private void update() {
// Can't update if setup isn't complete.
if (!setupComplete) {
return;
}
try {
// while the system is resetting, sensor data is useless so don't
// read until the reset period is complete
if (systemResetting) {
if ((System.currentTimeMillis() - systemResetStartTime) >= SENSACELL_RESET_PERIOD) {
systemResetting = false;
cellEndpoint.flush();
// restore settings
sendSensorReadModeCommand(readMode, sensorSpeed, latchMode);
} else {
return;
}
}
if (modeChanged) {
modeChanged = false;
// read mode was changed so flush the port
cellEndpoint.flush();
}
if (sensorResetting) {
if ((System.currentTimeMillis() - sensorResetStartTime) >= SENSACELL_SENSOR_RESET_PERIOD) {
sensorResetting = false;
cellEndpoint.flush();
// restore settings (not sure this is necessary, but playing
// it safe just in case
sendSensorReadModeCommand(readMode, sensorSpeed, latchMode);
} else {
return;
}
}
// the sensor is only updated at a rate of 10Hz or 20Hz (depending
// on value of sensorSpeed)
long timeDifference = System.currentTimeMillis() - lastTimeRead;
if (timeDifference >= updateRate) {
if (readMode == SENSACELL_MODE_READ_DIGITAL) {
sendDigitalReadCommand(id);
readDigitalData();
notifyListenersOfUpdate();
} else if (readMode == SENSACELL_MODE_READ_PROPORTIONAL) {
sendProportionalReadCommand(id);
readProporptionalData();
notifyListenersOfUpdate();
}
lastTimeRead = System.currentTimeMillis();
}
} catch (IOException e) {
throw new InteractiveSpacesException("Could not update the sensacell values", e);
}
}
/**
* Notify all listeners of an update event.
*/
private void notifyListenersOfUpdate() {
for (SensacellListener listener : listeners) {
listener.onSensacellUpdate(this);
}
}
/**
* Draw the current sensor data to the cell to visualize what is sensed.
*/
public void drawToSensacell() {
try {
sendGlobalWriteCommand(0x01, 0x00);
for (int i = 0; i < 16; i++) {
cellEndpoint.write(sensorData[i]);
cellEndpoint.write(sensorData[i]);
cellEndpoint.write(sensorData[i]);
}
cellEndpoint.write(CARRIAGE_RETURN);
} catch (IOException e) {
throw new InteractiveSpacesException("Cannot draw sensor data to sensacell", e);
}
}
/**
* Read the data from the SensacellDriver.
*
* @return {@code true} if the data was read correctly, {@code false}
* otherwise.
*/
private boolean readDigitalData() throws IOException {
if (!readSerialData(digitalReadBuffer))
return false;
// get reads into more usable 16 part array
placeInSensorData(hexToInt(digitalReadBuffer[3]), 15);
placeInSensorData(hexToInt(digitalReadBuffer[2]), 11);
placeInSensorData(hexToInt(digitalReadBuffer[1]), 7);
placeInSensorData(hexToInt(digitalReadBuffer[0]), 3);
return true;
}
/**
* Unpack a set of data into the stored sensor data.
*
* @param value
* the packed data
* @param pos
* where the data should be stored in the data array
*/
private void placeInSensorData(int value, int pos) {
for (int i = 0; i < 4; i++, pos--) {
sensorData[pos] = (value & 0x01);
value >>= 1;
}
}
/**
* read a sequence of proportional data from the SensacellDriver.
*
* @return {@code true} if the data was read correctly, {@code false}
* otherwise.
*/
private boolean readProporptionalData() throws IOException {
if (!readSerialData(proportionalReadBuffer))
return false;
for (int i = 0; i < 16; i++) {
sensorData[i] = (int) (hexToInt(proportionalReadBuffer[i]) / 16.0f * 255);
}
return true;
}
/**
* Read serial data from the cell into the buffer
*
* @param buffer
* the buffer to store the data in
*
* @return {@code true} if of the correct length and properly terminated.
*
* @throws IOException
*/
private boolean readSerialData(byte[] buffer) throws IOException {
int offset = 0;
int toRead = buffer.length;
while (toRead > 0) {
int readAmt = cellEndpoint.read(buffer, offset, toRead);
if (readAmt == -1) {
log.info("Reached EOF of sensacell stream");
return false;
}
offset += readAmt;
toRead -= readAmt;
}
if (buffer[buffer.length - 1] != CARRIAGE_RETURN) {
log.warn("No carriage return at end of sensacell packet");
return false;
}
return true;
}
/**
* Takes a HEX byte and returns the integer it represents.
*
* @param hexByte
* the hex byte, in ASCII
*
* @return the integer
*/
private int hexToInt(byte hexByte) {
for (int i = 0; i < NUMBER_TO_HEX.length; i++) {
if (NUMBER_TO_HEX[i] == hexByte)
return i;
}
throw new InteractiveSpacesException(String.format("Illegal hex byte %d", (int) hexByte));
}
/**
* Set the ID (address) of the sensacell module to be monitoring
*
* <p>
* This is only necessary if looking at an entire grid.
*
* @param id
* ID (0 - 255)
*/
public void setId(int id) {
if (id >= 0 && id <= 255) {
this.id = id;
} else {
this.id = SENSACELL_MASTER_MODULE;
// to do: or throw invalid id error
}
}
/**
* Set the threshold for the {@link #isActive(int, int)} method
*
* @param threshold
* the threshold for the isActive method (0 - 255)
*/
public void setSensorThreshold(int threshold) {
if (threshold >= 0 && threshold <= 255) {
sensorThreshold = threshold;
} else {
sensorThreshold = SENSACELL_DEFAULT_THRESHOLD;
}
}
/**
* Get the current sensor threshold value.
*
* @return the threshold, between 0 and 255
*/
public int getSensorThreshold() {
return sensorThreshold;
}
/**
* Set the speed (update rate) for the sensor.
*
* @param speed
* update rate of the sensor, valid values are:
* {@link #SENSACELL_SPEED_10HZ} and {@link #SENSACELL_SPEED_20HZ}.
*/
public void setSensorSpeed(int speed) {
if (speed == SENSACELL_SPEED_10HZ || speed == SENSACELL_SPEED_20HZ) {
sensorSpeed = speed;
updateRate = speed == SENSACELL_SPEED_10HZ ? 100 : 50;
try {
sendSensorReadModeCommand(readMode, sensorSpeed, latchMode);
} catch (IOException e) {
throw new InteractiveSpacesException("Cannot set speed on SensacellDriver", e);
}
} else {
// invalid speed value
}
}
/**
* Set the read mode for the sensor
*
* @param mode
* read mode of the senso, valid values are
* {@link #SENSACELL_NORMAL_READ} and {@link #SENSACELL_LATCHED_READ}
*/
public void setReadMode(int mode) {
if (mode == SENSACELL_NORMAL_READ || mode == SENSACELL_LATCHED_READ) {
readMode = mode;
try {
sendSensorReadModeCommand(readMode, sensorSpeed, latchMode);
} catch (IOException e) {
throw new InteractiveSpacesException("Cannot set read mode on sensacell", e);
}
modeChanged = true;
} else {
// invalid readMode value
}
}
/**
* Set the latch mode for the sensor
*
* @param latch
* latch mode of the sensor, valid values are
* {@link #SENSACELL_MODE_READ_DIGITAL} and
* {@link #SENSACELL_MODE_READ_PROPORTIONAL}
*/
public void setLatchMode(int latch) {
if (latch == SENSACELL_MODE_READ_DIGITAL || latch == SENSACELL_MODE_READ_PROPORTIONAL) {
latchMode = latch;
try {
sendSensorReadModeCommand(readMode, sensorSpeed, latchMode);
} catch (IOException e) {
throw new InteractiveSpacesException("Cannot set latch mode on SensacellDriver", e);
}
} else {
// invalid latch value
}
}
/**
* @return the current read mode of the system
*/
public int getReadMode() {
return readMode;
}
/**
* @return the current sensor speed (update rate) of the system
*/
public int getSensorSpeed() {
return sensorSpeed;
}
/**
* @return the current latch mode of the system
*/
public int getLatchMode() {
return latchMode;
}
/**
* @return a vector containing the current sensor values
*/
public int[] getReads() {
// TODO(keith): Return copy?
return sensorData;
}
/**
* @return the average of all 16 sensor values
*/
public int getAvgRead() {
int avg = 0;
for (int i = 0; i < 16; i++)
avg += sensorData[i];
avg /= 16;
return avg;
}
/**
* Get all sensor values as a string.
*
* @return a string containing all 16 sensor values
*/
public String getReadsAsString() {
StringBuilder readString = new StringBuilder();
readString.append(Integer.toString(sensorData[0]));
for (int i = 1; i < 16; i++) {
readString.append(':').append(Integer.toString(sensorData[i]));
}
return readString.toString();
}
/**
* Tests if the area defined by the point (x, y) is active
*
* @param x
* x coordinate of the point to test
* @param y
* y coordinate of the point to test
* @return true if object present, false if no object present (within range
* defined by sensorThreshold)
*/
boolean isActive(int x, int y) {
return sensorData[y * 4 + x] > sensorThreshold;
}
/**
* Tests if the area defined by the rectangle is active.
*
* <p>
* 50% of the points must have {@link #isActive(int, int)} returning
* {@code true}.
*
* @param r
* rectangle defining area to be tested
* @return {@code true} if sensing inside the rectangle
*/
public boolean isActive(Rectangle r) {
int numActive = 0;
for (int x = r.x; x < r.x + r.width; x++) {
for (int y = r.y; y < r.y + r.height; y++) {
if (isActive(x, y))
numActive++;
}
}
return numActive > r.width * r.height * ACTIVE_RECTANGLE_PERCENTAGE;
}
/**
* Request a read from the specified address.
*
* @param address
* The address to be read.
* @throws IOException
*/
private void sendDigitalReadCommand(int address) throws IOException {
command[0] = (byte) 'r';
command[1] = NUMBER_TO_HEX[(address >> 4) & 0x0f];
command[2] = NUMBER_TO_HEX[address & 0x0f];
command[3] = CARRIAGE_RETURN;
cellEndpoint.write(command, 0, 4);
}
/**
* Request a proportional read from the specified address.
*
* @param address
* The address to be read.
* @throws IOException
*/
private void sendProportionalReadCommand(int address) throws IOException {
command[0] = (byte) 'p';
command[1] = NUMBER_TO_HEX[(address >> 4) & 0x0f];
command[2] = NUMBER_TO_HEX[address & 0x0f];
command[3] = CARRIAGE_RETURN;
cellEndpoint.write(command, 0, 4);
}
/**
* Send the command for setting read modes to the sensacell.
*
* @param readMode
* the new read mode
* @param speed
* the new speed for updates
* @param latch
* whether or not the cell should latch
*
* @throws IOException
*/
private void sendSensorReadModeCommand(int readMode, int speed, int latch) throws IOException {
int ctrlBits = (latch << 2) | (speed << 1) | readMode;
command[0] = (byte) '0';
command[1] = (byte) 'B';
command[2] = NUMBER_TO_HEX[(ctrlBits >> 4) & 0x0f];
command[3] = NUMBER_TO_HEX[ctrlBits & 0x0f];
command[4] = (byte) 'a';
command[5] = (byte) '0';
command[6] = (byte) '0';
command[7] = CARRIAGE_RETURN;
cellEndpoint.write(command, 0, 8);
}
/**
* Send the global write command to the sensacell.
*
* @param numModules
* the number of modules in the full array
* @param address
* which module to send to
*
* @throws IOException
*/
private void sendGlobalWriteCommand(int numModules, int address) throws IOException {
command[0] = (byte) '0';
command[1] = (byte) '1';
command[2] = NUMBER_TO_HEX[(numModules >> 4) & 0x0f];
command[3] = NUMBER_TO_HEX[numModules & 0x0f];
command[4] = (byte) 'a';
command[5] = NUMBER_TO_HEX[(address >> 4) & 0x0f];
command[6] = NUMBER_TO_HEX[address & 0x0f];
command[7] = CARRIAGE_RETURN;
cellEndpoint.write(command, 0, 8);
}
/**
* Reset and recalibrate all sensacell modules
*
* @throws IOException
*/
private void sensorReset() throws IOException {
// reset all modules
sensorReset(0x00);
}
/**
* Reset and recalibrate the addressed sensacell module
*
* @param address
* address of the sensacell module to be reset
*
* @throws IOException
*/
private void sensorReset(int address) throws IOException {
sensorResetting = true;
sensorResetStartTime = System.currentTimeMillis();
command[0] = (byte) '0';
command[1] = (byte) '3';
command[2] = (byte) '0';
command[3] = (byte) '0';
command[4] = (byte) 'a';
command[5] = NUMBER_TO_HEX[(address >> 4) & 0x0f];
command[6] = NUMBER_TO_HEX[address & 0x0f];
command[7] = CARRIAGE_RETURN;
cellEndpoint.write(command, 0, 8);
}
/**
* Perform a full system reset. This is essential the same as powering down
* and repowering the device.
*
* @throws IOException
*/
public void systemReset() {
try {
command[0] = (byte) '1';
command[1] = (byte) '3';
command[2] = (byte) 'E';
command[3] = (byte) 'A';
command[4] = (byte) 'a';
command[5] = (byte) '0';
command[6] = (byte) '0';
command[7] = CARRIAGE_RETURN;
// Give the cell time to set up if first turning on.
InteractiveSpacesUtilities.delay(5000);
systemResetting = true;
systemResetStartTime = System.currentTimeMillis();
cellEndpoint.write(command, 0, 8);
cellEndpoint.flush();
} catch (Exception e) {
throw new InteractiveSpacesException("Could not send system reset command to sensacell", e);
}
}
/**
* Turn off all illumintated LEDs on all sensacells
*
* @throws IOException
*/
public void blackOut() throws IOException {
// send write command
sendGlobalWriteCommand(0x01, 0x00);
Random r = new Random();
// immediate follow by RGB values
for (int i = 0; i < 16; i++) {
cellEndpoint.write(r.nextInt(255));
cellEndpoint.write(r.nextInt(255));
cellEndpoint.write(r.nextInt(255));
}
cellEndpoint.write(CARRIAGE_RETURN);
cellEndpoint.flush();
// saveCurrentState();
}
/**
* Save the current state of the sensacell
*
* @throws IOException
*/
private void saveCurrentState() throws IOException {
// to do: update for all 4 write types
command[0] = (byte) '1';
command[1] = (byte) '7';
command[2] = (byte) '0';
command[3] = (byte) '0';
command[4] = (byte) 'a';
command[5] = (byte) '0';
command[6] = (byte) '1';
command[7] = CARRIAGE_RETURN;
cellEndpoint.write(command, 0, 8);
cellEndpoint.flush();
}
/**
* Listener for events from a sensacell.
*
* @author Keith M. Hughes
*/
public static interface SensacellListener {
/**
* The sensacell has updated.
*
* @param cell
* The cell which updated.
*/
void onSensacellUpdate(SensacellDriver cell);
}
}