Package: arduscope
ArduscopioArduscopio es una interfaz Arduino-Python desarrollada para la adquisición de datos
analógicos. El comportamiento de la interfaz imita algunas características propias de un
osciloscopio, tales como la funcionalidad de Trigger y el cambio de escala temporal.Arduscopio surge como una iniciativa para facilitar la adquisición de datos en la materia
Laboratorio 3 del Departamento de Física de la Facultad de Ciencias Exactas y Naturales de
la UBA.A partir de la versión 0.3 el proyecto cuenta además con un generador de funciones que
trabaja mediante la placa de sonido y un módulo de ajuste para señales periódicas.InstalaciónPythonEl paquete puede ser instalado utilizandoPIP:pip install arduscopeEl paquete instala las siguientes dependencias en caso de no encontrarse presentes:numpymatplotlibscipypyserialtqdmArduinoEl código de Arduino puede ser descargado desde el repositorio del proyecto.Click derecho en este link -> Guardar comoDebe ser cargado en una placa Arduino UNO (excluyente).Ninguna fracción del código fue pensada para ser compatible con otra placa.Cualquier funcionamiento del código fuera de Arduino UNO es mera coincidencia.Ejemplo de uso del ArduscopioLos elementos centrales del paquete son el objetoArduscope(la interfaz entre Python y
el Arduino)
y el objetoArduscopeMeasure(un contenedor para los resultados adquiridos).En primer lugar debemos importar estas clases del paquete que instalamos:fromarduscopeimportArduscope,ArduscopeMeasureLuego podemos crear una instancia del Arduscopio, indicando el puerto de conexión. Nos
conviene utilizar el contextowith ... aspara no olvidarnos de cerrar el puerto.Arduino IDE nos muestra el nombre del puerto en la esquina inferior derecha.En Windows los puertos suelen llamarse"COM1","COM2", etc. En Linux el formato suele
ser"/dev/ttyUSB0"o parecido.withArduscope(port='/dev/ttyUSB0')asarduino:El objeto que instanciamos bajo el nombre arduino (podría ser cualquier otro) tiene una
serie de propiedades y métodos.La lógica general es que las propiedades se configuren primero y luego se active la
adquisición.Si el usuario olvida detener la adquisición y cambia una propiedad la adquisición se
detiene y reanuda automáticamente.arduino.frequency=2000# Frecuencia de adquisición (en Hz)arduino.pulse_width=0.05# Ancho del pulso digital (en Segundos)arduino.trigger_value=2.5# Valor del trigger (en Volts)arduino.amplitude=5.0# Amplitud de la señal (en Volts)arduino.n_channels=2# Cantidad de canales (1 a 6)arduino.trigger_channel="A0"# Canal/Modo de trigger (ver apartado)arduino.trigger_offset=0.0# Offset del trigger (en fracción de pantalla)Para comenzar la adquisición se utiliza el métodostart_acquire().La misma puede ser detenida mediantestop_acquire().Estos métodos no detienen la ejecución principal del programa.El métodolive_plot()abre un gráfico de Matplotlib y actualiza su contenido para cada
nueva pantalla. Requiere que el modo ventana de Matplotlib se encuentre activo.Hasta que el gráfico no es cerrado la ejecución principal se detiene, generando un momento
para que el usuario vea los datos en tiempo real y decida si quiere continuar con su
script o detenerlo.arduino.start_acquire()arduino.live_plot()A continuación podemos trabajar con los datos adquiridos.En primer lugar conviene asegurar que fueron adquiridas la cantidad de pantallas que
necesitamos. Para eso podemos usar la funciónwait_until(n_screens, timeout).Esta función detiene la ejecución principal hasta que el buffer de pantallas
tengan_screensalmacenadas. También podemos pasarle un parámetro opcional detimeoutpara que se produzca un error por tiempo límite (en segundos).arduino.wait_until(n_screens=50,timeout=None)Si la cantidad de pantallas ya fue alcanzada al llamar esta función el resultado será
inmediato. También podríamos detener la adquisición para que el buffer deje de
sobreescribirse. Podemos hacerlo explícitamente mediante la funciónstop_acquireo
dejar que el cierre del contextowithlo haga automáticamente.Podemos acceder al las mediciones realizadas mediante la propiedadmeasure.
Esta propiedad nos devuelve un objetoArduscopeMeasure, que es nuestro contenedor de
resultados.measure=arduino.measureEste objeto almacena los resultados obtenidos junto con el estado del Arduscopio al
momento de realizar la medición (las mismas propiedades que configuramos antes).
También tiene un vectorxgenerado a partir de la frecuencia que corresponde al eje
temporal.Los canales quedan almacenados en una lista y podemos recuperarlos mediante la propiedadchannels. El formato para cada canal es[cantidad de pantallas] x [puntos por pantalla].
Podemos ver lo mencionado hasta acá con algunos ejemplos:# Eje temporal de la medición del canal A0>>measure.x[0]# Valor del trigger>>measure.trigger_value# Canal A0, pantalla más antigua>>measure.channels[0][0]# Canal A2, pantalla más reciente>>measure.channels[2][-1]# Canal A2, pantalla más reciente, primeros 50 puntos>>measure.channels[2][-1][0:50]# Canal A1, promedio de todas las pantallas>>measure.channels[1].mean(axis=0)# Canal A1, desviación estándar de todas las pantallas>>measure.channels[1].std(axis=0)# Canal A0, promedio de las últimas 50 pantallas>>measure.channels[1][-50:].mean(axis=0)Observando esos ejemplos podríamos hacer un gráfico muy simple del siguiente modo:fig,ax=plt.subplots(1,1,figsize=(8,6))ax.plot(measure.x[0],measure.channels[0].mean(axis=0),label='a0')ax.plot(measure.x[1],measure.channels[1].mean(axis=0),label='a1')ax.set_title(f"Valor del trigger:{measure.trigger_value}V")plt.show()Finalmente podríamos querer almacenar una medición en un archivo. El objeto screen provee
un métodosavepara facilitar esta tarea. El formato se decide a partir de la extensión
del archivo creado:measure.save("data.csv")# Formato CSV (separado por comas)measure.save("data.npz")# Formato NPZ (array comprimido de Numpy)measure.save("data.json")# Formato JSON (objeto de JavaScript)Para recuperar una pantalla guardada vamos a crear un nuevo objetoArduscopeMeasuredel
siguiente modo:measure=ArduscopeMeasure.load("data.csv")Ejemplo completo (sólo Arduscopio)importmatplotlib.pyplotaspltfromarduscopeimportArduscope# from arduscope import ArduscopeMeasurewithArduscope(port='/dev/ttyUSB0')asarduino:arduino.frequency=1000arduino.pulse_width=0.2arduino.trigger_value=1.0arduino.amplitude=5.0arduino.n_channels=2arduino.trigger_channel="D7OUT_HIGH"arduino.trigger_offset=0.0arduino.start_acquire()arduino.live_plot()arduino.wait_until(n_screens=10,timeout=None)measure=arduino.measureax:plt.Axesfig,ax=plt.subplots(1,1,figsize=(8,6))ax.plot(measure.x[0],measure.channels[0].mean(axis=0),label='a0')ax.plot(measure.x[1],measure.channels[1].mean(axis=0),label='a1')ax.set_title(f"Trigger:{measure.trigger_value}V")plt.show()measure.save("data.csv")# Formato CSV (separado por comas)measure.save("data.npz")# Formato NPZ (array comprimido de Numpy)measure.save("data.json")# Formato JSON (objeto de JavaScript)# measure = ArduscopeMeasure.load("data.csv")Ejemplo de uso del Módulo de ajusteEl módulo de ajuste consiste en un método principalfit_signalcuya tarea es ajustar
funciones periódicas de la forma:f(x, amplitud, frecuencia, fase, offset)Cualquier función que contenga esos 4 parámetros (más la variable independiente) puede
ser trabajada mediante este método. Internamente realiza las siguientes tareas:Estima la amplitud como la diferencia entre el percentil 95 y 5 de los datos.Estima el offset mediante el valor medio de la señal.Estima los parámetros iniciales de frecuencia y fase por 3 métodos diferentes:Transformada rápida de Fourier (conrfftdescipy.fft)Autocorrelación de la señal (concorrelatedescipy.signal.signaltools)Interpolado y búsqueda de ceros (conUnivariateSplinedescipy.interpolate).Utiliza el ajuste por cuadrados mínimoscurve_fitde la libreríascipy.optimizepara los 3 conjuntos de parámetros iniciales y selecciona de entre todos el que
presenta el menor RMSE (raíz del error cuadrático medio).Para utilizarla sólo debemos indicar la función objetivo (por ejemplo la funciónsineque está definida en el propio módulo y las tiras de datos paraxey):importnumpyasnpfromarduscopeimportfit_signal,sine# ...# Adquisición y almacenamiento en variables `x` e `y`# ...# Ajuste de la señalparams,stds,rmse=fit_signal(sine,x,y)A continuación podemos utilizar los parámetros obtenidos para generar una nueva curva:# Aumentamos la resolución en x 20 vecesx_fit=np.linspace(min(x),max(x),num=x.size*20)# Calculamos la función de ajuste con los parámetros óptimosy_fit=sine(x_fit,*params)Ejemplo de uso del generador de funcionesEl generador de funciones hace uso intensivo del módulosimpleaudio. El mismo debe
ser instalado aparte. En Windows bastará con hacer:pip install simpleaudio.
En Linux deberemos instalar además otras dependencias:sudo apt-get install -y python3-dev libasound2-devEste generador de funciones preparará un sonido y lo repetirá las veces que sea
necesario. La repetición sufre una pequeña demora entre ciclos, por lo que se recomienda
configurar una duración mínima del sonido mayor a los tiempos de adquisición esperados.fromarduscope.wave_generatorimportWaveGenerator# Nuevo generador de funcioneswg=WaveGenerator(duration=20)La configuración sólo puede realizarse mientras el sonido se encuentra detenido. Tenemos
los siguientes parámetros a nuestra disposición:# Propiedades del canal 1wg.channel1.amplitude=1.0# Amplitud (0.0 a 1.0)wg.channel1.frequency=440# Frecuencia (1 a 4000 en Hz)wg.channel1.phase=0# Fase en radianeswg.channel1.waveform="sine"# "sine", "square", "triangle"wg.channel1.enabled=True# Canal encendido / apagado# Propiedades del canal 2wg.channel2.amplitude=1.0wg.channel2.frequency=440wg.channel2.phase=0wg.channel2.waveform="sine"wg.channel2.enabled=TrueCuando queramos que un sonido se ejecute abriremos un contexto y dentro del mismo
ubicaremos las tareas a realizar. Por ejemplo, si queremos escuchar el sonido por 5
segundos podemos hacer lo siguienteimporttimewithwg.play():foriinrange(5):time.sleep(1.0)print(f"Transcurrieron{i+1}segundos")Ejemplo completo (Arduscopio + fit_signal + barrido de fase con WaveGenerator)importnumpyasnpimportmatplotlib.pyplotaspltfromarduscopeimportArduscope,fit_signal,sinefromarduscope.wave_generatorimportWaveGenerator# Nuevo generador de funcioneswg=WaveGenerator(duration=20)# Propiedades del canal 1wg.channel1.amplitude=1.0# Amplitud (0.0 a 1.0)wg.channel1.frequency=440# Frecuencia (1 a 4000 en Hz)wg.channel1.phase=0# Fase en radianeswg.channel1.waveform="sine"# "sine", "square", "triangle"wg.channel1.enabled=True# Canal encendido / apagado# Propiedades del canal 2wg.channel2.amplitude=1.0wg.channel2.frequency=440wg.channel2.phase=0wg.channel2.waveform="sine"wg.channel2.enabled=True# Contexto para conectar / desconectar el ArduscopiowithArduscope(port='/dev/ttyUSB0')asarduino:# Configura los parámetros del Arduscopioarduino.frequency=8000# Frecuencia de muestreoarduino.trigger_value=2.5# Valor de triggerarduino.amplitude=5.0# Amplitud máxima de la señal medidaarduino.n_channels=2# Cantidad de canalesarduino.trigger_channel="A0"# Modo del triggerarduino.trigger_offset=0.0# Offset del trigger# Barrido de faseforphaseinnp.linspace(0,90,11):# Configura la fase del canal 2wg.channel2.phase=phase*np.pi/180# Inicia la reproducción del sonidowithwg.play():# Inicia la adquisiciónarduino.start_acquire()# Espera 50 pantallasarduino.wait_until(50)# Detiene la adquisiciónarduino.stop_acquire()# Transfiere las mediciones a un contenedormeasure=arduino.measure# Grafica los canales (con el mejor ajuste disponible)fig:plt.Figureax:plt.Axesfig,ax=plt.subplots(1,1,figsize=(8,6))phases=[]foriinrange(arduino.n_channels):# Coordenada x (con el offset correspondiente a cada canal)x=measure.x[i]# Coordenada y promediada en las 50 pantallasy=measure.channels[i].mean(axis=0)# Realiza un ajusteparams,stds,rmse,=fit_signal(sine,x,y)# Grafica el ajuste, los datos y usa el RMSE para dibujar erroresx_fit=np.linspace(min(x),max(x),num=x.size*20)y_fit=sine(x_fit,*params)ax.plot(x,y,ls='',marker='o',color=f"C{i}")ax.plot(x_fit,y_fit,color=f"C{i}")ax.fill_between(x_fit,y_fit-rmse,y_fit+rmse,color=f"C{i}",alpha=0.3)# Recuerda la fase ajustada (para posterior comparación)phases.append(params[2])# Calcula la diferencia de fase (corregida al intervalo -pi, pi)phase_diff=((phases[0]-phases[1])-np.pi)%(2*np.pi)-np.pi# Imprime los resultadosprint(f"\n"f"Expected phase:{phase:.1f}\n"f"Measured phase:{phase_diff*180/np.pi:.1f}")plt.tight_layout()plt.show()# Borra el buffer antes de comenzar la siguiente mediciónarduino.clear_buffer()