viernes, 1 de enero de 2016

Tres en raya

Después de más de un año sin publicar vuelvo a la carga con un montaje para jugar al tres en raya con arduino. Para ello usaré una pantalla LCD y un joystick. El esquema es el siguiente:




En él hemos usado una pantalla Nokia LCD 5110, un módulo joystick, 6 resistencias de 560 Ω y 6 de 1KΩ. Las resistencias bajan el voltaje de 5V a 3.3V, que es el que usa el módulo LCD. Las conexiones del módulo LCD, que no se aprecian en el esquema son las siguientes:

  • Cables rojos. Pines VCC y Led
  • Cable amarillo. Pin RST
  • Cable verde. Pin D/C
  • Cable azul. Pin DN(MOSI)
  • Cable naranja. Pin SCLK
  • Cables negros. Pines GND y SCE

El joystick tiene los siguientes pines:

  • GND. Conectado a tierra
  • SW. Nombrado como SEL en el esquema. Es un pin digital. Detecta cuándo se ha pulsado el botón del joystick. En el que he usado devuelve LOW cuando se pulsa y HIGH en posición normal.
  • VRx. Nombrado como Xout en el esquema. Es un pin analógico que devuelve un valor entre 0 y 1023 dependiendo de la posición del eje X del joystick.
  • VRy. Nombrado Yout en el esquema. Es un pin analógico que devuelve un valor entre 0 y 1023 dependiendo de la posición del eje Y del joystick.
  • +5V. Nombrado Vcc en el esquema. Es la alimentación del joystick.
El funcionamiento es muy sencillo. Con el joystick moveremos un cursor por la pantalla LCD y al pulsar el botón del joystick pondremos nuestra ficha en la casilla seleccionada. Inmediatamente el microcontrolador hará su jugada y nos devolverá el control para la siguiente.
Aquí está montado en la placa de prototipo:

Y el montaje en funcionamiento:





El código fuente del sketch se divide en dos ficheros:

  • types.h. Este fichero es necesario porque el compilador de arduino obliga a que los tipos de datos definidos por el usuario y las funciones que tienen como valor de retorno alguno de estos tipos estén en un fichero separado del principal.
enum cellState{empty,black,white}; //Estados de cada casilla del juego

struct boardGame{
  byte xCursor; //Coordenada x del cursor  byte yCursor; //Coordenada y del cursor
  cellState board[3][3]{{empty,empty,empty},{empty,empty,empty},{empty,empty,empty}}; //Tablero de juego
  byte emptyCells=9; //Numero de casillas sin fichas
  bool stopPlay=false; //Parar el juego
};


/*****************************************************************************/
/* Funcion winner. Devuelve el color de la jugada ganadora en el  */
/* estado actual del juego. Empty si nadie gana                             */
/*                                                                                                    */
/* Parametros:                                                                                */
/* boardGame b-->Tablero de juego                                                */
/*****************************************************************************/


cellState winner(boardGame b){
 
  for(byte i=0;i<3;++i){
    for(byte j=0;j<3;++j){
      if(b.board[i][j]!=empty){
       
        //horizontal
        bool tmpWin=true;
        for(byte x=1;x<3;++x){
          tmpWin=tmpWin && (b.board[x][j]==b.board[x-1][j]);
        }
        if(tmpWin){
          return b.board[i][j];
        }
        //vertical
       
        if(!tmpWin){
          tmpWin=true;
          for(byte y=1;y<3;++y){
            tmpWin=tmpWin && (b.board[i][y]==b.board[i][y-1]);
          }
        }
       
        if(tmpWin){
          return b.board[i][j];
        }
        //diagonalVID_20151226_131816
       
        if(!tmpWin && i==0 && j==0){
          tmpWin=true;
           for(byte x=1,y=1;x<3 && y<3;++x,++y){
             tmpWin=tmpWin && (b.board[x][y]==b.board[x-1][y-1]);
          }
        }
        if(tmpWin){
          return b.board[i][j];
        }
       
        if(!tmpWin && i==0 && j==2){
          tmpWin=true;
            for(byte x=1,y=1;x>0 && y<3;++x,--y){
              tmpWin=tmpWin && (b.board[x][y]==b.board[x-1][y+1]);
             
            }
        }
        if(tmpWin){
          return b.board[i][j];
        }
       }
     }
    }
    return empty;
}

     
  • 3EnRaya.ino
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
#include "types.h"


//LCD

const byte SCLKpin = 13;
const byte DNpin = 11;
const byte DCpin = 12;
const byte RESETpin = 10;


//Joystick

const int xAxis=A1;
const int yAxis=A0;
const int button=2;

const char contrast=0;
int fontWidth=6;
int fontHeigth=10;
int textSize=2;

boardGame board;

Adafruit_PCD8544 lcd(SCLKpin,DNpin,DCpin,RESETpin);

/******************************************************************/
/* Funcion evaluatePlay. Devuelve el numero de jugadas ganadoras  */
/* si se juega una ficha en una casilla determinada               */
/*                                                                */
/* Parametros:                                                    */
/* boardGame b-->Tablero de juego                                 */
/* short x-->Coordenada x de la casilla que vamos a evaluar       */
/* short y-->Coordenada y de la casilla que vamos a evaluar       */
/* cellState c-->Color de la ficha con la que se pretende jugar   */
/******************************************************************/


short evaluatePlay(boardGame b,short x,short y,cellState c){
 
  short winCount=0;
  short cellCount=0;
  //Horizontal
  for(byte j=0;j<3;++j){
    if(b.board[x][j]==c || b.board[x][j]==empty){
      ++cellCount;
    }
  }
  if(cellCount==3){
    ++winCount;
  }
  cellCount=0;
  //Vertical
  for(byte i=0;i<3;++i){
    if(b.board[i][y]==c || b.board[i][y]==empty){
      ++cellCount;
    }
  }
  if(cellCount==3){
    ++winCount;
  }
  cellCount=0;
  // diagonal
  if(x==y){
    for(byte i=0,j=0;i<3 && j<3;++i,++j){
       if(b.board[i][j]==c || b.board[i][j]==empty){
         ++cellCount;
        }
     }
     if(cellCount==3){
       ++winCount;
     }
  }
  cellCount=0;
  if(x==y || abs(x-y)==2){
    for(byte i=0,j=0;i>0 && j<3;++i,--j){
       if(b.board[i][j]==c || b.board[i][j]==empty){
         ++cellCount;
       }
    }
    if(cellCount==3){
       ++winCount;
     }
  }
  return winCount;
}

/******************************************************************/
/* Funcion drawWinnerPlay. Dibuja una linea sobre la jugada       */
/* ganadora                                                       */
/*                                                                */
/* Parametros:                                                    */
/* boardGame b-->Tablero de juego                                 */
/******************************************************************/


void drawWinnerPlay(boardGame b){
 
  for(byte i=0;i<3;++i){
    for(byte j=0;j<3;++j){
      if(b.board[i][j]!=empty){
       
        //horizontal
        bool tmpWin=true;
        for(byte x=1;x<3;++x){
          tmpWin=tmpWin && (b.board[x][j]==b.board[x-1][j]);
        }
        if(tmpWin){
         
          lcd.drawLine(0, ((j+1)*16)-8, 84, ((j+1)*16)-8, BLACK);
        }
        //vertical
       
        if(!tmpWin){
          tmpWin=true;
          for(byte y=1;y<3;++y){
            tmpWin=tmpWin && (b.board[i][y]==b.board[i][y-1]);
          }
          if(tmpWin){
           
            lcd.drawLine(((i+1)*28)-14, 0, ((i+1)*28)-14, 84, BLACK);
          }
        }
       
       
        //diagonal
       
        if(!tmpWin && i==0 && j==0){
          tmpWin=true;
           for(byte x=1,y=1;x<3 && y<3;++x,++y){
             tmpWin=tmpWin && (b.board[x][y]==b.board[x-1][y-1]);
          }
          if(tmpWin){
           
            lcd.drawLine(0, 0, 84, 84, BLACK);
          }
        }
       
       
        if(!tmpWin && i==0 && j==2){
          tmpWin=true;
            for(byte x=1,y=1;x>0 && y<3;++x,--y){
              tmpWin=tmpWin && (b.board[x][y]==b.board[x-1][y+1]);
             
            }
           if(tmpWin){
            
              lcd.drawLine(0, 47, 84, 0, BLACK);
          }
        }
       
       }
     }
    }
}

/******************************************************************/
/* Funcion play. Ejecuta la jugada de la maquina                  */
/*                                                                */
/******************************************************************/


void play(){
  byte x;
  byte y;
  short maxEvaluation=-32768;
  short currentEvaluation=0;
  bool stopIteration=false;
  //Compruebo si alguna jugada me da la victoria
  for(byte i=1;i<4 && !stopIteration;++i){
    cellState *statesArray=board.board[i-1];
    for(byte j=1;j<4 && !stopIteration;++j){
     
      if(board.board[i-1][j-1]==empty){
       
        statesArray[j-1]=white;
        if(winner(board)==white){
         
          stopIteration=true;
          board.stopPlay=true;
        }
        else{
          statesArray[j-1]=empty;
        }
       }
      }
    }
    //Compruebo si alguna jugada da la victoria al oponente
    for(byte i=1;i<4 && !stopIteration;++i){
      cellState *statesArray=board.board[i-1];
      for(byte j=1;j<4 && !stopIteration;++j){
     
        if(board.board[i-1][j-1]==empty){
         
          statesArray[j-1]=black;
          if(winner(board)==black){
           
            statesArray[j-1]=white;
            stopIteration=true;
          }
          else{
            statesArray[j-1]=empty;
          }
         }
        }
      }
   
    if(!stopIteration){
      for(byte i=1;i<4;++i){
        for(byte j=1;j<4;++j){
          if(board.board[i-1][j-1]==empty){
           
            currentEvaluation=evaluatePlay(board,i-1,j-1,white);
           
            if(currentEvaluation>maxEvaluation){
              maxEvaluation=currentEvaluation;
              x=i;
              y=j;
            }
           }
          }
        }
       
        cellState *statesArray=board.board[x-1];
        statesArray[y-1]=white;
      }
     
  }

/******************************************************************/
/* Funcion drawTile. Dibuja una ficha en una posicion             */
/*                                                                */
/* Parametros:                                                    */
/* byte x-->Coordenada x de la casilla donde vamos a jugar        */
/* byte y-->Coordenada y de la casilla donde vamos a jugar        */
/* int color-->Color de la ficha con la que se pretende jugar     */
/******************************************************************/


void drawTile(byte x,byte y,int color){
  lcd.drawCircle((x*28)-14, (y*16)-8, 5,BLACK);
  if(color==BLACK){
    lcd.fillCircle((x*28)-14, (y*16)-8,5,color);
  }
}

/******************************************************************/
/* Funcion drawCross. Dibuja el cursor en una posicion            */
/*                                                                */
/* Parametros:                                                    */
/* byte x-->Coordenada x de la casilla donde vamos a jugar        */
/* byte y-->Coordenada y de la casilla donde vamos a jugar        */
/* int color-->Color de la ficha con la que se pretende jugar     */
/******************************************************************/


void drawCross(byte x,byte y,int color){
  lcd.drawFastVLine((x*28)-14,(y*16)-13, 10, color);
  lcd.drawFastHLine((x*28)-19, (y*16)-8, 10, color);
}

/******************************************************************/
/* Funcion drawGameScreen. Dibuja el tablero de juego             */
/*                                                                */
/******************************************************************/


void drawGameScreen(){
 
  lcd.drawRoundRect(0, 0, 84, 48,10,BLACK);
  lcd.drawLine(28, 0, 28, 84, BLACK);
  lcd.drawLine(56, 0, 56, 84, BLACK);
  lcd.drawLine(0, 16, 84, 16, BLACK);
  lcd.drawLine(0, 32, 84, 32, BLACK);
  lcd.drawLine(0, 32, 84, 32, BLACK);
 
  for(byte i=1;i<4;++i){
    cellState *statesArray=board.board[i-1];
    for(byte j=1;j<4;++j){
      if(statesArray[j-1]!=empty){
        drawTile(i,j,(statesArray[j-1]==black)?BLACK:WHITE);
      }
    }
   }
  
   cellState *statesArray=board.board[board.xCursor-1];
   drawCross(board.xCursor,board.yCursor,(statesArray[board.yCursor-1]==empty || statesArray[board.yCursor-1]==white)?BLACK:WHITE);
   drawWinnerPlay(board);
}



void setup(){            
  Serial.begin(115200) ;
 
  lcd.begin();
  lcd.clearDisplay();
  lcd.setTextColor(BLACK);
  lcd.setTextSize(textSize);
  board.xCursor=2;
  board.yCursor=2;
  drawGameScreen();
  lcd.display();
  pinMode(button,INPUT_PULLUP);
}

void loop(){ 
 
    if(!board.stopPlay){
      bool redrawScreen=false;
     
      if(analogRead(xAxis)==1023 && board.xCursor>1){
        //Se ha movido el joystick a la izquierda 
        board.xCursor--;
        redrawScreen=true;
      }
     else if(analogRead(xAxis)==0 && board.xCursor<3){
       //Se ha movido el joystick a la derecha
       board.xCursor++;
       redrawScreen=true;
     
     }
     else if(analogRead(yAxis)==1023 && board.yCursor<3){
       //Se ha movido el joystick hacia arriba
      board.yCursor++;
      redrawScreen=true;
     }
     else if(analogRead(yAxis)==0 && board.yCursor>1){
       //Se ha movido el joystick hacia abajo
      board.yCursor--;
      redrawScreen=true;
     }
     else if(!digitalRead(button)){
       //Se ha pulsado el boton
       cellState *statesArray=board.board[board.xCursor-1];
       if(statesArray[board.yCursor-1]==empty){
         statesArray[board.yCursor-1]=black;
         board.emptyCells--;
         if(board.emptyCells>0){
           play();
         }
         else{
           board.stopPlay=true;
         }
         redrawScreen=true;
       }
     }
  
     if(redrawScreen){
       lcd.clearDisplay();
        drawGameScreen();
        lcd.display();
        delay(500);
     
     }
    }
   
  }

     

lunes, 13 de octubre de 2014

Detectando intrusos

Hace unos días que me ha llegado la última remesa de juguetes para cacharrear con Arduino. Entre ellos hay un sensor de movimiento que ya hace tiempo que me apetecía probar. Es un sensor PIR que son las siglas de Passive Infrared, o lo que es lo mismo un detector de infrarrojos. El dispositivo detecta las variaciones de luz infrarroja y altera el voltaje del pin de salida cuando se produce. El resultado es que podemos detectar la presencia humana, o de cualquier ser vivo, ya que la radiación infrarroja no es más que calor. Al pasar por delante del sensor alteramos la radiación infrarroja detectada y salta la alarma. Vamos a empezar con un montaje básico para irlo complicando en sucesivas entradas hasta construir un sistema de alarma que nos envíe un aviso al móvil cuando detecte movimiento. Para el montaje de hoy vamos a usar un led, un piezo y el sensor PIR. El esquema es el siguiente:



Y aquí el esquema montado en la placa de prototipo



El funcionamiento del circuito es muy simple, cuando detecte movimiento encenderá el LED y el piezo emitirá un sonido. Aquí tenemos una imagen del PIR visto por la parte de abajo:


Como podemos ver, la información sobre la función de cada pin brilla por su ausencia. En este caso en concreto, el pin de la izquierda es GND, el del centro es el de datos y el de la derecha es VCC. Tenemos además los jumpers de la parte inferior izquierda que sirven para determinar cómo responderá el pin de datos si el evento de detección de movimiento se repite. Se puede configurar de modo que el pin de datos esté siempre en alto cuando detecte movimiento constantemente, o para que al detectar movimiento devuelva un pulso a HIGH y después vuelva a LOW, de forma que al detectar movimiento repetidamente devuelva una secuencia de unos y ceros. El de la foto está configurado del segundo modo. Para que esté siempre en alto solo hay que cambiar el jumper para que abarque los dos pines de arriba. Y por último, los dos tornillos anaranjados de la parte inferior sirven para modificar la distancia a la que detectará la variación de infrarrojos, es decir, lo cerca que tienes que estar para que te detecte, y el tiempo de la variación.
Y a continuación el sketch:

const byte sensorPin=2;
const byte ledPin=4;
const byte piezoPin=8;

const char contrast=57;

void setup(){
  Serial.begin(115200);
  pinMode(sensorPin, INPUT);
  pinMode(ledPin, OUTPUT);
  pinMode(piezoPin, OUTPUT);
  Serial.println("Inicializacion OK");
}

