Lectura de señales (temperatura, luz, humedad) y almacenamiento en base de datos

Hola,
Empecé hace poquito con Arduino. No tengo ni idea de electrónica. Digamos que lo más cercano a mí es la gestión de datos, y tengo algunas nociones de programación.

El caso, es que para tener un inicio en este, me planteé la lectura de unas variables ambientales sencillas (de momento lo he limitado a un DHT11: temperatura y humedad), y una fotorresistencia), aunque ya veréis que al final la parte de lectura es la menor del código.

El objeto es almacenar con una frecuencia de 30 segundos las medidas de esas variables en una base de datos.

El estado actual del proyecto es:

  • Raspberry Pi, con servidor de datos Web y de base de datos mysql
  • Dos placas Arduino Uno, con Shield Ethernet, y el mismo cableado.
    Cada una con IP fija, un módulo DS3231 para mantener referencia de hora, el sensor DHT11, una fotorresistencia y un buzzer para poder controlar de forma remota si están "vivas".

Esencialmente, las funciones que realizan son:

Responden a entrada por puerto serie para configurar inicialmente, o modificar, la MAC y las IP (la local y la del servidor)

Responden a comandos recibidos por Ethernet a través de una página web para poner en hora el reloj, enviar medidas a petición y realizar chequeo sonoro.

Cada 30 segundos mandan medidas y alarmas al servidor, llamando a un php encargado de subirlo a base de datos

Genera alarmas de control de funcionamiento, que guarda para enviar junto a las peticiones de datos.

En este primer mensaje copio el código que subo a las placas. Luego agregaré la información del otro lado.

El objeto de subir esto, es compartirlo, sobre todo para recibir críticas. Muchas cosas son copia-pega de cosas que he ido encontrando, para ir salvando los escollos que ma aparecían, ya que todo ha sido nuevo para mí.

Ahora me estoy peleando con los módulos Wifi ESP8266, para intentar eliminar cable, pero de momento me están venciendo.

Este es el código de las placas. Parte 1. (no me deja meterlo todo en el mismo mensaje)

//2016/02/03 - Jose M. Sanz
//Si recibe C, ejecuta protocolo de chequeo (buzzer), y genera alarma
//Si recibe R, lee y envía todas las analógicas
//Si recibe S, manda un OK al comando y éste envía la hora. Sincroniza el reloj, genera alarma.
//Alarmas: D1-arranca placa / D2-pasa una hora / D3-chequeo realizado / D4-reloj ajustado / D5-lectura forzada

//S por puerto serie muestra la fecha/hora y los parámetros configurables: 6 bit de MAC, IP placa e IP servidor
//C por puerto serie lanza el proceso de configuración de la IP

#include <SPI.h>
#include <Ethernet.h>
#include <avr/wdt.h>
#include <EEPROM.h>

//Para manejo del DHT11
#include <DHT.h>

#include <Wire.h>
#define DS3231_I2C_ADDRESS 0x68

int incomingByte;//se usa para el carácter recibido por serie
int recibido; //se usa para el carácter recibido por Ethernet
float valor; //valor final que se envía de respuesta
boolean enviado; //se pone a True después de responder a petición de datos (reinicia el envio de alarmas)
String sAlarma=""; //inicializa las alarmas con la cadena que indica que ha arrancado la placa
float horas=0; //controla las horas que lleva la placa encendida. Cada hora genera alarma y la envía.
int horaspas=0;
int frecuencia=30000; //son los milisegundos entre lecturas automáticas que envía la placa
unsigned long mspas=0;

String bseg, bmin, bhora, bmonth, bday, byear;
String cadena, enlace;

EthernetServer server(40); //abrirá puerto 40 a la escucha
EthernetClient clienteweb; //abrirá conexión con el servidor RASP

//DHT11 conectado a digital 2
DHT dht(2, DHT11); // O DHT22 según el sensor que usemos.

