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:
package com.kodegeek.blog.pervasive.perk;
/**
* Simple model for temperature readings
* @author josevnz
*
*/
public final class SimpleMessage {
/**
* Valid status codes for the message
* @author eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6a%6f%73%65%76%6e%7a%40%6b%6f%64%65%67%65%65%6b%2e%63%6f%6d%22%3e%6a%6f%73%65%76%6e%7a%40%6b%6f%64%65%67%65%65%6b%2e%63%6f%6d%3c%2f%61%3e%27%29%3b'))
*/
public enum Units {
FAHRENHEIT("Fahrenheit"),
CELCIUS("Celcius"),
UNKNOWN("Unknown");
private
String desc;
Units(
String desc) {
this.desc = desc;
}
public
String getDescription() {
return desc;
}
public static Units getCode(int code) {
for (Units codes : Units.values()) {
if (codes.ordinal() == code)
return codes;
}
throw new
IllegalArgumentException(
String.format("Unknown code: %d", code));
}
}
private long count;
private long moteId;
private double temperature;
private Units units;
/**
* Default constructor. By default is UNKLNOWN
*/
public SimpleMessage() {
this(0, 0, 0.0, Units.
UNKNOWN);
}
/**
* Parametric constructor
* @param count Message count
* @param moteId sensor id
* @param temperature Temperature
* @param isRequest Is a request or a response
*/
public SimpleMessage(long count, long moteId, double temperature, Units code) {
setCount(count);
setMoteId(moteId);
setTemperature(temperature);
setUnits(code);
}
/**
* Get the message cou
* @return the count
*/
public final long getCount() {
return count;
}
/**
* Set the message count
* @param count the count to set
*/
public final void setCount(long count) {
this.count = count;
}
/**
* Return the sensor id
* @return the moteId
*/
public final long getMoteId() {
return moteId;
}
/**
* Set the sensor id
* @param moteId the moteId to set
*/
public final void setMoteId(long moteId) {
this.moteId = moteId;
}
/**
* @return the temperature
*/
public final double getTemperature() {
return temperature;
}
/**
* Set the temperature
* @param temperature the temperature to set. No validation is made as it depends a lot on the units
*/
public final void setTemperature(double temperature) {
this.temperature = temperature;
}
/**
* Get the units
* @return The state code
*/
public final Units getUnits() {
return units;
}
/**
* Set the units
* @param code Valid code to pass
*/
public final void setUnits(Units code) {
this.units = code;
}
}
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"):
/**
*
*/
package com.kodegeek.blog.pervasive.perk;
/**
* Define the contract to encode / decode a temperature message.
* Not using the Sentilla radio protocol.
*/
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
/**
* @author josevnz
*
*/
public interface TemperatureCoder {
/**
* Decode message from wire
* @param buffer
* @return TempMessage decoded from the wire
*/
ArrayList<SimpleMessage> fromWire(byte [] buffer) throws
IOException;
/**
* Encoded message
* @param e
* @return byte array with serialized message
* @throws UnsupportedEncodingException
*/
byte [] toWire(ArrayList<SimpleMessage> messages) throws
IOException;
}
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:
/**
* Server that collects the temperature readings of
* all the motes in the wireless network.
* @author eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6a%6f%73%65%76%6e%7a%40%6b%6f%64%65%67%65%65%6b%2e%63%6f%6d%22%3e%6a%6f%73%65%76%6e%7a%40%6b%6f%64%65%67%65%65%6b%2e%63%6f%6d%3c%2f%61%3e%27%29%3b'))
*/
package com.kodegeek.blog.pervasive.perk;
...
/**
* Iterative Server that collects all the temperature readings coming from the Sentilla Perk on the networks
* @author eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6a%6f%73%65%76%6e%7a%40%6b%6f%64%65%67%65%65%6b%2e%63%6f%6d%22%3e%6a%6f%73%65%76%6e%7a%40%6b%6f%64%65%67%65%65%6b%2e%63%6f%6d%3c%2f%61%3e%27%29%3b'))
*
*/
public final class TemperatureService implements
Runnable {
private TemperatureService() {};
private static final Logger log = Logger.getLogger(TemperatureService.class.getName());
static {
log.setLevel(Level.FINEST);
}
private static final int DEFAULT_PORT = 1973;
protected static final int BUFFER_SIZE = 65507; // 65535 bytes - IP Header(20) - UDP header (8)
private static final int MOTE_WAIT = MoteTemperatureReader.WAIT_TIME;
private static final
Map <Long, TempMessage>sensorMap = new ConcurrentHashMap<Long, TempMessage>();
/**
* Capture sensor temperatures firever on a separate thread
*/
public void run() {
// Connect to the Sentilla server
HostClient client = new HostClient();
try {
client.connect();
log.log(Level.INFO,
String.format("Connected to sensor gateway at %s, capturing temperatures", client.getHost()));
while(true) {
Receiver receiver = ReceiverDriver.create(TempMessage.class);
receiver.setReceive().submit().block(MOTE_WAIT);
if (receiver.isDone()) {
TempMessage tmsg = receiver.getData();
sensorMap.put(tmsg.moteId, tmsg); // Save every sensor update, replacing previous one
log.log(Level.FINE,
String.format("Got update from sensor id: %s", tmsg.moteId));
}
}
} catch (
IOException ioExp) {
log.log(Level.SEVERE, "Severe error, response will be dropped", ioExp);
} finally {
try {
if (client != null)
client.disconnect();
} catch (
IOException ioExp) {
log.log(Level.SEVERE, "Error while closing sensor connection", ioExp);
}
}
}
/**
* Collect the statistics from all the motes
* @param args
*/
public static void main(
String[] args) throws
Throwable {
...
// Start capturing temperatures
Thread tempThread = new
Thread(new TemperatureService());
tempThread.start();
DatagramSocket socket;
TemperatureCoder coder = new TemperatureTextCoder();
socket = null;
try {
socket = new
DatagramSocket(port);
byte [] buffer = new byte[BUFFER_SIZE];
DatagramPacket packet = new
DatagramPacket(buffer, buffer.length);
ArrayList<SimpleMessage>messages = new ArrayList<SimpleMessage>();
while(true) { // Wait forever for new clients
try {
socket.receive(packet); // Get the packet and discard the contents
log.info(
String.format("Got a connection from: %s", packet.getAddress()));
// Send every available tick to the client in one shot
for (TempMessage tmsg : sensorMap.values()) {
SimpleMessage message = new SimpleMessage();
// Get the temperature for the sensor
double temp = tmsg.temperature.doubleValue(CELSIUS);
message.setCount(tmsg.count);
message.setMoteId(tmsg.moteId);
message.setTemperature(temp);
message.setUnits(SimpleMessage.Units.CELCIUS);
...
messages.add(message);
} // end for every sensor
// Send the whole pack to the client
buffer = coder.toWire(messages);
packet.setData(buffer, 0, buffer.length);
socket.send(packet);
...
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:
package com.kodegeek.blog.pervasive.perk;
...
/**
* Simple client that will get all the temperature readings from a TemperatureServer on the network.
* @author eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6a%6f%73%65%76%6e%7a%40%6b%6f%64%65%67%65%65%6b%2e%63%6f%6d%22%3e%6a%6f%73%65%76%6e%7a%40%6b%6f%64%65%67%65%65%6b%2e%63%6f%6d%3c%2f%61%3e%27%29%3b'))
*
*/
public final class TemperatureClient {
...
/**
* @param args host (args[0]), port number (arg[1])
* @throws Throwable On any fatal error
*/
public static void main(
String[] args) throws
Throwable {
...
log.log(Level.INFO, "Starting TemperatureClient");
TemperatureCoder coder = new TemperatureTextCoder();
DatagramSocket socket = new
DatagramSocket();
try {
InetAddress address =
InetAddress.getByName(host);
socket.connect(address, port);
log.info(
String.format("Contacting server on: %s:%s", address.getHostName(), port));
// Send empty packet
byte [] buffer = new byte[TemperatureService.BUFFER_SIZE];
DatagramPacket packet = new
DatagramPacket(buffer, buffer.length);
int attempt = 1;
while(true) {
try {
socket.setSoTimeout(SOCKET_TIMEOUT);
socket.send(packet); // Send empty packet
socket.receive(packet); // Get the temperature reading
buffer = new byte [packet.getLength()];
System.arraycopy(packet.getData(), packet.getOffset(), buffer, 0, packet.getLength());
for (SimpleMessage messg: coder.fromWire(buffer)) {
log.log(Level.INFO,
String.format(
"Mote ID: %d, Count: %d, Temperature: %.5f C\n",
messg.getMoteId(),
messg.getCount(),
messg.getTemperature()));
}
packet.setLength(TemperatureService.BUFFER_SIZE);
socket.setSoTimeout(0);
Thread.sleep(SLEEP_TIME);
...
}
}
} catch (
IOException ioexp) {
log.log(Level.SEVERE, "There was a problem", ioexp);
} finally {
socket.close();
}
}
}
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 :)
You have already tagged this post. Your tags: