Category Archives: Proyectos / Tutoriales

Cómo instalar Letsencrypt en una cuenta compartida de GoDaddy

23 Dic , 2017,
Jose Nunez
, , , ,
No Comments

Esta publicación se basa en el siguiente artículo Mike Seifriedhow-to-install-a-lets-encrypt-ssl-on-a-shared-godaddy-hosting-account

(!) Advertencia: Este procedimiento involucra crear una llave privada para su servidor desde un sitio de un tercero (ZeroSSL). Esto implica que ZeroSSL tendrá acceso a la llave privada que define la identidad de su sitio web. Se recomienda que para implementaciones que requieran una mayor seguridad, la llave privada de la identidad de su sitio web no sea compartida con terceros.


Paso 1 – ZeroSSL

Usando el servicio https://zerossl.com/ es posible generar los archivos necesarios para configurar un servicio SSL.

  1. Vaya a ZeroSSL, (una interface web para obtener certificados mediante Let’s Encrypt)
  2. Haga clic en “Online Tools” y luego escoja “FREE SSL Certificate Wizard”.
  3. Solicitud de firma de certificado: Digite la dirección del dominio al que desea configurar en la casilla de la derecha/arriba “Domains ONLY IF YOU DON’T HAVE CSR” y haga clic en NEXT. Esto generará un Certificate Sign Request (CSR) en la casilla de la derecha/abajo. Si ya tiene un CSR solamente pegue el contenido del mismo en la casilla derecha/abajo. Haga clic en el botón DOWNLOAD (derecha/abajo) para descargar el CSR que podrá reutilizar en futuras renovaciones de su certificado.
  4. Llave de su cuenta: Digite su dirección de email en la casilla izquierda/arriba “” y haga click en NEXT. Esto generará un “account key” en la casilla izquierda abajo. Si ya tiene un “account key” de Let’s Encrypt solo pegue el contenido en la casilla izquierda/abajo y haga clic en NEXT para el siguiente paso. Recuerde que puede usar el boton DOWNLOAD (izquierda/abajo) para descargar el “account key” y así reutilizarlo en futuras operaciones.
  5. ACME CHALLENGE: Seguidamente el sistema genera un archivo de prueba conocido como el “ACME CHALLENGE” para cada dominio que desea asegurar. Descargue el archivo y asegurese de copiarlo fisicamente en el servidor web que desea asegurar en la carpeta webroot/.well-known/acme-challenge/ de su dominio **. Haga clic en NEXT para que el sistema compruebe que el archivo está disponible desde su dominio.
  6. Luego de la comprobación del reto ACME, el sistema generará dos archivos: (1) la llave privada de su sitio web y (2) el certificado SSL de su sitio web. Descárguelos y guárdelos en un lugar seguro.

** La ruta de destino (carpeta de confirmación) luce así: /public_html/.well-known/acme-challenge/  . Este archivo será utilizados como prueba de que usted es dueño del dominio del sitio web que está configurando. Al solicitar un certificado ZeroSSL para una carpeta raíz es posible especificar ambas formas de acceso: yourdomain.com y www.yourdomain.com. Tome en cuenta que para instalar Let’s Encrypt en un sub-dominio no es necesario utilizar el sub-sub dominio www. También tenga presente que la carpeta de confirmación deberá ser accesible sin necesidad de autenticación.


Paso 2 – Configuración en CPANEL de GoDaddy

Ahora vaya al panel de control (cPanel) de GoDaddy y busque la sección de Seguridad (Security) y haga clic en SSL/TLS.

Alija “Install and Manage SSL for your site (HTTPS)”, y haga clic en  “Manage SSL sites”.

