viernes, 2 de mayo de 2014

Robot panorámico XS (El programa de Arduino 1 de 2)

Otra vez de nuevo por aqui...pasando el rato...

Hoy voy a explicar un poco a grandes pasos el programa de Arduino que gestiona el robot panoramico XS.
Como que algunos estais provando de construiros uno, mostraré la versión que funciona a traves de la LCD shield de 2x16 caracteres, de esta manera ya tendreis pulsadores y pantalla integrados.

Antes de nada, prepararos con el tostón jiji, son poco menos de 1700 lineas a comentar. Obviamente quién lo quiera me lo puede pedir, o podeis ir haciendo copiar+pegar...

Este programa está basado en mi primer robot panorámico al cual se le han ido haciendo mejoras, pero una de las caracteristicas de este programa es que tiene la virtud de servir para dos aplicaciones : Hacer fotografias de alta resolución y gestionar una slide dolly. Al fín y al cabo el hardware es el mismo y el control va hacia dos motores en ambos casos (si en la slide dolly sólo quereis usar uno no pasa nada, pero deberiais conectarlo en el eje X). En mi caso mediante unos conectores DB-9 conecto los motores que van a la dolly o hacia el robot.

Si no recuerdo mal, en el post en que explicaba el programa de mi robot original está mejor comentado el tema de variables de la función gigapan.

Pero...vamos al grano! Una justa explicación del programa de Arduino.

Empezamos con una pequeña descripción del programa:


/*
 GigaRobot_XS based on GigapanRobot 1.8beta2

 Photo Robot 2x L298N + Nema 17
 language: Wiring/Arduino

 This program drives two bipolar stepper motor.
 Photo-shots signals it's actuated by relay --
 Movement manual motor by array switchs of keypad

 Created 28 Dec. 2012
 Last updated 22 Feb. 2014

 by XavierGP

 */


Incluimos las tres librerias que nos haran falta, a destacar la <Stepper2.h> que es una modificación casera para trabajar con medios pasos. Si alguién la necesita que la pida! Si en cambio quereis usar la libreria <Stepper.h> que viene instalada por defecto en vuestro software de arduino también debereis modificar las variables motorStepsX y motorStepsY que debereis poner el numero de pasos del motor. En mi caso he puesto 400 en vez de los 200 típicos de un motor NEMA-17.


#include <Stepper2.h>        // incluimos la Stepper library
#include <LiquidCrystal.h>  // incluimos la LiquidCrystal LCD library
#include <Wire.h>  // Comes with Arduino IDE

Continuamos definiendo las variables, algunas de ellas están comentadas entre //

#define _360div2xPI     57.29577951
#define motorStepsX 400     // Pasos motor X 360º/1.8º   com que treballo en llibreria de 1/2 pas multiplico per 2
#define motorStepsY 400     // Pasos motor Y 360º/1.8º                      
#define pulsosvueltaejex 2075            // 200impulsos x reduccion mecanica   200*5.1*2
#define pulsosvueltaejey 2075            // 200impulsos x reduccion mecanica
float impX=pulsosvueltaejex/360;      // número de impulsos que corresponden a 1º del eje X    
float impY=pulsosvueltaejey/360;      // número de impulsos que corresponden a 1º del eje Y   
int pasoX;                            // pasos motor entre capturas eje X 
int pasoY;                            // pasos motor entre capturas eje Y  
float xact;               // º actual eje X
float yact;               // º actual eje Y
float xmax;               // º posición máxima eje X
float xmin;               // º posición mínima eje X
float ymax;               // º posición máxima eje Y
float ymin;               // º posición mínima eje Y
int xtemp;
int ytemp;
int nshot;
int columnasX;                 // número de filas eje X >> (xmax-xmin)/pasoX
int columnasXX;
int filasY;              // número de columnas eje Y    >> (ymax-ymin)/pasoY
int filasYY;
int previsionShot;          // prevision número shots   columnasX * filasY
int segundos ;
int minutos ;
int horas ;
int pause;

int pdisplay;
int pdisplay0;

float temperaturaX;  // variable to store the value of radiator
float temperaturaY;
float voltage;      // variable to store the voltage battery supply
float valorTempX;
float valorTempY;
int progreso;
float solape=0.20;
float retardo;
int origen;
int tempj;
int tempi;
int a;
int sentidoX;
int sentidoY;
int shot=25;
float pasoXX;              // valor temporal dolly eje X
float pasoYY;              // valor temporal dolly eje Y
int pasX;
int pasY;
long int xy;
long int resto;
float segundos_secuencia=3;
float posicion_actual_X;
float posicion_actual_Y;
int velocidadX;
int velocidadY;
float tiempo_reloj;
int espai;


Ahora definimos los pins: En nuestro caso el PinFan corresponde a la salida dónde deberiamos conectar un relé que accionaria el ventilador en caso de exceso de temperatura en los drivers (es una cosa opcional pero recomendable). El PinShot es la salida a la cual conectariamos un relé, optoacoplador o transistor para accionar el comandamiento remoto de nuestra cámara.

#define PinFan 24
#define PinShot 25


