¿Se puede usar una placa Circuit Playground Bluefruit como si fuera un micrófono conectado por USB y hacer que suene con puntos y rayas de código Morse cuando presionas un paddle? La respuesta corta es sí, y todo el proyecto ocupa menos de 200 líneas de CircuitPython. La respuesta larga es este tutorial, donde vamos a exprimir una feature nueva del firmware (USB audio nativo desde 10.3.0-alpha.3) para transformar la CPB en una interfaz de audio digital hacia el PC.

Este proyecto sirve para tres casos concretos: entrenar Morse con tono limpio sin sacar el equipo de HF, tener un "modo hotkey" que envía frases prearmadas a Discord/Zoom con solo un click, y como demo educativa de cómo funciona synthio + USB audio en CircuitPython. Es un proyecto de nivel principiante en el armado pero interesante técnicamente por lo que hay debajo.

¿Por qué esta combinación funciona?

TinyUSB. la librería que le da a CircuitPython su superpoder USB. soporta desde la versión 10.3.0-alpha.3 los descriptores de USB Audio Class. Eso significa que la placa puede presentarse al sistema operativo como un micrófono USB estándar, sin drivers extra. El OS la ve exactamente igual que un Blue Yeti o cualquier tarjeta de sonido, y podes seleccionarla como entrada en Audacity, Discord, OBS o cualquier app.

Synthio es el motor de síntesis interno de CircuitPython. En vez de reproducir samples pregrabados genera formas de onda en vivo (senoidales, cuadradas, triangulares, con envolventes ADSR opcionales). Cuando lo enchufas a la salida de usb_audio.usb_microphone, todo lo que sintetiza sale por el USB como si fuera audio capturado por un micrófono.

Un paddle de Morse es electricamente un pulsador momentáneo: cierra un circuito mientras lo pulsas, lo abre cuando lo sueltas. Conectando ese pulsador a un pin GPIO detectamos el evento con keypad.Keys, y cuando está cerrado le pedimos a synthio que toque una nota. Cuando se abre, apagamos la nota. Ese "beep controlado por dedo" ES tu Morse.

Materiales

  • Circuit Playground Bluefruit (Adafruit): la placa protagonista. Cualquier board CircuitPython con usb_audio compilado sirve, pero la CPB es la más práctica porque trae dos botones incorporados (A y B) que reutilizamos como "teclas de mensaje pregrabado". Requiere CircuitPython 10.3.0-alpha.3 o superior.
  • Paddle Morse con jack 3.5 mm (mono). Podes comprar uno tradicional en tiendas de radioaficionados, o improvisar uno con un pulsador NA (normalmente abierto) cableado a un plug 3.5 mm mono. Si tienes impresora 3D, hay diseños libres en Printables.
  • Cable adaptador jack 3.5 mm mono → dos caimanes (uno para señal, otro para GND).
  • Cable USB-A a micro USB (data capable, no de solo carga: esto es la trampa clásica de CircuitPython).
  • Buzzer piezo pasivo 5 V (opcional, para la variante ESP32 sin USB audio).

Circuit Playground Bluefruit con paddle Morse y cable de caimanes al jack 3.5mm

Cableado

El circuito es trivial:

  • Punta del jack 3.5 mm (contacto del centro) → pin A1 de la CPB
  • Anillo/mango del jackGND de la CPB

Los dos cables de caimán del adaptador se enganchan a los pads de tornillo A1 (blanco) y GND (negro). Cuando presionas el paddle, A1 queda conectado a GND (nivel BAJO), y keypad.Keys con value_when_pressed=False detecta un evento pressed. Al soltar, A1 queda flotando y el pull up interno lo lleva a nivel ALTO: evento released.

Diagrama de conexión del paddle Morse a la CPB por jack 3.5mm

Instalación de CircuitPython (requisito previo)

