Control del generador FeelTech FY2300 20M con Python: librería propia, GUI simple y carga de formas arbitrarias reales

https://github.com/hightechlog/fy2300-feeltech-waveform-tools/tree/main

En este proyecto trabajamos con el generador FeelTech FY2300 20M y construimos un conjunto de herramientas en Python para resolver una necesidad muy concreta: cargar y reproducir formas de onda arbitrarias reales medidas con osciloscopio, además de exponer una interfaz simple para que cualquier usuario pueda reutilizar el flujo sin editar código manualmente.

El problema

Existen repositorios públicos para controlar generadores FeelTech desde Python, y varios de ellos resultan útiles para funciones comunes. Sin embargo, en nuestro caso apareció un problema práctico al momento de enviar datos al generador para reproducción arbitraria.

La meta no era solamente activar una senoide o una cuadrada, sino lograr que el equipo reprodujera una forma medida experimentalmente, por ejemplo un segmento real tomado desde un archivo CSV del osciloscopio. Para ese flujo, los repositorios disponibles no resolvían correctamente nuestro escenario de trabajo con el FY2300 20M, así que desarrollamos una ruta propia orientada al comportamiento real del equipo en laboratorio.

Qué construimos

El proyecto quedó formado por tres piezas principales:

  • una biblioteca propia en Python para el FY2300,
  • un script funcional para cargar formas arbitrarias reales,
  • y una interfaz gráfica simple en Tkinter.

Con ello ya es posible:

  • leer un CSV del osciloscopio,
  • extraer un segmento temporal,
  • ajustarlo al número de puntos aceptado por el generador,
  • añadir ruido blanco opcional,
  • cargarlo como forma arbitraria,
  • y reproducirlo desde el equipo.

La biblioteca fy2300_serial.py

La biblioteca propia expone funciones directas para trabajar con el generador:

  • selección de formas integradas,
  • frecuencia,
  • amplitud,
  • offset,
  • duty cycle,
  • atenuación,
  • trigger,
  • y carga de formas arbitrarias.

Esto permitió construir una solución enfocada específicamente al FeelTech FY2300 20M, usando el flujo que sí funcionó en pruebas reales.

La GUI simple

Después de validar el flujo con scripts, el siguiente paso fue construir una GUI para simplificar el uso.

La interfaz permite:

  • probar conexión con el puerto COM,
  • configurar ondas comunes,
  • ajustar frecuencia, amplitud, offset y duty cycle,
  • seleccionar atenuación 0 dB / 20 dB,
  • cargar un segmento desde un CSV,
  • previsualizar localmente la señal,
  • y enviarla al generador como arb2.

La idea es que cualquier usuario pueda aprovechar la herramienta sin tener que modificar el código base.

Por qué puede ser útil

Este proyecto puede ser útil si trabajas con:

  • instrumentación electrónica,
  • generación de señales arbitrarias,
  • automatización de laboratorio,
  • síntesis de señales reales medidas,
  • o pruebas con generadores FeelTech desde Python.

Especialmente si quieres ir más allá del uso básico de senoides y cuadradas, y reproducir una señal experimental real en el generador.

Repositorio público

El proyecto fue preparado para publicarse como repositorio abierto en GitHub, incluyendo:

  • la biblioteca,
  • la GUI,
  • el script funcional de ejemplo,
  • documentación base,
  • y dependencias mínimas.

La intención es que otros usuarios puedan reutilizarlo, adaptarlo y extenderlo.

https://github.com/hightechlog/fy2300-feeltech-waveform-tools/tree/main

Cierre

No se trató de reemplazar todos los repositorios existentes, sino de resolver un caso específico que sí funcionara de manera reproducible con el FeelTech FY2300 20M, en particular para el envío de formas arbitrarias reales.

En siguientes entradas iré documentando con más detalle:

  • la estructura interna de la biblioteca,
  • el formato de la waveform,
  • y pruebas prácticas con señales reales.

CÓDIGO CRÍTICO Y LAS 10 REGLAS DE HOLZMANN

Las “10 Reglas” desarrolladas por Gerard J. Holzmann de la NASA son un conjunto de normas de codificación diseñadas para garantizar la seguridad y fiabilidad en sistemas de software críticos, es decir en aplicaciones por ejemplo de las industrias: aeroespacial, médico y de algunos sectores industriales. Estas reglas, a menudo aplicadas al lenguaje C, buscan reducir la complejidad y facilitar el análisis estático.

Software crítico: programas donde un error puede costar mucho: dinero, equipo, seguridad o incluso vidas. Por ejemplo: aeronáutica, automoción, equipo médico, control industrial, defensa, robótica peligrosa o firmware embebido importante.

La idea general no es “programar bonito”, sino:

Estas reglas buscan que el código sea:

En otras palabras: menos trucos, menos magia, menos libertad, más control.

  • No usar goto, setjmp, longjmp ni recursión directa o indirecta.
  • El programa debe seguir caminos simples y claros: if, else, switch, for, while.
  • Evitar mecanismos que “salten” de forma extraña o difícil de seguir.
  • Porque si el flujo del programa se vuelve enredado: cuesta entender qué pasó, cuesta depurarlo, aumenta la posibilidad de errores ocultos.

Ejemplos:

C
if (sensor_ok) {
    procesar();
} else {
    reportar_error();
}
C
goto etiqueta_error;
C
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

También se prohíbe recursión, porque en sistemas embebidos o críticos: consume pila de forma variable, puede desbordar stack, a veces no es fácil demostrar hasta dónde llegará.


Cada bucle debe tener un límite superior fijo y comprobable estáticamente.

Significa que cada for o while debe tener un número máximo de vueltas conocido de antemano.

Ejemplos:

C
for (int i = 0; i < 128; i++) {
    ...
}

Aquí se sabe que como máximo da 128 iteraciones.

C
while (dato != 0) {
    ...
}

Aquí no está tan claro cuándo termina.

En software crítico importa mucho saber:

  • cuánto tarda una función
  • si puede quedarse atorada
  • si puede bloquear el sistema

Si un bucle no tiene límite claro, puede:

  • tardar demasiado
  • volverse infinito
  • romper tiempos de tiempo real

En embebidos esto es muy importante. Por ejemplo, en un STM32:

  • si una rutina tarda demasiado, puedes perder datos del ADC
  • puedes romper la temporización
  • puedes afectar interrupciones o tareas

  • No utilice asignación dinámica de memoria después de la inicialización.
  • Significa, evitar: malloc(), calloc(), realloc(), free() durante la operación normal del sistema.
  • Porque la memoria dinámica puede causar:
    • fragmentación
    • tiempos impredecibles
    • fallos por falta de memoria
    • errores difíciles de reproducir
  • En su lugar, se prefiere:
    • memoria estática
    • buffers fijos
    • arreglos con tamaño definido

Ejemplos:

C
static uint16_t adc_buffer[256];
C
uint16_t *adc_buffer = malloc(256 * sizeof(uint16_t));

En firmware esto es muy común, en sistemas embebidos serios se inicializa todo al arranque y luego se trabaja con estructuras ya reservadas.


  • Ninguna función debe ser más larga de una hoja; normalmente no más de 60 líneas. Significa, que cada función debe hacer una cosa concreta y ser corta.
  • Porque las funciones largas:
    • mezclan demasiadas responsabilidades
    • cuesta mucho trabajo revisarlas
    • esconden errores
    • son difíciles de probar

Ejemplos:

  • lee ADC
  • filtra
  • detecta eco
  • actualiza pantalla
  • envía UART
  • maneja errores
C
ReadAdcBlock();
ApplyFilter();
DetectEcho();
UpdateDisplay();
SendTelemetry();

No es una ley física. Una función de 70 líneas no es automáticamente “mala”.

La regla busca obligar a dividir responsabilidades.

Si una función ya da flojera leerla completa, probablemente ya creció demasiado.


  • Al menos dos aserciones por función, en promedio.
  • Es decir, una comprobación que afirma: “esto debería ser siempre cierto; si no lo es, algo anda mal”.

Ejemplos:

C
assert(ptr != NULL);
assert(index < BUFFER_SIZE);
  • No sirve para manejar casos normales.
  • Sirve para detectar condiciones anómalas, que en teoría no deberían ocurrir.
C
if (archivo == NULL) {
    return ERROR_ARCHIVO;
}
C
assert(buffer_size > 0);
  • La aserción dice: “si esto falla, probablemente hay un bug de diseño o uso”.
  • Que la aserción solo debe comprobar, no modificar cosas.
C
assert(x < 10);
C
assert(++x < 10);

Si una aserción falla debe haber una acción clara:

  • devolver error
  • entrar a modo seguro
  • registrar falla
  • detener módulo
  • llevar a un estado seguro

Las aserciones son como sensores internos para detectar estados imposibles o peligrosos antes de que el sistema siga corrompiéndose.


Declarar todos los objetos de datos al nivel de ámbito más pequeño posible.

Significa que si una variable solo se usa dentro de un for, declárala ahí.

Si solo se usa en una función, no la hagas global.

Ejemplos:

C
void procesar(void) {
    int suma = 0;
    for (int i = 0; i < 10; i++) {
        suma += i;
    }
}
C
int i;
int suma;
void procesar(void) {
    ...
}

Porque mientras más grande el alcance de una variable:

  • más lugares pueden modificarla
  • más difícil es seguir su valor
  • más probable es que cause errores laterales