Allí encontrará un formulario sencillo donde podra registrar la siguiente información: a) el dominio, b) el certificado, c) la llave privada y d) el “bundle” de la autoridad certificadora. Los ítems b,c y d son elementos que ud recibió de ZeroSSL. Es importante notar que los certificados tienen marcadores de inicio y final que deben incluirse en el formulario (“—–BEGIN CERTIFICATE—–” y “—–END CERTIFICATE—–“; de lo contrario obtendrá un error indicando que el certificado no es válido.

También, el certificado obtenido de ZeroSSL tiene dos partes: el certificado y el “Bundle” de la autoridad certificadora (CABUNDLE); estos tambien tienen indicadores de inicio y final.

Una vez que ud tenga todos los datos en el formulario (en mi caso pude dejar el CABUNDLE vacio) se hace clic en “Install Certificate” y listo; su sitio web ya puede ser accesado usando el protocolo HTTPS.

Existe un plug-in de WordPress para asegurar el acceso via HTTPS que se llama Easy HTTPS (SSL) Redirection.

Experimentación Ondas Cerebrales

6 Dic , 2017,
Jose Nunez
, , , , ,
No Comments

Recientemente tuvimos la oportunidad de experimentar un poco con un sensor de ondas cerebrales (Emotiv Insight) que nos ha servido de introducción al fascinante mundo de los BCI (Brain-Computer Interface)

El proyecto que nos trajo a este punto trata de desarrollar formas de comunicación adicionales para personas con algunas dificultades físicas para comunicarse, incluyendo peronas con dificultades para el habla, la escucha o diversos niveles de parálisis cerebral.

En este artículo quiero condensar un poco una propuesta para una metodología de experimentación que nos permita capturar datos de este tipo de dispositivos y que sirvan de insumo para crear modelos de aprendizaje de máquinas que a su vez nos lleven a desarrollar modelos de interpretación de las ondas y por ende los deseos o necesidades de las personas.

Metodología de Experimentación

  1. Definir un repositorio para la documentación oficial de cada experimento y para los resultados de los experimentos.
  2. Definir personas y roles: facilitador, sujeto de experimentación, observadores.
  3. Definir objetivos y metas del experimento
  4. Definir características de los sujetos de experimentación.
  5. Definir un ambiente controlado para minimizar los estímulos no esperados y el ruido
  6. Establecer un guión o protocolo de pasos, tiempos y clases para el experimento,
    1. Definir tareas a realizar: preparación, arranque, ejecución, finalización y cierre.
    2. Identificar estímulos Intencionales: preparación, arranque, ejecución, finalización, cierre
    3. Identificar estímulos no intencionales: derivados, ruido aceptable, ruido no aceptable (invalidación temprana del experimento)
  7. El resultado de cada experimento será un archivo con la información sensada y la pre-clasificación de los diferentes eventos o estímulos ocurridos detectados por el observador. Este archivo se usará para generar modelos de aprendizaje de máquinas que nos permitan estudiar y entender los fenómenos documentados en cada experimento.

Como siempre, sus comentarios para enriquecer esta metodología serán de gran valor para nosotros.


Haga clic acá para una plantilla de ejemplo


Algunas Referencias Interesantes:

Anaconda y Jupyter Notebook como plataforma de Machine Learning en Python

3 Dic , 2017,
Jose Nunez
, , , , ,
No Comments

¿Qué es Anaconda?

Anaconda es una de las plataformas más prominentes de ciencia de datos para Python. Se puede descargar Anaconda de este enlace: http://www.continuum.io/downloads 


Distribución Anaconda

Expanda para ver Distribución ANACONDA

La distribución gratuita incluye una serie de librerías y programas utilitarios avanzados tales como

Image tomada de https://www.anaconda.com/distribution/

Dentro de los paquetes que me han resultado más interesantes podemos mencionar:

  1. Numpy: Librería de Python para computación científica
  2. Scipy: Meta-librería de computación científica para Python
  3. Pandas: Librerías para estructuras de datos y analítica de datos para Python
  4. Jupyter Notebook: Es un IDE que permite crear documentación “activa/viva” incluyendo visualizaciones, scripts, ejemplos, etc

Jupyter Notebook

Expanda para ver ¿Cómo iniciar con Jupyter Notebook?

Jupyter Notebook perimte generar documentación viva que incluya scripts, datos y visualizaciones de ejemplo.

En Windows, para ejecutar Jupyter Notebook se abre la consola de Anaconda (Windows > Anaconda Prompt) y se ejecuta el comando jupyter notebook

Este comando levanta un servidor jupyter y una ventana de navegador apuntando a http://localhost:8888 (el puerto puede variar)

Cada línea del documento puede ser de tipo Markup, Titulo o Código.

En las líneas que son de tipo código puede ejecutarse este con solo presionar [CTRL] [ENTER]

Desde esa ventana web se pueden crear o modificar proyectos Jupyter que incluyan código de ejemplo Python que puede ser ejecutado en el documento.


Flujo de Trabajo para Aprendizaje de Máquinas

Expanda para ver más sobre Machine Learning Workflow

Machine Learning Workflow: Es un patrón repetible y orquestado que permite la transformación y el procesamiento sistemático de informació para crear soluciones de predicción.

  1. Definir la pregunta a responder
  2. Preparar los datos
  3. Seleccionar un algoritmo
  4. Entrenar un modelo
  5. Validar el modelo
  6. Retro-alimentación

 

Realizando un Proyecto SCRUM exitoso en 7 pasos

27 Nov , 2017,
Jose Nunez
, , ,
No Comments

Continuando con nuestra serie sobre SCRUM les presentamos 7 pasos para un proyecto SCRUM exitoso.

Pueden ver nuestro artículo anterior que resume varios conceptos importantes de SCRUM.

También pueden ver acá que nuestra plantilla scrum incluye detalles de cada ceremonia SCRUM.

Paso 1Identifique una persona que esté dispuesta a llevar a cabo el rol de SCRUM master, una persona que lleve el rol de Dueño de Producto e identifique a su equipo de ejecución.


Paso 2 – El dueño de producto crea un backlog inicial que describa el producto en unidades de trabajo (historias de usuario). Para esto normalmente se requiere el trabajo conjunto entre el dueño de producto y el equipo de ejecución donde el scrum master facilita el proceso, principalmente orientando donde sea necesario al dueño de producto para que las historias de usuario tengan una buena descripción, criterio de aceptación y estimación superficial.


Paso 3 – Se realiza una ceremonia oficial de Planeamiento del Producto donde todo el equipo (dueño de producto, equipo de ejecución, scrum master) revisan el backlog, las estimaciones superficiales, agregando criterios técnicos y aclarando dudas sobre cada historia de usuario. Esta ceremonia es organizada y facilitada por el scrum master. El resultado de esta ceremonia es una estimación general del tamaño del proyecto.


Paso 4 – Se organiza el calendario del siguiente trimestre en iteraciones de dos semanas cada una (aproximadamente entre cinco a seis iteraciones en total). Para esto el scrum master organiza las siguientes ceremonias en el calendario del equipo:

  • Poda del backlog cada dos semanas. Acá se identifican dudas y se hace una estimación final del esfuerzo de cada historia de usuario.
  • Planeamiento de Iteraciones al inicio de cada iteración (cada dos semanas), incluyendo Cierre de Iteración, Retrospectiva y Planeamiento.
  • Actualizaciones Diarias (15 minutos a la misma hora)
  • Planeamiento incremental del producto al final del trimestre. El scrum master tiene la responsabilidad de asegurarse que cada ceremonia se ejecute y tenga el resultado esperado. Para esto cuenta con la colaboración indispensable de todo el equipo, especialmente del dueño del producto.

Paso 5 – Durante la ceremonia de poda del backlog, el equipo de ejecución junto con el dueño de producto identifican dudas en cada historia de usuario y hacen una estimación final del esfuerzo requerido para cada iteración medido en puntos (1,2,3,5,8,13). Se realiza el esfuerzo de dividir las historias de usuario más grandes (> 5 puntos) en historias de usuario más pequeñas alcanzables en una iteración. Se identifican también historias de usuario de investigación donde se requiera aprender cómo realizar alguna tarea o diseñar algo. Las historias de usuario que tengan las aclaraciones necesarias y la estimación final en puntos se denominan “historias podadas”. Esto es importante porque en una iteración solamente se pueden asumir historias podadas.


Paso 6 – En el planeamiento de iteración se identifican la disponibilidad de cada miembro del equipo para la iteración, incluyendo ausencias programadas (vacaciones, permisos, capacitaciones), la cantidad de horas que cada miembro del equipo (en especial el equipo de ejecución) tiene disponibles para realizar tareas de la iteración y se realiza la estimación detallada de cada historia de usuario que se vaya a adoptar en la iteración en términos de tareas y horas estimadas para cada tarea.Al final del planeamiento se tendrá:

  • Una lista de miembros del equipo de ejecución y horas disponibles para la iteración y días de ausencia programada.
  • Una lista de historias adoptadas (compromiso de la iteración) con el detalle de tareas y estimación de las horas que tomaría cada tarea y cual miembro del equipo está a cargo de cada tarea. La sumatoria de las horas estimadas para cada tarea por miembro de equipo se llama “asignación” (allocation) y no debe superar las horas disponibles para los miembros de equipo encargado. Con la cantidad de asignación entre la cantidad de horas disponibles por encargado calculamos el porcentaje de asignación que nos permite ver si un miembro de equipo tiene una signación muy baja (< 85%) o muy alta (>100%)

Paso 7 – El equipo de ejecución adquiere un compromiso por cada iteración. Durante las ceremonias de actualización diaria se actualizan las métricas de ejecución ( horas pendientes, puntos aceptados ) para calcular el gráfico de burndown y se aclaran dudas, se identifican dificultades que requieran ayuda de otros miembros del equipo. Durante todo este proceso el scrum master realiza una labor de facilitador y también da seguimiento a las necesidades de ayuda.

Durante la iteración se ejecuta el compromiso del equipo, se removen obstáculos y se monitorea la ejecución dentro del marco de la ceremonia de actualización diaria.

Al final de la iteración se realiza un cierre que involucra:

  • Una sesión retrospectiva (qué cosas debemos seguir haciendo, qué cosas debemos dejar de hacer y qué cosas debemos empezar a hacer) especialmente con miras a mejorar el % compromiso entregado.
  • Una demostración de las historias de usuario aceptadas por el dueño de producto como terminadas.
  • Y el reacomodo de historias de usuario que no se hayan terminado (compromiso fallido) hacia la siguiente iteración.

Y… en resumen:

123 ¡Listo! – Estación de Trabajo IONIC3 para desarrollo de aplicaciones móviles.

27 Sep , 2017,
Robin Gonzalez Ricz
, , , ,
No Comments

Con este artículo introducimos nuestra serie de tutoriales 123 ¡listo!”. Un formato que resume los pasos necesarios para alcanzar una meta particular.

Objetivo: Crear una estación de trabajo en Ubuntu 16.04 (para UpBoard o Raspberry PI o cualquier sistema x86/x64) para el desarrollo de aplicaciones móviles con IONIC3.

Utilice el siguiente comando para instalar Atom, Netbeans e Ionic en su computadora:

sudo apt install curl && sudo add-apt-repository ppa:webupd8team/atom && curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - && sudo apt install python-software-properties python g++ make nodejs atom netbeans -y && sudo npm install -g cordova ionic

O también puede instalarlos individualmente con estos comandos:

#ATOM:

sudo add-apt-repository ppa:webupd8team/atom

apt update

sudo apt install atom

#Netbeans

sudo apt install netbeans -y

#Ionic:

sudo apt install curl

curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash –

sudo apt install python-software-properties python g++ make nodejs

sudo npm install -g cordova ionic

Algunos conceptos interesantes de Machine Learning usando Anaconda / Python

23 Sep , 2017,
Jose Nunez
, ,
No Comments

Continuando con nuestras recientes publicaciones sobre “Machine Learning” (artículo anterior sobre fundamentos), en esta oportunidad compartimos algunas cosas que hemos aprendido siguiendo el tutorial “Machine Learning in Python Step by Step“.

Para poder entender este artículo recomendamos seguir el tutorial paso a paso… no se toma más de 30 minutos.

  1. Anaconda: Aprendimos que se puede configurar un ambiente relativamente completo para experimentación con Machine Learning y Python usando Anaconda.
  2. Dataset IRIS: Existe un “Hello World” para Machine Learning basado en un dataset llamado “IRIS” 3. Este consiste en un conjunto de datos que describe tres tipos de flores Iris (setosa, virginica y versicolor) por las dimensiones de su sépalo y pétalo; se puede usar para entrenar un modelo de aprendizaje de máquina para que este infiera el tipo de flor (clasificación) con base en la combinación de parámetros.
  3. Arreglos: Python provee mecanismos para expresar y manipular arreglos de forma sumamente robusta. Podemos resumirlos de la siguiente manera:
    • Básicamente [a:b,c:d] donde a:b representa un rango de filas y c:d representa otro rango de columnas.
    • array[:,0:4] retorna todas las filas de la matriz y las primeras 4 columnas a partir de la columna cero.
    • array[:,4] retorna todos los elementos (filas) de la quinta columna (índice 4)
      
      
  4. Entrenamiento y Validación: El entrenamiento y validación de modelos de aprendizaje de máquinas usualmente suele dividir los datos conocidos en 80% para aprendizaje o creación del modelo y 20% para validación del modelo generado. En este tutorial se usa la función model_selection.train_test_split(X,Y, test_size, random_state) de la libreria sklearn.
  5. SKLEARN LIB: Existen diversos algoritmos de clasificación en la librería sklearn:
    1. LogisticRegression
    2. LinearDiscriminationAnalysis
    3. KNeighborsClassifier
    4. DecisionTreeClassifier
    5. GaussianNB
    6. SVM/SVC
  6. Precisión de Los Algoritmos: Diferentes algoritmos presentan diferentes niveles de precisión dependiendo del problema a resolver. Estos se pueden evaluar usando funciones como model_selection.cross_val_score que da como resultado medidas estadísticas como la media y la desviación estandar. Esta validación se puede confirmar con gráficos de tipo box charts, scattered matrix e histogramas. Estos gráficos se generan en python usando librerías como matplotlib
  7. Aprender y Predecir: Una vez entrenado el modelo (con knn.fit()) se pueden generar predicciones (knn.predict())
  8. Matriz de Confusión: Las predicciones pueden ser validadas mediante mecanismos como confusion_matrix que provee una análisis simple de valores esperados y valores predichos de manera correcta y errónea.
    • La matriz de confusión tiene un eje (x) que representa los valores conocidos, y un eje (y) que representa los valores predichos.
      setosa     ==> [[ 7   0   0]
      versicolor ==>  [ 0  11   1]
      virginica  ==>  [ 0   2   9 ]]
                        se  ve  vi
    • Esto se interpreta así:
      • Se identificaron 7 setosas adecuadamente.
      • De las 12 versicolor se identificaron 11 correctamente y una como virginica
      • De las 11 virginicas se identificaron 9 correctamente y 2 como versicolor.

Referencias:

  1. Machine Learning Step by Step: https://machinelearningmastery.com/machine-learning-in-python-step-by-step/
  2. Confusion Matrix: http://scikit-learn.org/stable/auto_examples/model_selection/plot_confusion_matrix.html
  3. IRIS: https://es.wikipedia.org/wiki/Iris_flor_conjunto_de_datos
  4. Iris Setosa Imagehttps://www.rhs.org.uk/Plants/9355/Iris-setosa/Details

 

Modelando Bases de Datos Relacionales con XAMPP y phpMyAdmin

12 Sep , 2017,
Jose Nunez
, , , , , , ,
No Comments

Alguna vez habrán enfrentado la necesidad de modelar una base de datos relacional.

El mundo de bases de datos relacionales (RDBM) está regido por unos cuantos titanes en el tema. Desde Oracle, pasando por MS SQL Server y sin olvidar MySQL (y su hermana MariaDB) y PostgreSQL.

Para efectos de este artículo nos enfocaremos en MySQL y su nueva ramificación MariaDB.

MySQL forma parte de una pila de tecnologías web sumamente influyentes a nivel mundial. Nos referimos al “Stack LAMPP” que se traduce en Linux, Apache, MySQL, PHP/PERL.

En este conjunto de tecnologías, LINUX figura como un principal contendor en el mercado de sistemas operativos para servidores web (si es que existe tal cosa); mientras que Apache se desempeña como uno de los principales programas de servidor web a nivel mundial. Luego tenemos MySQL que es el componente de base de datos y finalmente PHP como lenguaje de programación para la web del lado del servidor.

Existen varios sistemas de instalación de la pila LAMPP; en nuestro caso usaremos una publicada por la organización ApacheFriends.org denominadada XAMPP. La “X” viene por la compatibilidad con sistemas operativos fuera de LINUX. Así XAMPP permite la instalación de la pila AMPP tanto en LINUX como en MS Windows y OSx de Mac.

A su vez, XAMPP trae algunas utilidades adicionales de las cuales vale la pena mencionar el sistema phpMyAdmin. Una herramienta avanzada que permite el diseño y manipulación de bases de datos MySQL.

Una vez que hemos descargado e instalado XAMPP en nuestro computador, y que nos hemos asegurado de que los servicios para Apache y MySQL están funcionando adecuadamente, podemos acceder al poder de XAMPP la dirección del servidor local: http://localhost/

La siguiente imagen muestra nuestra pantalla principal de XAMPP por medio del navegador.

Nótese entonces que en el menú superior a la derecha tenemos la opción “phpMyAdmin”

Seguidamente se ilustra como luce nuestra versión de phpMyAdmin en este momento.

Al lado izquierdo tenemos las diferentes bases de datos que están ya definidas en el servidor; mientras que a mano derecha tenemos el área de trabajo con diversos puntos de funcionalidad para editar bases de datos y cualquier otro elemento de base de datos.


PASO 1 – CREACIÓN DE BASE DE DATOS

Comenzamos este ejercicio haciendo clic en el nodo “New” que se encuentra a mano izquierda sobre la lista de bases de datos.

Esto nos muestra un formulario para crear una nueva base de datos que básicamente pregunta por el nombre de la base de datos y por el conjunto de caracteres (collation) predeterminado para la base de datos. Este parámetro define qué tipo de simbología usará la base de datos para almacenar texto. Uno de los conjuntos de caracteres más efectivos para occidente es el utf8 con sus variantes por idioma. Para este ejemplo que soporta símbolos en español y otros idiomas usaremos utf8_bin.

Una vez creada la base de datos podemos comenzar con el proceso de modelado.

Para esto utilizaremos el siguiente enunciado:

Se requiere diseñar una base de datos relacional para manejo de inventarios. El inventario contabiliza las cantidades disponibles de productos de diversos tipos; tomados del catálogo de productos que vende la organización. Para efectos de inventario se tienen dos tipos de producto: (1) los productos “serializables” que son productos que se pueden identificar de manera única (por número de serie asignado por el fabricante) y (2) los productos “a granel” que no se pueden identificar de manera unica sino por cantidades, por ejemplo 1 Kg de tornillos, o 300 destornilladores. El inventario se maneja en diversas bodegas y a su vez se contabilizan las entradas y salidas de inventario para y desde cada bodega.

Con este enunciado vamos a dar un siguiente paso para la identificación de los elementos fundamentales del modelo conceptual, es decir las entidades y sus relaciones.


PASO 2 – Identificación de Elementos

Una técnica que proponemos para esto es la de marcar el enunciado con un color (azul) aquellas partes que describen una entidad y con otro color (verde) aquellas partes que describen una relación. Así el enunciado queda de la siguiente manera:

Se requiere diseñar una base de datos relacional para manejo de inventarios. El inventario contabiliza las cantidades disponibles de productos de diversos tipos; tomados del catálogo de productos que vende la organización. Para efectos de inventario se tienen dos tipos de producto: (1) los productos “serializables” que son productos que se pueden identificar de manera única (por número de serie asignado por el fabricante) y (2) los productos “a granel” que no se pueden identificar de manera única sino por cantidades de alguna unidad de medida; por ejemplo 1 Kg de tornillos, o 300 destornilladores. El inventario se maneja en diversas bodegas y a su vez se contabilizan diferentes movimientos como entradas y salidas de inventario para y desde cada bodega.

De esta forma hemos identificado las siguientes entidades:

  1. Catálogo de Producto
  2. Tipo de Producto
  3. Unidad de Medida
  4. Bodega
  5. Movimiento
  6. Tipo de Movimiento
  7. Fabricante

También se identifican algunas relaciones:

  1. Catálogo de Producto >==> Tipo de Producto
  2. Catálogo de Producto >==> Fabricante
  3. Catálogo de Producto >==> Unidad de Medida
  4. Movimiento >==> Tipo de Movimiento (entrada/salida)
  5. Movimiento >==< Bodega

Finalmente algunos atributos relevantes son:

  1. Número de serie
  2. Cantidad
  3. Nombre del Producto
  4. Nombre de Bodega
  5. Descripción de Unidad de Medida
  6. Nombre de Tipo de Movimiento

PASO 3 – Modelo Conceptual

Una vez identificados los elementos básicos del modelo de datos, podemos realizar un diagrama de Entidad-Relación que nos permita comunicar cuales son las entidades que componen el modelo y como se relacionan entre si.

Aunque basta con Power Point o algun otro software que nos permita generar cajitas y conectarlas con líneas; en nuestro caso usaremos el poder de phpMyAdmin.

Para esto tomamos cada entidad y definimos las tablas respectivas con los valores más fundamentales que podemos pensar para cada entidad:

  1. Tipo de Producto (Código de Tipo de Producto, Descripción)
  2. Unidad de Medida (Código de Unidad de Medida, Descripción)
  3. Fabricante (Código de Fabricante, Nombre)
  4. Catálogo de Producto (Código de Producto, Descripción, Código de Tipo de Producto, Código de Fabricante, Código de Unidad de Medida)
  5. Bodega (Código de Bodega, Descripción)
  6. Tipo de Movimiento (Código de Tipo de Movimiento, Descripción)
  7. Movimiento (ID de Movimiento, Código de Bodega, Código de Producto, Código de Tipo de Movimiento, Cantidad)

Nótese que hemos reorganizado las entidades de forma que primero se definen aquellas de las cuales hay dependencia en otras entidades. También debemos acotar que los nombres de las tablas (entidades) y sus atributos los realizamos usando una nomenclatura estándar, donde “cd” refiere a código, “dsc” refiere a descripción y “id” refiere a identificador numérico automático. Se usan ID’s en las entidades transaccionales, mientras que se usan códigos en las entidades tipificadoras.

La siguiente imagen muestra cómo se definen los campos básicos de una tabla (entidad). Para poder acceder a este formulario se hace clic en el nodo de base de datos “inventario” a la izquierda. En el área de trabajo hay una opción para iniciar la creación de una tabla (entidad) con una cantidad predeterminada de columnas (atributos)

 


PASO 4 – Modelado de las Relaciones

Una vez definidas las entidades (tablas) podemos visualizarlas usando el área de la izquierda, expandiendo los nodos correspondientes como se muestra en la siguiente imagen.

A su vez, si hacemos clic en la base de datos “inventario” podremos acceder a la función “Designer” la cual nos permitirá ir construyendo las relaciones entre las entidades (tablas) mientras que construimos el diagrama Entidad-Relación correspondiente.

La siguiente imagen muestra la función de diseño, desde donde se pueden crear relaciones entre las diferentes tablas.

Si hacemos clic en “Toggle small/big” en el menú de la izquierda podremos ver todos los atributos.

También, si hacemos clic en “Create relationship” podremos seleccionar – primero – un atributo de tipo “llave primaria” y – segundo – un atributo en otra tabla de tipo “referencia foránea” y así conformar las relaciones.

De esta forma podemos ver el diagrama con relaciones logrado como sigue, el cual no solamente expresa las relaciones en sí, sino también la cardinalidad entre las entidades.

Volviendo al diagrama reducido a solamente las entidades podemos ver que queda así; incluyendo indicadores de cardinalidad entre las entidades.

Estos diagramas son sumamente útiles para discutir y razonar sobre el modelo de datos. A su vez se puede utilizar phpMyAdmin para agregar o modificar atributos en la fase de modelado detallado de la base de datos.

Posibilidades con el pequeño Wheely

12 Ago , 2017,
Jose Nunez
No Comments

¿Recuerdan nuestro pequeño Robot de dos llantas?

Bien, le agregamos unos cuantos dispositivos más y veremos si lo podemos hacer un poco más listo.

  • Batería de Lítio-Polímero de 3.7V recargable. Para no gastar en baterías AA.
  • Regulador de 3V a 5V que nos permite usar la batería LIPO para alimentar una gama interesante de controladores y los motores a 5V.
  • Sparkfun Thing 8266: Este controlador nos da capacidades WIFI y además un mecanismo para cargar la batería LIPO usando un cable micro-USB convencional.
  • Arduino 101: Microcontrolador de gran capacidad que además trae acelerómetro y giroscópio en 6 ejes y comunicación Bluetooth BLE. Lo alimentamos a 5V, pero operan sus señales a 3.3V.
  • Sensor de proximidad para detectar obstáculos o mapear áreas que opera a 5V.
  • El ya conocido controlador S4A-EDU con puente H desconectabe. Nos permite controlar los motores en diferentes direcciones e incluye comunicación tx/rx que podemos usar para conectarlo a los otros micro-controladores mencioandos.

De momento no he programado mayor cosa con todo esto, salvo por este ejemplo; pero esperamos hacer algunas cosas interesantes con todas estas capacidades.

RFID Usando RDM6300 y Arduino 101, TinyTILE o ARDUINO UNO

7 Ago , 2017,
Jose Nunez
, , , ,
No Comments

(!) Antes de seguir estas instrucciones asegúrese de entender las Condiciones de Uso de nuestro sitio.

El RDM6300 es un módulo bastante sencillo de utilizar en el entorno de ARDUINO gracias a la librería RDM6300 que se puede descargar de acá.

El módulo tiene tres conectores:

P1 (6 contactos): que contiene los pines para realizar comunicaciones. En este experimento utilizaremos solamente el pin 1 ubicado en la esquina que lo conectaremos al pin 6 del Arduino 101. Básicamente este es el único pin que se conecta al microcontrolador.

P2 (2 contactos) que es donde se conecta la antena (bobina)

P3 (3 contactos) que es donde se alimenta el sistema y donde se pude agregar un LED (todavía no tengo claro para qué exactamente) el punto es que de este solo utilizaremos el pin 2 y 3 que son +5V  y GND respectivamente.

El programa que utilizamos simula la apertura de un sistema mediante un pulso a un LED verde conectado al pin 3 de Arduino, y un pulso para cerrar simulado por un LED amarillo conectado al pin 2 del Arduino.

Algo interesante es que el Arduino 101 opera a 3.3V pero es tolerante a señales de 5V lo cual lo hace particularmente versatil (esto también aplica para TinyTILE. Con Arduino UNO también funciona.

Conexiones:

ARDUINO
PIN  2 ==> amarillo ====> LED Amarillo ===> Resistencia 150 Ohm ==> GND
PIN  3 ==> verde =======> LED Verde ======> Resistencia 150 Ohm ==> GND
PIN  6 <== café <======== PIN 1 del conector P1 del RDM6300 (TX)
PIN 13 ==> naranja =====> LED Verde ======> Resistencia 150 Ohm ==> GND
5V ======> rojo ========> Puerto + del protoboard
GND =====> negro =======> Puerto - del protoboard

RDM6300
PIN 2 del conector P3 (Vin) <===== Puerto + del protoboard (operando a 5V)
PIN 3 del conector P3 (GND) <===== Puerto - del protoboard

PIN 1 del conector P1 (TX) ======> PIN 6 del ARDUINO

Conector P2 =====< ANTENA (bobina)

 

Programa de ejemplo:

La versión oficial de este sketch se puede encontrar acá: https://github.com/janunezc/robotics/tree/master/rfid/rfid_rdm630_open_then_close

(!) También, será necesario que los archivos de la librería RDM6300 estén disponibles para compilar el sketch. Se puede copiar ambos archivos (rdm630.h y rdm630.cpp) en la misma carpeta del sketch, o en la carpeta de librerías del ARDUINO IDE (c:\user\myusername\documents\Arduino\libraries\rdm630\***). Es importante asegurarse que la librería RDM630 solo está copiada una vez en las diferentes rutas.

/*
 * Programa escrito por Jose Nunez como ejemplo didáctico del uso del RDM6300 con un ARDUINO 101.
 * Use bajo su propio riesgo.
 * Ejemplo de dominio público.
 *
*/
#include "rdm630.h"

rdm630 rfid(6, 0);  //TX-pin of RDM630 connected to Arduino pin 6
int led_yellow_close = 2;
int led_green_open = 3;
int led_signal = 13;
long OPEN_PULSE_WIDTH = 800;
long CLOSE_PULSE_WIDTH = 1000;
long OPEN_STATE_DURATION = 5000; 
long CODE_READ_DELAY = 10000;
long codeReadDelayMaxMillis = millis();

void setup()
{
    Serial.begin(115200);  // start serial to PC
    pinMode(led_yellow_close, OUTPUT);
    pinMode(led_green_open, OUTPUT);
    pinMode(led_signal, OUTPUT);
    rfid.begin();
    ledSignal(5,500);
    setMessage("READY!");
}

void loop()
{
  unsigned long rfidTagCode = 0;  
  
  if(rfid.available()){
    setMessage("RFID Data is available! reading it...");
    rfidTagCode = readRFIDCode();
    setMessage("Data: " + String(rfidTagCode));
    if(millis() > codeReadDelayMaxMillis) {
      if(rfidTagCode == 7598635) {
        setMessage("OPEN");
        openPulse();//This involves a closePulse() call inside the openPulse() function.
        codeReadDelayMaxMillis = millis() + CODE_READ_DELAY;
      } else {
        setMessage("I DONT KNOW YOU!");
        closePulse();
      }
    } else {
      setMessage("SkippingNewReads");
      delay(500);
    }   
  }
}

void openPulse(){
  setMessage("SENDING OPEN PULSE");
  digitalWrite(led_green_open, HIGH);
  delay(OPEN_PULSE_WIDTH);
  digitalWrite(led_green_open,LOW);
  setMessage("OPEN PULSE DONE! Waiting for OPEN STATE DURATION...");
  delay(OPEN_STATE_DURATION);
  setMessage("OPEN STATE COMPLETE");
  closePulse();
}

void closePulse(){
  setMessage("SENDING CLOSE PULSE");
  digitalWrite(led_yellow_close, HIGH);
  delay(CLOSE_PULSE_WIDTH);
  digitalWrite(led_yellow_close, LOW);
  setMessage("CLOSE PULSE DONE!");
}

unsigned long readRFIDCode(){
  byte data[6];
  byte length;

  rfid.getData(data,length);
  Serial.println("Data valid");
  for(int i=0;i<length;i++){
      Serial.print(data[i],HEX);
      Serial.print(" ");
  }
  Serial.println();
  //concatenate the bytes in the data array to one long which can be 
  //rendered as a decimal number
  unsigned long result = 
    ((unsigned long int)data[1]<<24) + 
    ((unsigned long int)data[2]<<16) + 
    ((unsigned long int)data[3]<<8) + 
    data[4];              
  Serial.print("decimal CardID: ");
  Serial.println(result);
  return result;  
}

void ledSignal(int times, int milliseconds){
  for(int i=0; i<times; i++){
    digitalWrite(led_signal, HIGH);
    delay(milliseconds);
    digitalWrite(led_signal, LOW);
    delay(milliseconds);
  }
}

void addToMessage(String message){
  setMessage(message, false);
}

void setMessage(String message){
  setMessage(message, true);
}

void setMessage(String message, bool newLine){
  String timeStamp = String(millis());
  String finalMessage = timeStamp + " - " + message;
  if(newLine){
    Serial.println(finalMessage);
  } else {
    Serial.print(message);
  }
}

Acá un ejemplo de la ejecución:

Espero que les sea de utilidad.

Usando la memoria FLASH del Intel Curie

24 Jul , 2017,
Jose Nunez
, ,
No Comments

Hola a todos.

Este es un tópico un poco más detallado.

El siguiente Sketch que pueden descargar de mi Github permite registrar datos en la memoria FLASH (persistente) del Intel Curie, buscarlos y eliminarlos.

Espero que el código sea bastante legible para todos.

Esta es una versión corregida del código original. Resulta que la librería SerialFlash no permite reutilizar porciones de memoria, así que después de varias operaciones creando y removiendo el archivo la memoria se acaba y se hace necesario borrar toda la flash (proceso que toma 1 segundo). Puede observarse en la línea 373 cómo se realiza este borrado cuando la creación “rápida” del archivo falla.

Acá implemento varias técnicas de “debugging” tales como parpadeos informativos, mensajes de consola estructurados y “pruebas unitarias” así como funciones de alcance y tamaño limitado.

Acá se resuelven  varios problemas, incluyendo aquello de ¿Cómo convierto un String a un const char*?  Bueno, no necesariamente una conversión, sino una implementación que es soportada por la función que estaba tratando de llamar. (ver linea 319)

En resumen, acá implemento funciones para crear un archivo en la memoria flash, con un formato predefinido. Leer el contenido de ese archivo y borrar el archivo.

Luego tambien implemento funciones para buscar contenido en el archivo y para eliminar contenido del archivo.

A su vez hay funciones para verificar el formato del archivo y realizar algunas pruebas unitarias.

El caso de uso es el almacenamiento de datos pequeños (códigos de acceso por ejemplo) que se puedan agregar, buscar o eliminar.

Capas:

  1. Acceso a Datos: Manejo de la memoria flash con archivos (crear archivo, guardar contenido, obtener contenido, eliminar archivo)
  2. Negocio: Agregar llave, eliminar llave, buscar llave.
  3. Desarrollo: diferentes pruebas y verificaciones.

Son una veintena de funciones pequeñas. Cada una hace lo suyo de manera muy explícita. Espero que les sea de utilidad.


// Escrito por José Núñez como un ejemplo didáctico para el dominio público.
// Utiliza la librería SerialFlash de Paul Stoffregen.
// Dicha librería se puede descargar de https://github.com/PaulStoffregen/SerialFlash

//Abstracción de la librería SerialFlash para el controlador Intel Curie
#include 
#include 

#define FSIZE 256 //Tamaño predefinido del archivo
#define ledPin 13 //PIN para un LED 
#define buttonPin 10 //PIN para leer un botón. Debe estar "pull-up" y el botón realiza una conexión a tierra.

const char *filename = "rfidble.txt"; //Nombre del archivo
#define CONTENT_SIZE 153 //Tamaño predefinido del contenido del archivo para este ejemplo.

void setup() {
  Serial.begin(9600);
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
  
  parpadear (5, 1000);

  debugMessage("Setup() BEGIN!");

  parpadear (3, 500);
  
  // Inicializar operación de la librería SerialFlash
  if (!SerialFlash.begin(ONBOARD_FLASH_SPI_PORT, ONBOARD_FLASH_CS_PIN)) {
    debugMessage("Setup() Unable to access SPI Flash chip");
  }

  debugMessage("Setup() DONE!");
}
/*------*/
/* LOOP */
/*------*/
bool executedOnce = false;
bool addKeyMode = false;
void loop() {
  if (!executedOnce) {
    debugMessage("loop() executing... reading file...");
    String fileContent = readFile();

    debugMessage("loop() file read: |" + fileContent + "| (" + String(fileContent.length()));

    debugMessage("loop() doing file verifications...");
    if (!verifyBegin(fileContent) || !verifyEnd(fileContent) || !verifySearch() || ! verifySize(fileContent)) {
      debugMessage("loop() Verifications Failed. Reformatting...");
      initFile();
      executedOnce = false;
    } else {
      debugMessage("loop() verifications ran OK");

      debugMessage("loop() testing key addition");
      test_addKey();

      debugMessage("loop() testing key removal");
      test_removeKey();
      executedOnce = true;
    }
  }
  
  processButtonCase();

}

void processButtonCase() {
  heartBeat();

  checkButton();

  if (addKeyMode) {
    addKey("1234");
    addKey("5678");
    addKey("91011");
    addKey("121314");
    addKey("151617");
    addKeyMode = false;
  }
}

unsigned long nextHeartBeat = 0;
void heartBeat() {
  if (millis() > nextHeartBeat) {
    debugMessage("Heartbeat!");
    parpadear (2, 200);
    nextHeartBeat = millis() + 2000;
  }
}

/******************************
   INTERACTION LAYER
 ******************************/

bool didResetMode = false;
void checkButton() {
  if (digitalRead(buttonPin) == LOW) {
    debugMessage("checkButton() Button is low!");
    long lowDetectionMillis = millis();

    while (digitalRead(buttonPin) == LOW) {
      long timeInLow = millis() - lowDetectionMillis;
      if (timeInLow > 3000) {
        debugMessage("checkButton() button was low for more than 3 seconds! Initializing File...");
        initFile();
        didResetMode = true;
        break;
      }
    }
    if (!didResetMode) {
      debugMessage("checkButton() Switching to addKey mode...");
      addKeyMode = true;
    }

    didResetMode = false;
  }
}

/*****************************
   DEBUG LAYER
 *****************************/
void test_addKey() {
  debugMessage("test_addKey() BEGIN!");
  String newKey = "2097876";
  debugMessage("test_addKey() Key to add: " + newKey);

  bool newKeyAlreadyFound = findKey(newKey);
  if (newKeyAlreadyFound) {
    debugMessage("test_addKey() WRONG: Key was not expected, but found. " + newKey);
  } else {
    debugMessage("test_addKey() CORRECT: New Key was not found, as expected. " + newKey);
  }

  debugMessage("test_addKey() Calling addKey()!");
  addKey(newKey);

  debugMessage("test_addKey() verifying addKey() results...");
  bool newKeyFound = findKey(newKey);
  if (newKeyFound) {
    debugMessage("test_addKey() CORRECT: New Key was found as expected. " + newKey);
  } else {
    debugMessage("test_addKey() WRONG: New Key was not found. " + newKey);
  }
}

void test_removeKey() {
  debugMessage("test_removeKey() BEGIN!");
  String keyToRemove = "2097876";
  debugMessage("test_removeKey() Key to remove: " + keyToRemove);

  bool keyToRemovePresent = findKey(keyToRemove);
  if (keyToRemovePresent) {
    debugMessage("test_removeKey() CORRECT: Key to remove is present as expected. " + keyToRemove);
  } else {
    debugMessage("test_removeKey() WRONG: Key to remove is not found!!!. " + keyToRemove);
  }

  debugMessage("test_removeKey() calling remove key...");
  removeKey(keyToRemove);

  if (findKey(keyToRemove)) {
    debugMessage("test_removeKey() WRONG: Key to remove is present. NOT EXPECTED!!!! " + keyToRemove);
  } else {
    debugMessage("test_removeKey() CORRECT: Key to remove not present as expected.");
  }

}
void parpadear(int times, int milliseconds) {
  for (int i = 0; i < times; i++) {
    digitalWrite(ledPin, HIGH);
    delay(milliseconds);
    digitalWrite(ledPin, LOW);
    delay(milliseconds);
  }
}

void debugMessageNoLF(String message) {
  Serial.print (message);
}

void debugMessage(String message) {
  long curMillis = millis();
  Serial.print (curMillis);
  Serial.print (": ");
  Serial.println (message);
}

bool verifyBegin(String fileContent) {
  debugMessage("verifyBegin() BEGIN!");
  if (fileContent.startsWith("FILE_BEGIN_OK")) {
    debugMessage("verifyBegin() CORRECT: File begin is OK");
    return true;
  } else {
    debugMessage("verifyBegin() WRONG: File begin is NOT AS EXPECTED");
    return false;
  }
}
bool verifyEnd(String fileContent) {
  debugMessage("verifyEnd() BEGIN!");
  if (fileContent.endsWith("FILE_END_OK")) {
    debugMessage("verifyEnd() Correct: End of file is as expected");
    return true;
  } else {
    debugMessage("verifyEnd() WRONG: End of file is NOT AS EXPECTED");
    return false;
  }
}

bool verifySize(String fileContent) {
  debugMessage("verifySize() BEGIN!");
  if (fileContent.length() == CONTENT_SIZE) {
    debugMessage("verifySize() CORRECT!");
    return true;
  } else {
    debugMessage("verifySize() WRONG!");
    return false;
  }
}
bool verifySearch() {
  debugMessage("verifySearch() BEGIN!");
  bool k1found = findKey("7598635");
  bool k2found = findKey("notfoundhere");

  if (k1found && ! k2found) {
    debugMessage("verifySearch() SEARCH IS OK!");
    return true;
  } else {
    debugMessage("verifySearch() ERROR!!!! SEARCH IS WRONG!!");
    return false;
  }
}


/******************************************
   BUSINESS LAYER
 ******************************************/

bool findKey(String key) {
  debugMessage("findKey(): " + key);
  String formattedKey = formatKey(key);
  String fileContent = readFile();
  int pos = fileContent.indexOf(formattedKey);
  if (pos > 0) {
    debugMessage("findKey() FOUND: " + key + " at " + String(pos));
    return true;
  } else {
    debugMessage("findKey() Not found: " + key);
    return false;
  }
}

void addKey (String key) {
  debugMessage("AddKey() BEGIN!");
  String fileContent = readFile();
  if (findKey(key) == true) {
    debugMessage("AddKey() Key already stored!");
    return; //Already there
  } else {
    int pos = fileContent.indexOf(",----------");
    String beginning = fileContent.substring(0, pos);
    String thisKey = formatKey(key);
    String restOfFile = fileContent.substring(pos + 11);
    String newFileContent = beginning + thisKey + restOfFile;
    debugMessage("OLD FILE:" + fileContent);
    debugMessage("NEW FILE:" + newFileContent);
    saveFile(newFileContent);
  }
}

String formatKey(String key) {

  int dashesRequired = 10 - key.length();
  String result = "";
  for (int i = 0; i < dashesRequired; i++) {
    result = result + "-";
  }
  return "," + key + result;
}
void removeKey(String key) {
  debugMessage("removeKey() BEGIN!");
  String fileContent = readFile();
  if (findKey(key) == false) {
    debugMessage("removeKey() key not present!");
    return; //Already not there
  } else {
    String formattedKey = formatKey(key);
    debugMessage("removeKey() Formatted Key: |" + formattedKey + "|");
    int pos = fileContent.indexOf(formattedKey);
    debugMessage("removeKey() Index of Formatted key: " + String(pos));
    if (pos > 0) {
      String beginning = fileContent.substring(0, pos);
      String thisKey = formatKey("");
      String restOfFile = fileContent.substring(pos + 11);
      String newFileContent = beginning + thisKey + restOfFile;
      debugMessage("removeKey() OLD FILE:" + fileContent);
      debugMessage("removeKey() NEW FILE:" + newFileContent);
      saveFile(newFileContent);
    } else {
      debugMessage("removeKey() Formatted Key not found! " + formattedKey);
    }
  }

}

/**
   This function initializes file with raw content.
*/
void initFile() {
  debugMessage("initFile() BEGIN!");

  String fileContent = "FILE_BEGIN_OK:123455,7598635---,----------,----------,----------,----------,----------,----------,----------,----------,----------,----------,FILE_END_OK";
  saveFile(fileContent);

  debugMessage("initFile() DONE!");
}


/********************
   DATA ACCESS LAYER
 ********************/

/**
   This function reads file content into an string. Implements full debugging mechanism
*/
String readFile() {
  debugMessage("readFile(): Begin!");

  debugMessage("readFile() Opening file: " + String(filename));
  SerialFlashFile file = SerialFlash.open(filename);

  // Get the size and position
  uint32_t fsize = file.size();
  debugMessage("readFile() Size: " + String(fsize));

  uint32_t filePos = file.position();
  debugMessage("readFile() Position: " + String(filePos));

  debugMessage("readFile() Seeking...");
  file.seek(filePos);

  debugMessage("readFile() Reading buffer...");
  char mybuff[fsize];
  file.read(mybuff, fsize);
  String result = String(mybuff);
  debugMessage("readFile() Closing file...");
  file.close();

  debugMessage("readFile() Content:");
  debugMessageNoLF("|||");
  debugMessageNoLF(result);
  debugMessageNoLF("|||");
  debugMessage("Size:" + String(result.length()));

  debugMessage("readFile() DONE!");
  return result;
}

/**
   This function drops existing file and saves a new one with proposed content.
*/
void saveFile(String newFileContent) {
  debugMessage("saveFile() BEGIN!");

  removeFile();

  uint8_t flashBuffer[CONTENT_SIZE + 1];
  newFileContent.getBytes(flashBuffer, CONTENT_SIZE + 1);
  SerialFlashFile file;


  debugMessage("saveFile() Creating file if not exist...");
  // Create the file if it doesn't exist
  while (!create_if_not_exists(filename)) {
    debugMessage("Memory filled up! Clearing Flash...");
    SerialFlash.eraseAll();

    while (SerialFlash.ready() == false) {
      // wait, 30 seconds to 2 minutes for most chips
    }
  }

  debugMessage("saveFile() Opening file for write!");
  // Open the file and write test data
  file = SerialFlash.open(filename);

  debugMessage("saveFile() Writing into file...!");
  file.write(flashBuffer, CONTENT_SIZE + 1 );
  debugMessage("saveFile() String \"" + String(newFileContent) + "\" written to file " + String(filename));

  debugMessage("saveFile() Closing file...");

  file.close();

  debugMessage("saveFile() DONE!");

}

/**
   This function drops the file.
*/
void removeFile() {
  debugMessage("removeFile() BEGIN!");

  if (SerialFlash.exists(filename)) {
    debugMessage("removeFile() Removing file...");
    SerialFlash.remove(filename);
  } else {
    debugMessage("removeFile() No file to remove..." + String(filename));
  }
  parpadear (3, 50);

  debugMessage("removeFile() DONE!");
}

bool create_if_not_exists (const char *filename) {
  if (!SerialFlash.exists(filename)) {
    debugMessage("Creating file " + String(filename));
    return SerialFlash.create(filename, FSIZE);
  }

  debugMessage("File " + String(filename) + " already exists");
  return true;
}