Acceso aleatorio a archivo .ini en tarjeta SD

Hola, recientemente me he estado metiendo con el tema de las SD y cómo no… ya que tenemos la posibilidad de leer y escribir en una memoria externa… ¿a nadie se le ha ocurrido el guardar valores de configuración en la SD? Por supuesto la respuesta es que a todo el mundo se le ha ocurrido… pero llegados a este punto… y sobre todo si vienes de otros lenguajes de programación de alto nivel… aparece un pequeño problemilla al tratar de hacer lo que uno quiere con el archivo de configuración…

Los tutoriales que he encontrado sin entrar en el tema de JSON… hablan de leer todo el archivo en un buffer… procesarlo poco a poco como si de chinos se tratase… y eso sólo para leer datos… cuando se trata de modificar un registro específico en el archivo de configuración el proceso es aún más tedioso puesto que hay que leer todo el archivo en un buffer… procesarlo hasta encontrar el valor a modificar… luego una vez eso hecho volcarlo todo el buffer de nuevo a la SD…

Aquí les propongo una forma distinta de hacerlo… algo menos engorrosa y que para los no iniciados creo que les será de gran ayuda…

Pongamos que tenemos un archivo config.ini como el siguiente:

[Creditos]
Autor = trystan4861
FechaCreacion = 2018/04/03
[/Creditos]
[Sensores]
Cantidad = 3
[Sensor 1]
Pin = A0
[/Sensor 1]
[Sensor 2]
Pin = 5
[/Sensor 2]
[Sensor 3]
Pin = 6
[/Sensor 3]
[/Sensores]

Como dice la mayoria de tutos que aparecen al googlear’ «acceso a archivos en tarjeta sd en arduino» (o algo por el estio…) si queremos acceder a la parte del archivo que representa el valor del pin del sensor 3… como dije antes, tenemos que o bien leer y luego procesar toooodo el archivo (que no es moco de pavo, más cuando el archivo puede ser de miles de líneas) hasta encontrar lo que necesitamos… o bien saber en qué posición se encuentra dicho dato y hace un seek… (que si no llevas un control exhaustivo del mismo es una tarea faraónica…) para luego leer y procesar a partir de la posición buscada…

Y si lo que queremos en vez de leer un valor es modificar por ejemplo el pin del sensor 2… tenemos que tener todo el archivo en memoria para que tras procesar el valor a modificar y dejarlo como queremos que se guarde, se haga un volcado completo al archivo de configuración…

Por el momento ésta es la única forma que he encontrado para atacar el asunto de los archivos ini…
(Repito sin meternos con temas de JSON)

La verdad me pareció que era un poco… cómo decirlo… ¿arcaico? ¿poco eficiente?.. en resumen… no me gustaba ni un pelo el tener que meterme con todo ese tedioso proceso para sacar un simple valor de un archivo de texto en una tarjeta SD de gigas de tamaño…

¿Gigas de tamaño? ¿Es cierto verdad? Ahora son tan baratas las microsd que por cuatro duros (unos 3 euros en realidad en aliexpress) puedes conseguirte tarjetas de hasta 16 gigas… ¿Teniendo taaaanto espacio libre por qué no…? y aquí llegó la idea…

En vez de tener la estructura del config.ini anterior… si extrapolamos el problema para que tanto el nombre del archivo de configuración como las secciones sean directorio y sus subdirectorios… que el nombre de la variable sea un nombre de archivo y el valor de dicha variable en realidad sea todo el contenido del mismo… ¿no tendríamos ya todo solucionado?

Se podría acceder directamente a un valor sin importarnos en qué subsección se encuentre, si tiene una o mil variables de configuración, además, podríamos modificar directamente el contenido de dicha variable sin necesidad de lidiar con el resto del archivo de configuración…

De modo que me puse al tema y creé 2 pequeñas, pero poderosas funciones que solventan el problema presentado de un plumazo… y esas son:

void Write_INI_Value(String INIFile,String KeyPath,String Key,String Value)
{
  String FullPath="/"+INIFile+"/"+KeyPath+"/";
  SD.mkdir(FullPath);
  File MyINI=SD.open(FullPath+Key+".txt",O_CREAT|O_TRUNC|O_WRITE);
  MyINI.print(Value);
  MyINI.close();
}
String Read_INI_Value(String INIFile,String KeyPath,String Key,String DefaultValue="UNKNOWN_KEY",bool CreateIfNotExists=false)
{
  String Value=DefaultValue;
  String FullPath="/"+INIFile+"/"+KeyPath+"/";
  File MyINI=SD.open(FullPath+Key+".txt",FILE_READ);
  if (MyINI)
  {
    Value="";
    while (MyINI.available()) Value+=(char) MyINI.read();
    MyINI.close();
  }
  else if (CreateIfNotExists) Write_INI_Value(INIFile,KeyPath,Key,Value);
  return Value;
}