Como el proyecto usa usb_audio, necesitas una release de desarrollo, no la estable. Al momento de escribir esto la 10.3.0-alpha.3 es la versión mínima.

  1. Descarga el .uf2 de la placa CPB desde circuitpython.org/board/circuitplayground_bluefruit. busca en la sección "latest development release", no en "latest stable".
  2. Conecta la CPB por cable USB de datos (crítico: hay cables que solo cargan y frustran horas).
  3. Doble click al pequeño botón Reset al centro de la placa. Los 10 NeoPixels se ponen rojos y luego verdes.
  4. Aparece un drive CPLAYBTBOOT. Arrastra el .uf2 adentro.
  5. La placa reinicia, el drive se transforma en CIRCUITPY. Listo.

Si los NeoPixels quedan rojos y no cambian a verde: mal cable USB, mal puerto o click no registrado. Prueba single click, otro cable, otro puerto.

Circuit Playground Bluefruit vista frontal con botones A, B y NeoPixels

Código: dos archivos, boot.py y code.py

El proyecto se parte en dos scripts porque la configuración USB tiene que hacerse antes del boot final del intérprete. boot.py corre una sola vez apenas la placa arranca, y ahí es donde declaramos que queremos USB audio con micrófono habilitado.

boot.py

Python
# SPDX-FileCopyrightText: 2026 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import usb_audio

usb_audio.enable(
    sample_rate=16000, channel_count=1, bits_per_sample=16, microphone=True
)

Ese microphone=True es lo que hace la magia. A partir de ahora, al enchufar la CPB en el PC vas a ver un nuevo dispositivo de entrada de audio. Si cambias boot.py tienes que hacer reset físico de la placa para que tome efecto (Ctrl+D en la consola serial NO lo re corre).

code.py

El programa principal está en code.py. Lo dividimos en cuatro secciones lógicas: configuración, tabla Morse, setup de synth/keypad y loop principal.

Python
# SPDX-FileCopyrightText: 2026 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""USB audio Morse code paddle. Connect Morse paddle 3.5mm jack to pin A1 and GND.

Connect the board to a host computer and select the CircuitPython USB
microphone as a recording/input device.

