ThiefWatcher, un sistema casero de vigilancia en interiores II
En este segundo artículo de la serie sobre el sistema casero de vídeo vigilancia ThiefWatcher, voy a explicar los diferentes protocolos que utiliza la aplicación para comunicarse con sus diferentes componentes, que pueden ser sustituidos por otros nuevos diferentes permitiendo un gran número de combinaciones. Existe un protocolo para comunicarse con la cámara, otro para disparar el sistema, otro más para avisar al usuario de forma remota y, por último, un protocolo para el intercambio de fotografías y mensajes para gestionar el servidor desde los equipos cliente.
Para empezar la serie por el principio, en este enlace tienes el primer artículo de la serie sobre el sistema de vigilancia ThiefWatcher.
En este enlace podéis descargar el código fuente de la solución ThiefWatcher, escrita en CSharp con Visual Studio 2015.
Voy a seguir el orden del esquema general de la aplicación para explicar los diferentes protocolos:
Todos ellos se encuentran definidos en la librería de clases WatcherCommons, en el espacio de nombres Interfaces. En la aplicación, los protocolos registrados se encuentran listados en el archivo de configuración, en la sección protocolsSection:
<protocolsSection>
<protocols>
<protocolData name="Arduino Simple Trigger"
class="trigger"
type="ArduinoSimpleTriggerProtocol.ArduinoTrigger,
ArduinoSimpleTriggerProtocol, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null" />
<protocolData name="Lync Notifications"
class="alarm"
type="LyncProtocol.LyncAlarmChannel,
LyncProtocol, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null" />
<protocolData name="AT Modem Notifications"
class="alarm"
type="ATModemProtocol.ATModemAlarmChannel,
ATModemProtocol, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null" />
<protocolData name="Azure Blob Storage"
class="storage"
type="AzureBlobProtocol.AzureBlobManager,
AzureBlobProtocol, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null" />
<protocolData name="NetWave IP camera"
class="camera"
type="NetWaveProtocol.NetWaveCamera,
NetWaveProtocol, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null" />
<protocolData name="VAPIX IP Camera"
class="camera"
type="VAPIXProtocol.VAPIXCamera,
VAPIXProtocol, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null" />
<protocolData name="DropBox Storage"
class="storage"
type="DropBoxProtocol.DropBoxStorage,
DropBoxProtocol, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null" />
</protocols>
</protocolsSection>
Desde el programa, se pueden instalar nuevos protocolos con la opción de menú Archivo / Instalar protocolo/s…, una misma librería de clases puede contener varios protocolos, yo he preferido separar cada protocolo en una librería diferente por claridad. El uso de los protocolos se especifica con el atributo class, que puede tomar los valores trigger, alarm, camera o storage.
Protocolo de disparo
En mi caso, el sistema comienza a funcionar en modo de vigilancia cuando un sensor detecta la presencia de un intruso, activando un relé que a su vez activa una placa Arduino, que transmite una señal a través del puerto serie para que el proceso de fondo del sistema dispare el modo de vigilancia activa.
El detector de presencia es un simple interruptor que se cierra cuando detecta movimiento en sus cercanías. Normalmente, estos interruptores funcionan con corriente de 220V, pues están pensados para activar aparatos eléctricos normales, como pueden ser sistemas de iluminación, con lo que no puedo activar directamente la placa Arduino.
En lugar de eso, y para aislar completamente el ordenador de la corriente de activación, voy a utilizar un relé para cerrar un circuito entre dos pines de la placa Arduino, el de la alimentación de 5V y un pin cualquiera de entrada. La bobina del relé funciona con alimentación de 12V, así que también necesito una fuente de alimentación que transforme los 220V de corriente alterna en 12V de corriente continua.
El esquema del montaje del relé es el siguiente:
A la izquierda está la entrada desde la fuente de alimentación, a la derecha las conexiones con la placa Arduino. PO puede estar conectado al pin de masa o a un pin de salida cualquiera en estado LOW (dependerá del conector que utilicéis, yo he usado un pin de salida porque el de masa queda muy alejado del de 5V en mi placa). PI es el pin de entrada, que se conecta al de 5V cuando el relé cierra el circuito.
Este es el programa que he utilizado para la placa Arduino:
int pin1 = 28;
int pin0 = 24;
void setup() {
// Initialize pins
pinMode(pin0, OUTPUT);
digitalWrite(pin0, LOW);
pinMode(pin1, INPUT);
digitalWrite(pin1, LOW);
Serial.begin(9600);
}
void loop() {
int val = digitalRead(pin1);
if (val == HIGH) {
Serial.write(1);
}
delay(1000);
}
En cuanto al protocolo, se implementa mediante el interfaz ITrigger, definido de la siguiente manera:
public interface ITrigger
{
event EventHandler OnTriggerFired;
string ConnectionString { get; set; }
void Initialize();
void Start();
void Stop();
}
- OnTriggerFired: Es el evento al que hay que suscribirse para que el protocolo avise al programa. El protocolo se ejecuta en su propia tarea separada, por lo que habrá que tener esto en cuenta cuando controlemos este evento.
- ConnectionString: es una cadena de texto con los parámetros de configuración.
- Initialize: Este método es llamado para realizar tareas de inicialización antes de poner en marcha el protocolo.
- Start: método para poner el protocolo en modo de escucha.
- Stop: método para detener la escucha.
La implementación de este protocolo se encuentra en la librería de clases ArduinoSimpleTriggerProtocol. La cadena de configuración tiene dos parámetros, port especifica el puerto serie para comunicarse con la placa Arduino, y baudrate se utiliza para especificar la velocidad del puerto. Por ejemplo:
port=COM4;baudrate=9600
Protocolo de control de cámaras
Una vez activado el sistema, entran en funcionamiento las cámaras que tengamos conectadas. El interfaz que se utiliza para controlarlas es IWatcherCamera, definido de la siguiente manera:
public class FrameEventArgs : EventArgs
{
public FrameEventArgs(Bitmap bmp)
{
Image = bmp;
}
public Bitmap Image { get; private set; }
}
public delegate void NewFrameEventHandler(object sender, FrameEventArgs e);
public interface IWatcherCamera
{
event NewFrameEventHandler OnNewFrame;
Size FrameSize { get; }
string ConnectionString { get; set; }
string UserName { get; set; }
string Password { get; set; }
string Uri { get; set; }
int MaxFPS { get; set; }
bool Status { get; }
ICameraSetupManager SetupManager { get; }
void Initialize();
void ShowCameraConfiguration(Form parent);
void Start();
void Close();
}
La clase FrameEventArgs se utiliza para pasar junto con el evento correspondiente a la obtención de una nueva imagen, dicha imagen en forma de un objeto Bitmap. Los miembros del interfaz son los siguientes:
- OnNewFrame: es el evento donde suscribirse para ser notificado cada vez que se obtiene una nueva imagen de la cámara. Hay que tener en cuenta que la cámara también utiliza su propia tarea para leer la cámara, por lo que tendremos que tener esto en cuenta en el controlador de este evento.
- FrameSize: Contiene las dimensiones de las imágenes según lo configurado por la cámara.
- ConnectionString: Cadena de texto con los parámetros para configurar la cámara.
- UserName: Nombre del usuario para acceder a la cámara.
- Password: Contraseña para el usuario de acceso a la cámara.
- Uri: Dirección de acceso a la cámara.
- MaxFPS: Número máximo de imágenes por segundo que queremos obtener.
- Status: Es true cuando la cámara está funcionando.
- SetupManager: Permite acceder al sistema de configuración de la cámara, para recibir eventos cuando se cambian determinados parámetros.
- Initialize: Se utiliza para realizar tareas de inicialización de la cámara.
- ShowCameraConfiguration: Muestra un cuadro de diálogo para configurar la cámara.
- Start: Pone en funcionamiento la cámara.
- Stop: Detiene la toma de imágenes.
He implementado dos protocolos de cámara diferentes, en el proyecto NetWaveProtocol se encuentra el protocolo NetWave, que está basado en el del artículo del enlace.
El otro protocolo está en el proyecto VAPIXProtocol, e implementa el Protocolo VAPIX, también basado en el artículo del enlace anterior.
Los dos protocolos utilizan la misma cadena de conexión, con tres parámetros diferentes, url, userName y password, como en este ejemplo:
url=http://192.168.1.20;userName=admin;password=123456
El interfaz ICameraSetupManager es muy simple:
public interface ICameraSetupManager
{
event EventHandler OnFrameSizeChanged;
void ReloadSettings();
}
Consiste únicamente en un evento al que suscribirse cuando cambiamos el tamaño de la imagen capturada (básicamente lo utilizo para redimensionar las ventanas de cámara), y el método ReloadSettings, usado para forzar la lectura de los valores de los parámetros almacenados en la cámara.
La configuración de las cámaras se almacena en la sección connectionStrings del archivo de configuración de la aplicación ThiefWatcher, y en una sección a medida, camerasSection:
<camerasSection>
<cameras>
<cameraData id="CAMNW"
protocolName="NetWave IP camera"
connectionStringName="CAMNW" />
<cameraData id="VAPIX"
protocolName="VAPIX IP Camera"
connectionStringName="VAPIX" />
</cameras>
</camerasSection>
Protocolo de alarma
Mediante este protocolo somos informados de cualquier eventualidad que el sistema pueda detectar. El interfaz que deben implementar este tipo de protocolos es IAlaramChannel, definido de la siguiente manera:
public interface IAlarmChannel
{
string ConnectionString { get; set; }
string MessageText { get; set; }
void Initialice();
void SendAlarm();
}
- ConnectionString, como siempre, es una cadena de texto con los parámetros de configuración.
- MessageText: es el texto de un mensaje que el protocolo puede enviar junto con el aviso, en caso de que el canal lo permita.
- Initialize: permite realizar labores de inicialización del protocolo.
- SendAlarm: ejecuta el aviso propiamente dicho. Este aviso se produce de forma síncrona.
El protocolo que yo he seleccionado para mi propio montaje está implementado en el proyecto ATModemProtocol y simplemente realiza una llamada a uno o más números de teléfono utilizando un modem AT conectado a un puerto serie del ordenador. La cadena de conexión tiene los siguientes parámetros:
- port: Identifica el puerto al que está conectado el modem.
- baudrate: velocidad en baudios del puerto.
- initdelay: retardo en milisegundos antes de enviar un comando AT.
- number: lista de números a llamar, separados por el carácter coma.
- ringduration: duración en milisegundos de la llamada antes de colgar.
Un ejemplo de cadena de conexión es la siguiente:
port=COM3;baudrate=9600;initdelay=2000;number=XXXXXXXXX;ringduration=20000
También he implementado otro ejemplo de protocolo que utiliza el cliente de Skype o Lync para realizar el aviso, en el proyecto LyncProtocol. Para utilizar este protocolo hay que tener el cliente Lync instalado tanto en el servidor como en el cliente, y la cadena de conexión es simplemente una lista de direcciones de Skype / Lync separadas por el carácter punto y coma.
Protocolo de almacenamiento
El último protocolo es el utilizado para transmitir las imágenes y los mensajes de control entre el servidor y los clientes. Las imágenes se envían en formato jpg, y los mensajes en formato JSON. Existen dos estructuras de datos para el intercambio de mensajes, definidas en el espacio de nombres Data del proyecto WatcherCommons. La primera de ellas se utiliza para enviar comandos desde los clientes al servidor:
[DataContract]
public class ControlCommand
{
public const int cmdGetCameraList = 1;
public const int cmdStopAlarm = 2;
public ControlCommand()
{
}
public static ControlCommand FromJSON(Stream s)
{
s.Position = 0;
StreamReader rdr = new StreamReader(s);
string str = rdr.ReadToEnd();
return JsonConvert.DeserializeObject<ControlCommand>(str);
}
public static void ToJSON(Stream s, ControlCommand cc)
{
s.Position = 0;
string js = JsonConvert.SerializeObject(cc);
StreamWriter wr = new StreamWriter(s);
wr.Write(js);
wr.Flush();
}
[DataMember]
public int Command { get; set; }
[DataMember]
public string ClientID { get; set; }
}
La clase ControlCommand representa dos comandos diferentes, según el valor de la propiedad Command. Cuando vale cmdGetCameraList (1) se utiliza para obtener una lista de cámaras, y cuando vale cmdStopAlarm (2) se utiliza para detener el estado de vigilancia y volver al modo de espera.
En la propiedad ClientID se indica un identificador único para registrar el cliente en la aplicación.
La otra clase de datos es CameraInfo, definida de la siguiente manera:
[DataContract]
public class CameraInfo
{
public CameraInfo()
{
}
public static List<CameraInfo> FromJSON(Stream s)
{
s.Position = 0;
StreamReader rdr = new StreamReader(s);
return JsonConvert.DeserializeObject<List<CameraInfo>>(rdr.ReadToEnd());
}
public static void ToJSON(Stream s, List<CameraInfo> ci)
{
s.Position = 0;
string js = JsonConvert.SerializeObject(ci);
StreamWriter wr = new StreamWriter(s);
wr.Write(js);
wr.Flush();
}
[DataMember]
public string ID { get; set; }
[DataMember]
public bool Active { get; set; }
[DataMember]
public bool Photo { get; set; }
[DataMember]
public int Width { get; set; }
[DataMember]
public int Height { get; set; }
[DataMember]
public string ClientID { get; set; }
}
Con esta estructura de datos se responde a los comandos indicando la lista de cámaras y su estado. También sirve para enviar peticiones a las cámaras, para activarlas, desactivarlas o tomar fotografías. Las propiedades tienen el siguiente uso:
- ID: es el identificador único de la cámara.
- Active: indica si la cámara está activa (true).
- Photo: cuando se trata de una petición de un cliente, se utiliza para indicar que la cámara debe tomar una fotografía.
- Width y Height: indican las dimensiones de las imágenes de la cámara.
- ClientID: se utiliza para identificar al cliente que realiza una petición.
El protocolo se implementa con el interfaz IStorageManager, definido de la siguiente manera:
public interface IStorageManager
{
string ConnsecionString { get; set; }
string ContainerPath { get; set; }
void UploadFile(string filename, Stream s);
void DownloadFile(string filename, Stream s);
void DeleteFile(string filename);
bool ExistsFile(string filename);
IEnumerable<string> ListFiles(string model);
IEnumerable<ControlCommand> GetCommands();
IEnumerable<List<CameraInfo>> GetRequests();
void SendResponse(List<CameraInfo> resp);
}
- ConnectionString: cadena de texto con los parámetros de configuración.
- ContainerPath: se utiliza para identificar el contenedor de archivos. Puede ser una ruta de carpeta, el nombre de un contenedor de blobs, etc.
- UploadFile: para subir un fichero al medio de almacenamiento. El fichero se encuentra en el Stream s, y se almacena con el nombre pasado en filename.
- DownloadFile: para descargar un archivo con nombre filename desde el almacenamiento al Stream s.
- DeleteFile: elimina el archivo con el nombre especificado.
- ExistsFile: comprueba si existe un archivo con el nombre especificado.
- ListFiles: devuelve una lista con los nombres de archivo que coinciden con el patrón indicado en el parámetro model.
- GetCommands: enumera los comandos enviados por los clientes.
- GetRequests: enumera las peticiones de los clientes a las cámaras.
- SendResponse: envía una respuesta a un comando o a una petición de un cliente.
He implementado el protocolo en el proyecto AzureBlobProtocol, que utiliza el almacenamiento en blobs de Azure. La cadena de conexión es la habitual cadena de conexión utilizada con este tipo de almacenamiento, y el ContainerPathn es el nombre de un contenedor de blobs.
Pero el protocolo que yo utilizo es más sencillo, está implementado en el proyecto DropBoxProtocol, y utiliza Dropbox para intercambiar los ficheros. En el lado del servidor no hace falta ninguna cadena de conexión, y en el ContainerPath se indica la carpeta de Dropbox que usaremos para realizar el intercambio de ficheros.
El protocolo funciona de la siguiente manera:
Un cliente se conecta con el sistema enviando un comando cmdGetCameraList, en formato de texto JSON en un fichero con nombre cmd_<ID del cliente>.json. El sistema registra este ID de cliente, elimina el fichero y responde con una lista de cámaras en formato JSON en un fichero con el nombre resp_<ID del cliente>.json. El cliente no puede escribir otro comando, ni el servidor otra respuesta, hasta que el anterior comando o respuesta hayan sido eliminados.
Una vez que se registra un cliente, la cámara empieza a guardar archivos jpg con la imagen actual. Solamente puede haber uno de estos archivos por cámara, con el nombre <ID de cámara>_FRAME_<ID de cliente>.jpg. Cada cliente toma el archivo que le corresponde, lo procesa, y lo elimina para que el servidor pueda escribir el siguiente.
Si el cliente quiere tomar una fotografía o activar o desactivar una cámara, envía un archivo en formato JSON con el nombre req_<ID del cliente>.json, conteniendo una estructura CameraInfo para indicar la petición. Cuando el servidor la procesa, la elimina y escribe un fichero resp_<ID del cliente>.json con el nuevo estado de la cámara para informar al cliente.
Las fotografías se guardan con el nombre <ID de la cámara>_PHOTO_yyyyMMddHHmmss.jpg.
Y esto es todo con respecto a los protocolos. En el siguiente artículo hablaré de la App cliente.