De modo que estas dos funciones te permitirán acceder directamente a un valor específico de nuestro archivo de configuración virtual (recordemos que en realidad es una estructura de directorios en vez de un archivo real en sí)

La primera permite crear una estructura donde se almacenará el valor de una variable de configuración, permitiéndote crear distintos “archivos de configuración” simplemente cambiando el nombre del archivo a través de la variable INIFile… crear múltiples subsecciones usando la barra “/” como separador de subsecciones en la variable KeyPath…

Tal vez algunos piensen que sería mejor en vez de usar directamente mkdir comprobar si existe o no la estructura de directorios antes de hacerlo… pero si existe usas una función, si no existe usas dos… no sé exactamente cuanto tiempo se puede ahorrar, si es que se ahorra algo al comprobar si existe o no antes de tratar de crearlo… pero la función mkdir crea la estructura si no existe… y si existe simplemente devuelve un valor false indicando que no ha sido posible crearla… por algún error, entre ellos el que aparece cuando ya existe dicha estructura…

Para ver cómo interactuar con las funciones simplemente usa este código que te propongo a continuación, la primera vez que se ejecute mostrará un valor “UNKNOWN_KEY” al tratar de acceder a la configuración de la ip del router… a partir de la siguiente ejecución mostrará directamente el valor introducido…

/*
 * Aproximación al acceso aleatorio a archivos ini en tarjeta sd para arduino
 * by trystan4861 201800403
 * 
 * Pines de la SDcard
 ** MOSI - pin 11
 ** MISO - pin 12
 ** CLK - pin 13
 ** CS - pin 10
*/

#include <SPI.h>
#include <SD.h>
#define INIFileName "Config"
String valor;
void Write_INI_Value(String INIFile,String KeyPath,String Key,String Value)
{
  String FullPath="/"+INIFile+"/"+KeyPath+"/";
  SD.mkdir(FullPath);
  File MyINI=SD.open(FullPath+Key+".txt",O_CREAT|O_TRUNC|O_WRITE);
  MyINI.println(Value);
  MyINI.close();
}
String Read_INI_Value(String INIFile,String KeyPath,String Key,String DefaultValue="UNKNOWN_KEY",bool CreateIfNotExists=false)
{
  String Value=DefaultValue;
  String FullPath="/"+INIFile+"/"+KeyPath+"/";
  File MyINI=SD.open(FullPath+Key+".txt",FILE_READ);
  if (MyINI)
  {
    Value="";
    while (MyINI.available()) Value+=(char) MyINI.read();
    MyINI.close();
  }
  else if (CreateIfNotExists) Write_INI_Value(INIFile,KeyPath,Key,Value);
  return Value;
}
void setup() {
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial); // wait for serial port to connect. Needed for native USB port only
  Serial.println("Iniciando  SDCard -> ");
  if (!SD.begin(10))
  {
    Serial.println("¡Error al inicializar la SDCard!");
  }
  else
  {
    Serial.println("Procesando datos.");
    valor=Read_INI_Value(INIFileName,"LAN","RouterIP");
    Serial.print("(Primer acceso a la configuración) RouterIP: ");Serial.println(valor);
    if (valor=="UNKNOWN_KEY") Write_INI_Value(INIFileName,"LAN","RouterIP","192.168.1.1");
    Serial.print("(Segundo acceso a la configuración) RouterIP: ");valor=Read_INI_Value(INIFileName,"LAN","RouterIP");
    Serial.println(valor);
    valor=Read_INI_Value("SIGNIFICADO","DE/LA","VIDA");
    Serial.print("LA RESPUESTA AL SIGNIFICADO DE LA VIDA ES: ");Serial.println(valor);
  }
}

void loop() {}

A ver si entendí: ¿le dejas al sistema de archivos que separe las "entradas" de una configuración en archivos pequeños y delimitados por directorios ("carpetas")? Tiene sentido...

No me lo tomes a mal, pero tengo una objeción a todo esto: a menos que uses un micro con al menos 4 KB de RAM, tus funciones tienen una "media-alta" probabilidad de colgar la ejecución del programa.

