Diseño a profundidad (aplicación Java)
En este capítulo diseñaremos un programa sin omitir un solo paso con el cual podremos visualizar los valores del protocolo de cifrado implementado el capítulo pasado.
Se necesita un nivel intermedio de programación para seguir esta guía. No se requiere conocimiento de diseño.
Lector de voltaje
Primero haremos una pequeñísima aplicación, la cual calibraremos usando un generador de señal. Esta aplicación la llamaremos el "mini-multímetro" ya que lo único que hará será imprimir el voltaje leído en consola.
Sería prácticamente imposible plasmar en este manual todas las mañas y trucos de programación que se tuvieron que usar al momento de programar la aplicación final, sin embargo, podemos hacer un intento en programar una versión más chiquita de esta aplicación para intentar transmitir el proceso de diseño.
Como ya dijimos, comenzaremos creando una aplicación chiquta, el mini-multímetro.
mini-multímetro
Toda la programación la realizaré en el IDE IntelliJ Idea Community (por preferencia personal, no por otra razón).
Este ejemplo ni siquiera va a depender de una interfaz gráfica, será todo por medio de consola. Introduciremos más adelante el desarrollo con interfaces gráficas.
- Creamos un nuevo proyecto en Java.
- Descargamos la biblioteca de JSerialComm ya que la necesitamos para el manejo de los puertos seriales.
- Importamos la biblioteca a nuestro proyecto.
- Nuestro primer objetivo será leer los valores de los bytes enviados por el puerto serial, ya después nos preocuparemos por la interpretación.
El primer código que podemos ejecutar (solo para demostrar que todos esté funcionando correctamente) es el siguiente:
package com.marroja;
import com.fazecast.jSerialComm.*;
public class Main {
public static void main(String[] args) {
SerialPort[] puertos = SerialPort.getCommPorts();
for(SerialPort p: puertos){
System.out.println(p.getSystemPortName());
}
}
}
El ejecutar este código deberíamos obtener una salida semejante a esta:
---En Mac y Linux---
cu.wlan-debug
tty.wlan-debug
cu.ESP32_LED_Control
tty.ESP32_LED_Control
cu.Bluetooth-Incoming-Port
tty.Bluetooth-Incoming-Port
cu.usbserial-0001
tty.usbserial-0001
---en Windows---
COM1
COM3
qph
Esto nos dice que en el listado de puertos sí está contemplando nuestro dispositivo. En el caso de Mac o Linux nuestro dispositivo sería el marcado con cu.usbserial-0001 y el tty.usbserial-0001 (son dos diferentes protocolos de acceder al mismo dispositivo).
El siguiente paso es abrir la comunicación con ese puerto en concreto. Existen tres diferentes tipos de comunicación, con bloqueo, con semi-bloqueo y la comunicación con bloqueo completo. En nuestro caso, ya que no debería de existir ningún otro dispositivo leyendo el flujo de bytes y queremos establecer la comunicación más estable y rápida posible usaremos comunicación con bloqueo completo. Esto significa que solo nosotros tendremos acceso a esta comuncación.
Según la documentación, si queremos abrir un puerto del cual leer un flujo de datos tenemos que usar el siguiente código ejemplo:
SerialPort comPort = SerialPort.getCommPorts()[0];
comPort.openPort();
comPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 0, 0);
InputStream in = comPort.getInputStream();
try
{
for (int j = 0; j < 1000; ++j)
System.out.print((char)in.read());
in.close();
} catch (Exception e) { e.printStackTrace(); }
comPort.closePort();
El código ejemplo recibe una cadena de bytes y las imprime como caracteres. En nuestro caso nosotros no queremos hacer eso, sin embargo, podemos imprimirlos en binario y revisar si en efecto los valores que leemos son los valores que esperamos que esté enviando la tarjeta ESP-32.
Modificamos nuestro código original:
public static void main(String[] args) {
//Hay que definir cuál va a ser nuestro puerto
//Lo podemos definir a partir de la lista que vimos antes
//O lo podemos detectar por medio de entrada de usuario
int numPort = 7;
//Imprimimos la lista completa de puertos
SerialPort[] puertos = SerialPort.getCommPorts();
for(SerialPort p: puertos){
System.out.println(p.getSystemPortName());
}
SerialPort comPort = SerialPort.getCommPorts()[numPort];
comPort.setBaudRate(921600); //Máxima velocidad estable
comPort.openPort();
comPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 0, 0);
InputStream in = comPort.getInputStream();
//Aquí guardaremos el número conforme lo decodifiquemos
int numDecodificado = 0;
//Ciclo de lectura e interpretación
while(true) {
try {
//Este valor es de una sola lectura (un solo byte acomodado en un entero)
//La cola inicial del número es de puros ceros.
int lectura = in.read();
//Revisamos el primer bit para ver si es inicio de paquete
if((lectura & 0b1000_0000) == 0b1000_0000) {
//Como sabemos que este es un nuevo inicio de paquete,
//limpiamos el entero donde guardamos la decodificación
numDecodificado = 0;
//revisamos el segundo bit para ver si es negativo
if ((lectura & 0b1100_0000) == 0b1100_0000) {
//Como es un número negativo, tenemos que llenar de 1's
//esto para simular el complemento a 2
numDecodificado = 0xFFFF_F000;
}
lectura &= 0b0011_1111;
numDecodificado += lectura << 6;
}
//Sino es el primer byte, esperamos lectura 0b0sxx_xxxx
//Trabajando en el segundo byte
else {
lectura &= 0b0011_1111;
numDecodificado += lectura;
//Imprimimos cuando acaba el paquete
System.out.println(Integer.toBinaryString(numDecodificado));
}
} catch (Exception e) {
e.printStackTrace();
break;
}
}
comPort.closePort();
}
El paso siguiente es la conversión de valores del ADC -4096 -> 4095 a valores de voltaje usando las fórmulas que obtuvimos en la fase del diseño de circuito.
Agregamos una manera de calcular el voltaje en RN a partir del valor de la diferencia de los valores entre ambos ADCs y a partir de este valor obtenemos el valor de voltaje en T:
public static double VTdeVR(double resol, double deltaADC, double VRef, double R1, double R2, double N){
//Calculamos el valor del voltaje en RN
double VRN = VRef * deltaADC /resol;
//Regresamos el valor de V_T según la función inversa que calculamos desde el circuito
return VRN*(R1 * R2 + N * R1 + N * R2)/(R1 * R2);
}
Y agregamos la impresión de este método para visualizar los valores:
//Sino, esperamos lectura 0b0sxx_xxxx
//Trabajando en el segundo byte
else {
lectura &= 0b0011_1111;
numDecodificado += lectura;
//Imprimimos cuando acaba el paquete
System.out.println("Lectura ADC : "+ numDecodificado);
System.out.println("Voltaje V_RN: "+ VTdeVR(4096.0, numDecodificado, 3.3, 217.8, 233.2, 3300.0));
}
Podemos agregar una arreglo con valores de prueba para hacer pruebas y verificar que los valores obtenidos sean los correctos:
Por ejemplo, si en el arreglo introdujéramos los valores [0, 10, 1000, 4095, -1, -10, -100, -1000, -4096] obtendremos los valores como esperaríamos obtenerlos.
cu.wlan-debug
tty.wlan-debug
cu.ESP32_LED_Control
tty.ESP32_LED_Control
cu.Bluetooth-Incoming-Port
tty.Bluetooth-Incoming-Port
cu.usbserial-0001
tty.usbserial-0001
Lectura ADC : 0
Voltaje V_RN: -9.43396226419055E-4
Lectura ADC : 10
Voltaje V_RN: 0.12112461306013829
Lectura ADC : 100
Voltaje V_RN: 1.2197366966391452
Lectura ADC : 255
Voltaje V_RN: 3.111790840580775
Lectura ADC : 1000
Voltaje V_RN: 12.205857532429242
Lectura ADC : 4095
Voltaje V_RN: 49.98590640661851
Lectura ADC : -1
Voltaje V_RN: -0.013150197155072955
Lectura ADC : -10
Voltaje V_RN: -0.12301140551297639
Lectura ADC : -100
Voltaje V_RN: -1.2216234890919833
Lectura ADC : -1000
Voltaje V_RN: -12.20774432488208
Lectura ADC : -4096
Voltaje V_RN: -50.0
Los que son realmente importantes son los valores 0, 4095 y -4096 ya que estos valores son los que señalarán los valores de V_cero, V_Max, V_Min.
Podemos ver que los valores que obtuvimos coinciden con la predicción que habíamos realizado. Al tener la lectura del ADC más baja obtuvimos el valor de voltaje que deseábamos desde la fase de diseño, -50[V]. Al obtener el valor más alto, obtuvimos +50, que era el máximo valor que queríamos medir. Lo mismo sucede con el valor cuando el valor del ADC es 0, obtenemos un valor pequeñísimo muy muy cercano a cero.
Hay que recordar, aquí no estamos recibiendo los valores del ADC directamente sino que estamos recibiendo el valor de la resta de las lecturas de los dos ADCs.
Con este pequeño programa podríamos enviar el flujo de datos a cualquier aplicación gráfica por medio de protocolos del sistema operativo, sin embargo, por facilidad de programación, ya que estamos trabajando en un programa de Java, podremos agregar esa interfaz gráfica a este mismo programa.
Cómo haremos el proyecto
Ya que la aplicación se utilizará principalmente en computadoras personales (por diversas cuestiones convenientes como el uso de múltiples puertos USB y por extensión la posibilidad de usar varias tarjetas ESP-32) podemos pensar que en realidad el diseño del código que implementamos previamente es únicamente para interpretar los valores de un dispositivo cualquiera.
Podemos considerar que existen varios puertos USB en una computadora de la misma forma que existen otros puertos por los cuales podemos interpretar un valor de voltaje (por ejemplo la entrada de audio auxiliar). Pensemos que existen varios dispositivos conectados a la computadora. Inlcuso podríamos considerar la existencia de un "generador de señal" virtual, el cual genere una onda perfecta sinusoidal, una señal de escalón o simplemente un impulso cada determinado tiempo.
La solución que propongo yo para el manejo de todos estos dispositivos, y dejar abierta la posibilidad al desarrollo de dispositivos posteriores (como el uso de tarjetas integradas distintas a futuro así como generadores de señal personalizados) es manejar todos los valores que se introduzcan al osciloscopio por medio del protocolo UDP.
En este sentido, el osciloscopio, el intérprete de señal desde la ESP-32 así como cualquier dispositivo adicional posterior será una especia de aplicación independiente por sí sola. Esto nos dará ciertas ventajas a posteriori, no estaremos sujetos al desarrollo de ninguna aplicación por medio de ningún lenguaje de programación en específico. Además esto nos permitirá desarrollar incluso otros programas sustituto al osciloscopio así como de cada uno de los orígenes de señal que aquí hemos propuesto.
Implementación final
Los programas en los que se muestra el funcionamiento básico son los siguientes tres:
La arquitectura final de diseño será como en el siguiente diagrama:
En ánimos de darle modularidad a este proyecto, así como permitir que cualquiera pueda crear programas adicionales para su funcionamiento, el proyecto estará dividido en dos partes: generadores de señal y un graficador.
El graficador será un programa con interfaz gráfica de usuario donde se podrán visualizar los valores de voltaje a lo largo del tiempo, tal y como lo haríamos con un osciloscopio común. Estos valores de voltaje tendrán que llegar por medio de algún medio. Este medio deberá ser un medio accesible por cualquier otro programa que otras personas quieran programar. Es por esto que se optó por usar el protocolo UDP por medio de un puerto red para recibir los valores en el graficador.
Los generadores de señales serán únicamente programas que mandarán valores de voltaje a esos puertos UDP. En concreto, vamos a enviar un valor de 8 bytes entero "long" (el tiempo en nanosegundos cuando se generó ese voltaje) y el valor del voltaje que se generó "double".
Los valores del tiempo en nanosegundos se usarán para calcular el eje X en el que se graficará el valor de voltaje y los valores de voltaje para calcular el eje Y.
Uno de estos generadores de señal será el traductor de la ESP-32 a UDP. Este progama lo que hará será usar los valores de voltaje suministrados por el ADC de la tarjeta ESP-32 que llegarán por medio del puerto de monitor serial. Al tomar esos valores mandará esos valores por medio del protocolo UDP para que se reciban en el programa graficador.
Es importantísimo enfatizar, no es necesario utilizar todos los programas que estoy aquí suministrando para usar los demás. Es decir, si un usuario encontrara que mi programa generador de función seno es poco intuitiva, lenta o carente de funcionalidad, este usuario podría programar su propio programa para generar señales. El único verdader requisito sería mandar los valores "long" de tiempo y "double" de voltaje por los puertos UDP correctos.
Lo mismo ocurre con el programa graficador; el programa que estoy aquí presentando es un programa demostración con utilidad suficiente para muchos escenarios prácticos de los estudiantes, sin embargo, si un estudiante quisiera mejorarlo podría tomar su código fuente para agregarle las funcionalidades que considere faltantes, modificarlo más a su gusto o incluso reprogramarlo completamente y crear su propia implementación. Bajo ninguna circunstancia mi diseño es el mejor, hay mucho espacio para mejoría pero conceputalmente con este trabajo queda demostrado que se puede obtener la funcionalidad de un osciloscopio a partir de una computadora personal y una tarjeta de desarrollo si se usa el circuito y la programación correctas.
Omitiré las explicaciones del código en este documento.
Conclusiones
Invito a los usuarios de este proyecto a desarollar sus propios generadores de función y realizar sus modificaciones al programa graficador. Aquí he presentado los tres programas que he considerado más vitales para su uso: un programa que nos permite realizar mediciones con la ESP-32; un programa para generar funciones de referencia sinusoidales; un programa graficador para esas señales. Sin embargo, existen muchos programas que se podrían crear a partir de estas plantillas básicas.
- Generador de función escalón
- Generador de función sierra
- Generador de funciones arbitrarias a partir de un modelo
- Conección con el micrófono de la computadora
También existen cuestiones que no contempla mi graficador que se podrían agregar.
- Cálculo de frecuencias más frecuentes a partir de la transformada de Fourier
- Cálculo y correcta representación del momento "trigger" para centrar señales de manera estática
- Guardado y reproducción de señales personalizadas (i.e. a partir de archivos de audio)
Las únicas consideraciones a tener es la velocidad límite del protocolo UDP. De manera experimental logré determinar que mandar 50'000 lecturas (50'000 longs y 50'000 doubles) por segundo era suficientemente rápido como para empezar a ser poco confiable esto nos pone una cota superior a la máxima frecuencia confiable en 50kHz. Este fue el caso haciendo uso de mi computadora y mis circunstancias específicas de diseño. Quizá alguien con más conocimientos en este campo consiga velocidades más altas. Igualmente, con el paso del tiempo y el gradual incremento en las capacidades de las computadoras, quizá esta velocidad aumente con el tiempo también.
Suministraré aquí las plantillas básicas para aquellos que quieran programar una nueva señal sin crear un programa con interfaz gráfica de cero. Los comentarios en el código funcionarán de guía así como el documento de presentación visible en Github. Como siempre, recomiendo el uso del IDE Intellij Idea Community, pero cualquier otro editor de código sería igualmente funcional.
- Implementación sencilla: Plantilla generador de señal sin GUI
- Implementación completa: Plantilla generador de señal con GUI