//Declaración de la MAC y las direcciones IP
byte mac5=EEPROM.read(0);
byte mac[]={0xDE, 0xAD, 0xBE, 0xEF, 0xFE, mac5};
IPAddress iplocal(EEPROM.read(1), EEPROM.read(2), EEPROM.read(3), EEPROM.read(4)); //IP asignada a la placa
IPAddress ipserver(EEPROM.read(5), EEPROM.read(6), EEPROM.read(7), EEPROM.read(8)); //IP del servidor Web

void setup() {
  wdt_disable(); 
  pinMode(5, OUTPUT); //en el 5 un piezospeaker. Para enterarme desde lejos
  dht.begin();
  Serial.begin(9600);
  Wire.begin();  
  Ethernet.begin(mac,iplocal);
  delay(1000);
  server.begin(); //Arranca el servidor en puerto 40
  
  
  //Genera la alarma de arranque
    sAlarma= sAlarma + "D1";
    sAlarma=sAlarma + "11";
    sAlarma=sAlarma + ReturnTime();
    sAlarma=sAlarma + "*";
    enviado=0;
    
  //Watchdog. Establece tiempo de 8 segundos antes de resetear contador. Si no, resetea la placa.
  wdt_enable(WDTO_8S);
  }

void loop() {

  //Mira si le toca mandar lectura de variables
  if ((millis()-mspas)>=frecuencia){
      Serial.print(ReturnTime());
      Serial.println(" - Intenta envio de medidas");
      mspas=millis();
      cadena= ReadVariables();
      //Gestiona las alarmas pendientes de envio
      cadena=cadena + sAlarma;
      cadena=cadena + "END";
      cadena.replace(" ", "_"); //da problemas el enlace si tiene espacios. Se le han metido guiones bajos que revierte el php
      enlace="/led/getdata.php?ip=";
      enlace=enlace + DisplayAddress(Ethernet.localIP());
      enlace=enlace + "&cadena=";
      enlace=enlace + cadena;
      if (sendGET(enlace)==1){
          enviado = 1;
          sAlarma="";
      }
  }
  wdt_reset();
 
  if (Serial.available()>0) {  
    incomingByte=Serial.read();
    if(incomingByte == 'S'){
      Serial.println(ReturnTime());
      Serial.print("Sexto byte de MAC: ");
      Serial.println (mac[5]);
      Serial.print("IP local: ");
      Serial.println(DisplayAddress(iplocal));
      Serial.print("IP servidor: ");
      Serial.println(DisplayAddress(ipserver));
      }
    if(incomingByte == 'C'){configIP();}
  }
  
  //Prueba de alarma con la función millis()
  //Intenta que se genere una alarma cada hora: 60*60*1000=3600000
  horas=millis()/3600000.0;
  if (horas>(horaspas+1)){
    sAlarma=sAlarma + "D2";
    sAlarma=sAlarma + "11";
    sAlarma=sAlarma + ReturnTime();
    sAlarma=sAlarma + "*";
    enviado=0;
    horaspas=horaspas+1;
  }
  if (horaspas==12){ //Cuando marca 12 horas, hace el reset.
    softwareReset(WDTO_15MS);
    delay(500);
  }
  wdt_reset();
    
  EthernetClient client = server.available();
  if (client.available()>0) {
         wdt_reset();
         recibido=client.read();
         
    //si es S es que quiere sincronizar
    if (recibido == 'S'){
         //Manda OK al python, y éste le manda ahora la hora
         client.println("OK");
         cadena="";        
         //Espera hasta recibir respuesta en el socket
         while(true){if(client.available()>0){break;}}
         while (true){
           recibido=client.read();
           if (recibido==-1){break;}
           cadena=cadena+char(recibido);
           }
           Serial.println(cadena);
        bseg=cadena.substring(12,14);
        bmin=cadena.substring(10,12);
        bhora=cadena.substring(8,10);
        bday=cadena.substring(6,8);
        bmonth=cadena.substring(4,6);
        byear=cadena.substring(2,4);
        setDS3231time(bseg.toInt(), bmin.toInt(), bhora.toInt(), 0, bday.toInt(), bmonth.toInt(), byear.toInt());
        client.println("END");
        Serial.println(ReturnTime());

        //Genera la alarma D4
        sAlarma=sAlarma + "D4";
        sAlarma=sAlarma + "11";
        sAlarma=sAlarma + ReturnTime();
        sAlarma=sAlarma + "*";
        enviado=0;
        }
        
    
    //si es C realiza el protocolo de chequeo. Suena buzzer
    if (recibido == 'C'){
      digitalWrite(5, HIGH);
      delay(500);
      digitalWrite(5, LOW);
      delay(500);
      client.println("END");

      //Genera la alarma D3
        sAlarma=sAlarma + "D3";
        sAlarma=sAlarma + "11";
        sAlarma=sAlarma + ReturnTime();
        sAlarma=sAlarma + "*";
        enviado=0;
      wdt_reset();
      }
    //si es R lee analógicas
    if (recibido == 'R'){
      delay(200);
      client.print(ReadVariables());
      //Crea la alarma de petición de medidas (D5)
        sAlarma=sAlarma + "D5";
        sAlarma=sAlarma + "11";
        sAlarma=sAlarma + ReturnTime();
        sAlarma=sAlarma + "*";
        enviado=0;
      //Gestiona las alarmas pendientes de envio
      client.print(sAlarma);
      enviado = 1;
      sAlarma="";
      client.println("END");
      wdt_reset();
      }
  }
wdt_reset();  
}