En firmware las variables globales son útiles a veces, pero hay que limitar su uso.

Si todo es global, el sistema se vuelve muy difícil de razonar.

Cada dato debe vivir en el lugar más pequeño donde realmente se necesita.


Quien llama debe revisar el retorno; quien recibe debe validar parámetros.

Si llamas una función que devuelve algo, debes comprobarlo.

Ejemplos:

C
status = InitSensor();
if (status != OK) {
    return ERROR_SENSOR;
}
C
InitSensor();   // y se ignora si falló

La función llamada no debe confiar ciegamente en lo que le mandan.

C
int ProcesarBuffer(uint16_t *buf, size_t len) {
    if (buf == NULL) return -1;
    if (len == 0) return -2;
    ...
}

Porque muchos errores nacen en interfaces entre módulos:

  • punteros nulos
  • tamaños incorrectos
  • índices fuera de rango
  • valores inválidos

El que llama debe verificar que todo salió bien.

La función llamada debe desconfiar de sus entradas.


  • Limitar el preprocesador a includes y macros sencillas (#include, #define, #if, etc.).
  • Porque el preprocesador puede volver el código:
    • difícil de leer
    • difícil de analizar
    • diferente según configuración
    • propenso a errores raros

Ejemplos:

C
#define MAX_SAMPLES 256
#define ADC_REF_MV 3300
C
#define SQUARE(x) ((x)*(x))
C
SQUARE(a+b)
C
SQUARE(i++)

También restringir compilación condicional, es decir, mucho #ifdef puede hacer que en realidad tengas 5 programas distintos ocultos en uno mismo.

El preprocesador debe usarse poco y con cuidado. Nada de magia excesiva.


No más de un nivel de desreferenciación; no punteros a funciones.

Ejemplos:

C
*ptr
C
**ptr2

ya es más complejo.

Porque los punteros son poderosos, pero también una gran fuente de errores:

  • acceso inválido
  • corrupción de memoria
  • errores difíciles de rastrear

En especial evitar:

  • punteros a punteros
  • punteros a funciones
  • casts extraños
  • ocultar punteros en macros o typedefs

Ejemplos:

C
void FillBuffer(uint16_t *buf, size_t len);
C
void Process(uint16_t **buf);

En C real, sobre todo en sistemas embebidos, a veces sí se necesitan punteros.

La regla no dice “prohibidos totalmente”, sino “úsarlos con mucha disciplina”.

Usa punteros solo cuando de verdad hagan falta, y mantén su uso lo más directo posible.


Compilar siempre con warnings estrictos y pasar análisis estático sin advertencias, desde el inicio del proyecto:

  • activar warnings máximos del compilador
  • no tolerar advertencias
  • usar analizadores estáticos

Ejemplos de warnings útiles:

  • variable no usada
  • conversión peligrosa de tipos
  • comparación entre signed/unsigned
  • posible acceso fuera de rango
  • función sin retorno correcto

El análisis estático son herramientas que revisan el código sin ejecutarlo y detectan posibles fallos.

Por ejemplo:

  • ramas imposibles
  • punteros nulos potenciales
  • desbordamientos
  • bucles sospechosos
  • variables no inicializadas

Porque muchos errores se pueden detectar antes de probar en hardware.

Para firmware STM32 esto es extremadamente útil, porque a veces un warning “pequeño” termina siendo un bug real en runtime.

Un warning no es decoración. En software crítico, se trata como posible defecto real.

Estas reglas tienen una filosofía muy concreta:

1. Predictibilidad: Que el programa haga siempre lo esperado.

2. Analizabilidad: Que una persona o herramienta pueda demostrar propiedades del código.

3. Simplicidad: Reducir complejidad accidental.

4. Contención del daño: Si algo sale mal, que falle de forma controlada.

En software crítico, el criterio no es “qué tan elegante o poderoso es”, sino: ¿qué tan fácil es demostrar que no fallará de forma peligrosa?

1. Nada de saltos raros ni recursión → que el flujo sea claro.

2. Bucles con máximo conocido → que no se cuelgue ni tarde de más.

3. Sin malloc/free en operación normal → evitar problemas de memoria.

4. Funciones cortas → más fáciles de entender y probar.

5. Usar aserciones → detectar estados imposibles o bugs.

6. Variables en el menor alcance posible → menos confusión y menos errores.

7. Revisar retornos y validar entradas → interfaces seguras entre funciones.

8. Pocas macros y simples → menos código oculto o engañoso.

9. Punteros restringidos → menos errores de memoria.

10. Cero warnings + análisis estático → detectar problemas temprano.

Si el software es importante, no programes con libertad total; programa de forma limitada, controlada y demostrable.