Acto seguido es cuando introducimos los valores obtenidos al pulsar las teclas de la shield LCD (ver más info en el post anterior dónde se explica cómo obtener dichos valores). Como "extra" hay la variable margen que es una tolerancia que se le aplica al valor para que no sea tan crítico. Cuanto más grande más tolerancia pero luego puede existir "solapes" entre valores de teclas y que no nos responda correctamente al pulsar los pulsadores.

#define abajo 306      //definimos valores teclas 255
#define arriba 131     //99      // 132
#define left 480       // 407   
#define right 0
#define select 721     // 636
#define margen 50    //abans era 30

Ahora unas cuantas variables que serviran para optimizar los movimientos de nuestro robot teniendo pequeños detalles fotograficos.

float d1;         // distancia 1 sensor CMOS (ancho)
float d2;         // distancia 2 sensor CMOS (alto) 
float diagonal_sensor;
float grados_diagonal;
float grados_horizontales;
float grados_verticales;
float factor=1.5;     // en su defecto : FF = 1, Nikon DX=1.5 , Canon DX=1.6 , Olympus= 2 
float focal = 200;  
int paso;
int menu=2;
int menu0;
int modenight=2;


Aunque no lo tengo montado, el programa está preparado para montar dos sensores de origen en nuestra dolly , para que cuando arranque antes de nada vaya a un punto de inicio limite y podamos estar seguros de que no se nos va a perder el carro...

#define limitX 39             // pin entrada sensor origen X             
#define limitY 49             // pin entrada sensor origen Y                           


Finalmente todas las variables con el nombre logoxx son la definicion de caracteres especiales encargados de dar la forma de camara fotográfica haciendo fotos cuando estamos en modo time-lapse.

byte logo1a[8] = {B01111, B00110, B01111, B11111, B11111, B11111, B11110, B11110};
byte logo1d[8] = {B00000, B00000, B11110, B11111, B11111, B11111, B11111, B11111};
byte logo1e[8] = {B11110, B11110, B11110, B11111, B11111, B11111, B11111, B01111};
byte logo1h[8] = {B01111, B01111, B01111, B11111, B11111, B11111, B11111, B11110};


Aqui definimeros las funciones correspondientes a los motores.En este caso el motorX lo estamos definiendo en que utilizaremos las salidas 36, 38, 40 y 42 de nuestro arduino, mientras que en el caso del motorY seran las 46, 48, 50 y 52
.
Stepper2 myStepperX(motorStepsX, 36, 38, 40, 42); // initialize the library motor X
Stepper2 myStepperY(motorStepsY, 46, 48, 50, 52); // initialize the library motor Y


También definimos los pins que corresponden a las salidas que van al LCD. Si quisieramos utilizar un LCD via i2C la cosa cambia y habria que redefinir unas algunas cosas que no voy a explicar ahora para no hacer un tostón de post.

LiquidCrystal lcd(8, 9, 4, 5, 6, 7); // definimos los pins utilizados para la comunicacion con la pantalla LCD

Luego es cuando establecemos los pins como entradas o salidas

void setup() {
Serial.begin(9600);
lcd.begin(16, 2);

pinMode(PinShot, OUTPUT);      // Output digital shot
pinMode(PinFan, OUTPUT);          // Output digital fan
pinMode(PinSelect, INPUT);
digitalWrite(PinShot, LOW);
digitalWrite(PinFan,LOW);
pinMode(limitX, INPUT);
pinMode(limitY, INPUT);

Generamos los caracteres que nos compondrá la imagen de camara fotografica y salimos del void setup , esta función sólo será ejecutada una vez en todo el programa, a diferencia del void loop que se repite infinitamente.

lcd.createChar(1, logo1a);
lcd.createChar(4, logo1d);
lcd.createChar(5, logo1e);
lcd.createChar(8, logo1h);
}

Empezamos definiendo la velocidad de los motores X e Y. Y acto seguido llamamos a la subrutina presentacioninicio() que es la encargada de darnos la bienvenida.


void loop(){
myStepperX.setSpeed(5);  // set the motor X speed 
myStepperY.setSpeed(8);  // set the motor Y speed 
presentacioninicio();
progreso=0;


A continuación elejimos nuestro modo de trabajo , ya si queremos realizar una gigafoto o bién un time-lapse , mejor dicho motion-lapse pues nuestra dolly se va a mover! Dependiendo de nuestra elección se ejecutará la subrutina gigapan() o slidedolly() y una vez terminen su ciclo se realizará un borrado de todas las variables mediante la subrutina reset() . Estas subrutinas ya las explicaré cuando llegen. Llegado a este punto os habreis dado cuenta que no explico línea a línea sinó esto daria para escribir una novela...

lcd.clear();
lcd.setCursor(0,0);
lcd.print("[UP]=Slide Dolly");
lcd.setCursor(0,1);
lcd.print("[DOWN] = Gigapan");
while(progreso!=1){ //enter
delay(150);
if((analogRead(0)/margen)==(arriba/margen)){  //arriba
slide_dolly();
reset();
}
if((analogRead(0)/margen)==(abajo/margen)){   //abajo
gigapan();
reset();
}
}
}





Ahora toca comentar la subrutina presentacioninicio() : Tras mostrarse un "Hola XavierGP!" ,despues de un borrado de pantalla y nos muestra el nombre del proyecto mientras aparece una camara fotografica haciendo dos fotos. La subrutina dónde se almacenan / generan los caracteres especiales del "gif" se llaman logo2() y logo3()


