En un articulo anterior les mostraba lo fácil que era utilizar el sensor (mote) de Sentilla Perk para capturar temperatura; Les comentaba también que una de las cosas más molestas del kit era que el gateway (el servidor con el cual instalamos software en los sensores y capturamos datos desde la red) sólo corre bajo Windows XP (aunque los ingenieros ya están trabajando en mejoras como drivers para Linux ys OSX).

OK, ¡suficiente habladera de tonterias!. Una forma de remediar esto es agregar otro protocolo (como TCP/IP) encima al programa original que recolectaba las temperaturas de todos los sensores, es decir lo convertimos en un servidor.

Lo primero es decidir si queremos usar un protocolo existente o si queremos reinventar la rueda; Para este experimento decidí que usar un protocolo existente con algunas modificaciones era lo más sencillo (Daytime, RFC686):

  • El servidor escucha en el puerto 1973 (cualquiera sirve si está libre ;)).
  • El cliente envia un paquete (vacio o no) al puerto 1973 en la máquina en donde corre el servidor
  • El servidor recibe el paquete y lo descarta. Envia de vuelta uno nuevo con una lista temperatura de todos los sensores que ha escuchado hasta el momento
  • El servidor no envia más temperaturas

El protocolo es sencillo, es puro texto (con una cabecera el cual el cliente utiliza como validación):
HEADER SENSORID1 SENSORCOUNT1 TEMPERATURE1 UNITS1 SENSORID2 SENSORCOUNT2 TEMPERATURE2 UNITS2 ...

Siempre es bueno encapsular los paquetes de un protocolo en una clase, para hacer más fácil su manejo:

JAVA:
  1. package com.kodegeek.blog.pervasive.perk;
  2.  
  3. /**
  4. * Simple model for temperature readings
  5. * @author josevnz
  6. *
  7. */
  8. public final class SimpleMessage {
  9.    
  10.     /**
  11.      * Valid status codes for the message
  12.      * @author josevnz@kodegeek.com
  13.      */
  14.     public enum Units {
  15.         FAHRENHEIT("Fahrenheit"),
  16.         CELCIUS("Celcius"),
  17.         UNKNOWN("Unknown");
  18.        
  19.         private String desc;
  20.        
  21.         Units(String desc) {
  22.             this.desc = desc;
  23.         }
  24.        
  25.         public String getDescription() {
  26.             return desc;
  27.         }
  28.        
  29.         public static Units getCode(int code) {
  30.             for (Units codes : Units.values()) {
  31.                 if (codes.ordinal() == code)
  32.                     return codes;
  33.             }
  34.             throw new IllegalArgumentException(String.format("Unknown code: %d", code));
  35.         }
  36.        
  37.     }
  38.    
  39.     private long count;
  40.     private long moteId;
  41.     private double temperature;
  42.     private Units units;
  43.    
  44.     /**
  45.      * Default constructor. By default is UNKLNOWN
  46.      */
  47.     public SimpleMessage() {
  48.         this(0, 0, 0.0, Units.UNKNOWN);
  49.     }
  50.    
  51.     /**
  52.      * Parametric constructor
  53.      * @param count Message count
  54.      * @param moteId sensor id
  55.      * @param temperature Temperature
  56.      * @param isRequest Is a request or a response
  57.      */
  58.     public SimpleMessage(long count, long moteId, double temperature, Units code) {
  59.         setCount(count);
  60.         setMoteId(moteId);
  61.         setTemperature(temperature);
  62.         setUnits(code);
  63.     }
  64.    
  65.     /**
  66.      * Get the message cou
  67.      * @return the count
  68.      */
  69.     public final long getCount() {
  70.         return count;
  71.     }
  72.    
  73.     /**
  74.      * Set the message count
  75.      * @param count the count to set
  76.      */
  77.     public final void setCount(long count) {
  78.         this.count = count;
  79.     }
  80.    
  81.     /**
  82.      * Return the sensor id
  83.      * @return the moteId
  84.      */
  85.     public final long getMoteId() {
  86.         return moteId;
  87.     }
  88.    
  89.     /**
  90.      * Set the sensor id
  91.      * @param moteId the moteId to set
  92.      */
  93.     public final void setMoteId(long moteId) {
  94.         this.moteId = moteId;
  95.     }
  96.    
  97.     /**
  98.      * @return the temperature
  99.      */
  100.     public final double getTemperature() {
  101.         return temperature;
  102.     }
  103.    
  104.     /**
  105.      * Set the temperature
  106.      * @param temperature the temperature to set. No validation is made as it depends a lot on the units
  107.      */
  108.     public final void setTemperature(double temperature) {
  109.         this.temperature = temperature;
  110.     }
  111.    
  112.     /**
  113.      * Get the units
  114.      * @return The state code
  115.      */
  116.     public final Units getUnits() {
  117.         return units;
  118.     }
  119.    
  120.     /**
  121.      * Set the units
  122.      * @param code Valid code to pass
  123.      */
  124.     public final void setUnits(Units code) {
  125.         this.units = code;
  126.     }
  127. }