Código de Arduino. Parte 2 (el anterior llega hasta el final del loop. Ahora vienen las funciones)

// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{
  return( (val/10*16) + (val%10) );
}
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{
  return( (val/16*10) + (val%16) );
}

void setDS3231time(byte second, byte minute, byte hour, byte dayOfWeek, byte
dayOfMonth, byte month, byte year)
{
  // sets time and date data to DS3231
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0); // set next input to start at the seconds register
  Wire.write(decToBcd(second)); // set seconds
  Wire.write(decToBcd(minute)); // set minutes
  Wire.write(decToBcd(hour)); // set hours
  Wire.write(decToBcd(dayOfWeek)); // set day of week (1=Sunday, 7=Saturday)
  Wire.write(decToBcd(dayOfMonth)); // set date (1 to 31)
  Wire.write(decToBcd(month)); // set month
  Wire.write(decToBcd(year)); // set year (0 to 99)
  Wire.endTransmission();
}
void readDS3231time(byte *second,
byte *minute,
byte *hour,
byte *dayOfWeek,
byte *dayOfMonth,
byte *month,
byte *year)
{
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0); // set DS3231 register pointer to 00h
  Wire.endTransmission();
  Wire.requestFrom(DS3231_I2C_ADDRESS, 7);
  // request seven bytes of data from DS3231 starting from register 00h
  *second = bcdToDec(Wire.read() & 0x7f);
  *minute = bcdToDec(Wire.read());
  *hour = bcdToDec(Wire.read() & 0x3f);
  *dayOfWeek = bcdToDec(Wire.read());
  *dayOfMonth = bcdToDec(Wire.read());
  *month = bcdToDec(Wire.read());
  *year = bcdToDec(Wire.read());
}
String ReturnTime()
{
  byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
  // retrieve data from DS3231
  String sFH="20";
  readDS3231time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month,
  &year);

  sFH=sFH + year;
  sFH=sFH + "-";
  if (month<10){sFH=sFH + '0';sFH=sFH + month;}
  else{sFH=sFH + month;}
  sFH=sFH + "-";
  if (dayOfMonth<10){sFH=sFH + '0';sFH=sFH + dayOfMonth;}
  else{sFH=sFH + dayOfMonth;}
  sFH=sFH + " ";
  if (hour<10){sFH=sFH + '0';sFH=sFH + hour;}
  else{sFH=sFH + hour;}
  sFH=sFH + ":";
  if (minute<10){sFH=sFH + '0';sFH=sFH + minute;}
  else{sFH=sFH + minute;}
  sFH=sFH + ":";
  if (second<10){sFH=sFH + '0';sFH=sFH + second;}
  else{sFH=sFH + second;}
    
  return sFH;
}