void presentacioninicio(){
lcd.clear();
lcd.setCursor(1,0);
lcd.print("Hola XavierGP!");
delay(3000);
lcd.clear();
lcd.setCursor(1,0);
lcd.print("Panorama");
lcd.setCursor(1,1);
lcd.print("Robot");
logo3();
delay(800);
logo2();
delay(400);
logo3();
lcd.setCursor(7,1);
lcd.print("XS");
delay(800);
logo2();
delay(400);
logo3();
delay(3000);  // fin GIF
}





En la subrutina logo2() y logo3() generamos los caracteres, de la siguiente manera: cada caracter sabemos que está compuesto por 8 lineas de 5 pixeles cada una. Cuando el valor es 0 el pixel está apagado mientras que cuando tiene valor 1 está encendido.

void logo2(){
byte logo1b[8] = {B00000, B00111, B11100, B10001, B00111, B01111, B01111, B11111};
byte logo1c[8] = {B00000, B11100, B00111, B10001, B11100, B11110, B11110, B11111};
byte logo1f[8] = {B11110, B11111, B01111, B01111, B00111, B10001, B11100, B11111};
byte logo1g[8] = {B01111, B11111, B11111, B11110, B11100, B10001, B00111, B11111};
byte logo1d[8] = {B00000, B00000, B11110, B11111, B11111, B11111, B11111, B11111};
lcd.createChar(2, logo1b);
lcd.createChar(3, logo1c);
lcd.createChar(6, logo1f);
lcd.createChar(7, logo1g);
lcd.createChar(4, logo1d);
lcd.setCursor(12,0);
lcd.write(1);
lcd.setCursor(13,0);
lcd.write(2);
lcd.setCursor(14,0);
lcd.write(3);
lcd.setCursor(15,0);
lcd.write(4);
lcd.setCursor(12,1);
lcd.write(5);
lcd.setCursor(13,1);
lcd.write(6);
lcd.setCursor(14,1);
lcd.write(7);
lcd.setCursor(15,1);
lcd.write(8);
}

void logo3(){
byte logo1b[8] = {B00000, B00111, B11100, B10001, B00110, B01100, B01000, B10000};
byte logo1c[8] = {B00000, B11100, B00111, B10001, B01100, B00110, B00010, B00001};
byte logo1f[8] = {B10000, B10000, B01000, B01100, B00110, B10001, B11100, B11111};
byte logo1g[8] = {B00001, B00001, B00011, B00110, B01100, B10001, B00111, B11111};
byte logo1d[8] = {B00000, B00000, B11110, B11111, B11111, B11111, B11111, B11111};
lcd.createChar(2, logo1b);
lcd.createChar(3, logo1c);
lcd.createChar(6, logo1f);
lcd.createChar(7, logo1g);
lcd.createChar(4, logo1d);
lcd.setCursor(12,0);
lcd.write(1);
lcd.setCursor(13,0);
lcd.write(2);
lcd.setCursor(14,0);
lcd.write(3);
lcd.setCursor(15,0);
lcd.write(4);
lcd.setCursor(12,1);
lcd.write(5);
lcd.setCursor(13,1);
lcd.write(6);
lcd.setCursor(14,1);
lcd.write(7);
lcd.setCursor(15,1);
lcd.write(8);
}

Ahora en la subrutina gigapan() es dónde en realidad empieza lo divertido!


Primero de todo llamamos a la subrutina defineparametro() que explicaré más adelante (se definen los parametros fotograficos tales como tipo de camara, zoom a utilizar y solape entre capturas).

Acto seguido nos pide que busquemos el punto de inicio, llamando a la subrutina buscaorigen() , este punto siempre deve ser la esuina inferior izquierda de nuestra foto panorámica. Seguidamente nos pedirá que busquemos el punto final (esquina superior derecha).

Una vez tenemos confirmados estos puntos nos servirá para calcular tanto el numero de filas y columnas , y multiplicando entre ellas obtenemos la previsión de capturas