void loop(){
    if(digitalRead(sensorPin)){
      digitalWrite(ledPin,HIGH);
      tone(piezoPin,3000,10);
    }
    else{
      digitalWrite(ledPin,LOW);
    }
    delay(250);
     


El código es muy sencillo. Simplemente consulta el estado del sensor cada 250 milisegundos y enciende el led y hace sonar el piezo si se detecta movimiento. En caso contrario se apaga el led.


jueves, 6 de febrero de 2014

Mudanza

Recientemente he mudado mi estudio desde mi antigua habitación en casa de mis padres en la que me alojaba provisionalmente a la que va a ser mi nueva casa cuando la reforme. Echaba de menos tener una mesa grande en un comedor amplio con luz natural para trabajar en mis cosas. Y ya puestos he resuelto un problema que hace ya un tiempo que me atormentaba. Como la estancia en casa de mis padres iba a ser temporal y mi hermana vive a dos casas, decidí no contratar una conexión a internet y usar su wifi. Cuando me da la clave ya me llevo la primera sorpresa. Su wifi está cifrada con WEP. En mi antigua casa mi portátil tardó once segundos en encontrar la clave de un vecino que también usaba WEP. Si mis datos van a viajar por su red más vale que tenga algo más de seguridad, así que cambio la clave, le pongo cifrado WPA2 y desactivo el WPS que también es un colador. Voy a mi habitación y conecto el portátil, llega poca señal pero no importa para mis quehaceres habituales no necesito más que unos pocos KB de ancho de banda. Los próximos días son un suplicio de cortes de conexión. Me desespero, cambio la wifi de canal, sigue habiendo cortes, whatsapp a mi hermana: Reinicia el router. Funciona durante un rato. Tengo que hacer algo. La solución lógica (y fácil) es instalar un repetidor wifi. Pero esta solución tiene varios problemas:

  1. Es fácil, y a un servidor en cuestiones de tecnología no le suelen gustar las cosas fáciles.
  2. Tengo en casa todo lo necesario para resolverlo y por lo tanto me parece un gasto inútil comprar un nuevo aparato teniendo cacharros viejos que aún funcionan y que te pueden solucionar la papeleta.
  3. No es divertido.
Así que me pongo manos a la obra. La idea es tener un router más cerca de mi habitación conectado con el de mi hermana para que mi portátil tenga una señal más fuerte. El router que tenía en mi antigua casa puede servir, ya que tiene la opción de funcionar en modo bridge. El problema es que el de mi hermana no. Por suerte tengo una Raspberry. La Raspberry es un mini ordenador poco potente y de bajo consumo, ya que tiene arquitectura ARM (la misma que los móviles) y al no tener partes móviles (ventilador, disco duro, etc) y una baja frecuencia de procesador (700 MHz)  necesita muy poca energía para funcionar. Esto lo hace ideal para tenerlo funcionando 24 horas al día como puente entre los dos routers. En mi caso también como lo tengo como servidor de descargas bittorrent y emule y como sistema de videovigilancia con una webcam, pero esto ya os lo cuento otro día.
El montaje definitivo es el siguiente:


La Raspberry usa como sistema operativo Raspbian, una versión modificada de Debian para arquitectura ARM. Para conectarla a la wifi usaremos un adaptador USB que tenía muerto de risa en casa. Lo primero que tenemos que hacer es conectarlo al puerto USB y averiguar qué dispositivo es. Para ello usaremos el comando lsusb que nos mostrará todos los dispositivos USB conectados:

Bus 001 Device 006: ID 0ace:1215 ZyDAS ZD1211B 802.11g
Bus 001 Device 005: ID 046d:082b Logitech, Inc.
Bus 001 Device 004: ID 1a40:0101 TERMINUS TECHNOLOGY INC. USB-2.0 4-Port HUB
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Nuestro adaptados wifi es un Zydas. Lo buscamos en el repositorio de paquetes con el comando:

 apt-cache search zydas

que nos devuelve un único paquete que contiene el firmware de nuestro adaptador:

zd1211-firmware - Firmware images for the zd1211rw wireless driver

Lo instalamos con el comando:

sudo apt-get install zd1211-firmware

Una vez instalado tenemos que cargar el módulo zd1211rw con el comando

modprobe zd1211rw

Y para comprobar si se ha cargado usaremos el comando

lsmod|grep zd1211rw

que si todo es correcto dará la siguiente salida que nos indica que el módulo se ha cargado en el kernel

zd1211rw               55298  0
mac80211              273413  1 zd1211rw
cfg80211              184163  2 zd1211rw,mac80211

Para que cargue el módulo automáticamente en el arranque tenemos que añadir esta línea al fichero /etc/modules

zd1211rw

Una vez tenemos el módulo cargado tenemos que configurarlo para que conecte a la wifi del router 2. Para ello necesitamos instalar el paquete wpasupplicant:

apt-get install wpasupplicant

y por último crear el archivo de configuración, que en mi caso es /etc/wpasupplicant.conf, con el siguiente contenido:

network={
        ssid="Nombre de la red wifi"
        scan_ssid=1
        psk="Clave de la red wifi"
}

Ahora ya tenemos lista nuestra Raspberry para conectar con los comandos

wpa_supplicant -B -iwlan0 -c/etc/wpa_supplicant.conf
dhclient wlan0

El siguiente paso es convertir nuestra Raspberry en un router, es decir que acepte peticiones por la tarjeta de red y las reenvíe a internet por el adaptador wifi. El router 2 le ha dado a nuestro adaptador la dirección 192.168.0.250. Siempre le va a dar la misma porque la tengo reservada en el router ya que entro por ssh desde fuera de la red. La tarjeta de red está configurada con la dirección 192.168.2.4 como se puede ver en el fichero /etc/network/interfaces

iface eth0 inet static
address 192.168.2.4
netmask 255.255.255.0
broadcast 192.168.2.255
network 192.168.2.0
dns-nameservers 8.8.8.8

Con lo cual el router 1 y los equipos conectados a él deben estar en ese rango de IP y tener como gateway la dirección 192.168.2.4, que es la de la tarjeta de red de la Raspberry. Un detalle importante es que el router 1 tiene desactivado el servidor DHCP, ya que si no se asignaría a sí mismo como gateway en lugar de a la Raspberry. Esto implica asignar las IP de los equipos conectados al router 1 como estáticas, lo cual otrorga un poco más de seguridad a nuestra wifi ya que un posible intruso debería conocer el rango de IP que tenemos asignado. Y por último tenemos que escribir las reglas de filtrado para que redirija el tráfico desde nuestra red hacia internet. Lo haríamos con el siguiente script que se ejecutaría al inicio:

iptables -F
iptables -t nat -F

iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT

export LAN=eth0
export WAN=wlan0

iptables -I INPUT 1 -i ${LAN} -j ACCEPT
iptables -I INPUT 1 -i lo -j ACCEPT
iptables -A INPUT -p UDP --dport bootps ! -i ${LAN} -j REJECT
iptables -A INPUT -p UDP --dport domain ! -i ${LAN} -j REJECT
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

iptables -A INPUT -p TCP --dport 2222 -i ${WAN} -j ACCEPT

iptables -A INPUT -p TCP ! -i ${LAN} -d 0/0 --dport 0:1023 -j DROP
iptables -A INPUT -p UDP ! -i ${LAN} -d 0/0 --dport 0:1023 -j DROP

iptables -A FORWARD -i ${LAN} -s 192.168.2.0/255.255.255.0 -j ACCEPT
iptables -A FORWARD -i ${WAN} -d 192.168.2.0/255.255.255.0 -j ACCEPT
iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -t nat -A POSTROUTING -o ${WAN} -j MASQUERADE


echo 1 > /proc/sys/net/ipv4/ip_forward
for f in /proc/sys/net/ipv4/conf/*/rp_filter ; do echo 1 > $f ; done

Con este script ya tenemos configurada la Raspberry para funcionar como un router. Ahora solo tenemos que conectar los equipos al router 1 (tanto por wifi como por cable) y asignarles una dirección en la red 192.168.2.0/24 y la puerta de enlace 192.168.2.4.

Y ahora es cuando viene la parte divertida, es decir, la de resolver los problemas. El primero de ellos son las desconexiones de la wifi. Nuestra Raspberry es un cacharro poco potente que no lleva muy bien alimentar dispositivos por USB. La mía tiene conectado, además del adaptador wifi, una webcam. Aunque los tengo conectados mediante un hub USB con alimentación externa, la Raspberry no lo soporta bien y la wifi tiene desconexiones y en algunos casos de mucho tráfico puede fallar toda la red (incluida la ethernet). Como no tiene ningún dispositivo de entrada/salida conectado (ni teclado, ni ratón ni pantalla), la única forma de ver qué le pasa es entrando por ssh, lo cual no es posible si lo que nos está fallando es precisamente la red. Además está el pequeño detalle de que la conexión voy a usarla desde otro edificio y no me apetece desplazarme para reiniciar la raspberry. Así que tenemos que conseguir que nuestra Raspberry sea autónoma en este sentido, es decir, que se resuelva ella solita sus problemas. El problema de las desconexiones lo he resuelto con el siguiente script, como siempre escrito en ruby, mi lenguaje favorito.


Este script lo que hace es comprobar si hay conexión a internet intentando cargar google.es. También puede hacerse haciendo ping al gateway. Ambas formas están en el script (la del ping está comentada). Si no hay conexión a internet intenta reconectar a la wifi. Si después de diez intentos no lo consigue reinicia la Raspberry y envía un mail de notificación.  Antes de lanzarlo por primera vez tenemos que crear los ficheros  /usr/local/data/check_connection.info, que contiene la fecha y hora de la última comprobación, y /usr/local/data/count.txt, que contiene el número de reintentos fallidos de reconexión. Este último debemos crearlo con una sola línea cuyo valor tiene que ser 0. El script acepta los siguientes parámetros

check_connection.rb fichero_configuracion_mail mail TLS intervalo

El primer parámetro es el fichero de configuración de la cuenta de mail que usaremos para la notificación. Debe contener las siguientes líneas:

servidor_smtp
puerto_smtp
usuario_smtp
clave_smtp

El segundo parámetro es el destinatario del mail. El tercero es 0 ó 1 según vayamos a usar autenticación segura o no y el último es el número de segundos entre los reintentos de enviar el mail. Está programado para tres reintentos.

Con esto nuestra Raspberry es capaz de solucionarse las desconexiones. Pero el caso de la saturación de la red es curioso porque no se puede entrar por ssh, pero según parece sigue teniendo conexión a internet y en algunos casos incluso sigue ejerciendo de router correctamente (en otros no) con lo cual es complicado saber realmente cuándo está fallando de forma automática. Para resolverlo he programado otro script:


La idea es sencilla. Nos bajaremos periódicamente un fichero que contiene comandos que ejecutará la Raspberry. De esta forma accediendo al fichero con el móvil podremos añadir al fichero el comando sudo reboot que la reiniciará restaurándose así todo el sistema de red. El script necesita los ficheros /usr/local/data/remote_commands.info, que contiene la fecha y hora de la última ejecución de comandos. El formato de fecha es AAAAMMDDHHMMSS, como por ejemplo 20140206120010 para el 6 de febredo de 2014 a las 12:00:10, que es la fecha de mi última ejecución. Debemos crearlo con una fecha válida y anterior a hoy. El otro fichero es el fichero de comandos /usr/local/data/command.txt. Este fichero es el que nos descargaremos de internet (yo por ejemplo uso dropbox) y contiene la fecha en formato AAAAMMDDHHMMSS y el comando a ejecutar separados por un tabulador. Por ejemplo

20140206113100  lsusb
20140206113100  sudo reboot

El script compara la fecha de cada linea de este fichero con la de remote_commands.info y si es posterior ejecuta los comandos y envía el resultado por mail. El script acepta los siguientes parámetros

remote_commands.rb fichero_configuracion_mail mail TLS intervalo https://link_to_command.txt command.txt 

Los cuatro primeros parámetros son los mismos que en el script anterior y los dos últimos son el enlace al fichero de comandos y su nombre. El fichero de comandos será guardado en /usr/local/data.

Por último ambos scripts necesitan el fichero script_lib.rb en /usr/local/bin. Y con esto ya tenemos nuestro repetidor wifi autosuficiente DIY. Ahora solo nos queda lanzar los dos scripts periódicamente. Para ello modificaremos la el fichero /etc/crontab para añadirle estas dos líneas

0,15,30,45 *    * * *   Usuario /usr/local/bin/remote_commands.rb /usr/local/data/script/config mail@mail.com 1 15 https://link_to_command.txt command.txt
*  *    * * *   root    /usr/local/bin/check_connection.rb /usr/local/data/script/config mail@mail.com 1 15

La primera ejecuta los comandos remotos cada 15 minutos y la segunda comprueba la conexión cada minuto.


lunes, 30 de diciembre de 2013

Nivelando superficies con un acelerómetro

Cómo funciona un acelerómetro

Un acelerómetro, como su nombre indica, mide la aceleración a la que está sometido. Si lo movemos, nos medirá no sólo la aceleración, sino hacia dónde acelera. Para ello nuestro acelerómetro tiene una entrada para cada dimensión del espacio y nos devolverá los tres componentes del vector aceleración. Pero no solo es capaz de medir la aceleración, sino que en reposo mide la componente de la gravedad que soporta cada uno de sus ejes. Si lo conectamos y lo ponemos sobre una mesa en su posición normal veremos cómo las entradas de los ejes x e y tienen más o menos el mismo valor, pero la del eje z no. Esto es porque está perpendicular al centro de la tierra y por lo tanto la atracción gravitatoria recae enteramente sobre el eje z.



Si lo inclinamos un poco z disminuirá al repartirse la gravedad entre los tres ejes.



Así pues, la superficie sobre la que se apoye el acelerómetro estará nivelada cuando el eje perpendicular a ella soporte toda la fuerza de la gravedad si estamos nivelando en horizontal y ninguna fuerza si estamos nivelando en vertical. Un detalle importante es que el acelerómetro puede detectar la gravedad pero no la orientación, es decir, que si lo rotamos alrededor de uno de los ejes no se enterará. Para detectar el giro necesitaríamos un giróscopo . Este detalle es importante a la hora de mostrar en la pantalla LCD la inclinación que tiene el acelerómetro, ya que nos obliga a orientar correctamente los ejes para realizar el nivelado de superficies verticales.

El acelerómetro

Para el circuito vamos a usar un acelerómetro GY-61. Como podemos ver en la imagen tiene dibujados los ejes para que podamos orientarlo. Para que el sketch funcione correctamente lo tenemos que orientar de forma que el eje Z quede perpendicular a la placa de prototipo. El acelerómetro funciona a 3.3V, por lo que tenemos que ir con cuidado de no conectarlo al pin de 5V de Arduino sino al 3.3V ya que lo podemos quemar.



La pantalla LCD

Para mostrar la orientación del acelerómetro vamos a usar una pantalla Nokia LCD 5110. Supuestamente es la que llevan varios modelos de móviles Nokia, así que, al menos en teoría, los podríamos desmontar y reutilizar la pantalla. En mi caso la he comprado, ya que tampoco tienen un precio excesivo. Para programarla usaremos la biblioteca de arduino desarrollada por Adafruit. La podemos descargar aquí. También tenemos que descargar la biblioteca GFX también desarrollada por Adafruit.
La pantalla LCD también funciona a 3.3V y por lo tanto también tendremos que conectarla al pin 3.3V de arduino. Además tendremos que usar resistencias para bajar el voltaje que le llega desde los pines digitales, ya que su voltaje de salida es 5V. En la conexión del acelerómtro no las necesitamos porque únicamente vamos a leer, pero en los pines del LCD vamos a escribir y por lo tanto les tenemos que enviar 3.3V en lugar de 5V, que es lo que sale de los pines del arduino. El esquema de conexión es el siguiente:

  
El pin LED del LCD es el que se encarga de iluminar la pantalla. Lo hemos conectado a corriente para que esté siempre iluminada. Si quisiéramos encenderla mediante programación tendríamos que conectarla a uno de los pines digitales de arduino y encenderla o apagarla con la función analogWrite poniendo el pin a HIGH o LOW. El resto de pines digitales son los estándares del protocolo SPI que es el que usa la biblioteca de Adafruit. Un detalle importante es que conectamos el pin VCC y LED directamente a corrienta porque todo el circuito está funcionando a 3.3V. Si conectamos el LCD a un circuito cuyos componentes funcionen a 5V deberemos ponerles una resistencia de 10kΏ o correremos el riesgo de quemar el LCD. Aquí lo tenemos montado en la placa de prototipo



El sketch

El sketch va a dibujar en el LCD dos círculos, uno señalará el punto en el que la superficie está nivelada y el otro se moverá conforme vayamos inclinando el acelerómetro. La superficie estará nivelada cuando el segundo círculo esté dentro del primero.
Antes de poder usar el acelerómetro tenemos que calibrarlo para averiguar los valores para 1g, 0g y -1g. Para ello lo colocaremos sobre una superficie horizontal y anotaremos los valores para el eje Z, que será 1g, y el valor para X o Y que serán 0g. Para averiguar el -1g pondremos el acelerómetro al revés y lo rotamos hacia ambos lados. El menor valor que tome será -1g. Las constantes que guardan estos valores son:

const int negativegValue=270;
const int zerogValue=350;
const int onegValue=424;

El código fuente completo:

#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>

const int xPin = 0;
const int yPin = 1;
const int zPin = 2;

const byte SCLKpin = 13;
const byte DNpin = 11;
const byte DCpin = 12; 
const byte RESETpin = 10;

const byte ballRadius=5;
const byte errorMargin=1;
int xReadOld=0;
int yReadOld=0;
int zReadOld=0;

const int negativegValue=270;
const int zerogValue=350;
const int onegValue=424;
char contrast=57;
char buffer[10];

Adafruit_PCD8544 lcd(SCLKpin,DNpin,DCpin,RESETpin);

void setup(){
  Serial.begin(115200); 
  lcd.begin();
  lcd.setContrast(contrast);
}


void loop(){

  int xRead = analogRead(xPin);
  int yRead = analogRead(yPin);
  int zRead = analogRead(zPin);
  int currentX;
  int currentY;
  /* Elegimos la orientacion que estamos nivelando por el valor del eje z que siempre sera perpendicular a la superficie
  Cambiaremos segun su valor este mas cerca de cero o de 1g*/
  if(zRead<(zerogValue+((onegValue-zerogValue)/2))){
    //Orientacion vertical
    //currentX=lcd.width()/2;
    currentX=map(zerogValue,negativegValue,onegValue,0,lcd.width());
    currentY=map(zRead,negativegValue,onegValue,0,lcd.height());
  }
  else{
    currentX=map(xRead,negativegValue,onegValue,0,lcd.width());
    currentY=map(yRead,negativegValue,onegValue,0,lcd.height());
  }
  
  if(abs(zRead-zReadOld)>=errorMargin){
  
    lcd.clearDisplay();
    
    drawBall(currentX,currentY,5);
    
    xReadOld=xRead;
    yReadOld=yRead;
    zReadOld=zRead;
  }
  
  
  Serial.print("xRead: ");
  Serial.print(xRead);
  Serial.print(" | yRead: ");
  Serial.print(yRead);
  Serial.print(" | zRead: ");
  Serial.println(zRead);

  

  delay(100);
}