String ReadVariables()
{
  int lec_analog;
  String cadena;
  
  //temperatura DHT11
  float t = dht.readTemperature();
  if (isnan(t)){t=65535;} //Si no hay lectura buena, mete un 655535
  //humedad DHT11
  float h = dht.readHumidity();
  if (isnan(h)){h=65535;} //Si no hay lectura buena, mete un 655535
  
  delay(500);
  //lectura de luz en analogica 1
  lec_analog = analogRead(1);
    
  cadena="VALUES*";
  //A0 es temperatura del DHT11
  cadena=cadena+"A0";
  cadena=cadena+ReturnTime();
  cadena=cadena + t;
  cadena=cadena + "*";    
  //A1 es la intensidad de luz
  cadena=cadena + "A1";
  cadena=cadena+ReturnTime();
  cadena=cadena + lec_analog;
  cadena=cadena + "*";
  //A2 es humedad del DHT11
  cadena=cadena+"A2";
  cadena=cadena + ReturnTime();
  cadena=cadena + h;
  cadena=cadena + "*";
  wdt_reset();
  return cadena;
} 

void softwareReset( uint8_t prescaller) {
  // start watchdog with the provided prescaller
  wdt_enable( prescaller);
  // wait for the prescaller time to expire
  // without sending the reset signal by using
  // the wdt_reset() method
  while(1) {}
}

boolean sendGET(String enlace) //client function to send GET request data.
{
  Serial.println(enlace);
  if (clienteweb.connect(ipserver, 80)) {
    delay(200);
    clienteweb.print("GET ");
    clienteweb.print(enlace);
    clienteweb.println(" HTTP/1.0");
    clienteweb.println();    
  }
  else {
    Serial.println("Conexion a servidor web erronea");
    Serial.println();
    return 0;
  }
  wdt_reset();
  delay(500);
  char a;
  while (clienteweb.available()){
    a=char(clienteweb.read());
    Serial.print(a);
  }
  
  clienteweb.flush();
  clienteweb.stop();
  return 1;
}

String DisplayAddress(IPAddress address)
{
 return String(address[0]) + "." + 
        String(address[1]) + "." + 
        String(address[2]) + "." + 
        String(address[3]);
}

//Protocolo de configuración de los datos IP, y grabación en la EEPROM.
//Luego hace un reset, para ya reiniciar con los parámetros grabados en la EEPROM.
void configIP(){

String cIP[4]={"","","",""};
int i,j;
char c;

for (i=1;i<5;i++){
  digitalWrite(5, HIGH);delay(100);
  digitalWrite(5, LOW);delay(100);
}

Serial.print("Sexto bit de la direccion MAC (");
Serial.print(mac[5]);
Serial.println(") Z acaba");

cadena="";
while(true){
  wdt_reset();
  if(Serial.available()>0){
    recibido=Serial.read();
    if (recibido == 'Z'){break;}
    cadena=cadena+char(recibido);
  }
}
Serial.println(cadena);
EEPROM.write(0,cadena.toInt());

Serial.print("Direccion IP local (");
Serial.print(DisplayAddress(iplocal));
Serial.println(") Z acaba");

cadena="";
while(true){
  wdt_reset();
  if(Serial.available()>0){
    recibido=Serial.read();
    if (recibido == 'Z'){break;}
    cadena=cadena+char(recibido);
  }
}
Serial.println(cadena);

j=0;
cIP[0]="";cIP[1]="";cIP[2]="";cIP[3]="";
for (i=0;i<cadena.length();i++){
  c=cadena.charAt(i);
  if (c!='.'){cIP[j]=cIP[j]+c;}
  else{j++;}
}
EEPROM.write(1,cIP[0].toInt());
EEPROM.write(2,cIP[1].toInt());
EEPROM.write(3,cIP[2].toInt());
EEPROM.write(4,cIP[3].toInt());

Serial.print("Direccion IP servidor Web (");
Serial.print(DisplayAddress(ipserver));
Serial.println(") Z acaba");

cadena="";
while(true){
  wdt_reset();
  if(Serial.available()>0){
    recibido=Serial.read();
    if (recibido == 'Z'){break;}
    cadena=cadena+char(recibido);
  }
}
Serial.println(cadena);
j=0;
cIP[0]="";cIP[1]="";cIP[2]="";cIP[3]="";
for (i=0;i<cadena.length();i++){
  c=cadena.charAt(i);
  if (c!='.'){cIP[j]=cIP[j]+c;}
  else{j++;}
}
EEPROM.write(5,cIP[0].toInt());
EEPROM.write(6,cIP[1].toInt());
EEPROM.write(7,cIP[2].toInt());
EEPROM.write(8,cIP[3].toInt());

//Hace el reset
delay(1500);
softwareReset(WDTO_15MS);
delay(500);

}

Aquí va el php al que llama la placa en su proceso de pasar datos al servidor: getdata.php

<!DOCTYPE html>
<html>
<head><title>Recibe datos de Arduino</title></head>
<?php
include("funciones.php");
if (!isset($_GET["ip"])){registra_error("No se ha recibido dirección IP");}
else {$ip=$_GET["ip"];}
if(!isset($_GET["cadena"])){registra_error("No se ha recibido la cadena de datos");}
else {$cadena=$_GET["cadena"];}
echo "
" . $ip;
echo "
" . $_SERVER['REMOTE_ADDR'];
echo "
" . $cadena;
$cadena=str_replace("_"," ",$cadena);
sqlmedida($ip, $cadena);
?>
<body></body></html>

Este es el funciones.php (lo usan varios scripts) que incluye al inicio

<?php
//funciones.php

function sqlmedida($puerto,$scadena)
{
$alarma = 0;
$parciales = explode("*",$scadena);
$j = count($parciales) - 1;
//El primer parcial es el encabezado. Se desecha
for ($i = 1; $i <=$j ; $i++){
    //Para cada parcial el primer carácter indica si es analógica o digital
    $andig=substr($parciales[$i],0,1);
    if ($andig=="A"){
        //Es una analógica
        $alarma=$alarma + agrega_medida($puerto, $parciales[$i]);
    }
    if ($andig=="D"){
        agrega_alarma($puerto, $parciales[$i]);
    }
 }

return 0;
}