El siguiente paso es decidir como codificar y decodificar el mensaje que enviamos a traves de la red; En el mundo Java podemos usar RMI, Sockets + Serialization sin embargo me pareció mejor utilizar UDP, por lo pequeño de los mensajes además de que no requiero las características extras de TCP, y no serialización (hay que ser MUY cuidadoso cuando se implementa, no es sólo decir que la clase implenta la interfaz "Serializable"):

JAVA:
  1. /**
  2. *
  3. */
  4. package com.kodegeek.blog.pervasive.perk;
  5.  
  6. /**
  7. * Define the contract to encode / decode a temperature message.
  8. * Not using the Sentilla radio protocol.
  9. */
  10. import java.io.IOException;
  11. import java.io.UnsupportedEncodingException;
  12. import java.util.ArrayList;
  13.  
  14. /**
  15. * @author josevnz
  16. *
  17. */
  18. public interface TemperatureCoder {
  19.  
  20.     /**
  21.      * Decode message from wire
  22.      * @param buffer
  23.      * @return TempMessage decoded from the wire
  24.      */
  25.      ArrayList<SimpleMessage> fromWire(byte [] buffer) throws IOException;
  26.    
  27.     /**
  28.      * Encoded message
  29.      * @param e
  30.      * @return byte array with serialized message
  31.      * @throws UnsupportedEncodingException
  32.      */
  33.     byte [] toWire(ArrayList<SimpleMessage> messages) throws IOException;
  34.    
  35. }

Si, una interfaz la cual va a implementar el codificador especifico (código aquí). De esta manera, si cambiamos el protocolo (usamos Java + Serialization por ejemplo), nuestra aplicación sólo tiene que cambiar la declaración del codificador concreto y listo.

¿Que viene después? Ah, envenenar al cliente original que capturaba la temperatura; Ahora va a ser un servidor El cual:

  • Va a capturar constantemente nuevas temperaturas desde la inalámbrica de los sensores en una hebra separada
  • Va a enviar todas las temperaturas disponibles a quien se las pida

Si bien el código del servidor no es muy largo (haga click aqui para verlo completo) prefiero mostrarle sólo las piezas críticas:

JAVA:
  1. /**
  2. * Server that collects the temperature readings of
  3. * all the motes in the wireless network.
  4. * @author josevnz@kodegeek.com
  5. */
  6.  
  7. package com.kodegeek.blog.pervasive.perk;
  8.  
  9. ...
  10.  
  11. /**
  12. * Iterative Server that collects all the temperature readings coming from the Sentilla Perk on the networks
  13. * @author josevnz@kodegeek.com
  14. *
  15. */
  16. public final class TemperatureService implements Runnable {
  17.  
  18.     private TemperatureService() {};
  19.  
  20.     private static final Logger log = Logger.getLogger(TemperatureService.class.getName());
  21.    
  22.     static {
  23.         log.setLevel(Level.FINEST);
  24.     }
  25.     private static final int DEFAULT_PORT = 1973;
  26.     protected static final int BUFFER_SIZE = 65507; // 65535 bytes - IP Header(20) - UDP header (8)
  27.     private static final int MOTE_WAIT = MoteTemperatureReader.WAIT_TIME;
  28.     private static final Map <Long, TempMessage>sensorMap = new ConcurrentHashMap<Long, TempMessage>();
  29.  
  30.     /**
  31.      * Capture sensor temperatures firever on a separate thread
  32.      */
  33.     public void run() {
  34.  
  35.         // Connect to the Sentilla server
  36.         HostClient client = new HostClient();
  37.         try {
  38.             client.connect();
  39.             log.log(Level.INFO, String.format("Connected to sensor gateway at %s, capturing temperatures", client.getHost()));
  40.             while(true) {
  41.                 Receiver receiver = ReceiverDriver.create(TempMessage.class);
  42.                 receiver.setReceive().submit().block(MOTE_WAIT);
  43.                 if (receiver.isDone()) {
  44.                     TempMessage tmsg = receiver.getData();
  45.                     sensorMap.put(tmsg.moteId, tmsg); // Save every sensor update, replacing previous one
  46.                     log.log(Level.FINE, String.format("Got update from sensor id: %s", tmsg.moteId));
  47.                 }
  48.             }
  49.         } catch (IOException ioExp) {
  50.             log.log(Level.SEVERE, "Severe error, response will be dropped", ioExp);
  51.         } finally {
  52.             try {
  53.                 if (client != null)
  54.                     client.disconnect();
  55.             } catch (IOException ioExp) {
  56.                 log.log(Level.SEVERE, "Error while closing sensor connection", ioExp);
  57.             }
  58.         }
  59.     }
  60.  
  61.     /**
  62.      * Collect the statistics from all the motes
  63.      * @param args
  64.      */
  65.     public static void main(String[] args) throws Throwable {
  66. ...
  67.  
  68.         // Start capturing temperatures
  69.         Thread tempThread = new Thread(new TemperatureService());
  70.         tempThread.start();
  71.  
  72.         DatagramSocket socket;
  73.         TemperatureCoder coder = new TemperatureTextCoder();
  74.         socket = null;
  75.         try {
  76.             socket = new DatagramSocket(port);
  77.             byte [] buffer = new byte[BUFFER_SIZE];
  78.  
  79.             DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
  80.             ArrayList<SimpleMessage>messages = new ArrayList<SimpleMessage>();
  81.  
  82.             while(true) { // Wait forever for new clients            
  83.                 try {
  84.                     socket.receive(packet); // Get the packet and discard the contents
  85.                     log.info(String.format("Got a connection from: %s", packet.getAddress()));
  86.                     // Send every available tick to the client in one shot
  87.                     for (TempMessage tmsg : sensorMap.values()) {
  88.                         SimpleMessage message = new SimpleMessage();
  89.                         // Get the temperature for the sensor
  90.                         double temp = tmsg.temperature.doubleValue(CELSIUS);
  91.                         message.setCount(tmsg.count);
  92.                         message.setMoteId(tmsg.moteId);
  93.                         message.setTemperature(temp);
  94.                         message.setUnits(SimpleMessage.Units.CELCIUS);
  95. ...
  96.                         messages.add(message);
  97.  
  98.                     } // end for every sensor
  99.                    
  100.                     // Send the whole pack to the client
  101.                     buffer = coder.toWire(messages);
  102.                     packet.setData(buffer, 0, buffer.length);
  103.                     socket.send(packet);
  104.     ...

Si, el servidor no es muy robusto ni soporta un gran número de clientes a la vez (es iterativo), pero para jugar un rato está bien :)

Bueno, si aún no le ha dado un ataque leyendo todo este código lo invito a que vea el cliente (código completo aqui) . Muy sencillo (sólo imprime por pantalla las temperaturas) sin embargo tiene una caracteristica muy importante: No tiene ninguna dependencia con el código de Sentilla:

JAVA:
  1. package com.kodegeek.blog.pervasive.perk;
  2.  
  3. ...
  4.  
  5. /**
  6. * Simple client that will get all the temperature readings from a TemperatureServer on the network.
  7. * @author josevnz@kodegeek.com
  8. *
  9. */
  10. public final class TemperatureClient {
  11.    
  12. ...
  13.    
  14.     /**
  15.      * @param args host (args[0]), port number (arg[1])
  16.      * @throws Throwable On any fatal error
  17.      */
  18.     public static void main(String[] args) throws Throwable {
  19. ...
  20.    
  21.         log.log(Level.INFO, "Starting TemperatureClient");
  22.        
  23.         TemperatureCoder coder = new TemperatureTextCoder();
  24.        
  25.         DatagramSocket socket = new DatagramSocket();
  26.         try {
  27.             InetAddress address = InetAddress.getByName(host);
  28.             socket.connect(address, port);
  29.             log.info(String.format("Contacting server on: %s:%s", address.getHostName(), port));
  30.            
  31.             // Send empty packet
  32.             byte [] buffer = new byte[TemperatureService.BUFFER_SIZE];
  33.             DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
  34.             int attempt = 1;
  35.             while(true) {
  36.                 try {
  37.                 socket.setSoTimeout(SOCKET_TIMEOUT);
  38.                 socket.send(packet); // Send empty packet
  39.                 socket.receive(packet); // Get the temperature reading
  40.                 buffer = new byte [packet.getLength()];
  41.                 System.arraycopy(packet.getData(), packet.getOffset(), buffer, 0, packet.getLength());
  42.  
  43.                 for (SimpleMessage messg: coder.fromWire(buffer)) {
  44.                     log.log(Level.INFO, String.format(
  45.                             "Mote ID: %d, Count: %d, Temperature: %.5f C\n",
  46.                             messg.getMoteId(),
  47.                             messg.getCount(),
  48.                             messg.getTemperature()));
  49.                 }
  50.                 packet.setLength(TemperatureService.BUFFER_SIZE);
  51.                 socket.setSoTimeout(0);
  52.                 Thread.sleep(SLEEP_TIME);
  53.     ... 
  54.                 }
  55.             }
  56.         } catch (IOException ioexp) {
  57.             log.log(Level.SEVERE, "There was a problem", ioexp);
  58.         } finally {
  59.             socket.close();
  60.         }
  61.     }
  62. }

Bueno, ahora lo que queda es ponerse a jugar conectándole otros sensores e incluso volviendo más inteligentes a los sensores.

¿Y ustedes que piensan? Intercambio de ideas y código van de la mano :)


6 Respuestas a “Capturando temperatura utilizando Sentilla Perk (II)”

  1. 1 Roman

    Pienso que usando un SunSPOT és brutalmente más sencillo (aunque quizà un poco más caro)... cuando te costó el mote ?

  2. 2 Roman

    Ouch, me retracto de lo dicho, el código es muy similar en simplicidad a cómo se puede hacer con un spot :-S

  3. 3