¿Por qué? Por ejemplo: el Arduino Uno, Nano, y Pro Mini usan el ATmega328P; un microcontrolador con apenas 2 KB de RAM. La librería SD necesita aprox. 700 bytes (34% de la RAM) solo desde el arranque (sin contar los objetos File que se crean en tiempo de ejecución). Ese no es el problema, lo es cuando le sumamos que cada objeto File como variable global (en contexto local ese espacio se libera cuando la función "muere") ocupa al menos el espacio de una entrada de la FAT (que creo que son 32 bytes). Dependiendo del código, esto último tampoco puede ser el problema; sin embargo, LA AMENAZA LATENTE ESTÁ EN COMO SE USA String, en especial cuando se concatena. Lo que llamó mi atención es esto:

if (MyINI)
{
  Value="";
  while (MyINI.available()) Value+=(char) MyINI.read();
  MyINI.close();
}

Entiendo que es para archivos de un solo dato, pero si por algún motivo dicho archivo es de 100 bytes, probable y la ejecución acaba ahí. ¿Razón? Para no dar la historia larga, se debe simplemente a que concatenar strings de esa manera puede llevar a "fragmentar" el espacio libre y eventualmente colgar el programa por no encontrar uno lo suficientemente grande para crear cierto objeto que el código "por fuerza" solicita.

Reitero, con Arduino Mega no tengo nada en contra, es cuando se usan los basados en ATmega328P donde hay que tener ese cuidado. En tu ejemplo de prueba de seguro que funcionó sin problemas, pero combínalo con otro proyecto que haga el código más complejo (en especial que agregue más librerías) y me cuentas cómo te fue. Por experiencia propia, me atrevo a decir que con que se detecten menos de 300 bytes libres (RAM) en tiempo de compilación, a partir de ahí empieza el dolor de cabeza.

Es cierto que no es la panacea, está claro que el uso de la tarjeta SD conlleva un gran consumo de recursos, sé que no es la mejor de las opciones... pero...

La idea radica en el hecho de que no vas a ponerte a hacer esto si no lo necesitas... colocar una sd a un sistema tiene sus ventajas y sus inconvenientes... pero una vez que ya tienes puesto el shield... y has agregado la librería SD... nada más puedes hacer... no? las funciones que he presentado son para solventar el problema de tener que acceder a un archivo de configuración único y procesarlo tediosamente... claro está que si no has colocado previamente tanto el shield como la librería y te has puesto a tratar con el archivo de marras... no necesitas las funciones...

El aporte no es para "obligar" a colocar una SD, es para ayudar a aquellos que ya la tienen colocada a reducir drásticamente el código fuente encargado de procesar el archivito...

Para aquellos que no lo necesitan... siempre les puede aportar la idea de que con un simple cambio de enfoque se pueden solventar problemas engorrosos...

En cuanto a lo de usar los strings... es porque al leer los datos me presentaba la cadena contenida como números ascii bueno, hay que recordar que en realidad estás accediendo a un "archivo de configuración"... no es muy común tener la biblia en pastas escrita en sánscrito como contenido de una variable de configuración... está en la discreción de cada uno el aplicar las cosas como mejor le vengan en cada caso... ¿hay otra forma de conseguir lo que se precisa? Bienvenida sea :) estamos para aprender en la vida y es triste llegar al sobre, a las 3 de la mañana, y no poder decir que "he aprendido algo hoy" :P

No entiendo eso de sumar cada objeto file... no sé cómo se tratan las variables en c... yo vengo de otro leguaje de programación... pero entiendo que una vez que la ejecución de la función ha finalizado la variable... ¿muere? si no es así ¿tal vez debería crear realmente una variable global? Si se va a usar más de un registro de memoria (de los bytes que sean) para acceder a los ficheros... mejor crear uno único... total... no se va a acceder simultáneamente a más de un archivo ¿verdad?

Gracias por tus comentarios

trystan4861: El aporte no es para "obligar" a colocar una SD, es para ayudar a aquellos que ya la tienen colocada a reducir drásticamente el código fuente encargado de procesar el archivito...

Lo sé, pero solo comentaba el detalle de la memoria. Dejando eso de lado, tu solución está genial. :)

trystan4861: En cuanto a lo de usar los strings... es porque al leer los datos me presentaba la cadena contenida como números ascii

Caracteres, bytes; en ASCII son lo mismo. Por "números ASCII" no está claro si te refieres al valor binario de los caracteres, o caracteres que representan números.

trystan4861: bueno, hay que recordar que en realidad estás accediendo a un "archivo de configuración"... no es muy común tener la biblia en pastas escrita en sánscrito como contenido de una variable de configuración...