void gigapan(){
defineparametro();
lcd.clear();
lcd.setCursor(1,0);
lcd.print("Define  origen");
lcd.setCursor(0,1);
lcd.print("mediante flechas");
delay(2500);
lcd.setCursor(0,1);
lcd.print("[SELECT]  valida");
delay (3000);
lcd.clear();
buscaorigen();
lcd.clear();
lcd.setCursor(1,0);
lcd.print("Define  final");
lcd.setCursor(0,1);
lcd.print("mediante flechas");
delay(2500);
lcd.setCursor(0,1);
lcd.print("[SELECT]  valida");
delay (3000);
lcd.clear();
buscafinal();
xact=-xmin;
yact=-ymin;
columnasX=((xmax-xmin)/pasoX)+1;
columnasXX=columnasX;
filasY=((ymax-ymin)/pasoY)+1;
filasYY=filasY;
previsionShot=columnasX*filasY;




Es hora de calcular el tiempo necesario en segundos para realizar el proyecto,  ello dependerá de si hemos elejido la opción de retardo o no.


if (retardo>1000){
segundos=(previsionShot*((retardo+500)/1000))+((filasY-1)*pasoY*1.3)+((columnasX-1)*pasoX*1.3)+(previsionShot*0.6); 
}else{
segundos=(previsionShot*retardo/1000)+((filasY-1)*pasoY*1.2)+((columnasX-1)*pasoX*1.2)+(previsionShot*0.6);   //   (previsionShot*retardo/1000)+(0.5*(filasY-1))+(0.7*(filasY*pasoY-1))+(0.7*(columnasX*pasoX-1))  
}

Luego el robot se irá a la posición inicio del eje X. Si hemos elejido modo noche el robot no descenderá en su eje Y ya que en vez de empezar las capturas de abajo a arriba lo hará luego de arriba a abajo. Esta función está pensada para aprovechar la "hora azul" que no dura una hora y somos nosotros los que decidimos cuando empezamos a fotografiar el cielo.



lcd.clear();
lcd.setCursor(0,0);
lcd.print("Moviendo  hacia");
lcd.setCursor(0,1);
lcd.print("posicion  inicio");

myStepperX.step(-(columnasX-1)*pasoX); 

if(modenight==0){                          // bajamos el carro si trabajamos en modo clasico
myStepperY.step(-(filasY-1)*pasoY);    
}

Es hora de mostrar los datos previstos de nuestro proyecto, es decir, las filas, columnas, numero de fotos, grados en vertical y horizontal , la duración prevista en tiempo y la dimensión de nuestra fotografia resultante en megapixeles.




lcd.clear();
lcd.setCursor(0,1);
lcd.print("Home position !");
delay(2000);
lcd.clear();
lcd.print("Datos proyecto:");
lcd.setCursor(0,1); 
lcd.print(previsionShot);
lcd.print(" capturas:"); 
delay(2500);
lcd.setCursor(0,0);  
lcd.print((-xmin+xmax)/impX/1.17,1);
lcd.print(char(223));
lcd.print("H // ");
lcd.print((-ymin+ymax)/impY/1.17,1);
lcd.print(char(223));
lcd.print("V");
lcd.setCursor(0,1);
lcd.print(columnasX);
lcd.print("filas / ");
lcd.print(filasY);
lcd.print("columnas");
delay (4500);
  

Para convertir los segundos a horas y minutos utilizaremos la funcion conversiotiempo() y a continuación está todo a punto para empezar, sólo falta pulsar [SELECT]
  

conversiontiempo();

lcd.clear();
lcd.setCursor(0,0);
lcd.print("[SELECT] para");
lcd.setCursor(0,1);
lcd.print("iniciar y pausar");

delay(2000);
lcd.clear();
lcd.print("[SELECT] = start");
pause=1;

while(pause!=0){
if((analogRead(0)/margen)==(select/margen)){ //enter)
pause=0;
}
}
xmax=xmax-xmin;
ymax=ymax-ymin;
xmin=0;
ymin=0;
xact=0;
yact=0;

En el caso que trabajemos en modo noche , invertimos el sentido de giro del motor del eje Y, para ello invertimos el signo del  valor de PasoY (pasos a efectuar por el motor del eje Y entre filas). Acto seguido empieza la rutina:
* Mostrar pantalla (pantallaGigapan())
* Petición de pausa siempre y cuando no sea la primera toma (como que para arrancar la secuencia es el mismo pulsador que para pausar se generaba la pausa automáticamente al iniciar el ciclo)
* Esperamos un segundo
* Chequeamos temperatura en los drivers (si es necesario arrancará o se parará el ventilador)
* Analizamos si el retardo es superior a 1" por lo que si es asi le aplicaremos un retardo "extra" de medio segundo antes de disparar la foto
* Volveremos a pedir petición de pausa (este paso se repetirá continuadamente, es la única manera de poder pausar en cualquier momento)
* Realizamos una captura
* Actualizamos datos en la pantalla
* Esperamos medio segundo aparte del retardo seleccionado
* Desplazamos el motor horizontal X hasta la siguiente captura, hasta llegar al limite de capturas horizontales , momento en que retrocederá el robot hasta el punto inicial del eje X mientras nos muestra en pantalla la temperatura y voltaje. Una vez llegada a la posición inicial será momento de mover el motor del eje Y hasta la siguiente fila, y luego volvemos a iniciar todo el ciclo hasta llegar a la última posición en que nos mostrará el mensaje de "Panorámica completa!".


if (modenight==1) {      // si trabajamos en modo night invertimos el sentido de paso del eje Y
pasoY=pasoY*(-1);
}
lcd.clear();
pantallaGigapan();
for(int i=0; i<filasY; i++){      // empieza secuencia panoramica         
if (nshot!=0){
  peticiopausa();
}
delay(1000);
for(int j=0; j<(columnasX-1); j++){   //columnasX
peticiopausa();
analisisTemperatura();
tempi=i+1;
tempj=j+1;
if(retardo>1000){    //per compensar modo night
delay(500);
}
peticiopausa();
digitalWrite(PinShot, HIGH);
delay(500);                       
nshot=nshot++;
pantallaGigapan();
digitalWrite(PinShot, LOW);
peticiopausa();   
delay(500+retardo); 
if(j!=(columnasX+0)){   
myStepperX.step(pasoX);
xact=xact+(pasoX);
tempj=j+2;
pantallaGigapan();
tempj=j+1;

delay(500);                                                            
}

nshot=nshot++;
digitalWrite(PinShot, HIGH);
tempj=tempj+1;
pantallaGigapan();
delay(500);                         
digitalWrite(PinShot, LOW);
delay(500+retardo);

pantallatemperatura();
       
myStepperX.step((-(columnasXX)+1)*pasoX);   
xact=0;
if(i!=filasY-1){myStepperY.step(pasoY);}                                 
yact=yact+(pasoY);
delay(1000);
lcd.clear();
}
lcd.clear();
lcd.setCursor(1,0);
lcd.print("Panoramica");
lcd.setCursor(6,1);
lcd.print("completa !");
if(modenight==0){
myStepperY.step((-filasYY+1)*pasoY);
}

delay(5000);
}


Y una vez llegemos al final de las capturas es cuando aparecerá el mensaje de Panorámica completa!



En la siguiente subrutina sirve para definir los parametros de la cámara y el solape entre imagenes. No todas las camaras tienen el mismo tamaño del sensor, y ese tamaño nos influye en el factor recorte resspecto el formato de 35mm hoy en dia llamado con el nombre de Full Frame. En principio podemos determinar que tipo de camara reflex vamos a usar (está pensado para las marcas más populares, pero es muy fácil incluir otras).




void defineparametro(){ // rutina definimos tipo de camara y focal
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Formato d camara");
lcd.setCursor(2, 1);
lcd.print("[UP] & [DOWN]");
delay(2500);
lcd.setCursor(0,1);
lcd.print("[SELECT]  valida");
delay(2500);
while((analogRead(0)/margen)!=(select/margen)){ //enter
delay(300);
if((analogRead(0)/margen)==(abajo/margen)){  //arriba
menu=menu+1;
if(menu>=4){menu=4;}
}
if((analogRead(0)/margen)==(arriba/margen)){   //abajo
menu=menu-1;
if(menu<=1){menu=1;}
}
if(menu==1){ // FF
lcd.setCursor(0,1);
lcd.print("Full Frame 35mm.");
d1=35.8;         //canon 5d
d2=23.9;         //canon 5d
factor=1;
diagonal_sensor=43.0447441623248;
}
if(menu==2){ // Nikon DX
lcd.setCursor(0,1);
lcd.print(" Nikon DX  1.5x  ");
factor=1.5;
d1=23.5;         // distancia 1 sensor CMOS (ancho)   Valor de la Nikon d5200 !
d2=15.6;         // distancia 2 sensor CMOS (alto)    Valor de la Nikon d5200 !
diagonal_sensor=28.2899275361390772;
}
if(menu==3){ // Canon DX
lcd.setCursor(0,1);
lcd.print(" Canon DX  1.6x  ");
factor=1.6;
d1=22.2;         // eos1000d
d2=14.8;         // eos1000d
diagonal_sensor=26.68107943843352;
}
if(menu==4){ // Olympus 4/3
lcd.setCursor(0,1);
lcd.print(" Olympus 4/3 2x ");
factor=2;
d1=17.3;         // e-520
d2=13.0;         // e-520
diagonal_sensor=21.6400092421422;
}
}

Debido a que tenia problemas con las fórmulas y me hacia cosas raras, decidí implementar directamente la medida del sensor en diagonal en vez de hacer la hipotenusa de los dos lados, de esa manera ahorraba algunas lineas pero los problemas aparecidos no compensaban...

Acto seguido definimos la focal que vamos a usar en la cámara, para ello nos fijaremos o bién en los datos exif o en el zoom , pero nunca deberemos aplicar el factor recorte ocasionado por el tamaño del sensor ya que el propio Arduino ya se encargará de aplicarlo a la hora de mostrarnos la focal equivalente. Dependiendo de nuestra focal a la hora de propgramar veremos que los pasos empiezan de milimetro en milimetro y a medida que vamos subiendo de zoom también augmenta este escalonado. Es poco útil subir con pasos pequeños cuando las focales ya pasan de los 100 mm. !

Obviamente estos valores introducidos son cruciales para optimizar las tomas.



lcd.clear();     // rutina definimos focal
lcd.setCursor(0,0);
lcd.print("Longitud  focal:");
lcd.setCursor(1, 1);
lcd.print("[UP] & [DOWN]");
delay(2500);
lcd.setCursor(0,1);
lcd.print("[SELECT]  valida");
delay(2500);
lcd.setCursor(0,1);
lcd.print("                ");
while((analogRead(0)/margen)!=(select/margen)){ //enter
if (focal<100){
paso=1;
delay(75);
}
if (focal>=100){
paso=25;
delay(120);
}
if (focal>399){
paso=50;
delay(150);
}
if((analogRead(0)/margen)==(arriba/margen)){  //arriba
focal=focal+(paso);
}
if((analogRead(0)/margen)==(abajo/margen)){   //abajo
if(focal>1){focal=focal-(paso);}
}
lcd.setCursor(5,1);
if (focal<100){
lcd.print(" ");
}
if (focal<10){
lcd.print(" ");
}  
lcd.print(focal,0);
lcd.setCursor(9,1);
lcd.print("mm.");
}

Acto seguido es hora de definir el sopale, yo siempre suelo usar un 20% aunque desde foros y expertos dicen que ideal un 25-33%. Os puedo asegurar que nunca he tenido problemas, pero todo depende de la mecánica de vuestro robot... Por ejemplo cuando disparamos a 200mm equivale a unos 8º de visión, y nuestro solape andará un poco por encima del 1.5º, si tenemos un juego superior a ese 1.5º el resultado  puede ser un autentico desastre. Mediante software se han marcado unos limites entre 1-50% aunque creo que debería de "caparlo" mas que luego cuando la gente pone valores anormales y no sale la panorámica. 

Obviamente si vamos cortos de memoria o de bateria siempre seria mejor reducir el solape, pero cada uno mismo con su mecanismo!


lcd.clear();     // rutina definimos solape
lcd.setCursor(0,0);
lcd.print(" Define solape: ");
lcd.setCursor(1, 1);
lcd.print("[UP] & [DOWN]");
delay(2500);
lcd.setCursor(0,1);
lcd.print("[SELECT]  valida");
delay(2500);
lcd.setCursor(0,1);
lcd.print("                ");
while((analogRead(0)/margen)!=(select/margen)){ //enter
delay(150);
if((analogRead(0)/margen)==(arriba/margen)){  //arriba
if(solape<=0.49){solape=solape+0.01;}
}
if((analogRead(0)/margen)==(abajo/margen)){   //abajo
if(solape>=0.1){solape=solape-0.01;}
}
lcd.setCursor(5, 1);
lcd.print(solape*100,0);
lcd.print(" % ");
}


Acto seguido nos pide si queremos hacer una foto clasica o bién modo noche, en realidad la diferencia radica en que en el modo clasico empieza desde abajo y va subiendo filas mientras que en el modo noche empieza desde arriba y va bajando , ideal para tomas en hora azul explicada anteriormente.


lcd.clear();
lcd.setCursor(1,0);
lcd.print("Modo secuencia");
delay(2500);
lcd.clear();
lcd.setCursor(1,0);
lcd.print("[UP] = Clasico");
lcd.setCursor(1,1);
lcd.print("[DOWN] = Noche");
while(modenight==2){ //enter
delay(150);
if((analogRead(0)/margen)==(arriba/margen)){  //arriba
modenight=0;
}
if((analogRead(0)/margen)==(abajo/margen)){   //abajo
modenight=1;
}
}

Finalmente nos deja elejir un retardo, este sirve para generar una pausa después de dar señal de foto, ideal para fotos de larga exposición , de esta manera evitaremos que salga movida. En teoria debería ir bién pero siendo sincero, aun no he hecho ninguna nocturna...de cara al verano con los mosquitos...ya falta menos !

lcd.clear();     // rutina definimos retardo (modo noche)
lcd.setCursor(0,0);
lcd.print("Define  retardo ");
lcd.setCursor(0,1);
lcd.print("larga exposicion");
delay(2500);
lcd.setCursor(0,0);
lcd.print(" [UP] & [DOWN] ");
lcd.setCursor(0,1);
lcd.print("[SELECT]  valida");
delay(2500);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Define  retardo:");
while((analogRead(0)/margen)!=(select/margen)){ //enter
delay(100);
if((analogRead(0)/margen)==(arriba/margen)){  //arriba
if(retardo<=9800){retardo=retardo+100;}
}
if((analogRead(0)/margen)==(abajo/margen)){   //abajo
if(retardo>=100){retardo=retardo-100;}
}
lcd.setCursor(1, 1);
lcd.print("    ");
lcd.print(retardo/1000,1);
lcd.print(" seg.   ");

}

Aqui es cuando se calculan los pasos que deberan dar nuestros motores en función de los datos que le hemos definido y a continuación nos indicará nuestra focal equivalente que no deja de ser el zoom por el factor recorte del objetivo.



// diagonal_sensor=sqrt((d1*d1)+(d2*d2));

focal=focal*factor;
grados_diagonal=2* (atan(diagonal_sensor/(2*focal))*_360div2xPI);  // 2*focal*factor...
grados_horizontales=factor*(grados_diagonal*d1/diagonal_sensor)*(1-solape);  //originalment 2* (atan(diagonal_sensor/(2*focal*factor))*_360div2xPI);
grados_verticales =factor*(grados_diagonal*d2/diagonal_sensor)*(1-solape);        //originalment grados_horizontales * d2 / d1

pasoX=(pulsosvueltaejex*(grados_horizontales/360));
pasoY=(pulsosvueltaejey*(grados_verticales/360));

lcd.clear();
lcd.setCursor(0,0);
lcd.print ("Focal equivalente");
lcd.setCursor(4, 1);
lcd.print(focal,0);
lcd.print(" mm.");
delay(2000);
}

Acto seguido toca explicar la subrutina de busqueda de origen que es muy simple. Esta llama a la subrutina movimiento que es la encargada de habilitar el movimiento mediante los pulsadores UP/DWN/LFT/RGT y una vez pulsamos sobre SELECT valida los datos.

La busqueda de posición final es igual que la de origen, lo único que se memorizan las posiciones actuales en memorias diferentes y una vez generada el resto es cuando en realidad se determina los grados que van a acaparar nuestra fotografia resultante.


void buscaorigen(){
while((analogRead(0)/margen)!=(select/margen)){ //enter
movimiento();
}
xact=0; 
yact=0;   
xtemp=0;
ytemp=0;  
xmin=xact;   
ymin=yact;
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Posicion  inicio");
lcd.setCursor(1, 1);
lcd.print("- CONFIRMADA -");
origen=1;
delay(3500);
}

void buscafinal(){
while((analogRead(0)/margen)!=(select/margen)){ //enter
movimiento();
}
xmax=xact;
ymax=yact;
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Posicion  final");
lcd.setCursor(1, 1);
lcd.print("- CONFIRMADA - ");
delay(3500);
}
  

Vamos a por la subrutina movimiento(), antes de nada ejecuta la subrutina de analisisTemperatura() para comprobar que no exista mucha temperatura en los drivers de potencia, y acto seguido mediante los pulsadores nos deja hacer movimientos memorizandolos en las variables xact e yact (de actual). Luego más tarde actualiza los datos en la pantalla.

El programa es una simplificación del que yo uso con el LCD de 4 x 20 caracteres, en cambio con este LCD estamos más limitados. En mi caso si determina que estamos haciendo movimientos referidos al punto final también nos mostrará el número previsto de tomas a realizar, eso es útil para nuestra información, yo por ejemplo sé que en la Nikon D5200 puedo hacer poco mas de 800 fotos con una bateria cuando trabaja con el robot panorámico (estabilizador y lcd apagada! ). También el programa nos fuerza a que la posición final siempre sea en sentido más hacia la derecha y mas hacia arriba. Lo siento pero el software de edición de Gigapan's que uso (AutoPanoGiga) sólo contempla 4 modos de reconocimiento y y creo que siempre son de izquierda a derecha...

void movimiento(){
analisisTemperatura();

if((analogRead(A0)/margen)==(right/margen)) {xact=xact+(pasoX);}    //  derecha +X       
if((analogRead(A0)/margen)==(arriba/margen)){yact=yact+(pasoY);}    //   arriba +Y

if((analogRead(A0)/margen)==(left/margen))  {       //  izquierda -X
if (origen==1 && xact>=pasoX){
xact=xact-(pasoX);
}
if (origen==0){
xact=xact-(pasoX);

}     

if((analogRead(A0)/margen)==(abajo/margen)) {    //  abajo -Y 
if (origen==1 && yact>=pasoY){
yact=yact-(pasoY);
}
if (origen==0){
yact=yact-(pasoY);

}     
  


lcd.setCursor(0,0);
lcd.print("Pos X:    Pos Y:");
lcd.setCursor(0, 1);
lcd.print(xact/impX/1.17,1);   
lcd.print(char(223));
lcd.print(" ");
lcd.setCursor(10, 1);
lcd.print(yact/impY/1.17,1);   
lcd.print(char(223));
lcd.print(" ");

/*lcd.setCursor(4,2);
lcd.print((xact/pasoX)+origen,0);
lcd.print(" ");
lcd.setCursor(14,2);
lcd.print((yact/pasoY)+origen,0);
lcd.print(" ");
if (origen!=0){
lcd.setCursor(0,3);
lcd.print("Numero de fotos: ");
lcd.print(((xact/pasoX)+1)*((yact/pasoY)+1),0);
lcd.print(" ");
}
*/

Acto seguido los motores se mueven si han determinado alguna variación en los valores actuales respecto los temporales (antigua lectura)

myStepperX.step(xact-xtemp);
xtemp=xact;
myStepperY.step(yact-ytemp);
ytemp=yact;
delay(20);
}

La siguiente subrutina es la que nos servirá para convertir los segundos a horas, minutos y segundos de una manera un poco "bruta" pero efectiva y acto seguido nos informará de lo mismo.

void conversiontiempo(){  // convertimos tiempo necesario en realizar proyecto 
while(segundos>=3600){   
horas = horas++;
segundos = segundos - 3600;
}
while(segundos>=60){
minutos = minutos++;
segundos = segundos - 60;
}
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Tamano previsto");
lcd.setCursor(0,1);

Previamente nos dirá el tamaño previsto, yo lo tengo configurado para mi Nikon d5200, habria que modificar el 24.2 de la siguiente linea por los megapixeles de vuestra cámara.



lcd.print((((xmax/pasoX)+1)*((ymax/pasoY)+1)*24.2*(1-solape)));

Serial.println(((xmax/pasoX)+1)*((ymax/pasoY)+1));

lcd.print("Mb.");
delay(3500);

Y acto seguido la duración:

lcd.setCursor(0,0);
lcd.print("Duracion prevista");
lcd.setCursor(0,1);
lcd.print(horas);
lcd.print("hr ");
if (minutos<10){
lcd.print("0");
}
lcd.print(minutos);
lcd.print("min ");
if (segundos<10){
lcd.print("0");
}
lcd.print(segundos);
lcd.print("seg");
delay(5000);
}

Venga, que ya falta menos para llegar al fín de la primera parte del tostón... Ahora viene la subrutina pantallaGigapan() que es dónde se ejecutan las instrucciones para mostrar en el LCD la posición actual.



void pantallaGigapan(){
// lcd.clear();
progreso=((nshot*100)/(previsionShot));
lcd.setCursor(0,0);
lcd.print(progreso);
lcd.print("%");
lcd.setCursor(6,0);
lcd.print("X:    Y:");
lcd.setCursor(0,1);
lcd.print(nshot);
lcd.print("s");
lcd.setCursor(6,1);
lcd.print(tempj);
lcd.print("  ");
lcd.setCursor(12,1);
lcd.print(tempi);
lcd.print("  ");
/*
lcd.setCursor(7,3);
lcd.print(xact/5.85,1);   // possar /5
lcd.print(char(223));
lcd.print("  ");
lcd.setCursor(13,3);
if(modenight==0){
lcd.print(yact/5.85,1);     // possar /5
}
else{
lcd.print(-yact/5.85,1);     // possar /5
}

lcd.print(char(223));
lcd.print("  ");
*/
}

Y ahora la visualización de la pantalla de temperatura que se ejecuta cuando retrocede el robot al final de cada fila.
No hay mucho que explicar pues son simples de programar, pero si quieres que quede bonito hay que acabar modificando la posición de los caracteres seguro con la pega que hay que probar y probar. No le hagais mucho caso a los valores mostrados en la siguiente foto , estaba todas estas entradas desconectadas. Si vais a usar la LCD shield tocará modificar el pineado de el voltimetro y del sensor de temperatura, o bién hacer alguna ñapa en la shield conectado como se pueda...



void pantallatemperatura(){
analisisTemperatura();
lcd.clear();
lcd.print("Voltage: ");
voltage=analogRead(A5)*1.212;
lcd.print(voltage/100);   
lcd.print(" v.");
lcd.setCursor(0,1);
lcd.print("Temp.:  ");
lcd.print(valorTempX,1);
lcd.print(char(223));
lcd.print("C");
}

La subrutina peticiopausa() se caracteriza por pedir 10 veces seguidas si está el pulsador SELECT activado, ¿por qué diez veces? Pues porque con una simple NUNCA se enteraba y pasaba de largo. Si detecta que está pulsado se activa una Pausa y hasta que no pulsemos la tecla DOWN no continuará. Acto seguido llama a la subrutina de la pantallaGigapan para actualizar y seguir con el ciclo.


void peticiopausa(){
for (int k=0; k<10; k++){ //prova pausa
if((analogRead(0)/margen)==(select/margen)){     // tecla SELECT si apretem provoquem una pausa fins que no apretem ENTER   
lcd.clear();
lcd.setCursor(2,0);
lcd.print("P a u s a  !  ");
lcd.setCursor(0,1);
lcd.print("[DOWN] = reStart");
delay(1000);  //abans no hi era i em rearrancaba
pause=2;
while(pause>1){
if((analogRead(0)/margen)==(abajo/margen)){  //abajo
pause=0;
lcd.clear();
}
}
}
}
pantallaGigapan();
}

Finalmente la subrutina de analisis de temperatura está pensada para usar dos sensores de temperatura aunque en la pantalla lcd sólo os marcará el del driver X . En este caso si detecta que que la temperatura excede en cualquiera de ellos de 33ºC arranca el ventilador dando señal al PinFan y este sólo se parará cuando la temperatura en ambos drivers sea inferior a 29ºC.


void analisisTemperatura(){
temperaturaX=analogRead(A6);         // comprobamos temperaturaX en disipador LM35DT
temperaturaY=analogRead(A7);         // comprobamos temperaturaY en disipador LM35DT
valorTempX = ((5.0 * temperaturaX * 100 )/1024.0);
valorTempY = ((5.0 * temperaturaY * 100 )/1024.0);
if(valorTempX<29  && valorTempY<29){digitalWrite(PinFan,LOW);}              // deshabilita tensión en ventilador - energy saving - si temperatura < 29ºC
if(valorTempX>33 || valorTempY>33){digitalWrite(PinFan, HIGH);}            // habilita tensión en ventilador si temperatura > 33ºC
}

void slide_dolly(){


Y por hoy esto es todo! Aqui es dónde empieza las funciones de la slide dolly.
Os dejo un video para que veais el comportamiendo de los menus. Las imégenes también tienen un efecto barril provocadas por la camara deportiva AEE SD19 , pero más vale esto que nada no?


Saludos!

3 comentarios:

  1. Hola, un trabajo genial. Enhorabuena, pero he ido copiando el codigo y me sale error. Podrias mandarme el codigo completo.
    Gracias y un saludo.
    dimeblas@hotmail.com

    ResponderEliminar
  2. buenos dias , rebuscando por internet algo de arduino para el tema de gigapanes ,encontre tu blog , y despues de montar la maquina y tods los accesorios ,fui copiando el codigo ... pero no hay manera de ponerlo en marcha . te agradeceria si pudieras enviarme el codigo a mi correo . litryjm@gmail.com , mil gracias de antemano .

    ResponderEliminar
  3. Lo mismo que los anteriores. Si pudieras mandar el código a mi email visitasvirtuales(arroba)hotmail.com Muchas gracias

    ResponderEliminar