Aplicacion multiplataforma (Windows/macOS/Linux, Python 3.11+) para practicar QSOs CW con audio real (llave + oscilador/micro/line-in/virtual cable) o teclado (VBan o teclas Ctrl), validacion por estados y respuesta automatica en Morse.
- Captura de audio en tiempo real desde dispositivo seleccionable.
- Selector de entrada
AudiooKeyboard. - Deteccion de tono CW (Goertzel + auto-tone opcional).
- Decodificacion Morse a texto con histeresis, AGC basico y estimacion de WPM.
- Maquina de estados de QSO con validaciones y mensajes de error claros.
- TX automatica en CW hacia dispositivo de salida seleccionable.
- Modo simulacion por stdin (
--simulate) para pruebas sin audio. - Exportacion de sesion/logs en JSON.
core/decoder.pycore/encoder.pycore/qso_state_machine.pydata/exchange_patterns.yamlui/app.pyconfig.yamltests/.github/workflows/windows-build.yml
Para Windows podrás encontrar un paquete precompilado en ZIP la sección de releases (https://github.com/dacacioa/cw-keyer-trainer/releases). Descomprime la carpeta CWKeyTrainer y ejecuta el CWKeyTrainer.exe
- Configura tu indicativo.
- Verifica que estás en modo Simple
- Configurada dispositivo de audio de entrada o KeyBoard para trabajar con VBand o teclas CTRL
- Carga tu base de datos de indicativos personalizados (puedes localizar base de datos aquí https://n1mmwp.hamdocs.com/mmfiles/categories/callhistory/) usando el botón Load Calls File
- Pulsa Run y empieza a hacer QSOs.
Pasos para ejecutar desde el código fuente:
git clone https://github.com/dacacioa/cw-keyer-trainer.git
cd cw-keyer-trainer
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS/Linux
source .venv/bin/activate
python -m pip install -r requirements.txtGUI:
python -m appAlternativa:
python ui/app.pyListar dispositivos:
python -m app --list-devicesPreseleccionar por CLI:
python -m app --input-device 3 --output-device 6La parte superior esta compactada en dos columnas:
- Columna izquierda:
QSO Status+Signal. - Columna derecha:
Runtime Controls+QSO/Decoder/Encoder Settings.
Notas de UI recientes:
QSO Statusmuestra soloState.- Boton
Clear Decodingpara limpiar el buffer de texto decodificado. - Boton
Logpara expandir/contraer el panel de logs. - El campo
other_callya no se edita en GUI.
Controles runtime:
Mode(Audio/Keyboard)RunPauseStopRestart QSOCalibrateExport LogLoad Calls File(guarda copia local endata/other_calls.csv)Auto WPMAuto Tone- Seleccion
Input/Output
Modo Keyboard:
Ctrlizquierda =dit(punto),Ctrlderecha =dah(raya).- Keyer iambico
A(doble paleta) con repeticion continua al mantener paleta. - La velocidad usa
wpm_target. - Se inyecta audio interno al decoder (sin
input_device). - Se emite sidetone por
outputcontone_hz_rxy volumenencoder.volume. - El keying solo actua con la ventana enfocada y runtime en
RUNNING. - En este modo no aplican
Input,Auto ToneniCalibrate Noise. - El sidetone se mezcla con el TX automatico de la app.
Uso rapido de Keyboard:
- Selecciona
Mode = Keyboard. - Ajusta
wpm_targetytone_hz_rx. - Pulsa
Apply Settingsy luegoRun. - Manipula con
Ctrlizquierda/derecha (iambico A).
Parametros en settings:
my_callcq_mode(Simple/POTA/SOTA, exclusivo)prosign+Use Prosignswpm_target(RX),wpm_out_startywpm_out_end(TX aleatorio por QSO)farnsworth_wpm(TX efectivo; botonON/OFF, enOFFse fuerza a0)tone_hz_rx,tone_hz_out_startytone_hz_out_end(TX aleatorio por QSO)threshold_on,threshold_offpower_smooth,gap_char_dots,min_up_ratiomessage_gap_smax_stations(cantidad maxima de estaciones en cola tras cada CQ)incoming_call_%(0/25/50/75/100)p2p_%(0/25/50/75/100, solo en modoPOTA)my_park_ref(referencia propia para cierre P2P en S5)allow_599,allow_tu
- Estado del QSO (
S0..S6) - Nivel de audio (barra + dBFS)
- Tono detectado
- WPM estimado + dot ms
- Estado de key (
UP/DOWN) - Buffer de texto copiado
- Logs de eventos/errores
Formato:
- Texto/CSV por lineas.
- Se ignoran lineas vacias y lineas que empiezan por
#. - De cada linea valida se usa solo el primer campo separado por
,como indicativo.
Ejemplo:
# comentario
N1MM,John,MA
K1ABC,Anna
EA4XYZ
Comportamiento:
- Cada nuevo QSO elige un indicativo aleatorio del pool.
- Al cargar en GUI se copia a
data/other_calls.csv. - Ese fichero local se reutiliza en siguientes arranques.
- Si no hay pool cargado, se usa
qso.other_callcomo fallback interno.
- La app usa
qso.parks_file(por defectodata/all_parks_ext.csv). - Se cargan referencias activas (
active=1) de la columnareference. - En modo
POTA,p2p_%define la probabilidad de que la primera estacion que llama salga comoP2P. - Solo puede haber una estacion
P2Pactiva por tanda de llamadas.
La validacion de intercambios (s0, s2, s5) y las plantillas de TX (tx) se definen en fichero externo.
Fichero y carga:
- Fichero activo:
qso.exchange_patterns_fileenconfig.yaml(por defectodata/exchange_patterns.yaml). - Se carga al crear
QSOStateMachine(arranque de GUI/simulador). - Si cambias el YAML, reinicia la app para recargarlo.
- Si el fichero no existe, no es YAML valido o tiene raiz invalida, la app usa defaults internos y deja
WARNen logs.
Formato del YAML:
- Se acepta raiz
patterns:o raiz directa cons0/s2/s5/tx. s0,s2,s5usan lista de regex por clave.txusa plantillas de texto.- El merge es parcial sobre defaults: solo sobrescribes lo que declares.
- Las listas vacias o strings vacios no borran defaults; simplemente se ignoran.
Normalizacion antes de hacer match:
- RX se convierte a mayusculas.
- Se compacta sin espacios.
- Prosigns tipo
<BK>se compactan comoBK. - Ejemplo:
CQ POTA DE EA3IPX Kse evalua comoCQPOTADEEA3IPXK.
Placeholders disponibles:
{MY_CALL},{OTHER_CALL},{OTHER_CALL_REAL},{PROSIGN},{TX_PROSIGN},{CALL},{PARK_REF},{MY_PARK_REF}.- En regex, los placeholders se sustituyen escapados como literal (no como sub-regex).
- En
tx, los placeholders se sustituyen como texto.
Claves que usa el motor:
s0:SIMPLE,POTA,SOTAs2:report_require_call,report_require_call_allow_599,report_no_call,report_no_call_allow_599,p2p_acks5:with_prosign,with_prosign_allow_tu,without_prosign,without_prosign_allow_tu,p2p_with_prosign,p2p_with_prosign_allow_tu,p2p_without_prosign,p2p_without_prosign_allow_tutx:caller_call,repeat_selected_call,ack_rr,report_reply,qso_complete,p2p_repeat_call,p2p_repeat_ref,p2p_station_reply_without_tu,p2p_station_reply_with_tu
Notas importantes de claves:
- En
s0, las claves se normalizan a MAYUSCULAS (simpleySIMPLEequivalen). - En
s2/s5/tx, las claves son case-sensitive y deben coincidir exacto. - Puedes anadir claves extra, pero si el motor no las consulta no tendran efecto.
Ejemplo 1 (override minimo solo en S2):
patterns:
s2:
report_no_call:
- '^(?:5NN){2}$'Ejemplo 2 (override de plantillas TX):
patterns:
tx:
report_reply: 'CUSTOM REPORT {TX_PROSIGN}'
qso_complete: 'TU EE'Recorte real del default (data/exchange_patterns.yaml):
patterns:
s0:
SIMPLE:
- '^.*(?:CQ)+.*DE.*(?:{MY_CALL})+.*K.*$'
s2:
p2p_ack:
- '^{OTHER_CALL}$'
tx:
repeat_selected_call: '{OTHER_CALL} {OTHER_CALL}'
p2p_station_reply_without_tu: 'R R {OTHER_CALL_REAL} {OTHER_CALL_REAL} MY REF {PARK_REF} {PARK_REF} 73 {TX_PROSIGN}'
s5:
with_prosign:
- '^.*{PROSIGN}.*73.*EE.*$'- CQ segun
qso.cq_mode:POTA:CQ POTA DE {my_call} KSOTA:CQ SOTA DE {my_call} KSIMPLE:CQ DE {my_call} K
- App TX: llama entre
1..max_stationsestaciones (aleatorias del pool), cada una con delay aleatorio0..2s. - Usuario: selecciona una estacion por indicativo exacto y envia reporte (
{other_call} RST RST, p. ej.5NN 5NNo57N 599). - App TX (QSO normal):
{prosign_literal_tx} UR 5NN 5NN TU 73 {prosign_literal_tx}. - Usuario (QSO normal, S5):
- sin
allow_tu:{prosign_literal_rx} 73 EE(o73 EEsiuse_prosigns=false) - con
allow_tu:{prosign_literal_rx} TU 73 EE(oTU 73 EEsiuse_prosigns=false)
- sin
- App TX:
EEy, si quedan estaciones pendientes, vuelven a llamar ignorandoincoming_call_%. - Solo cuando no hay pendientes se aplica
incoming_call_%para meter una nueva estacion automaticamente.
- Si
p2p_% > 0, la primera estacion de la tanda puede salir comoP2P P2P. - Usuario (activador): responde
P2P. - App TX (estacion P2P, por defecto en
data/exchange_patterns.yaml):R R {other_call_real} {other_call_real} MY REF {park_ref} {park_ref} [TU] 73 {prosign_literal_tx}.park_refse envia compactado (sin guion), por ejemploUS-1234 -> US1234.TUsolo se incluye cuandoallow_tu=true.
- Usuario (S5 P2P):
- sin
allow_tu:{prosign_literal_rx} {other_call_real} {my_call} MY REF {my_park_ref} {my_park_ref}(o sin prosign siuse_prosigns=false) - con
allow_tu:{prosign_literal_rx} {other_call_real} {my_call} MY REF {my_park_ref} {my_park_ref} TU 73 {prosign_literal_rx}(o sin prosign siuse_prosigns=false) - ayuda en S5 (solo P2P):
CALL?repite el indicativo llamante,REF?repite la referencia del parque remoto.
- sin
- En el log de completions se guarda:
{real_call} (P2P) {PARK}. - Todo este flujo P2P se valida/forma desde
data/exchange_patterns.yaml(s2.p2p_ack,tx.p2p_repeat_*,tx.p2p_station_reply_*,s5.p2p_*).
Comportamiento en S2:
- Indicativo completo con
?(ej.EA3IMR?) => selecciona esa estacion y respondeRR. - Parcial con
?(ej.EA3?,EA?) => responden solo las estaciones en cola que coinciden. - Si ya hay estacion seleccionada, cualquier token con
?repite ese indicativo (tx.repeat_selected_call) y se mantiene enS2. - Si no hay coincidencias para el patron, no responde ninguna estacion.
python -m app --simulateComandos:
/reset/export/quit
--input-mode audio|keyboard--my-call--my-park-ref--other-call(fallback cuando no hay pool dinamico)--cq-mode SIMPLE|POTA|SOTA--other-calls-file--parks-file--wpm-target,--wpm-out--wpm-out-start,--wpm-out-end--farnsworth-wpm(0desactiva Farnsworth)--tone-hz,--tone-out-hz--tone-out-start-hz,--tone-out-end-hz--message-gap-sec--auto-wpm,--fixed-wpm--auto-tone,--fixed-tone--max-stations--p2p-percent--allow-599--allow-tu--disable-prosigns--prosign-literal--s4-prefix R|RR
- Ajusta
tone_hz_rxcerca del oscilador (ej.600-700 Hz). - Si conoces tono fijo, usa
Auto Tone = OFF. - Si hay falsos positivos, sube
threshold_on. - Si corta mensajes antes/despues de tiempo, ajusta
message_gap_s. - Si una
C(-.-.) sale comoM(--), prueba:Auto Tone = OFFpower_smooth_alpha = 1.0threshold_on = 2.8-3.2,threshold_off = 1.6-2.0gap_char_threshold_dots = 1.8min_key_up_dot_ratio = 0.0
- Usa
Calibratecuando cambie ruido o nivel.
Workflow: .github/workflows/windows-build.yml
Comportamiento:
- Se ejecuta al publicar una Release (
release: published). - Ejecuta tests.
- Construye paquete
onedircon PyInstaller (arranque mas rapido queonefile). - Genera ZIP:
CWKeyTrainer-windows-<tag>.zip. - Sube el ZIP como:
- artifact del workflow
- asset de la Release
- Dentro del ZIP, ejecuta
CWKeyTrainer\CWKeyTrainer.exe. - Descarga siempre la ultima version desde la
latest release:- URL:
https://github.com/<owner>/<repo>/releases/latest - Ahí encontraras el asset
CWKeyTrainer-windows-<tag>.zip.
- URL:
python -m pytest -qIncluye:
- Roundtrip encoder->decoder sintetico (15/20/25 WPM) con precision >95%.
- Validaciones de maquina de estados (CQ, cola de estaciones, seleccion por indicativo y casos con
?).