function detcomando($puerto){
//Determina si el puerto es serie o una dirección IP
if (strstr($puerto,"/dev/tty"))
    {$comando="/usr/bin/python3 /var/www/html/cgi-bin/serie.py ";}
elseif (strstr($puerto, "192.168."))
    {$comando="/usr/bin/python3 /var/www/html/cgi-bin/tcpip.py ";}
else
    {echo "
Puerto de placa no esperado";return 1;}
return $comando;
}

function agrega_medida($puerto,$mvariable){
    $long=strlen($mvariable);
    $codigo=substr($mvariable,0,2);
    $fechahora=substr($mvariable,2,19);
    $fecharecibido=date("Y-m-d H:i:s");
    $valor=substr($mvariable,21,$long-2);
    $numalarmas=0;
          
//Abre conexion a base de datos
$mysqli = new mysqli('localhost','root','password','ADQDATA');
//$mysqli = new mysqli($ServerDB, $UserDB, $PasswordDB, $NameDB);

if ($mysqli->connect_error)
 {echo "
" . "ERROR EN LA CONEXION SQL"; return 0; }

//Busca el codigo de la variable
$sql = "SELECT acodigo, afaditivo, afmultip, aDecimales, amin, amax FROM analogicas 
WHERE (apuerto = '$puerto' AND adirecc = '$codigo' AND aActivado)";
$resultado = $mysqli->query($sql);

if (!$resultado)
 {echo "
" . "ERROR EN LA CONSULTA SQL"; return 0; }

if ($resultado->num_rows == 0)
    {registra_error("No encontrada variable " . $puerto . "-" . $codigo);return 0;}
elseif ($resultado->num_rows == 1)
 {
 //Recupera la matriz de campos de la fila
 $linea = $resultado->fetch_assoc();
 //Extrae los valores de los campos
 $code = $linea["acodigo"];
        $aditivo=$linea["afaditivo"];
        $multip=$linea["afmultip"];
 $decimales = $linea["aDecimales"];
        $calcu = $valor + $aditivo;
        $calcu = $calcu * $multip;
 $calcu = round($calcu, $decimales);
               
 //Inserta el registro
 $sql = "INSERT INTO historia_medidas (hm_codigo, hm_fecha, hm_fecha_recibido, hm_valor) 
VALUES ($code, '$fechahora' , '$fecharecibido' ,$calcu)";
 if ($mysqli->query($sql) === TRUE)
 {echo "
" . "Registro insertado OK: " . $fechahora . "-" . $code . "-" . $calcu;}
 else
 {echo "
" . "Error en la insercion de registro"; return 0;}
 }

return $numalarmas;
}

function agrega_alarma($puerto,$mvariable){
    $long=strlen($mvariable);
    $codigo=substr($mvariable,0,2);
    $valor=substr($mvariable,2,2);
    $fechahora=substr($mvariable,4,19);
    $fecharecibido=date("Y-m-d H:i:s");
    $numalarmas=0;
        
//Abre conexion a base de datos
$mysqli = new mysqli('localhost','root','password,'ADQDATA');
//$mysqli = new mysqli($ServerDB, $UserDB, $PasswordDB, $NameDB);

if ($mysqli->connect_error)
 {echo "
" . "ERROR EN LA CONEXION SQL"; return 0; }

//Busca el codigo de la variable
$sql = "SELECT dcodigo FROM digitales 
WHERE (dpuerto = '$puerto' AND ddirecc = '$codigo')";
$resultado = $mysqli->query($sql);

if (!$resultado)
 {echo "
" . "ERROR EN LA CONSULTA SQL"; return 0; }

if ($resultado->num_rows == 0)
    {registra_error("No encontrada variable " . $puerto . "-" . $codigo);return 0;}
elseif ($resultado->num_rows == 1)
 {
 //$fechahora = date("Y-m-d H:i:s");
 //Recupera la matriz de campos de la fila
 $linea = $resultado->fetch_assoc();
 //Extrae los valores de los campos
 $code = $linea["dcodigo"];
 if ($valor=='11'){$al=1;}         
        elseif ($valor=='00'){$al=0;}
        else{$al=2;}
               
 //Inserta el registro
 $sql = "INSERT INTO historia_alarmas (ha_codigo, ha_fecha, ha_fecha_recibido, ha_valor) 
VALUES ($code, '$fechahora' , '$fecharecibido' , $al)";
 if ($mysqli->query($sql) === TRUE)
 {echo "
" . "Registro insertado OK: " . $fechahora . "-" . $code . "-" . $al;}
 else
 {echo "
" . "Error en la insercion de registro"; return 0;}
 }

return $numalarmas;
}

function registra_error($cadena){
    //Registra algunos mensajes de error en la tabla de historia_errores
    //Abre conexion a base de datos
$mysqli = new mysqli('localhost','root','ketinueb','ADQDATA');

if ($mysqli->connect_error) {return 0; }

$fecharecibido=date("Y-m-d H:i:s");

$sql = "INSERT INTO historia_errores (he_fecha_recibido, he_texto) 
VALUES ('$fecharecibido', '$cadena')";
 if ($mysqli->query($sql) === TRUE)
 {return 0;}
 else
 {return 0;}
}

?>

Faltaría el código de los python para enviar órdenes a las placas, así como la página que uso para llamarlos. Luego los copio.