void drawBall(int x,int y,int radius){
  
  for(int i=0;i<radius;i++){
    lcd.drawCircle(x,y,i,BLACK);
  }
lcd.drawCircle((int)map(zerogValue,negativegValue,onegValue,0,lcd.width()),(int)map(zerogValue,negativegValue,onegValue,0,lcd.height()),radius+2,BLACK);
  lcd.display();
}

Básicamente lo que hace el sketch es leer los valores de los tres ejes del acelerómetro y redibujar la pantalla cuando el eje Z se ha movido más de un margen de error indicado en la variable errorMargin. La orientación de la superficie a nivelar lo decidimos en función del valor del eje Z, si está más cerca de 1g asumiremos que estamos nivelando una superficie horizontal y si está más cerca de 0g asumiremos que nivelamos una superficie vertical. Para las superficies horizontales la bola se moverá en cualquier dirección del plano XY y para las verticales sólo se moverá arriba y abajo. Para que funcione correctamente con las superficies verticales tenemos que situar el acelerómetro con la flecha del eje Y apuntando hacia el centro de la Tierra. Aquí tenemos un vídeo mostrando su funcionamiento


lunes, 4 de noviembre de 2013

El algoritmo de la compra del billete de tren

No es el título de un capítulo de The big bang theory, sino el dilema al que me enfrento al comprar un billete de tren. LLamadme raro, pero yo necesito un algoritmo para comprar mi abono mensual de renfe para ir a trabajar. Y no es que vuelva loca a la amable taquillera de mi estación como haría  Sheldon Cooper, por mucho que cierta persona se empeñe en que a veces le recuerdo a él, entre otras cosas porque soy más del estilo de Roy Trenneman. La razón es que el abono de renfe incluye un viaje de ida y vuelta al día durante un mes. Al cabo de un mes te caduca el abono independientemente del número de viajes que hayas hecho. Es que yo no trabajo todos los días, señor directivo de renfe, alego. Todo lo que empieza por es que es una excusa, excepto esqueleto y esquema, aduce el señor directivo. ¿Y esqueje?
He aquí nuestro problema, la abolición de la esclavitud. Si trabajásemos todos los días no tendríamos que andar contando cuántos días laborables incluye nuestro abono, aunque tal como está evolucionando la legislación laboral es probable que en un futuro no necesitemos hacerlo. La idea es minimizar el precio diario del billete retrasando la compra del abono para incluir el mayor número de días laborables con la dificultad añadida de que los días que la retrasemos también tenemos que ir a trabajar y por lo tanto también debemos pagar el billete sencillo que es mucho más caro. Explicándolo de una forma sencilla, tenemos que averiguar cuántos billetes sencillos compensan la inclusión de unos días más en el abono. Para ello nos podemos basar en dos criterios, aunque ambos suelen coincidir: el precio por día laborable y el precio por día real. El primero se calcularía dividiendo el número de días laborables que hemos ido a trabajar por el precio que hemos pagado. El segundo son los días que han pasado (incluidos festivos y vacaciones) desde que empezamos a contar dividido por el coste de los billetes. Para ello he desarrollado un script en Ruby. El resultado del script es el cálculo del precio si comprásemos el abono durante varios días teniendo en cuenta los billetes sencillos que hemos comprado previamente. Como hoy tocaba comprar el billete, ejecuto el script y este es el resultado:

./abono.rb --initial_date 20131104 --simple_ticket_price 5.70 --subscription_price 77.90 --months 1 --no-sunday --no-saturday --calculate_days 30

Condiciones:
Fecha inicial: 2013-11-04
Fecha final: 2013-12-03
Días de cálculo: 30
Precio del billete sencillo: 5.7
Precio del abono: 77.9
Trabaja los lunes: true
Trabaja los martes: true
Trabaja los miércoles: true
Trabaja los jueves: true
Trabaja los viernes: true
Trabaja los sábados: false
Trabaja los domingos: false
**2013-11-04: TOTAL PAGADO: 77.9. 22 DíAS EFECTIVOS. DíAS SIN BONO: 0. PRECIO POR DíA LABORABLE: 3.54. DíAS REALES: 30. PRECIO POR DíA REAL: 2.6
2013-11-05: Total pagado: 83.6. 22 días efectivos. Días sin bono: 1. Precio por día laborable: 3.63. Días reales: 31. Precio por día real: 2.7
2013-11-06: Total pagado: 89.3. 22 días efectivos. Días sin bono: 2. Precio por día laborable: 3.72. Días reales: 32. Precio por día real: 2.79
2013-11-07: Total pagado: 95.0. 22 días efectivos. Días sin bono: 3. Precio por día laborable: 3.8. Días reales: 33. Precio por día real: 2.88
2013-11-08: Total pagado: 100.7. 21 días efectivos. Días sin bono: 4. Precio por día laborable: 4.03. Días reales: 34. Precio por día real: 2.96
--2013-11-09: Total pagado: 106.4. 20 días efectivos. Días sin bono: 5. Precio por día laborable: 4.26. Días reales: 35. Precio por día real: 3.04
--2013-11-10: Total pagado: 106.4. 21 días efectivos. Días sin bono: 5. Precio por día laborable: 4.09. Días reales: 36. Precio por día real: 2.96
2013-11-11: Total pagado: 106.4. 22 días efectivos. Días sin bono: 5. Precio por día laborable: 3.94. Días reales: 37. Precio por día real: 2.88
2013-11-12: Total pagado: 112.1. 22 días efectivos. Días sin bono: 6. Precio por día laborable: 4.0. Días reales: 38. Precio por día real: 2.95
2013-11-13: Total pagado: 117.8. 22 días efectivos. Días sin bono: 7. Precio por día laborable: 4.06. Días reales: 39. Precio por día real: 3.02
2013-11-14: Total pagado: 123.5. 22 días efectivos. Días sin bono: 8. Precio por día laborable: 4.12. Días reales: 40. Precio por día real: 3.09
2013-11-15: Total pagado: 129.2. 21 días efectivos. Días sin bono: 9. Precio por día laborable: 4.31. Días reales: 41. Precio por día real: 3.15
--2013-11-16: Total pagado: 134.9. 20 días efectivos. Días sin bono: 10. Precio por día laborable: 4.5. Días reales: 42. Precio por día real: 3.21
--2013-11-17: Total pagado: 134.9. 21 días efectivos. Días sin bono: 10. Precio por día laborable: 4.35. Días reales: 43. Precio por día real: 3.14
2013-11-18: Total pagado: 134.9. 22 días efectivos. Días sin bono: 10. Precio por día laborable: 4.22. Días reales: 44. Precio por día real: 3.07
2013-11-19: Total pagado: 140.6. 22 días efectivos. Días sin bono: 11. Precio por día laborable: 4.26. Días reales: 45. Precio por día real: 3.12
2013-11-20: Total pagado: 146.3. 22 días efectivos. Días sin bono: 12. Precio por día laborable: 4.3. Días reales: 46. Precio por día real: 3.18
2013-11-21: Total pagado: 152.0. 22 días efectivos. Días sin bono: 13. Precio por día laborable: 4.34. Días reales: 47. Precio por día real: 3.23
2013-11-22: Total pagado: 157.7. 21 días efectivos. Días sin bono: 14. Precio por día laborable: 4.51. Días reales: 48. Precio por día real: 3.29
--2013-11-23: Total pagado: 163.4. 20 días efectivos. Días sin bono: 15. Precio por día laborable: 4.67. Días reales: 49. Precio por día real: 3.33
--2013-11-24: Total pagado: 163.4. 21 días efectivos. Días sin bono: 15. Precio por día laborable: 4.54. Días reales: 50. Precio por día real: 3.27
2013-11-25: Total pagado: 163.4. 22 días efectivos. Días sin bono: 15. Precio por día laborable: 4.42. Días reales: 51. Precio por día real: 3.2
2013-11-26: Total pagado: 169.1. 22 días efectivos. Días sin bono: 16. Precio por día laborable: 4.45. Días reales: 52. Precio por día real: 3.25
2013-11-27: Total pagado: 174.8. 22 días efectivos. Días sin bono: 17. Precio por día laborable: 4.48. Días reales: 53. Precio por día real: 3.3
2013-11-28: Total pagado: 180.5. 22 días efectivos. Días sin bono: 18. Precio por día laborable: 4.51. Días reales: 54. Precio por día real: 3.34
2013-11-29: Total pagado: 186.2. 21 días efectivos. Días sin bono: 19. Precio por día laborable: 4.65. Días reales: 55. Precio por día real: 3.39
--2013-11-30: Total pagado: 191.9. 20 días efectivos. Días sin bono: 20. Precio por día laborable: 4.8. Días reales: 56. Precio por día real: 3.43
--2013-12-01: Total pagado: 191.9. 21 días efectivos. Días sin bono: 20. Precio por día laborable: 4.68. Días reales: 57. Precio por día real: 3.37
2013-12-02: Total pagado: 191.9. 22 días efectivos. Días sin bono: 20. Precio por día laborable: 4.57. Días reales: 58. Precio por día real: 3.31
2013-12-03: Total pagado: 197.6. 22 días efectivos. Días sin bono: 21. Precio por día laborable: 4.6. Días reales: 59. Precio por día real: 3.35
2013-12-04: Total pagado: 203.3. 22 días efectivos. Días sin bono: 22. Precio por día laborable: 4.62. Días reales: 60. Precio por día real: 3.39


El script devuelve los días festivos precedidos de "--", el mejor precio por día laborable precedido de "**" y el mejor precio por día real precedido de "$$". Como noviembre es un mes sin festivos ni puentes nos conviene comprar nuestro abono el siguiente día laborable después de su caducidad. El problema es el siguiente abono que incluye varios días festivos y vacaciones.

./abono.rb --initial_date 20131204 --simple_ticket_price 5.70 --subscription_price 77.90 --months 1 --calculate_days 30 --no-sunday --no-saturday --holiday 20131225 --holiday 20140101 --holiday_start 20131215 --holiday_end 20131225
Condiciones:
Fecha inicial: 2013-12-04
Fecha final: 2014-01-03
Días de cálculo: 30
Precio del billete sencillo: 5.7
Precio del abono: 77.9
Trabaja los lunes: true
Trabaja los martes: true
Trabaja los miércoles: true
Trabaja los jueves: true
Trabaja los viernes: true
Trabaja los sábados: false
Trabaja los domingos: false
Festivo: 2013-12-25
Festivo: 2014-01-01
Vacaciones: 2013-12-15 a 2013-12-25
2013-12-04: Total pagado: 77.9. 14 días efectivos. Días sin bono: 0. Precio por día laborable: 5.56. Días reales: 31. Precio por día real: 2.51
2013-12-05: Total pagado: 83.6. 13 días efectivos. Días sin bono: 1. Precio por día laborable: 5.97. Días reales: 32. Precio por día real: 2.61
2013-12-06: Total pagado: 89.3. 12 días efectivos. Días sin bono: 2. Precio por día laborable: 6.38. Días reales: 33. Precio por día real: 2.71
--2013-12-07: Total pagado: 95.0. 12 días efectivos. Días sin bono: 3. Precio por día laborable: 6.33. Días reales: 34. Precio por día real: 2.79
--2013-12-08: Total pagado: 95.0. 13 días efectivos. Días sin bono: 3. Precio por día laborable: 5.94. Días reales: 35. Precio por día real: 2.71
2013-12-09: Total pagado: 95.0. 14 días efectivos. Días sin bono: 3. Precio por día laborable: 5.59. Días reales: 36. Precio por día real: 2.64
2013-12-10: Total pagado: 100.7. 14 días efectivos. Días sin bono: 4. Precio por día laborable: 5.59. Días reales: 37. Precio por día real: 2.72
2013-12-11: Total pagado: 106.4. 14 días efectivos. Días sin bono: 5. Precio por día laborable: 5.6. Días reales: 38. Precio por día real: 2.8
2013-12-12: Total pagado: 112.1. 13 días efectivos. Días sin bono: 6. Precio por día laborable: 5.9. Días reales: 39. Precio por día real: 2.87
2013-12-13: Total pagado: 117.8. 12 días efectivos. Días sin bono: 7. Precio por día laborable: 6.2. Días reales: 40. Precio por día real: 2.95
--2013-12-14: Total pagado: 123.5. 12 días efectivos. Días sin bono: 8. Precio por día laborable: 6.18. Días reales: 41. Precio por día real: 3.01
--2013-12-15: Total pagado: 123.5. 13 días efectivos. Días sin bono: 8. Precio por día laborable: 5.88. Días reales: 42. Precio por día real: 2.94
--2013-12-16: Total pagado: 123.5. 14 días efectivos. Días sin bono: 8. Precio por día laborable: 5.61. Días reales: 43. Precio por día real: 2.87
--2013-12-17: Total pagado: 123.5. 15 días efectivos. Días sin bono: 8. Precio por día laborable: 5.37. Días reales: 44. Precio por día real: 2.81
--2013-12-18: Total pagado: 123.5. 16 días efectivos. Días sin bono: 8. Precio por día laborable: 5.15. Días reales: 45. Precio por día real: 2.74
--2013-12-19: Total pagado: 123.5. 16 días efectivos. Días sin bono: 8. Precio por día laborable: 5.15. Días reales: 46. Precio por día real: 2.68
--2013-12-20: Total pagado: 123.5. 16 días efectivos. Días sin bono: 8. Precio por día laborable: 5.15. Días reales: 47. Precio por día real: 2.63
--2013-12-21: Total pagado: 123.5. 17 días efectivos. Días sin bono: 8. Precio por día laborable: 4.94. Días reales: 48. Precio por día real: 2.57
--2013-12-22: Total pagado: 123.5. 18 días efectivos. Días sin bono: 8. Precio por día laborable: 4.75. Días reales: 49. Precio por día real: 2.52
--2013-12-23: Total pagado: 123.5. 19 días efectivos. Días sin bono: 8. Precio por día laborable: 4.57. Días reales: 50. Precio por día real: 2.47
--2013-12-24: Total pagado: 123.5. 20 días efectivos. Días sin bono: 8. Precio por día laborable: 4.41. Días reales: 51. Precio por día real: 2.42
--2013-12-25: Total pagado: 123.5. 21 días efectivos. Días sin bono: 8. Precio por día laborable: 4.26. Días reales: 52. Precio por día real: 2.38
$$2013-12-26: TOTAL PAGADO: 123.5. 21 DíAS EFECTIVOS. DíAS SIN BONO: 8. PRECIO POR DíA LABORABLE: 4.26. DíAS REALES: 53. PRECIO POR DíA REAL: 2.33
2013-12-27: Total pagado: 129.2. 20 días efectivos. Días sin bono: 9. Precio por día laborable: 4.46. Días reales: 54. Precio por día real: 2.39
--2013-12-28: Total pagado: 134.9. 20 días efectivos. Días sin bono: 10. Precio por día laborable: 4.5. Días reales: 55. Precio por día real: 2.45
--2013-12-29: Total pagado: 134.9. 21 días efectivos. Días sin bono: 10. Precio por día laborable: 4.35. Días reales: 56. Precio por día real: 2.41
**2013-12-30: TOTAL PAGADO: 134.9. 22 DíAS EFECTIVOS. DíAS SIN BONO: 10. PRECIO POR DíA LABORABLE: 4.22. DíAS REALES: 57. PRECIO POR DíA REAL: 2.37
2013-12-31: Total pagado: 140.6. 22 días efectivos. Días sin bono: 11. Precio por día laborable: 4.26. Días reales: 58. Precio por día real: 2.42
--2014-01-01: Total pagado: 146.3. 22 días efectivos. Días sin bono: 12. Precio por día laborable: 4.3. Días reales: 59. Precio por día real: 2.48
2014-01-02: Total pagado: 146.3. 22 días efectivos. Días sin bono: 12. Precio por día laborable: 4.3. Días reales: 60. Precio por día real: 2.44
2014-01-03: Total pagado: 152.0. 21 días efectivos. Días sin bono: 13. Precio por día laborable: 4.47. Días reales: 61. Precio por día real: 2.49