No lo decía en ese extremo, sino que tu idea no necesariamente puede aplicarse sólo a temas de configuración; también podría ser una especie de base de datos, donde (por algún motivo) haya un campo donde se permitan textos "relativamente" largos.

trystan4861: ¿hay otra forma de conseguir lo que se precisa?

En mi caso, yo hubiera preferido los registros de longitud fija; pero como todo, tiene sus desventajas.

trystan4861: No entiendo eso de sumar cada objeto file... no sé cómo se tratan las variables en c... yo vengo de otro leguaje de programación... pero entiendo que una vez que la ejecución de la función ha finalizado la variable... ¿muere? si no es así ¿tal vez debería crear realmente una variable global? Si se va a usar más de un registro de memoria (de los bytes que sean) para acceder a los ficheros... mejor crear uno único... total... no se va a acceder simultáneamente a más de un archivo ¿verdad?

Cuando una variable permanece durante toda la ejecución, es global y el compilador la toma en cuenta a la hora de reportar la memoria libre inicial. Las variables locales se crean y destruyen dentro de una función; como son temporales, no se toman en cuenta por el compilador.

Ahora, cuando hablo de sumar el uso de memoria con objetos File, es porque la creación (declaración) de estos dentro de una función, no se toma en cuenta en el reporte del compilador.

Para tu ejemplo claramente sólo un archivo a la vez se abre; sin embargo, combinado con otro proyecto puede darse el caso que más de uno esté abierto al mismo tiempo. Uno mismo no se puede abrir dos veces simultaneas, distintos sí. ¿Problemas? Prácticamente ninguno ya que la ejecución del programa siempre es secuencial. El único muy extraño caso donde sí podría, es que el programa principal y una rutina de interrupción hagan uso del mismo objeto File.

Dices venir de otro lenguaje; si es de Java, comprenderás mucho lo que voy a decir a continuación:

Al ser Arduino programado con un lenguaje de alto nivel, la memoria RAM se distribuye en dos secciones de tamaño acorde al momento (variable en el tiempo): la pila de ejecución y el heap.

En la pila de ejecución, se almacenan las variables "primitivas" (las de tipo meramente numérico, incluidos los punteros de cualquier tipo de dato) declaradas localmente (solamente dentro de una función); crece cuando se invoca una función, y decrece cuando una termina. El mal manejo de esta también provoca cuelgues, el error es conocido como "desbordamiento de pila" ("stack overflow" en inglés) y usualmente es causado por un nivel muy profundo (o infinito) de recursividad. Consume la memoria desde el final hacia el principio.

Luego tenemos el heap; aquí es donde se almacenan las variables "primitivas" globales (o declaradas con la palabra static) y las estructuras de datos (struct, vectores y atributos de objetos). Las cadenas de caracteres son vectores, los objetos String realmente ocupan 6 bytes en el heap (puntero a un vector, tamaño de este y longitud real de la cadena; respectivamente cada uno de tipo char*, int, int). ¿Recuerdas cuando dije que concatenar con String podía colgar el programa? Bueno, pues el hacerlo muy repetidamente provoca que el objeto esté solicitando reasignar espacio al vector que contiene la cadena; y el estar en eso puede generar "huecos". El problema con estos es que el manejador de memoria dinámica (el heap) de Arduino no implementa compactación; entonces si entre tanto "hueco" no encuentra espacio para un objeto, lanza una excepción (alerta de situación excepcional) que Arduino tampoco maneja (al no hacerlo, el micro recurre a "congelarse" o "colgarse").

Estas deficiencias en la implementación de C++ para Arduino tienen un único motivo: rendimiento. No es justo comparar un PC que tiene procesador de 4 o hasta 6 núcleos, de 64 bits y que van a velocidades en el rango de los GHz; con un pobre ATmega328P que tiene un procesador de 8 bits, un único núcleo y va a 16 MHz. Y en memoria RAM sobra decir... La compactación y "recolección de basura" en un PC tarda máximo un parpadeo de ojos; mientras que un Arduino podría llevarse hasta varios segundos (asumiendo hipotéticamente que tiene dichas funcionalidades). Sólo imagínate que ridículo sería que un Arduino repentinamente deje de funcionar por entrar en un ciclo de "recolección de basura".

Creo que he hablado "sin frenos", así que avísame si te he dejado atrás (tienes algo sin entender) para que me alcances.

Ummm nop... no es de java... java lo di en un ciclo de especialización como optativa y de lo poco que me ha quedado de cuando lo di... es de lo rico que sabe el café... por el logo de java... realmente me he vuelto adicto al café XD

