/* * 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); } }