Use the paddle to enter Morse code live, update the button messages to
automatically convert and send pre-programmed strings with the built-in
buttons.
"""

import time

import board
import keypad
import synthio
import usb_audio

# Configuration

BTN_A_MESSAGE = "HELLO WORLD"
BTN_B_MESSAGE = "CIRCUITPYTHON"

TONE_NOTE = 60  # synthio note number for the tone pitch (MIDI note, 60 = C4)

# Morse timing is defined in "units". A dot is one unit long; everything else
# is a multiple of that unit. Increase UNIT_SECONDS to slow the code down.
UNIT_SECONDS = 0.085

DOT_DURATION = UNIT_SECONDS  # length of a dot
DASH_DURATION = UNIT_SECONDS * 3  # length of a dash
SYMBOL_GAP = UNIT_SECONDS  # silence between dots/dashes in one letter
LETTER_GAP = UNIT_SECONDS * 3  # silence between letters
WORD_GAP = UNIT_SECONDS * 7  # silence between words (the " " character)

# Morse code table

MORSE_CODE = {
    "A": ".-",
    "B": "-...",
    "C": "-.-.",
    "D": "-..",
    "E": ".",
    "F": "..-.",
    "G": "--.",
    "H": "....",
    "I": "..",
    "J": ".---",
    "K": "-.-",
    "L": ".-..",
    "M": "--",
    "N": "-.",
    "O": "---",
    "P": ".--.",
    "Q": "--.-",
    "R": ".-.",
    "S": "...",
    "T": "-",
    "U": "..-",
    "V": "...-",
    "W": ".--",
    "X": "-..-",
    "Y": "-.--",
    "Z": "--..",
    "0": "-----",
    "1": ".----",
    "2": "..---",
    "3": "...--",
    "4": "....-",
    "5": ".....",
    "6": "-....",
    "7": "--...",
    "8": "---..",
    "9": "----.",
    ".": ".-.-.-",
    ",": "--..--",
    "?": "..--..",
    "'": ".----.",
    "!": "-.-.--",
    "/": "-..-.",
    "(": "-.--.",
    ")": "-.--.-",
    "&": ".-...",
    ":": "---...",
    ";": "-.-.-.",
    "=": "-...-",
    "+": ".-.-.",
    "-": "-....-",
    "_": "..--.-",
    '"': ".-..-.",
    "$": "...-..-",
    "@": ".--.-.",
}

# Setup

mic = usb_audio.usb_microphone
synth = synthio.Synthesizer(sample_rate=16000, channel_count=1)
mic.play(synth)


def play_tone(duration):
    \"\"\"Sound the tone for ``duration`` seconds, then go silent.\"\"\"
    synth.press(TONE_NOTE)
    time.sleep(duration)
    synth.release(TONE_NOTE)


def play_morse(text):
    \"\"\"Translate ``text`` to Morse code and play it with synthio.\"\"\"
    words = text.upper().split(" ")
    for word_index, word in enumerate(words):
        if word_index > 0:
            # gap between words
            time.sleep(WORD_GAP)

        for letter_index, letter in enumerate(word):
            pattern = MORSE_CODE.get(letter)
            if pattern is None:
                # skip characters we don't know how to send
                continue

            if letter_index > 0:
                # gap between letters
                time.sleep(LETTER_GAP)

            for symbol_index, symbol in enumerate(pattern):
                if symbol_index > 0:
                    # gap between symbols within a letter
                    time.sleep(SYMBOL_GAP)
                if symbol == ".":
                    play_tone(DOT_DURATION)
                else:
                    play_tone(DASH_DURATION)


btns_builtin = keypad.Keys((board.BUTTON_A, board.BUTTON_B), value_when_pressed=True)
btn_morse_key = keypad.Keys((board.A1,), value_when_pressed=False)

# Main loop

while True:
    event = btns_builtin.events.get()
    if event is not None:
        # built-in A button
        if event.pressed and event.key_number == 0:
            play_morse(BTN_A_MESSAGE)
        # built-in B button
        elif event.pressed and event.key_number == 1:
            play_morse(BTN_B_MESSAGE)

    event = btn_morse_key.events.get()
    if event is not None:
        if event.pressed and event.key_number == 0:
            synth.press(TONE_NOTE)
        elif not event.pressed and event.key_number == 0:
            synth.release(TONE_NOTE)

Copia los dos archivos a la unidad CIRCUITPY, haz reset de la placa una vez (para que boot.py corra), y listo.

Cómo funciona el código (los cinco puntos importantes)

  1. TONE_NOTE = 60 es un número MIDI. 60 es el Do central (C4, 261.63 Hz). Si quieres el tono más agudo típico de Morse (~600-800 Hz que es el estándar de práctica) subelo a 77 (F5, 698 Hz) o 80 (G#5, 830 Hz).
  2. UNIT_SECONDS = 0.085 define la velocidad de todo lo demás. Con 0.085 s por unidad estás enviando a ~14 palabras por minuto (WPM). Fórmula: WPM = 1.2 / UNIT_SECONDS. Para 20 WPM usa 0.06; para 8 WPM (principiante) usa 0.15.
  3. Dots y dashes con proporción 1:3:1:3:7 (dot, dash, gap simbolo, gap letra, gap palabra) es el estándar ITU-R M.1677 de Morse internacional. No cambies esas proporciones si quieres que decodificadores automáticos lean bien lo que envías.
  4. play_tone y play_morse son bloqueantes (usan time.sleep). Mientras suena un mensaje pregrabado no vas a responder al paddle. Si quieres interrumpibilidad, hay que reescribir a state machine no bloqueante. más complejo pero mejor UX.
  5. btn_morse_key con value_when_pressed=False funciona porque el paddle cierra a GND (nivel BAJO cuando se pulsa), y la CPB tiene pull up interno en A1. Si usas un pin sin pull up, agrega uno externo de 10 kΩ a 3.3 V.

Seleccionar el dispositivo de audio en el PC

Con boot.py y code.py corriendo, la CPB aparece en el sistema como un dispositivo llamado "Circuit Playground Bluefruit" o "CircuitPython USB Audio" en la lista de entradas de audio.

  • Windows: Panel de control → Sonido → pestaña Grabación. Botón derecho → Establecer como predeterminado.
  • macOS: Ajustes del sistema → Sonido → Entrada. Selecciona el dispositivo.
  • Linux/Ubuntu: Configuración → Sonido → Entrada. Selecciona el dispositivo.
  • Discord: rueda de la vista de usuario → Voz y video → Dispositivo de entrada → Circuit Playground Bluefruit.
  • Audacity: Audio Setup → Recording Device → Circuit Playground Bluefruit → apretar Record para grabar los beeps.

Modo live vs modo hotkey

El código hace dos cosas simultáneamente:

  • Modo live (paddle): mantén presionada la tecla y suena el tono. Presión corta = punto; presión larga (3×) = raya. Depende de tu pulso.
  • Modo hotkey (botones A/B integrados): presión rápida a A envía "HELLO WORLD" en Morse; B envía "CIRCUITPYTHON". Podes cambiar BTN_A_MESSAGE y BTN_B_MESSAGE por cualquier frase.

Los dos modos coexisten porque el loop principal escanea los tres botones en cada iteración sin bloquear (excepto durante un play_morse de mensaje pregrabado).

Variantes y mejoras

Cinco extensiones que le suman valor real:

  • Portar a ESP32 + BLE en vez de USB audio: el ESP32 no soporta USB Audio Class nativo, pero sí puede tocar Morse en un buzzer piezo pasivo conectado a un GPIO con PWM, y en paralelo enviar el texto decodificado por BLE HID como teclado inalámbrico. Se transforma en una teclita Morse portátil que escribe en cualquier dispositivo emparejado.
  • Registrar el mensaje decodificado en pantalla: agrega decodificación en tiempo real de los pulsos → letras (algoritmo Farnsworth de decodificación) y muestra en un OLED SSD1306 el mensaje transcrito mientras lo envías.
  • Metrónomo de práctica ("Farnsworth spacing"): para aprender Morse el método Farnsworth manda los símbolos rápido pero deja pausas grandes entre letras. Agrega un parámetro FARNSWORTH_GAP separado que se aplique solo entre letras.
  • Grabación automática a WAV: en vez de audio streaming al PC, agrega un módulo microSD y guarda los beeps en un archivo qso.wav datado por RTC. Útil para archivar sesiones de práctica.
  • Interfaz con radio real (CAT control): usa el mismo paddle para gatillar el PTT de una radio HF vía interfaz serial CAT (Icom CI-V, Yaesu). Ya no simula sonido. envía Morse real por HF.

Personalización para Chile

Componentes con equivalentes locales:

  • Circuit Playground Bluefruit: es la placa central. En Chile no siempre está en stock. Alternativas funcionales que soportan CircuitPython + USB audio (una vez que 10.3.0 estable salga): Adafruit Feather RP2040, Adafruit QT Py RP2040, Raspberry Pi Pico con firmware CircuitPython. La CPB tiene la ventaja de traer botones onboard y bocinita interna; con las otras vas a necesitar botones separados. Si prefieres MicroPython puro y ESP32, existe un port similar pero pierde la parte de USB audio (ver variantes).
  • Paddle Morse con jack 3.5 mm: no es un producto masivo. Alternativas: (a) improvisá uno con un pulsador momentáneo NA de 12 mm + un cable jack 3.5 mm mono a pelado, (b) imprime uno 3D desde Printables, (c) cualquier tecla de teclado mecánico (Cherry MX) también sirve como paddle rudimentario.
  • Cable jack 3.5 mm mono a caimanes: fácil en tiendas de electrónica local. Si no lo consigues, corta un cable auxiliar 3.5 mm barato y suelda dos caimanes al extremo pelado.
  • Cable USB-A a micro USB (data capable): cualquier cable de datos serio. Los "de solo carga" que vienen con productos de línea blanca no funcionan para CircuitPython, filtrá bien.
  • Buzzer piezo pasivo 5 V (para variante ESP32): componente estándar, cualquier tienda de electrónica lo tiene bajo $2.000 CLP.

Estimación de costo del proyecto en Chile a julio 2026 (versión CPB): ~CLP $35.000 - $55.000 según si consigues la CPB local o la importas. La variante ESP32 + buzzer sale menos de $15.000 CLP y sirve para aprender Morse igual, aunque perdés el modo micrófono USB.

Recursos

Versión chilena con alternativas de placas disponibles en stock local y variante ESP32 + BLE HID que no depende de USB audio.