Hablando en serio... la verdad es que tampoco tenía muchas conmigo cuando hice estas funciones... sólo se basan en tratar de lanzar la pelota al tejado de otro sabiendo que por tu cara bonita te dará el resultado que quieres... es decir... solventar el procesamiento de un archivo para obtener un dato específico... ¿que es poco eficiente si tienes que abrir 100 archivos para 100 variables? pues claro... pero la idea es que el uso de estas funciones se realicen en el setup() no en el loop() de modo que aunque se haga un poco lento la carga inicial... sería sólo la carga inicial... tras realizar todo lo que necesites y antes de finalizar el setup() debería ejecutarse alguna rutina para "tirar la basura a la papelera"

Cada usuario es libre de realizar las modificaciones que crea oportunas... yo puse lo del += (char) para evitar que se mostrase en el ejemplo algo como 341978236481725634781 (he aporreado el teclado, no tiene siginificado real) en vez de 192.168.1.1

Deberías tratar este aporte como una pequeña herramienta que te puede ayudar en determinados casos... no deberías pensar en el asunto como el cuento del árbol y el columpio |500x346

trystan4861: pero la idea es que el uso de estas funciones se realicen en el setup() no en el loop() de modo que aunque se haga un poco lento la carga inicial... sería sólo la carga inicial... tras realizar todo lo que necesites y antes de finalizar el setup() debería ejecutarse alguna rutina para "tirar la basura a la papelera".

Ahh ya veo, no se pretende usar tanto como base de datos; sino como una forma más intuitiva de usar una SD por EEPROM de configuraciones. Lo de "tirar la basura a la papelera" sí sucede mientras todas las variables usadas en el proceso sean locales. Aunque el problema de la memoria sigue aplicando, ya no por la posible fragmentación, sino porque desde el comienzo (reportado por el compilador) no haya suficiente. Bueno... ese no es tu problema, lo es de quien hace el resto del código del proyecto que, evidentemente, puede mejorarse.

trystan4861: Cada usuario es libre de realizar las modificaciones que crea oportunas... yo puse lo del += (char) para evitar que se mostrase en el ejemplo algo como 341978236481725634781 (he aporreado el teclado, no tiene siginificado real) en vez de 192.168.1.1

Simple explicación: read() retorna int; y si se intenta concatenar un objeto String con este tipo de dato, el resultado es que se adjunta la representación textual del valor de ese int, al final de la cadena. Entonces, efectivamente ese "341978236481725634781" era la concatenación de los valores decimales de cada caracter en el archivo; y hacer la conversión ("casting") explícita a char era la solución, porque así String iba a agregar el caracter en sí, en vez de su valor decimal.

Según la tabla ASCII, los caracteres "imprimibles" rondan entre el valor decimal 32 y 126; entonces la secuencia "341978236481725634781" la puedo decodificar como... ¿"ÅR$0¬8"N? Es evidente que me perdí; aunque en mi defensa, si tomábamos las cifras de dos en dos, se leían valores menores a 32.

trystan4861: Deberías tratar este aporte como una pequeña herramienta que te puede ayudar en determinados casos... no deberías pensar en el asunto como el cuento del árbol y el columpio

Por supuesto que no.

Todo esto anduvo entre mal entendido y crítica "constructiva"; sin embargo es obvio que hay que reconocer que toda intención de colaborar es bienvenida y se agradece ;)

341978236481725634781 (he aporreado el teclado, no tiene siginificado real)

Sí, la idea era evitar la EEPROM... En mi caso es un sistema de IoT que controla a través de unos tag rfid, un rdm6300, un rtc, un sd shield, un relé, un buzzer, un display, un UNO y un ESP8266 (los créditos están en una base de datos en la nube) el encendido de las luces de una cancha deportiva, donde los "abonados" gastarán créditos, previamente pagados, para encender dichas luces, a razón de 1 crédito / 1 hora, permitiendo que, en caso de corte del suministro, el sistema "recuerde" por dónde iba... y continúe como si nada hubiera pasado, aparte de existir tags especiales para eventos/mantenimiento/etc que poseen bien créditos infinitos o distinto tiempo asignado a cada crédito... (mi otro aporte sobre el RDM6300 es también parte de este proyecto)

Leer datos desde EEPROM o SD puede parecer lo mismo, pero la ventaja de tener la sd es que si hay que modificar algún dato se puede hacer fácilmente desde incluso un móvil sacando la sd del arduino... cosa que si se guardasen los códigos tag especiales en la EEPROM... ya me dirás la parafernaria que hay que llevar al poli para realizar cambios :S