El resultado es algo sorprendente, ya que si elegimos bien el día de compra nos puede salir más barato el transporte cuando hay festivos y vacaciones, ya que cubrimos el mayor número de días al menor precio. El código fuente del script es este:

#!/usr/bin/ruby
#encoding: utf-8

require 'optparse'
require 'date'
#require 'colorize'

def working_day(f,holidays,week_days,holiday_periods)
holiday=false
holidays.each do |h|
if h==f
holiday=true
end
end
holiday_periods.each do |k,v|
if f>=k && f<=v
holiday=true
end
end
return !holiday && week_days[f.wday]
end

initial_date=Date.today
simple_ticket_price=5.4
subscription_price=77.8
week_days=Array.new(7,true)
holidays=Array.new
holiday_periods=Hash.new
final_date=initial_date>>1
calculate_days=10
holiday_period_start=Date.new

opt_parser = OptionParser.new do |opts|

opts.banner="Uso abono.rb [options]"
opts.separator ""
opts.on("--initial_date Fecha inicial", String , :required, "Fecha de inicio del bono") do |switch|
initial_date=Date.iso8601(switch)
final_date=(initial_date>>1)-1
end
opts.on("--simple_ticket_price Precio", Float , :required, "Precio del billete sencillo") do |switch|
simple_ticket_price=switch
end
opts.on("--subscription_price Precio", Float , :required, "Precio del abono") do |switch|
subscription_price=switch
end
opts.on("--months Duración", Integer , :required, "Duración del abono en meses") do |switch|
final_date=(initial_date>>switch)-1
end
opts.on("--days Duración", Integer , :required, "Duración del abono en días") do |switch|
final_date=initial_date+switch-1
end
opts.on("--years Duración", Integer , :required, "Duración del abono en años") do |switch|
final_date=initial_date.next_year(switch)-1
end
opts.on("--calculate_days Duración", Integer , :required, "Días de cálculo") do |switch|
calculate_days=switch
end
opts.on("--no-sunday", :none, "No trabaja los domingos") do |switch|
week_days[0]=false
end
opts.on("--no-monday", :none, "No trabaja los lunes") do |switch|
week_days[1]=false
end
opts.on("--no-tuesday", :none, "No trabaja los martes") do |switch|
week_days[2]=false
end
opts.on("--no-wednesday", :none, "No trabaja los miércoles") do |switch|
week_days[3]=false
end
opts.on("--no-thursday", :none, "No trabaja los jueves") do |switch|
week_days[4]=false
end
opts.on("--no-friday", :none, "No trabaja los viernes") do |switch|
week_days[5]=false
end
opts.on("--no-saturday", :none, "No trabaja los sábados") do |switch|
week_days[6]=false
end
opts.on("--holiday Fecha", String, :required, "Fecha de un día festivo") do |switch|
holidays.push Date.iso8601(switch)
end
opts.on("--holiday_start Fecha", String, :required, "Fecha de inicio de las vacaciones") do |switch|
holiday_period_start=Date.iso8601(switch)
end
opts.on("--holiday_end Fecha", String, :required, "Fecha de final de las vacaciones") do |switch|
holiday_periods[holiday_period_start]=Date.iso8601(switch)
end
end

opt_parser.parse!
puts "Condiciones: "
puts "Fecha inicial: #{initial_date}"
puts "Fecha final: #{final_date}"
puts "Días de cálculo: #{calculate_days}"
puts "Precio del billete sencillo: #{simple_ticket_price}"
puts "Precio del abono: #{subscription_price}"
puts "Trabaja los lunes: #{week_days[1]}"
puts "Trabaja los martes: #{week_days[2]}"
puts "Trabaja los miércoles: #{week_days[3]}"
puts "Trabaja los jueves: #{week_days[4]}"
puts "Trabaja los viernes: #{week_days[5]}"
puts "Trabaja los sábados: #{week_days[6]}"
puts "Trabaja los domingos: #{week_days[0]}"
holidays.each do |holiday|
puts "Festivo: #{holiday}"
end
holiday_periods.each do |k,v|
puts "Vacaciones: #{k} a #{v}"
end

result=Hash.new
min_price=1.0/0.0 #Sí amigos, ruby sabe que 1/0 es infinito
min_real_price=1.0/0.0

min_price_string=String.new
min_real_price_string=String.new

0.upto(calculate_days) do |i|
effective_days_count=0
no_subscription_days_count=0
global_price=subscription_price
if i>0
(initial_date..initial_date+(i-1)).each do |f|
if working_day(f,holidays,week_days,holiday_periods)
global_price+=simple_ticket_price
no_subscription_days_count+=1
end
end
end
(initial_date+i..final_date+i).each do |f|
if working_day(f,holidays,week_days,holiday_periods)
effective_days_count+=1
end
end
price_per_day=(global_price/(effective_days_count+no_subscription_days_count)).round(2)
real_price_per_day=(global_price/((final_date+i)-initial_date)).round(2)
if min_price>price_per_day
min_price=price_per_day
min_price_string="#{initial_date+i}: Total pagado: #{global_price.round(2)}. #{effective_days_count} días efectivos. Días sin bono: #{no_subscription_days_count}. Precio por día laborable: #{price_per_day}. Días reales: #{((final_date+i)-initial_date).to_i()+1}. Precio por día real: #{(global_price/(((final_date+i)-initial_date)+1)).round(2)}"
end
if min_real_price>real_price_per_day
min_real_price=real_price_per_day
min_real_price_string="#{initial_date+i}: Total pagado: #{global_price.round(2)}. #{effective_days_count} días efectivos. Días sin bono: #{no_subscription_days_count}. Precio por día laborable: #{price_per_day}. Días reales: #{((final_date+i)-initial_date).to_i()+1}. Precio por día real: #{(global_price/(((final_date+i)-initial_date)+1)).round(2)}"
end
result["#{initial_date+i}: Total pagado: #{global_price.round(2)}. #{effective_days_count} días efectivos. Días sin bono: #{no_subscription_days_count}. Precio por día laborable: #{price_per_day}. Días reales: #{((final_date+i)-initial_date).to_i()+1}. Precio por día real: #{(global_price/(((final_date+i)-initial_date)+1)).round(2)}"]=price_per_day
end

result.each do |k,v|
if k==min_price_string
puts "**#{k.upcase}"
elsif k==min_real_price_string
puts "$$#{k.upcase}"
elsif !working_day(Date.iso8601(k[0,10]),holidays,week_days,holiday_periods)
puts "--#{k}"
else
puts k
end
end


Las opciones del script son las siguientes:

--initial_date Fecha desde la que queremos calcular la compra
--simple_ticket_price Precio del billete sencillo

--subscription_price Precio del abono mensual
--days Duración del abono en días

--months Duración del abono en meses

--years Duración del abono en años

--calculate_days Número de días que queremos calcular

--no-sunday No trabaja los domingos

--no-monday No trabaja los lunes

--no-tuesday No trabaja los martes

--no-wednesday No trabaja los miércoles

--no-thursday No trabaja los jueves

--no-friday No trabaja los viernes

--no-saturday No trabaja los sábados

--holiday Fecha de un día festivo

--holiday_start Fecha de inicio de vacaciones

--holiday_end Fecha de fin de vacaciones