From 102093f9df44e902ac521e89540b6ea0e8935347 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Wed, 25 Mar 2026 11:04:10 +0100 Subject: [PATCH 01/22] docs: add italian translations for shortcut keys --- shortcuts_keys_comand_it.txt | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 shortcuts_keys_comand_it.txt diff --git a/shortcuts_keys_comand_it.txt b/shortcuts_keys_comand_it.txt new file mode 100644 index 0000000..9a76202 --- /dev/null +++ b/shortcuts_keys_comand_it.txt @@ -0,0 +1,25 @@ +Riepilogo dei Tasti Rapidi e Comandi per cwsim: + +**Tasti Funzione (Invio messaggi CW):** +* F1 = Invia CQ +* F2 = Invia Exchange (Rapporto e Progressivo) +* F3 = Invia TU (Ringraziamento per confermare il QSO) +* F4 = Invia il tuo nominativo +* F5 = Invia il nominativo del corrispondente (quello inserito nel campo Call) +* F6 = Invia QSO B4 (QSO già a log) +* F7 = Invia il punto interrogativo (?) +* F8 = Invia NIL (Not in log) + +**Azioni del Log e Controllo Simulatore:** +* Invio (Return) = Passa al campo successivo o registra il QSO a log +* Esc (Escape) = Ferma la trasmissione CW corrente +* Alt+W = Pulisce (Wipe) i campi della riga corrente +* Alt+X = Avvia o Ferma il contest (Start/Stop) +* Freccia Su / Freccia Giù = Naviga tra i campi / righe del log + +**Controlli Audio e Radio:** +* PgSu / PgGiù (PgUp/PgDown) = Aumenta / Diminuisce la velocità del CW (WPM) +* Shift+Freccia Su / Shift+Freccia Giù = Regola il RIT (sposta la frequenza di ricezione su/giù) +* Alt+C = Azzera il RIT (lo riporta al centro) +* Ctrl+Freccia Su / Ctrl+Freccia Giù = Allarga o restringe la larghezza di banda del filtro audio (Bandwidth) +* Alt+Freccia Su / Alt+Freccia Giù = Alza o abbassa il Pitch (la tonalità del CW) From 79b9f28fc44ac8169408af0098c0e492abeedab6 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Wed, 25 Mar 2026 14:27:36 +0100 Subject: [PATCH 02/22] feat: integrate CWzator dynamic straight key logic into envelope generator --- python/contest.py | 4 ++- python/cwsim.py | 5 +++ python/cwsimgui.ui | 74 ++++++++++++++++++++++----------------------- python/dxstation.py | 12 ++++++++ python/keyer.py | 53 +++++++++++++++++++++++--------- python/mystation.py | 2 +- python/station.py | 5 ++- 7 files changed, 101 insertions(+), 54 deletions(-) diff --git a/python/contest.py b/python/contest.py index 9d765f7..8b773d1 100644 --- a/python/contest.py +++ b/python/contest.py @@ -283,6 +283,7 @@ def getAudio(self,outdata,nf,tinfo,status): lidRstProb=self.lidRstProb,qsb=self.qsb, flutterProb=self.flutterProb, rptProb=self.rptProb,fast=self.fast,slow=self.slow, + straightKeyProb=self.straightKeyProb, isSingle=True,bufsize=self._bufsize,rate=self._rate) self.stations.append(s) s.processEvent(StationEvent.MeFinished) @@ -401,6 +402,7 @@ def readConfig(self,filename): self.flutter = int(conditionsdict['flutter']) self.flutterProb = float(conditionsdict['flutterprob']) self.lids = int(conditionsdict['lids']) + self.straightKeyProb = float(conditionsdict.get('straightkeyprob', '0.25')) self.activity = int(conditionsdict['activity']) self.lidRstProb = float(conditionsdict['lidrstprob']) self.lidNrProb = float(conditionsdict['lidnrprob']) @@ -426,7 +428,7 @@ def writeConfig(self,filename): ,'qskdecaytime', 'cwreverse', 'rit', 'monitor']: p.set('Station',i,str(eval('self.'+i))) p.add_section('Conditions') - for i in ['qrn','qrm','tqrm','qsb','flutter','qsy','lids','activity' + for i in ['qrn','qrm','tqrm','qsb','flutter','qsy','lids','straightKeyProb','activity' ,'lidRstProb','lidNrProb','rptProb','flutterProb']: p.set('Conditions',i,str(eval('self.'+i))) p.add_section('Contest') diff --git a/python/cwsim.py b/python/cwsim.py index 208b573..67be5a8 100755 --- a/python/cwsim.py +++ b/python/cwsim.py @@ -171,6 +171,7 @@ def __init__(self,parent=None): self.tqrmSpinBox.valueChanged.connect(self.tqrm) self.lidRstProbSpinBox.valueChanged.connect(self.lidRstProb) self.lidNrProbSpinBox.valueChanged.connect(self.lidNrProb) + self.straightKeyProbSpinBox.valueChanged.connect(self.straightKeyProb) self.rptProbSpinBox.valueChanged.connect(self.rptProb) self.flutterProbSpinBox.valueChanged.connect(self.flutterProb) self.fastSpinBox.valueChanged.connect(self.fast) @@ -253,6 +254,7 @@ def syncGui(self): self.tqrmSpinBox.setValue(self.contest.tqrm) self.lidRstProbSpinBox.setValue(self.contest.lidRstProb) self.lidNrProbSpinBox.setValue(self.contest.lidNrProb) + self.straightKeyProbSpinBox.setValue(self.contest.straightKeyProb) self.rptProbSpinBox.setValue(self.contest.rptProb) self.flutterProbSpinBox.setValue(self.contest.flutterProb) self.fastSpinBox.setValue(self.contest.fast) @@ -732,6 +734,9 @@ def lidRstProb(self,s): def lidNrProb(self,s): self.contest.lidNrProb = s + def straightKeyProb(self,s): + self.contest.straightKeyProb = s + def rptProb(self,s): self.contest.rptProb = s diff --git a/python/cwsimgui.ui b/python/cwsimgui.ui index 9ecb078..2319bb2 100644 --- a/python/cwsimgui.ui +++ b/python/cwsimgui.ui @@ -1,4 +1,4 @@ - + CwsimMainWindow @@ -30,7 +30,7 @@ Qt::TabFocus - + Log @@ -101,15 +101,15 @@ false - - - - - - - - - + + + + + + + + + @@ -133,7 +133,7 @@ Qt::NoFocus - + @@ -810,7 +810,7 @@ - + Straight Key %Qt::StrongFocusProbability of stations using a straight key (0 to 1)Straight Key probability1.0000000000000000.010000000000000 @@ -1303,7 +1303,7 @@ Qt::NoFocus - + @@ -1538,11 +1538,11 @@ false - - - - - + + + + + @@ -1621,15 +1621,15 @@ &File - - - - - - - - - + + + + + + + + + @@ -1638,14 +1638,14 @@ &Help - - - + + + - - + + - + toolBar @@ -1779,7 +1779,7 @@ qsyCheck qrmCheck lidsCheck - lidRstProbSpinBox + straightKeyProbSpinBoxlidRstProbSpinBox rptProbSpinBox fastSpinBox qsbCheck @@ -1800,7 +1800,7 @@ trCallEntry trExchangeEntry - + action_Exit @@ -1851,4 +1851,4 @@ - + \ No newline at end of file diff --git a/python/dxstation.py b/python/dxstation.py index b9c2e8c..3e05341 100644 --- a/python/dxstation.py +++ b/python/dxstation.py @@ -27,8 +27,20 @@ class DxStation(station.Station): def __init__(self,rng,keyer,callList,cqstn,minutes=0, lids=True,lidNrProb=0.1,lidRstProb=0.03,qsb=True,flutterProb=0.3, rptProb=0.1,fast=1.1,slow=0.9, + straightKeyProb=0.25, isSingle=False,bufsize=512,rate=11025): super().__init__(rng,keyer,bufsize=bufsize,rate=rate) + if self._rng.random() < straightKeyProb: + while True: + l = self._rng.integers(low=20, high=43) + p = self._rng.integers(low=18, high=55) + s = self._rng.integers(low=25, high=76) + if (l <= 24 and p <= 25 and s <= 35) or (l >= 38 and p >= 47 and s >= 65): + continue + self.l = l + self.p = p + self.s = s + break self.cqstn = cqstn self.hisCall = self.cqstn.myCall self.myCall = callList.pickCall() diff --git a/python/keyer.py b/python/keyer.py index 18df9a8..9cdc885 100644 --- a/python/keyer.py +++ b/python/keyer.py @@ -79,35 +79,60 @@ def encode(self,txt): s += "~" return s - def getenvelop(self,msg,wpm): + def getenvelop(self,msg,wpm,l=30,s=50,p=50): """ Arguments msg: morse encoding of dits and dahs wpm: speed in words per minute (PARIS) + l: dash weight (default 30) + s: space weight (default 50) + p: dot weight (default 50) Returns keying envelop for audio samples """ nr = len(self.rise) - count = 2*(msg.count('.')+msg.count(' ')+2*msg.count('-'))+msg.count('~') - samples = int(np.rint(1.2*self.rate/wpm)) - n = int(self._bufsize*np.ceil((count*samples+nr)/self._bufsize)) + T_samples = 1.2 * self.rate / wpm + dot_on = int(np.rint(T_samples * (p / 50.0))) + dash_on = int(np.rint(3.0 * T_samples * (l / 30.0))) + intra_off = int(np.rint(T_samples * (s / 50.0))) + letter_gap_added = int(np.rint(3.0 * T_samples * (s / 50.0))) - intra_off + pad_added = int(np.rint(T_samples)) + + # Calculate total length + total_samples = 0 + for i in range(len(msg)): + if msg[i] == '.': + total_samples += dot_on + intra_off + elif msg[i] == '-': + total_samples += dash_on + intra_off + elif msg[i] == ' ': + total_samples += letter_gap_added - nr + elif msg[i] == '~': + total_samples += pad_added - nr + + n = int(self._bufsize*np.ceil((total_samples+nr+max(dot_on,dash_on))/self._bufsize)) env = np.zeros(n,dtype=np.float32) - dit = np.ones(nr+samples,dtype=np.float32) + + dit = np.ones(nr+dot_on,dtype=np.float32) dit[:nr] = self.rise - dit[samples:] = self.fall - dah = np.ones(nr+3*samples,dtype=np.float32) + dit[dot_on:] = self.fall + + dah = np.ones(nr+dash_on,dtype=np.float32) dah[:nr] = self.rise - dah[3*samples:] = self.fall + dah[dash_on:] = self.fall + k = 0 for i in range(len(msg)): if msg[i] == '.': - env[k:k+len(dit)] = dit - k += 2*samples + if k+len(dit) <= n: + env[k:k+len(dit)] = dit + k += dot_on + intra_off elif msg[i] == '-': - env[k:k+len(dah)] = dah - k += 4*samples + if k+len(dah) <= n: + env[k:k+len(dah)] = dah + k += dash_on + intra_off elif msg[i] == ' ': - k += 2*samples-nr + k += letter_gap_added - nr elif msg[i] == '~': - k += samples-nr + k += pad_added - nr return env diff --git a/python/mystation.py b/python/mystation.py index 1c2d604..87852a3 100644 --- a/python/mystation.py +++ b/python/mystation.py @@ -84,7 +84,7 @@ def updateCallInMessage(self,call): #check if sound thread problem? res = False if res: s = self._keyer.encode(call.lower()) - ne = self._keyer.getenvelop(s,self.wpm)*self._amplitude + ne = self._keyer.getenvelop(s,self.wpm,self.l,self.s,self.p)*self._amplitude res = len(ne) >= self._sendpos if res: res = np.array_equiv(self._envelop[0:self._sendpos] diff --git a/python/station.py b/python/station.py index 5f5846b..abe0eb6 100644 --- a/python/station.py +++ b/python/station.py @@ -110,6 +110,9 @@ def __init__(self,rng,keyer,bufsize=512,rate=11025): self._timeout = NEVER self._amplitude = 0.7 self.wpm = 30 + self.l = 30 + self.s = 50 + self.p = 50 self._rst = 599 self.nr = 1 self.nrWithError = False @@ -154,7 +157,7 @@ def sendText(self,msg): else: self._msgtext = msg s = self._keyer.encode(self._msgtext.lower()) - self._envelop = self._keyer.getenvelop(s,self.wpm)*self._amplitude + self._envelop = self._keyer.getenvelop(s,self.wpm,self.l,self.s,self.p)*self._amplitude self.state = StationState.Sending self._timeout = NEVER From 0fa473625769ea524f13a59af80bdd7e0f4fe7f1 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Wed, 25 Mar 2026 14:52:58 +0100 Subject: [PATCH 03/22] new file: cwsim.txt new file: cwzator_reference.py new file: extract_cwzator.py new file: plan_dx_expedition.md new file: python/add_dx_ui.py new file: python/add_ui_element.py new file: python/add_ui_element2.py modified: python/contest.py modified: python/cwsim.py modified: python/cwsimgui.ui modified: python/dxoper.py modified: python/dxstation.py --- cwsim.txt | 53 ++++ cwzator_reference.py | 589 ++++++++++++++++++++++++++++++++++++++ extract_cwzator.py | 20 ++ plan_dx_expedition.md | 39 +++ python/add_dx_ui.py | 67 +++++ python/add_ui_element.py | 57 ++++ python/add_ui_element2.py | 65 +++++ python/contest.py | 9 +- python/cwsim.py | 31 +- python/cwsimgui.ui | 4 +- python/dxoper.py | 20 +- python/dxstation.py | 4 +- 12 files changed, 937 insertions(+), 21 deletions(-) create mode 100644 cwsim.txt create mode 100644 cwzator_reference.py create mode 100644 extract_cwzator.py create mode 100644 plan_dx_expedition.md create mode 100644 python/add_dx_ui.py create mode 100644 python/add_ui_element.py create mode 100644 python/add_ui_element2.py diff --git a/cwsim.txt b/cwsim.txt new file mode 100644 index 0000000..bb41f8a --- /dev/null +++ b/cwsim.txt @@ -0,0 +1,53 @@ + +IZ4APU Report cwsim 25/03/2026 14:12:23 +Durata 00:02:26 +Durata (QSO) 4 +Velocità CW 26 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 4 +Prefissi totali 4 +Punteggio totale 16 +Punti verificati 4 +Prefissi verificati 4 +Punteggio verificato 16 +Percentuale d'errore 0.0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 48 +5-9 0 + +IZ4APU Report cwsim 25/03/2026 14:45:11 +Durata 00:00:52 +Durata (QSO) 1 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 1 +Prefissi totali 1 +Punteggio totale 1 +Punti verificati 1 +Prefissi verificati 1 +Punteggio verificato 1 +Percentuale d'errore 0.0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 12 +5-9 0 + +IZ4APU Report cwsim 25/03/2026 14:46:17 +Durata 00:00:02 +Durata (QSO) 0 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 0 +Prefissi totali 0 +Punteggio totale 0 +Punti verificati 0 +Prefissi verificati 0 +Punteggio verificato 0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 0 +5-9 0 diff --git a/cwzator_reference.py b/cwzator_reference.py new file mode 100644 index 0000000..f7502ea --- /dev/null +++ b/cwzator_reference.py @@ -0,0 +1,589 @@ +def CWzator(msg, wpm=35, pitch=550, l=30, s=50, p=50, fs=44100, ms=1, vol=0.5, wv=1, sync=False, file=False): + """ + V8.2 di mercoledì 28 maggio 2025 - Gabriele Battaglia (IZ4APU), Claude 3.5, ChatGPT o3-mini-high, Gemini 2.5 Pro + da un'idea originale di Kevin Schmidt W9CF + Genera e riproduce l'audio del codice Morse dal messaggio di testo fornito. + Parameters: + msg (str|int): Messaggio di testo da convertire in Morse. + se == -1 restituisce la mappa morse come dizionario. + wpm (int): Velocità in parole al minuto (range 5-100). + pitch (int): Frequenza in Hz per il tono (range 130-2800). + l (int): Peso per la durata della linea (default 30). + s (int): Peso per la durata degli spazi tra simboli/lettere (default 50). + p (int): Peso per la durata del punto (default 50). + fs (int): Frequenza di campionamento (default 44100 Hz). + ms (int): Durata in millisecondi per i fade-in/out sui toni (default 1). + vol (float): Volume (range 0.0 a 1.0, default 0.5). + wv (int): Tipo d’onda (scipy.signal): 1=Sine(default), 2=Square, 3=Triangle, 4=Sawtooth. + sync (bool): Se True, la funzione aspetta la fine della riproduzione; altrimenti ritorna subito. + file (bool): Se True, salva l’audio in un file WAV. + Returns: + Un oggetto PlaybackHandle e rwpm (velocità effettiva wpm), o (None, None) in caso di errore. + """ + import numpy as np + import sounddevice as sd + import wave + from datetime import datetime + import threading + import sys + from scipy import signal # Importato per le forme d'onda + BLOCK_SIZE = 256 + MORSE_MAP = { + "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":"----.", ".":".-.-.-", "-":"-....-", ",":"--..--", + "?":"..--..", "/":"-..-.", ";":"-.-.-.", "(":"-.--.", "[":"-.--.", + ")":"-.--.-", "]":"-.--.-", "@":".--.-.", "*":"...-.-", "+":".-.-.", + "%":".-...", ":":"---...", "=":"-...-", '"':".-..-.", "'":".----.", + "!":"-.-.--", "$":"...-..-", " ":"", "_":"", + "ò":"---.", "à":".--.-", "ù":"..--", "è":"..-..", + "é":"..-..", "ì":".---."} + if msg==-1: return MORSE_MAP + elif not isinstance(msg, str) or msg == "": print("CWzator Error: msg deve essere una stringa non vuota.", file=sys.stderr); return None, None + if not (isinstance(wpm, int) and 5 <= wpm <= 100): print(f"CWzator Error: wpm ({wpm}) non valido [5-100].", file=sys.stderr); return None, None + if not (isinstance(pitch, int) and 130 <= pitch <= 2800): print(f"CWzator Error: pitch ({pitch}) non valido [130-2000].", file=sys.stderr); return None, None + if not (isinstance(l, int) and 1 <= l <= 100): print(f"CWzator Error: l ({l}) non valido [1-100].", file=sys.stderr); return None, None + if not (isinstance(s, int) and 1 <= s <= 100): print(f"CWzator Error: s ({s}) non valido [1-100].", file=sys.stderr); return None, None + if not (isinstance(p, int) and 1 <= p <= 100): print(f"CWzator Error: p ({p}) non valido [1-100].", file=sys.stderr); return None, None + if not (isinstance(fs, int) and fs > 0): print(f"CWzator Error: fs ({fs}) non valido [>0].", file=sys.stderr); return None, None + if not (isinstance(ms, (int, float)) and ms >= 0): print(f"CWzator Error: ms ({ms}) non valido [>=0].", file=sys.stderr); return None, None + if not (isinstance(vol, (int, float)) and 0.0 <= vol <= 1.0): print(f"CWzator Error: vol ({vol}) non valido [0.0-1.0].", file=sys.stderr); return None, None + if not (isinstance(wv, int) and wv in [1, 2, 3, 4]): print(f"CWzator Error: wv ({wv}) non valido [1-4].", file=sys.stderr); return None, None + # --- Calcolo Durate (con arrotondamento campioni implicito dopo) --- + T = 1.2 / float(wpm) + dot_duration = T * (p / 50.0) + dash_duration = 3.0 * T * (l / 30.0) # Usato 3.0 per float + intra_gap = T * (s / 50.0) + letter_gap = 3.0 * T * (s / 50.0) + word_gap = 7.0 * T * (s / 50.0) + # --- Funzioni Generazione Segmenti (con forme d'onda scipy e arrotondamento) --- + def generate_tone(duration): + # Arrotonda qui per il numero di campioni + N = int(round(fs * duration)) + if N <= 0: return np.array([], dtype=np.int16) # Ritorna array vuoto se durata troppo breve + # Usa float64 per tempo e fase per precisione + t = np.linspace(0, duration, N, endpoint=False, dtype=np.float64) + # Forme d'onda via scipy.signal (output in [-1, 1]) + if wv == 1: # Sine + signal_float = np.sin(2 * np.pi * pitch * t) + elif wv == 2: # Square + signal_float = signal.square(2 * np.pi * pitch * t) + elif wv == 3: # Triangle (width=0.5) + signal_float = signal.sawtooth(2 * np.pi * pitch * t, width=0.5) + else: # Sawtooth (width=1) + signal_float = signal.sawtooth(2 * np.pi * pitch * t, width=1) + signal_float = signal_float.astype(np.float32) # Converti a float32 per audio + # Applica Fade In/Out + fade_samples = int(round(fs * ms / 1000.0)) # Arrotonda campioni fade + # Condizione robusta per sovrapposizione fade + if fade_samples > 0 and fade_samples <= N // 2: + ramp = np.linspace(0, 1, fade_samples, dtype=np.float32) + signal_float[:fade_samples] *= ramp + signal_float[-fade_samples:] *= ramp[::-1] # Usa slicing negativo per l'ultimo pezzo + # Applica volume e converti a int16 + # Clipping prima della conversione int16 + signal_float = np.clip(signal_float * vol, -1.0, 1.0) + return (signal_float * 32767.0).astype(np.int16) + def generate_silence(duration): + # Arrotonda qui per il numero di campioni + N = int(round(fs * duration)) + return np.zeros(N, dtype=np.int16) if N > 0 else np.array([], dtype=np.int16) + # --- Assemblaggio Sequenza (invariato) --- + segments = [] + words = msg.lower().split() + for w_idx, word in enumerate(words): + # Usa una stringa per accumulare le lettere valide invece di una lista + valid_letters = "".join(ch for ch in word if ch in MORSE_MAP) + for l_idx, letter in enumerate(valid_letters): + code = MORSE_MAP.get(letter) # Usa .get() per sicurezza? No, già filtrato. + if not code: continue # Salta se per qualche motivo non c'è codice (non dovrebbe succedere) + for s_idx, symbol in enumerate(code): + if symbol == '.': + segments.append(generate_tone(dot_duration)) + elif symbol == '-': + segments.append(generate_tone(dash_duration)) + # Aggiungi gap intra-simbolo solo se non è l'ultimo simbolo + if s_idx < len(code) - 1: + segments.append(generate_silence(intra_gap)) + # Aggiungi gap tra lettere solo se non è l'ultima lettera + if l_idx < len(valid_letters) - 1: + segments.append(generate_silence(letter_gap)) + # Aggiungi gap tra parole solo se non è l'ultima parola + if w_idx < len(words) - 1: + # Controlla se la parola precedente non era solo spazi o caratteri ignorati + if valid_letters or any(ch in MORSE_MAP for ch in words[w_idx+1]): + segments.append(generate_silence(word_gap)) + # --- Concatenazione e Aggiunta Silenzio Finale --- + audio = np.concatenate(segments) if segments else np.array([], dtype=np.int16) + if audio.size > 0: # Aggiungi solo se c'è audio + silence_samples_end = int(round(fs * 0.005)) # Es. 5ms di silenzio finale + if silence_samples_end > 0: + final_silence = np.zeros(silence_samples_end, dtype=np.int16) + audio = np.concatenate((audio, final_silence)) + # --- Calcolo rwpm (con gestione divisione per zero robusta) --- + rwpm = wpm # Default se pesi standard o nessun elemento contato + if (l, s, p) != (30, 50, 50): + dots = dashes = intra_gaps = letter_gaps = word_gaps = 0 + words_list = msg.lower().split() + processed_letters_count = 0 # Contatore per gestire gaps + for w_idx, w in enumerate(words_list): + current_word_letters = 0 + code_lengths_in_word = [] + for letter in w: + if letter in MORSE_MAP: + code = MORSE_MAP[letter] + if code: # Ignora spazi o caratteri mappati a stringa vuota + dots += code.count('.') + dashes += code.count('-') + code_len = len(code) + if code_len > 1: + intra_gaps += (code_len - 1) + code_lengths_in_word.append(code_len) + current_word_letters += 1 + if current_word_letters > 1: + letter_gaps += (current_word_letters - 1) + processed_letters_count += current_word_letters + # Aggiungi word gap solo se la parola conteneva elementi e non è l'ultima + if current_word_letters > 0 and w_idx < len(words_list) - 1: + # E controlla anche se la parola successiva contiene elementi + if any(ch in MORSE_MAP and MORSE_MAP[ch] for ch in words_list[w_idx+1]): + word_gaps += 1 + # Calcola durate totali (in unità di dot) + # Durata standard: 1 (dot) + 1 (gap) = 2, 3 (dash) + 1 (gap) = 4 + # Gap tra lettere = 3, Gap tra parole = 7 + # L'unità base è la durata del dot standard (T * p/50 dove p=50) + standard_total_units = dots + 3*dashes + intra_gaps + 3*letter_gaps + 7*word_gaps + # Durata attuale con pesi + actual_dot_units = p / 50.0 + actual_dash_units = 3.0 * (l / 30.0) + actual_intra_gap_units = s / 50.0 + actual_letter_gap_units = 3.0 * (s / 50.0) + actual_word_gap_units = 7.0 * (s / 50.0) + actual_total_units = (dots * actual_dot_units) + \ + (dashes * actual_dash_units) + \ + (intra_gaps * actual_intra_gap_units) + \ + (letter_gaps * actual_letter_gap_units) + \ + (word_gaps * actual_word_gap_units) + # Calcola rapporto e rwpm solo se ci sono state durate + if standard_total_units > 0 and actual_total_units > 0: + ratio = actual_total_units / standard_total_units + rwpm = wpm / ratio + elif standard_total_units == 0 and actual_total_units == 0: + rwpm = wpm # Messaggio vuoto, rwpm è uguale a wpm nominale + else: + # Caso anomalo (es. solo spazi?), imposta rwpm a wpm o 0? + # Manteniamo wpm per ora, ma potrebbe essere indice di errore input. + rwpm = wpm + print("CWzator Warning: Calcolo rwpm anomalo, possibile input solo con spazi?", file=sys.stderr) + # --- Classe PlaybackHandle (invariata ma ora riceve audio con silenzio finale) --- + class PlaybackHandle: + def __init__(self, audio_data, sample_rate): + self.audio_data = audio_data + self.sample_rate = sample_rate + self.stream = None + self.is_playing = threading.Event() # Usa Event per thread-safety + self._thread = None # Riferimento al thread + def _playback_target(self): + """Target function per il thread di riproduzione.""" + self.is_playing.set() # Segnala inizio riproduzione + stream = None # Inizializza per blocco finally + try: + with sd.OutputStream( + samplerate=self.sample_rate, channels=1, dtype=np.int16, + blocksize=BLOCK_SIZE, latency='low' + ) as stream: + # Salva riferimento allo stream *dopo* che è stato creato con successo + self.stream = stream + # Scrittura a blocchi, controllando il flag ad ogni blocco + for i in range(0, len(self.audio_data), BLOCK_SIZE): + if not self.is_playing.is_set(): # Controlla l'evento + # print("Debug: Stop richiesto durante la riproduzione.") + stream.stop() # Prova a fermare lo stream corrente + break + block = self.audio_data[i:min(i + BLOCK_SIZE, len(self.audio_data))] + stream.write(block) + # Se il loop finisce normalmente, attendi che lo stream finisca l'output bufferizzato + if self.is_playing.is_set(): + # print("Debug: Loop terminato, attendo stream.close() implicito.") + pass # 'with' gestisce la chiusura e l'attesa implicita + except sd.PortAudioError as pae: + print(f"CWzator Playback PortAudioError: {pae}", file=sys.stderr) + except Exception as e: + print(f"CWzator Playback Error: {e}", file=sys.stderr) + finally: + # print("Debug: Uscita blocco try/finally _playback_target.") + self.is_playing.clear() # Segnala fine riproduzione o errore + self.stream = None # Rilascia riferimento allo stream + def play(self): + """Avvia la riproduzione in un thread separato.""" + if not self.is_playing.is_set() and self.audio_data.size > 0: + # Crea e avvia il thread solo se non sta già suonando e c'è audio + self._thread = threading.Thread(target=self._playback_target) + self._thread.daemon = False # Assicura non-daemon + self._thread.start() + # else: print("Debug: Play chiamato ma già in esecuzione o audio vuoto.") + def wait_done(self): + """Attende la fine della riproduzione corrente.""" + # Attende che l'evento is_playing sia clear O che il thread termini + if self._thread is not None and self._thread.is_alive(): + # print("Debug: wait_done chiamato, joining thread...") + self._thread.join() + # print("Debug: wait_done terminato.") + def stop(self): + """Richiede l'interruzione della riproduzione.""" + # print("Debug: stop richiesto.") + self.is_playing.clear() # Segnala al loop di playback di fermarsi + # Nota: l'interruzione effettiva dipende da quanto velocemente il loop + # controlla l'evento e da quanto tempo impiega stream.stop(). + # Non chiudiamo lo stream qui, il blocco 'with' lo farà. + # --- Creazione Oggetto e Avvio Playback (Logica Originale) --- + play_obj = PlaybackHandle(audio, fs) + # Avvia la riproduzione nel thread interno all'oggetto + play_obj.play() # Il metodo play ora gestisce l'avvio del thread + # --- Salvataggio File (invariato) --- + if file: + filename = f"cwapu Morse recorded at {datetime.now().strftime('%Y%m%d%H%M%S')}.wav" + try: + with wave.open(filename, 'wb') as wf: + wf.setnchannels(1) # Mono + wf.setsampwidth(2) # 16-bit + wf.setframerate(fs) + wf.writeframes(audio.tobytes()) + # print(f"CWzator: Audio salvato in {filename}") + except Exception as e: + print(f"CWzator Error durante salvataggio file: {e}", file=sys.stderr) + # --- Gestione Sync (usa wait_done dell'oggetto) --- + if sync: + play_obj.wait_done() # Usa il metodo dell'oggetto per attendere + # --- Ritorno Oggetto e rwpm --- + return play_obj, rwpm + +class Mazzo: + ''' + V5.2 - settembre 2025 b Gabriele Battaglia & Gemini 2.5 + Classe autocontenuta che rappresenta un mazzo di carte italiano o francese, + con supporto per mazzi multipli, mescolamento, pesca con rimescolamento + automatico degli scarti, e gestione flessibile delle carte. + Non produce output diretto (print), ma restituisce valori o stringhe informative. + ''' + import random + from collections import namedtuple + Carta = namedtuple("Carta", ["id", "nome", "valore", "seme_nome", "seme_id", "desc_breve"]) + _SEMI_FRANCESI = ["Cuori", "Quadri", "Fiori", "Picche"] + _SEMI_ITALIANI = ["Bastoni", "Spade", "Coppe", "Denari"] + _VALORI_FRANCESI = [("Asso", 1)] + [(str(i), i) for i in range(2, 11)] + [("Jack", 11), ("Regina", 12), ("Re", 13)] + _VALORI_ITALIANI = [("Asso", 1)] + [(str(i), i) for i in range(2, 8)] + [("Fante", 8), ("Cavallo", 9), ("Re", 10)] + _VALORI_DESCRIZIONE = {1: 'A', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '0', 11: 'J', 12: 'Q', 13: 'K'} + _SEMI_DESCRIZIONE = {"Cuori": 'C', "Quadri": 'Q', "Fiori": 'F', "Picche": 'P', + "Bastoni": 'B', "Spade": 'S', "Coppe": 'O', "Denari": 'D'} # 'O' per Coppe + def __init__(self, tipo_francese=True, num_mazzi=1): + ''' + Inizializza uno o più mazzi di carte. + Parametri: + - tipo_francese (bool): True per mazzo francese (default), False per mazzo italiano. + - num_mazzi (int): Numero di mazzi da includere (default 1). Deve essere >= 1. + ''' + if not isinstance(num_mazzi, int) or num_mazzi < 1: + raise ValueError("Il numero di mazzi deve essere un intero maggiore o uguale a 1.") + self.tipo_francese = tipo_francese + self.num_mazzi = num_mazzi + # Liste per tracciare lo stato delle carte + self.carte = [] # Mazzo principale da cui pescare + self.scarti = [] # Pila degli scarti, possono essere rimescolati + self.scarti_permanenti = [] # Carte rimosse permanentemente + self._costruisci_mazzo() + def _costruisci_mazzo(self): + ''' + (Metodo privato) Costruisce il mazzo di carte in base al tipo e al numero di mazzi. + ''' + self.carte = [] # Resetta il mazzo + semi = self._SEMI_FRANCESI if self.tipo_francese else self._SEMI_ITALIANI + valori = self._VALORI_FRANCESI if self.tipo_francese else self._VALORI_ITALIANI + id_carta_counter = 1 + for _ in range(self.num_mazzi): + for id_seme, nome_seme in enumerate(semi, 1): + # Correzione: L'ID seme per mazzi italiani dovrebbe partire da 5 per distinguerli? + # No, l'ID seme è relativo al tipo di mazzo (1-4 per entrambi), + # il nome_seme è ciò che li distingue. Manteniamo 1-4. + seme_id_effettivo = id_seme + if not self.tipo_francese: + # Se si volesse un ID globale unico (1-4 Francese, 5-8 Italiano) + # seme_id_effettivo = id_seme + 4 # Questa è un'opzione di design, ma la lasciamo 1-4 per ora + pass # Manteniamo 1-4 come da codice originale + for nome_valore, valore_num in valori: + desc_val = self._VALORI_DESCRIZIONE.get(valore_num, '?') + desc_seme = self._SEMI_DESCRIZIONE.get(nome_seme, '?') + desc_breve = f"{desc_val}{desc_seme}" + nome_completo = f"{nome_valore} di {nome_seme}" + # Usiamo la definizione di Carta interna alla classe + carta = self.Carta(id=id_carta_counter, + nome=nome_completo, + valore=valore_num, + seme_nome=nome_seme, + seme_id=seme_id_effettivo, + desc_breve=desc_breve) + self.carte.append(carta) + id_carta_counter += 1 + def mescola_mazzo(self): + ''' + Mescola le carte nel mazzo principale (self.carte). + Non restituisce nulla. + ''' + if not self.carte: + return # Non fare nulla se il mazzo è vuoto + self.random.shuffle(self.carte) + def pesca(self, quante=1): + ''' + Pesca carte dal mazzo principale. Se le carte nel mazzo non sono sufficienti, + rimescola automaticamente gli scarti prima di pescare. + Le carte pescate vengono spostate nella lista 'pescate'. + Parametri: + - quante (int): Numero di carte da pescare (default 1). + Ritorna: + - list[Carta]: Lista delle carte pescate. Può contenere meno carte di 'quante' + se il mazzo e gli scarti combinati non sono sufficienti. + ''' + if quante < 0: + raise ValueError("Il numero di carte da pescare deve essere non negativo.") + if quante == 0: + return [] + # NUOVA LOGICA: Se le carte nel mazzo sono meno di quelle richieste, rimescola gli scarti. + if len(self.carte) < quante and self.scarti: + print("\n--- Carte insufficienti nel mazzo. Rimescolo gli scarti... ---") # Feedback utile per il giocatore + self.carte.extend(self.scarti) + self.scarti = [] + self.mescola_mazzo() + print(f"--- Rimescolamento completato. Carte nel mazzo: {len(self.carte)} ---") + # Ora procedi con la pesca + num_da_pescare = min(quante, len(self.carte)) + carte_pescate_ora = [] + if num_da_pescare > 0: + for _ in range(num_da_pescare): + carte_pescate_ora.append(self.carte.pop()) + return carte_pescate_ora + def scarta_carte(self, carte_da_scartare): + ''' + Aggiunge una lista di carte alla pila degli scarti. + Parametri: + - carte_da_scartare (list[Carta]): Lista di oggetti Carta da spostare negli scarti. + ''' + if not carte_da_scartare: + return + self.scarti.extend(carte_da_scartare) + def rimescola_scarti(self, include_pescate=False): + ''' + Rimette le carte dalla pila degli scarti nel mazzo principale e mescola. + Opzionalmente, può includere anche le carte attualmente pescate. + Non reintegra le carte scartate permanentemente. + Parametri: + - include_pescate (bool): Se True, anche le carte in self.pescate sono rimesse (default False). + Ritorna: + - str: Messaggio che riepiloga l'operazione. + ''' + carte_da_reintegrare = [] + msg_parts = [] + num_scarti = len(self.scarti) + if num_scarti > 0: + carte_da_reintegrare.extend(self.scarti) + self.scarti = [] + msg_parts.append(f"{num_scarti} scarti reintegrati.") + else: + msg_parts.append("Nessuno scarto da reintegrare.") + num_pescate = len(self.pescate) + if include_pescate: + if num_pescate > 0: + carte_da_reintegrare.extend(self.pescate) + self.pescate = [] + msg_parts.append(f"{num_pescate} carte pescate reintegrate.") + else: + msg_parts.append("Nessuna carta pescata da reintegrare.") + if not carte_da_reintegrare: + return "Nessuna carta da rimescolare. " + " ".join(msg_parts) + self.carte.extend(carte_da_reintegrare) + self.mescola_mazzo() + msg_parts.append(f"Mazzo ora contiene {len(self.carte)} carte.") + return " ".join(msg_parts) + def _rimuovi_carte_da_lista(self, lista_sorgente, condizione, destinazione, nome_destinazione): + ''' Funzione helper per rimuovere carte da una lista in base a una condizione. ''' + carte_da_mantenere = [] + carte_rimosse = [] + for carta in lista_sorgente: + if condizione(carta): + carte_rimosse.append(carta) + else: + carte_da_mantenere.append(carta) + if carte_rimosse: + destinazione.extend(carte_rimosse) + # Modifica la lista originale inplace + lista_sorgente[:] = carte_da_mantenere + return carte_rimosse + def rimuovi_semi(self, semi_id_da_rimuovere, permanente=False): + ''' + Rimuove dal mazzo principale (self.carte) tutte le carte con i semi specificati. + Le carte rimosse vengono spostate negli scarti temporanei o permanenti. + Parametri: + - semi_id_da_rimuovere (list[int]): Lista di ID numerici dei semi da rimuovere. + - permanente (bool): Se True, sposta in scarti_permanenti, altrimenti in scarti (default False). + Ritorna: + - int: Numero di carte rimosse dal mazzo principale. + ''' + destinazione = self.scarti_permanenti if permanente else self.scarti + nome_dest = "permanenti" if permanente else "temporanei" + condizione = lambda carta: carta.seme_id in semi_id_da_rimuovere + carte_rimosse = self._rimuovi_carte_da_lista(self.carte, condizione, destinazione, nome_dest) + return len(carte_rimosse) + def rimuovi_valori(self, valori_da_rimuovere, permanente=True): + ''' + Rimuove dal mazzo principale (self.carte) tutte le carte con i valori specificati. + Le carte rimosse vengono spostate negli scarti permanenti o temporanei. + Parametri: + - valori_da_rimuovere (list[int]): Lista di valori numerici da rimuovere. + - permanente (bool): Se True, sposta in scarti_permanenti (default), altrimenti in scarti. + Ritorna: + - int: Numero di carte rimosse dal mazzo principale. + ''' + destinazione = self.scarti_permanenti if permanente else self.scarti + nome_dest = "permanenti" if permanente else "temporanei" + condizione = lambda carta: carta.valore in valori_da_rimuovere + carte_rimosse = self._rimuovi_carte_da_lista(self.carte, condizione, destinazione, nome_dest) + return len(carte_rimosse) + def aggiungi_jolly(self, quanti_per_mazzo=2): + ''' + Aggiunge jolly al mazzo principale fino a raggiungere il numero corretto + per ogni mazzo originale (quanti_per_mazzo * num_mazzi). + Funziona solo per mazzi di tipo francese. Jolly esistenti non vengono duplicati. + Parametri: + - quanti_per_mazzo (int): Numero di jolly desiderato per ciascun mazzo originale (default 2). + Ritorna: + - str: Messaggio che indica quanti jolly sono stati aggiunti o se erano già presenti. + ''' + if not self.tipo_francese: + return "I jolly possono essere aggiunti solo ai mazzi di tipo francese." + if quanti_per_mazzo < 0: + # Non ha senso avere un numero negativo di jolly per mazzo + return "Numero di jolly per mazzo non valido (deve essere >= 0)." + + # Calcola il numero totale di jolly che dovrebbero esserci + jolly_attesi_totali = self.num_mazzi * quanti_per_mazzo + # Controlla quanti jolly esistono già in *tutte* le liste + all_cards = self.carte + self.pescate + self.scarti + self.scarti_permanenti + jolly_esistenti_count = sum(1 for c in all_cards if c.nome == "Jolly") + # Determina quanti jolly mancano (se ce ne sono) + jolly_da_aggiungere = jolly_attesi_totali - jolly_esistenti_count + if jolly_da_aggiungere <= 0: + # Se non ne mancano o ce ne sono addirittura di più (improbabile ma gestito) + return f"Nessun nuovo jolly aggiunto (numero richiesto: {jolly_attesi_totali}, già presenti: {jolly_esistenti_count})." + # Se dobbiamo aggiungere jolly: + # Trova l'ID massimo attuale per continuare la sequenza + max_id = 0 + if all_cards: + ids = [c.id for c in all_cards if c.id is not None] + if ids: + max_id = max(ids) + jolly_aggiunti_count = 0 + for i in range(jolly_da_aggiungere): + jolly_id = max_id + 1 + i + # Crea il jolly e aggiungilo al mazzo principale + jolly = self.Carta(id=jolly_id, nome="Jolly", valore=None, seme_nome="N/A", seme_id=0, desc_breve="XY") + self.carte.append(jolly) + jolly_aggiunti_count += 1 + # Aggiorna max_id per il prossimo ciclo (se ce n'è più di uno) + max_id = jolly_id + if jolly_aggiunti_count > 0: + return f"Aggiunti {jolly_aggiunti_count} jolly al mazzo principale." + else: + # Questo caso non dovrebbe verificarsi data la logica precedente, ma per sicurezza + return "Nessun nuovo jolly aggiunto." + def rimuovi_jolly(self, permanente=False): + ''' + Rimuove tutti i jolly dalle pile modificabili (mazzo, pescate, e scarti se permanente=True) + e li sposta nella destinazione appropriata (scarti temporanei o permanenti). + Parametri: + - permanente (bool): Se True, sposta in scarti_permanenti e pulisce anche gli scarti temporanei. + Se False, sposta solo in scarti temporanei. + Ritorna: + - str: Messaggio che indica quanti jolly unici sono stati rimossi e dove sono stati spostati. + ''' + jolly_rimossi_total_obj = [] # Lista per collezionare gli oggetti jolly rimossi + destinazione = self.scarti_permanenti if permanente else self.scarti + tipo_destinazione = "permanenti" if permanente else "temporanei" + condizione = lambda carta: carta.nome == "Jolly" + # Helper per evitare codice duplicato e gestire la collezione degli oggetti + def _processa_lista(lista_sorgente): + carte_rimosse = self._rimuovi_carte_da_lista(lista_sorgente, condizione, destinazione, tipo_destinazione) + jolly_rimossi_total_obj.extend(carte_rimosse) + # Rimuove da self.carte + _processa_lista(self.carte) + # Rimuove da self.pescate + _processa_lista(self.pescate) + # Rimuove da self.scarti SOLO SE la destinazione NON è self.scarti + # Questo previene che gli elementi appena aggiunti a self.scarti vengano rimossi di nuovo. + if permanente: + _processa_lista(self.scarti) # Pulisce gli scarti temporanei spostando i jolly in quelli permanenti + # Calcola quanti jolly unici sono stati effettivamente spostati + # Utile se per errore un jolly fosse presente in più liste (non dovrebbe accadere) + num_rimossi_unici = len({j.id for j in jolly_rimossi_total_obj}) + if num_rimossi_unici > 0: + return f"Rimossi {num_rimossi_unici} jolly unici. Spostati negli scarti {tipo_destinazione}." + else: + return "Nessun jolly trovato da rimuovere." + def _rimuovi_carte_da_lista(self, lista_sorgente, condizione, destinazione, nome_destinazione): + ''' Funzione helper per rimuovere carte da una lista in base a una condizione. ''' + carte_da_mantenere = [] + carte_rimosse = [] + for carta in lista_sorgente: + if condizione(carta): + carte_rimosse.append(carta) + else: + carte_da_mantenere.append(carta) + if carte_rimosse: + # Aggiunge gli elementi rimossi alla lista di destinazione + destinazione.extend(carte_rimosse) + # Modifica la lista originale inplace rimuovendo gli elementi + lista_sorgente[:] = carte_da_mantenere + # Ritorna la lista degli elementi rimossi + return carte_rimosse + def stato_mazzo(self): + ''' Ritorna una stringa che riepiloga lo stato attuale del mazzo. ''' + return (f"Mazzo: {len(self.carte)} carte | " + f"Scarti: {len(self.scarti)} carte | " + f"Scarti Permanenti: {len(self.scarti_permanenti)} carte") + def __len__(self): + ''' Ritorna il numero di carte attualmente nel mazzo principale (self.carte). ''' + return len(self.carte) + def __str__(self): + ''' Rappresentazione stringa dell'oggetto Mazzo (mostra lo stato). ''' + return self.stato_mazzo() + def mostra_carte(self, lista='mazzo'): + ''' + Restituisce una stringa con le descrizioni brevi delle carte + in una specifica lista (mazzo, pescate, scarti, permanenti). + Parametri: + - lista (str): Nome della lista ('mazzo', 'pescate', 'scarti', 'permanenti'). + Ritorna: + - str: Stringa formattata con le carte o messaggio di lista vuota/non valida. + ''' + target_lista_ref = None + nome_lista = "" + if lista == 'mazzo': + target_lista_ref = self.carte + nome_lista = "Mazzo Principale" + elif lista == 'pescate': + target_lista_ref = self.pescate + nome_lista = "Carte Pescate" + elif lista == 'scarti': + target_lista_ref = self.scarti + nome_lista = "Pila Scarti" + elif lista == 'permanenti': + target_lista_ref = self.scarti_permanenti + nome_lista = "Scarti Permanenti" + else: + return "Lista non valida. Scegli tra: 'mazzo', 'pescate', 'scarti', 'permanenti'." + if not target_lista_ref: + return f"Nessuna carta nella lista '{nome_lista}'." + # Usa la lista referenziata per ottenere le carte + return f"{nome_lista} ({len(target_lista_ref)}): " + ", ".join([c.desc_breve for c in target_lista_ref]) diff --git a/extract_cwzator.py b/extract_cwzator.py new file mode 100644 index 0000000..cbd7152 --- /dev/null +++ b/extract_cwzator.py @@ -0,0 +1,20 @@ +import re + +with open(r'e:\git\Mine\GBUtils\gbutils.py', 'r', encoding='utf-8') as f: + content = f.read() + +# Find the start of the CWzator function +start_idx = content.find('def CWzator(') +if start_idx != -1: + # Find the next def to mark the end, or end of file + next_def_idx = content.find('\ndef ', start_idx + 1) + if next_def_idx == -1: + extracted = content[start_idx:] + else: + extracted = content[start_idx:next_def_idx] + + with open('cwzator_reference.py', 'w', encoding='utf-8') as f2: + f2.write(extracted) + print("Estrazione completata con successo.") +else: + print("Funzione CWzator non trovata.") diff --git a/plan_dx_expedition.md b/plan_dx_expedition.md new file mode 100644 index 0000000..666019f --- /dev/null +++ b/plan_dx_expedition.md @@ -0,0 +1,39 @@ +# Piano di Implementazione: Modalità DX Expedition + +L'obiettivo è introdurre in `cwsim` la modalità "DX Expedition" come alternativa alla modalità "Contest". Nella modalità DX Expedition, lo scambio dei numeri progressivi (NR) viene omesso e il QSO si conclude con la sola verifica del nominativo (Call) e del rapporto (RST). + +Ecco i passaggi dettagliati per implementare questa nuova funzionalità: + +## 1. Modifiche all'Interfaccia Grafica e Configurazione (COMPLETATO) +* **`cwsimgui.ui`**: + * Aggiunto un nuovo elemento per la scelta tra "Contest" e "DX Expedition" (il `typeComboBox`). +* **`cwsim.py` (Gestione GUI)**: + * Collegato il nuovo selettore a una variabile per aggiornare il file di configurazione (`self.contest.isDxExpedition`). +* **`contest.py` (Configurazione)**: + * Aggiunto un flag booleano `isDxExpedition` alla classe `Contest` (con supporto I/O config). + +## 2. Modifiche alla Macchina a Stati dei Bot (Stazioni DX) (COMPLETATO) +Il cuore del comportamento delle stazioni chiamanti risiede in `dxoper.py` e `dxstation.py`. Attualmente, la macchina a stati si aspetta obbligatoriamente un NR. +* **`dxstation.py`**: + * Passato il parametro `isDxExpedition` dalla classe `Contest` giù fino a `DxStation` e al suo `DxOperator`. +* **`dxoper.py` (Classe `DxOperator`)**: + * Modificato il metodo `msgReceived()` (che analizza cosa hai trasmesso tu): se `isDxExpedition` è vera, la macchina a stati si ritiene soddisfatta saltando la richiesta del numero progressivo e passando a `NeedEnd`. + * Modificato il metodo `getReply()`: omesso l'invio del proprio NR se `isDxExpedition` è attiva. + +## 3. Logica di Validazione del QSO dell'Utente (`cwsim.py`) +Il motore principale di log deve adattarsi all'assenza dell'NR. +* **`saveQso()`**: + * Rimuovere l'obbligatorietà del campo `self._nr` nel controllo iniziale (`if not (self._hiscall and self._rst)`). + * Inserire un valore fittizio o vuoto nel log per quanto riguarda la colonna `Sent` o `Rcvd` del progressivo, senza che questo causi crash al simulatore. +* **`checkQso()`**: + * Disabilitare il confronto di `self._lastLog[1] != self._lastQso[1]` (che rappresenta l'NR) quando la modalità DX Expedition è attiva. +* **Motore di Pressione Tasti (`enter()` e `;`)**: + * Nella funzione `enter()`, quando l'operatore preme Invio, il sistema verifica se ha inviato Call e NR per passare allo stato successivo. Bisogna dire al sistema di non forzare l'invio del messaggio `StationMessage.NR` o del punto interrogativo se `isDxExpedition` è attiva e l'NR è assente. + +## 4. Aggiornamento delle Statistiche Finali +* **`writeSummary()` in `cwsim.py`**: + * L'intestazione del file TXT prodotto deve chiarire se la sessione era "Contest" o "DX Expedition". + * Quando si scansionano i log per riportare gli errori di scambio ("Exchanges miscopied"), la funzione non dovrà segnalare errore se l'NR è assente ma il resto è corretto. + +--- +**Sei d'accordo con questo approccio passo-passo?** Possiamo iniziare dalla modifica dell'interfaccia e della configurazione, per poi passare al motore a stati dei bot e infine alla validazione del log. diff --git a/python/add_dx_ui.py b/python/add_dx_ui.py new file mode 100644 index 0000000..5310d26 --- /dev/null +++ b/python/add_dx_ui.py @@ -0,0 +1,67 @@ +import xml.etree.ElementTree as ET +import sys + +tree = ET.parse('cwsimgui.ui') +root = tree.getroot() + +# Find contestBox gridLayout_5 to add the typeComboBox +for layout in root.iter('layout'): + if layout.get('name') == 'gridLayout_5': + names = [widget.get('name') for widget in layout.iter('widget') if widget.get('name')] + if 'typeComboBox' in names: + break + + # We have: + # row 0, col 0: contestComboBox (Pileup/Single) + # row 0, col 1: durationComboBox (Min/QSO) + # row 0, col 2: activityLabel + # row 0, col 3: startStopButton + # Let's shift things or add to a new row. Adding a new combobox in a new place: row 1, col 0 + + item_combo = ET.Element('item', attrib={'row': '1', 'column': '0', 'colspan': '2'}) + widget_combo = ET.SubElement(item_combo, 'widget', attrib={'class': 'QComboBox', 'name': 'typeComboBox'}) + + prop_focus = ET.SubElement(widget_combo, 'property', attrib={'name': 'focusPolicy'}) + enum_focus = ET.SubElement(prop_focus, 'enum') + enum_focus.text = 'Qt::TabFocus' + + prop_tt = ET.SubElement(widget_combo, 'property', attrib={'name': 'toolTip'}) + string_tt = ET.SubElement(prop_tt, 'string') + string_tt.text = 'Choose operating mode' + + prop_an = ET.SubElement(widget_combo, 'property', attrib={'name': 'accessibleName'}) + string_an = ET.SubElement(prop_an, 'string') + string_an.text = 'Operating Mode' + + # Item 1: Contest + item1 = ET.SubElement(widget_combo, 'item') + prop_text1 = ET.SubElement(item1, 'property', attrib={'name': 'text'}) + string_text1 = ET.SubElement(prop_text1, 'string') + string_text1.text = 'Contest' + + # Item 2: DX Expedition + item2 = ET.SubElement(widget_combo, 'item') + prop_text2 = ET.SubElement(item2, 'property', attrib={'name': 'text'}) + string_text2 = ET.SubElement(prop_text2, 'string') + string_text2.text = 'DX Expedition' + + layout.append(item_combo) + + break + +# Insert tabstop just after contestComboBox +tabstops = root.find('.//tabstops') +if tabstops is not None: + insert_idx = -1 + for i, ts in enumerate(list(tabstops)): + if ts.text == 'contestComboBox': + insert_idx = i + 1 + break + + if insert_idx != -1: + new_ts = ET.Element('tabstop') + new_ts.text = 'typeComboBox' + tabstops.insert(insert_idx, new_ts) + +tree.write('cwsimgui.ui', encoding='utf-8', xml_declaration=True) +print("Successfully added typeComboBox to cwsimgui.ui") diff --git a/python/add_ui_element.py b/python/add_ui_element.py new file mode 100644 index 0000000..b172e2c --- /dev/null +++ b/python/add_ui_element.py @@ -0,0 +1,57 @@ +import xml.etree.ElementTree as ET +import sys + +tree = ET.parse('cwsimgui.ui') +root = tree.getroot() + +for layout in root.iter('layout'): + if layout.get('name') == 'gridLayout_6': + + # Check if already added + names = [widget.get('name') for widget in layout.iter('widget') if widget.get('name')] + if 'straightKeyLabel' in names: + break + + # gridLayout_6 has rows 0 to 3 used (from grep). Let's add to row 4, column 0 and 1. + item_label = ET.Element('item', attrib={'row': '4', 'column': '0'}) + widget_label = ET.SubElement(item_label, 'widget', attrib={'class': 'QLabel', 'name': 'straightKeyLabel'}) + prop_text = ET.SubElement(widget_label, 'property', attrib={'name': 'text'}) + string_text = ET.SubElement(prop_text, 'string') + string_text.text = 'Straight Key %' + layout.append(item_label) + + item_spin = ET.Element('item', attrib={'row': '4', 'column': '1'}) + widget_spin = ET.SubElement(item_spin, 'widget', attrib={'class': 'QDoubleSpinBox', 'name': 'straightKeyProbSpinBox'}) + + prop_focus = ET.SubElement(widget_spin, 'property', attrib={'name': 'focusPolicy'}) + enum_focus = ET.SubElement(prop_focus, 'enum') + enum_focus.text = 'Qt::StrongFocus' + + prop_tt = ET.SubElement(widget_spin, 'property', attrib={'name': 'toolTip'}) + string_tt = ET.SubElement(prop_tt, 'string') + string_tt.text = 'Probability of stations using a straight key (0 to 1)' + + prop_an = ET.SubElement(widget_spin, 'property', attrib={'name': 'accessibleName'}) + string_an = ET.SubElement(prop_an, 'string') + string_an.text = 'Straight Key probability' + + prop_max = ET.SubElement(widget_spin, 'property', attrib={'name': 'maximum'}) + double_max = ET.SubElement(prop_max, 'double') + double_max.text = '1.000000000000000' + + prop_step = ET.SubElement(widget_spin, 'property', attrib={'name': 'singleStep'}) + double_step = ET.SubElement(prop_step, 'double') + double_step.text = '0.010000000000000' + + layout.append(item_spin) + + # Add to tabstops + tabstops = root.find('.//tabstops') + if tabstops is not None: + ts = ET.SubElement(tabstops, 'tabstop') + ts.text = 'straightKeyProbSpinBox' + + break + +tree.write('cwsimgui.ui', encoding='utf-8', xml_declaration=True) +print("Successfully updated cwsimgui.ui") diff --git a/python/add_ui_element2.py b/python/add_ui_element2.py new file mode 100644 index 0000000..67a2c0b --- /dev/null +++ b/python/add_ui_element2.py @@ -0,0 +1,65 @@ +import xml.etree.ElementTree as ET +import sys + +tree = ET.parse('cwsimgui.ui') +root = tree.getroot() + +# Find gridLayout_6 and append our widgets +for layout in root.iter('layout'): + if layout.get('name') == 'gridLayout_6': + + names = [widget.get('name') for widget in layout.iter('widget') if widget.get('name')] + if 'straightKeyLabel' in names: + break + + item_label = ET.Element('item', attrib={'row': '4', 'column': '0'}) + widget_label = ET.SubElement(item_label, 'widget', attrib={'class': 'QLabel', 'name': 'straightKeyLabel'}) + prop_text = ET.SubElement(widget_label, 'property', attrib={'name': 'text'}) + string_text = ET.SubElement(prop_text, 'string') + string_text.text = 'Straight Key %' + layout.append(item_label) + + item_spin = ET.Element('item', attrib={'row': '4', 'column': '1'}) + widget_spin = ET.SubElement(item_spin, 'widget', attrib={'class': 'QDoubleSpinBox', 'name': 'straightKeyProbSpinBox'}) + + prop_focus = ET.SubElement(widget_spin, 'property', attrib={'name': 'focusPolicy'}) + enum_focus = ET.SubElement(prop_focus, 'enum') + enum_focus.text = 'Qt::StrongFocus' + + prop_tt = ET.SubElement(widget_spin, 'property', attrib={'name': 'toolTip'}) + string_tt = ET.SubElement(prop_tt, 'string') + string_tt.text = 'Probability of stations using a straight key (0 to 1)' + + prop_an = ET.SubElement(widget_spin, 'property', attrib={'name': 'accessibleName'}) + string_an = ET.SubElement(prop_an, 'string') + string_an.text = 'Straight Key probability' + + prop_max = ET.SubElement(widget_spin, 'property', attrib={'name': 'maximum'}) + double_max = ET.SubElement(prop_max, 'double') + double_max.text = '1.000000000000000' + + prop_step = ET.SubElement(widget_spin, 'property', attrib={'name': 'singleStep'}) + double_step = ET.SubElement(prop_step, 'double') + double_step.text = '0.010000000000000' + + layout.append(item_spin) + + break + +# Insert tabstop just after lidsCheck +tabstops = root.find('.//tabstops') +if tabstops is not None: + # Find index of lidsCheck + insert_idx = -1 + for i, ts in enumerate(list(tabstops)): + if ts.text == 'lidsCheck': + insert_idx = i + 1 + break + + if insert_idx != -1: + new_ts = ET.Element('tabstop') + new_ts.text = 'straightKeyProbSpinBox' + tabstops.insert(insert_idx, new_ts) + +tree.write('cwsimgui.ui', encoding='utf-8', xml_declaration=True) +print("Successfully updated cwsimgui.ui") diff --git a/python/contest.py b/python/contest.py index 8b773d1..2f279a8 100644 --- a/python/contest.py +++ b/python/contest.py @@ -284,10 +284,10 @@ def getAudio(self,outdata,nf,tinfo,status): flutterProb=self.flutterProb, rptProb=self.rptProb,fast=self.fast,slow=self.slow, straightKeyProb=self.straightKeyProb, - isSingle=True,bufsize=self._bufsize,rate=self._rate) + isSingle=True,isDxExpedition=self.isDxExpedition,bufsize=self._bufsize,rate=self._rate) self.stations.append(s) s.processEvent(StationEvent.MeFinished) - + # np.savetxt(self.ef,audio) # self.wf.writeframesraw((audio*30000).astype(np.int16)) @@ -329,7 +329,7 @@ def onMeFinishedSending(self): lidRstProb=self.lidRstProb,qsb=self.qsb, flutterProb=self.flutterProb, rptProb=self.rptProb,fast=self.fast,slow=self.slow, - isSingle=False,bufsize=self._bufsize,rate=self._rate)) + isSingle=False,isDxExpedition=self.isDxExpedition,bufsize=self._bufsize,rate=self._rate)) for s in self.stations: s.processEvent(StationEvent.MeFinished) @@ -413,6 +413,7 @@ def readConfig(self,filename): self.savewave = int(contestdict['savewave']) self.saveini = int(contestdict['saveini']) self.savesummary = int(contestdict['savesummary']) + self.isDxExpedition = contestdict.get('isdxexpedition', 'False') == 'True' def writeConfig(self,filename): with open(filename,'w') as f: @@ -432,6 +433,6 @@ def writeConfig(self,filename): ,'lidRstProb','lidNrProb','rptProb','flutterProb']: p.set('Conditions',i,str(eval('self.'+i))) p.add_section('Contest') - for i in ['duration','mode','savewave','saveini','savesummary']: + for i in ['duration','mode','savewave','saveini','savesummary','isDxExpedition']: p.set('Contest',i,str(eval('self.'+i))) p.write(f) diff --git a/python/cwsim.py b/python/cwsim.py index 67be5a8..6d9bf0a 100755 --- a/python/cwsim.py +++ b/python/cwsim.py @@ -136,6 +136,7 @@ def __init__(self,parent=None): self.activitySpinBox.valueChanged.connect(self.activity) self.durationComboBox.currentIndexChanged.connect(self.modecombo) self.contestComboBox.currentIndexChanged.connect(self.modecombo) + self.typeComboBox.currentIndexChanged.connect(self.typecombo) self.trF1Button.clicked.connect(self.f1) self.trF2Button.clicked.connect(self.f2) self.trF3Button.clicked.connect(self.f3) @@ -259,6 +260,7 @@ def syncGui(self): self.flutterProbSpinBox.setValue(self.contest.flutterProb) self.fastSpinBox.setValue(self.contest.fast) self.slowSpinBox.setValue(self.contest.slow) + self.typeComboBox.setCurrentIndex(1 if getattr(self.contest, 'isDxExpedition', False) else 0) if self.contest.mode == RunMode.pileup: self.contestComboBox.setCurrentIndex(0) self.durationComboBox.setCurrentIndex(0) @@ -685,6 +687,9 @@ def flutter(self,s): def lids(self,s): self.contest.lids = (s // 2) + def typecombo(self,s): + self.contest.isDxExpedition = (s == 1) + def modecombo(self,s): i = self.contestComboBox.currentIndex() j = self.durationComboBox.currentIndex() @@ -948,23 +953,31 @@ def sendMsg(self,msg): def checkQso(self): if self._lastLog[0] != self._lastQso[0]: return "NIL" - if self._lastLog[1] != self._lastQso[1]: return "NR" - if self._lastLog[2] != self._lastQso[2]: return "RST" + if getattr(self.contest, 'isDxExpedition', False): + if self._lastLog[2] != self._lastQso[2]: return "RST" + else: + if self._lastLog[1] != self._lastQso[1]: return "NR" + if self._lastLog[2] != self._lastQso[2]: return "RST" return "" def saveQso(self): time.sleep(0) #yield #check needed if period or equivalent typed before QSO info set up - if not (self._hiscall and self._nr and self._rst): - return + dx_exp = getattr(self.contest, 'isDxExpedition', False) + if dx_exp: + if not (self._hiscall and self._rst): + return + else: + if not (self._hiscall and self._nr and self._rst): + return self._nrsent = False self._callsent = False self._rawQsoCount += 1 h,m,s = self.contest.time() if self._rst == "": self._rst = "599" - self._lastLog = [self._hiscall, int(self._nr), int(self._rst)] + self._lastLog = [self._hiscall, int(self._nr) if self._nr else 0, int(self._rst)] time.sleep(0) #yield rawPfx = self.prefix.getPrefix(self._hiscall) time.sleep(0) #yield @@ -992,8 +1005,12 @@ def saveQso(self): self._lastLog = [None,None,None] self._lastQso = [None,None,None] tstr = '{:02d}:{:02d}:{:02d}'.format(h,m,s) - rcvd = '{:03d} {:04d}'.format(int(self._rst),int(self._nr)) - sent = '{:03d} {:04d}'.format(599,self.contest.me.nr) + if dx_exp: + rcvd = '{:03d}'.format(int(self._rst)) + sent = '{:03d}'.format(599) + else: + rcvd = '{:03d} {:04d}'.format(int(self._rst),int(self._nr)) + sent = '{:03d} {:04d}'.format(599,self.contest.me.nr) time.sleep(0) #yield r = self.logTable.rowCount() time.sleep(0) #yield diff --git a/python/cwsimgui.ui b/python/cwsimgui.ui index 2319bb2..75f930e 100644 --- a/python/cwsimgui.ui +++ b/python/cwsimgui.ui @@ -1440,7 +1440,7 @@ - + Qt::TabFocusChoose operating modeOperating ModeContestDX Expedition @@ -1789,7 +1789,7 @@ flutterProbSpinBox slowSpinBox contestComboBox - startStopButton + typeComboBoxstartStopButton durationComboBox durationSpinBox activitySpinBox diff --git a/python/dxoper.py b/python/dxoper.py index a2cef22..a61be11 100644 --- a/python/dxoper.py +++ b/python/dxoper.py @@ -208,12 +208,18 @@ def msgReceived(self,msgs): isme = self.ismycall() if isme == Mc.Yes: if self.state in [Os.NeedPrevEnd, Os.NeedQso, Os.NeedCallNr]: - self.setState(Os.NeedNr) + if self.isDxExpedition: + self.setState(Os.NeedEnd) + else: + self.setState(Os.NeedNr) elif self.state == Os.NeedCall: self.setState(Os.NeedEnd) elif isme == Mc.Almost: if self.state in [Os.NeedPrevEnd, Os.NeedQso, Os.NeedNr]: - self.setState(Os.NeedCallNr) + if self.isDxExpedition: + self.setState(Os.NeedCall) + else: + self.setState(Os.NeedCallNr) elif self.state == Os.NeedEnd: self.setState(Os.NeedCall) elif isme == Mc.No: @@ -270,18 +276,20 @@ def getReply(self): elif self.state == Os.NeedCall: r1 = self._rng.random() # Morserunner's probabilities are 0.5, 0.5*0.25 if r1 < 0.5: - res = StationMessage.DeMyCallNr1 + res = StationMessage.DeMyCall if self.isDxExpedition else StationMessage.DeMyCallNr1 elif r1 < 0.625: - res = StationMessage.DeMyCallNr2 + res = StationMessage.DeMyCall if self.isDxExpedition else StationMessage.DeMyCallNr2 else: - res = StationMessage.MyCallNr2 + res = StationMessage.MyCall if self.isDxExpedition else StationMessage.MyCallNr2 elif self.state == Os.NeedCallNr: if self._rng.random() < 0.5: res = StationMessage.DeMyCall1 else: res = StationMessage.DeMyCall2 else: #NeedEnd - if (self.patience == (DxOperator.FULL_PATIENCE-1) + if self.isDxExpedition: + res = StationMessage.TU + elif (self.patience == (DxOperator.FULL_PATIENCE-1) or self._rng.random() < 0.9): res = StationMessage.R_NR else: diff --git a/python/dxstation.py b/python/dxstation.py index 3e05341..9206617 100644 --- a/python/dxstation.py +++ b/python/dxstation.py @@ -28,7 +28,7 @@ def __init__(self,rng,keyer,callList,cqstn,minutes=0, lids=True,lidNrProb=0.1,lidRstProb=0.03,qsb=True,flutterProb=0.3, rptProb=0.1,fast=1.1,slow=0.9, straightKeyProb=0.25, - isSingle=False,bufsize=512,rate=11025): + isSingle=False,isDxExpedition=False,bufsize=512,rate=11025): super().__init__(rng,keyer,bufsize=bufsize,rate=rate) if self._rng.random() < straightKeyProb: while True: @@ -49,7 +49,6 @@ def __init__(self,rng,keyer,callList,cqstn,minutes=0, rng, minutes, call=self.myCall, - skills=rng.integers(low=1,high=4), s2bfac=rate/bufsize, lids=lids, rptProb=rptProb, @@ -57,6 +56,7 @@ def __init__(self,rng,keyer,callList,cqstn,minutes=0, fast=fast, slow=slow, isSingle = isSingle, + isDxExpedition = isDxExpedition, state=Os.NeedPrevEnd, cqstn=cqstn) self.nrWithError = lids and (self._rng.random() < lidNrProb) From 81a6b025755b9ecc5e3ac80797cbaf9fc2df6248 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 26 Mar 2026 16:21:20 +0100 Subject: [PATCH 04/22] Implement DX Expedition mode and fix QSO mechanics --- cwsim.txt | 141 +++++++++++++++++++++++++++++++++++ plan_dx_expedition.md | 20 ++--- python/contest.py | 5 +- python/cwsim.py | 170 ++++++++++++++++++++++++++++-------------- python/dxoper.py | 17 ++--- python/dxstation.py | 2 +- python/mystation.py | 4 +- python/station.py | 16 +++- 8 files changed, 293 insertions(+), 82 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index bb41f8a..327f65b 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -51,3 +51,144 @@ Punteggio verificato 0 QSO per ora, suddiviso in intervalli da 5 minuti 0-4 0 5-9 0 + +P55L Report cwsim (DX Expedition) 26/03/2026 14:00:31 +Durata 00:00:36 +Durata (QSO) 0 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 0 +Prefissi totali 0 +Punteggio totale 0 +Punti verificati 0 +Prefissi verificati 0 +Punteggio verificato 0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 0 +5-9 0 + +P55L Report cwsim (DX Expedition) 26/03/2026 14:01:42 +Durata 00:00:15 +Durata (QSO) 0 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 0 +Prefissi totali 0 +Punteggio totale 0 +Punti verificati 0 +Prefissi verificati 0 +Punteggio verificato 0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 0 +5-9 0 + +P55L Report cwsim (DX Expedition) 26/03/2026 14:02:28 +Durata 00:00:36 +Durata (QSO) 0 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 0 +Prefissi totali 0 +Punteggio totale 0 +Punti verificati 0 +Prefissi verificati 0 +Punteggio verificato 0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 0 +5-9 0 + +P55L Report cwsim (DX Expedition) 26/03/2026 15:36:55 +Durata 00:01:24 +Durata (QSO) 0 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 0 +Prefissi totali 0 +Punteggio totale 0 +Punti verificati 0 +Prefissi verificati 0 +Punteggio verificato 0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 0 +5-9 0 + +P55L Report cwsim (DX Expedition) 26/03/2026 15:48:24 +Durata 00:01:54 +Durata (QSO) 0 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 0 +Prefissi totali 0 +Punteggio totale 0 +Punti verificati 0 +Prefissi verificati 0 +Punteggio verificato 0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 0 +5-9 0 + +P55L Report cwsim (DX Expedition) 26/03/2026 16:07:52 +Durata 00:03:03 +Durata (QSO) 2 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 2 +Prefissi totali 2 +Punteggio totale 4 +Punti verificati 2 +Prefissi verificati 2 +Punteggio verificato 4 +Percentuale d'errore 0.0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 24 +5-9 0 + +P55L Report cwsim (DX Expedition) 26/03/2026 16:12:50 +Durata 00:00:33 +Durata (QSO) 1 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 1 +Prefissi totali 1 +Punteggio totale 1 +Punti verificati 1 +Prefissi verificati 1 +Punteggio verificato 1 +Percentuale d'errore 0.0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 12 +5-9 0 + +P55L Report cwsim (DX Expedition) 26/03/2026 16:17:15 +Durata 00:03:07 +Durata (QSO) 10 +Velocità CW 30 WPM +Condizioni Difficoltà +Attività 8 +Durata 10 +Punti totali 10 +Prefissi totali 10 +Punteggio totale 100 +Punti verificati 8 +Prefissi verificati 8 +Punteggio verificato 64 +Percentuale d'errore 20.0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 120 +5-9 0 +Nominativi copiati male +YZ9CCU JJ2ONS diff --git a/plan_dx_expedition.md b/plan_dx_expedition.md index 666019f..4e162c2 100644 --- a/plan_dx_expedition.md +++ b/plan_dx_expedition.md @@ -20,20 +20,22 @@ Il cuore del comportamento delle stazioni chiamanti risiede in `dxoper.py` e `dx * Modificato il metodo `msgReceived()` (che analizza cosa hai trasmesso tu): se `isDxExpedition` è vera, la macchina a stati si ritiene soddisfatta saltando la richiesta del numero progressivo e passando a `NeedEnd`. * Modificato il metodo `getReply()`: omesso l'invio del proprio NR se `isDxExpedition` è attiva. -## 3. Logica di Validazione del QSO dell'Utente (`cwsim.py`) +## 3. Logica di Validazione del QSO dell'Utente (`cwsim.py`) (COMPLETATO) Il motore principale di log deve adattarsi all'assenza dell'NR. * **`saveQso()`**: - * Rimuovere l'obbligatorietà del campo `self._nr` nel controllo iniziale (`if not (self._hiscall and self._rst)`). - * Inserire un valore fittizio o vuoto nel log per quanto riguarda la colonna `Sent` o `Rcvd` del progressivo, senza che questo causi crash al simulatore. + * Rimossa l'obbligatorietà dell'NR se `isDxExpedition` è attiva. + * Le colonne "Sent" e "Recv" ora mostrano solo l'RST in modalità DX Expedition. * **`checkQso()`**: - * Disabilitare il confronto di `self._lastLog[1] != self._lastQso[1]` (che rappresenta l'NR) quando la modalità DX Expedition è attiva. + * Disabilitato il controllo dell'NR per la verifica del punto se la modalità DX Expedition è attiva. * **Motore di Pressione Tasti (`enter()` e `;`)**: - * Nella funzione `enter()`, quando l'operatore preme Invio, il sistema verifica se ha inviato Call e NR per passare allo stato successivo. Bisogna dire al sistema di non forzare l'invio del messaggio `StationMessage.NR` o del punto interrogativo se `isDxExpedition` è attiva e l'NR è assente. + * Modificato `enter()`: ora in modalità DX Expedition, premere invio invia TU e salva il QSO subito dopo aver trasmesso il call del corrispondente. + * Inibito l'invio dell'NR tramite tasto `F2` o `;` quando in modalità DX Expedition. -## 4. Aggiornamento delle Statistiche Finali +## 4. Aggiornamento delle Statistiche Finali (COMPLETATO) * **`writeSummary()` in `cwsim.py`**: - * L'intestazione del file TXT prodotto deve chiarire se la sessione era "Contest" o "DX Expedition". - * Quando si scansionano i log per riportare gli errori di scambio ("Exchanges miscopied"), la funzione non dovrà segnalare errore se l'NR è assente ma il resto è corretto. + * L'intestazione del file TXT prodotto ora specifica tra parentesi se la sessione era "(Contest)" o "(DX Expedition)". + * Nella sezione degli errori di scambio ("Exchanges miscopied"), gli errori relativi all'NR vengono ignorati se la modalità DX Expedition è attiva, evitando segnalazioni errate. --- -**Sei d'accordo con questo approccio passo-passo?** Possiamo iniziare dalla modifica dell'interfaccia e della configurazione, per poi passare al motore a stati dei bot e infine alla validazione del log. +**Implementazione completata.** La modalità DX Expedition è ora pienamente operativa, integrata nella GUI, salvata nelle impostazioni e rispettata sia dai Bot che dalla logica di validazione del log. + diff --git a/python/contest.py b/python/contest.py index 2f279a8..5ebd6b2 100644 --- a/python/contest.py +++ b/python/contest.py @@ -86,6 +86,8 @@ def __init__(self,rng,inifile=None): self.savewave = 0 self.saveini = 1 self.savesummary = 1 + self.isDxExpedition = False + self.straightKeyProb = 0.25 self.fontsize = 12 # not used self._qskdecayfactor = 1.0/(self._rate*self.qskdecaytime) @@ -98,7 +100,7 @@ def __init__(self,rng,inifile=None): self._callList = calllist.CallList(rng) self.stations = [] self.me = MyStation(self._rng,self._keyer,self,self.call,self.pitch - ,self.wpm,bufsize=self._bufsize,rate=self._rate) + ,self.wpm,bufsize=self._bufsize,rate=self._rate,isDxExpedition=self.isDxExpedition) self.bufcount = 0 self._rfg0 = 1.0 self._rfg = np.zeros(self._bufsize+1,dtype=np.float64) @@ -329,6 +331,7 @@ def onMeFinishedSending(self): lidRstProb=self.lidRstProb,qsb=self.qsb, flutterProb=self.flutterProb, rptProb=self.rptProb,fast=self.fast,slow=self.slow, + straightKeyProb=self.straightKeyProb, isSingle=False,isDxExpedition=self.isDxExpedition,bufsize=self._bufsize,rate=self._rate)) for s in self.stations: s.processEvent(StationEvent.MeFinished) diff --git a/python/cwsim.py b/python/cwsim.py index 6d9bf0a..5c82e99 100755 --- a/python/cwsim.py +++ b/python/cwsim.py @@ -186,6 +186,7 @@ def __init__(self,parent=None): self._goodQsoCount = 0 self._qtimes = [] scdict = { "Alt+W":self.wipe, "Return":self.enter, "Escape":self.escape, + "Tab":self.tab, "Shift+Tab":self.backtab, "F1":self.f1, "F2":self.f2, "F3":self.f3, "F4":self.f4, "F5":self.f5, "F6":self.f6, "F7":self.f7, "F8":self.f8, "Shift+Up":self.ritup, "Shift+Down":self.ritdown, "Alt+C":self.ritclear, "Ctrl+Up":self.bwup, @@ -260,7 +261,9 @@ def syncGui(self): self.flutterProbSpinBox.setValue(self.contest.flutterProb) self.fastSpinBox.setValue(self.contest.fast) self.slowSpinBox.setValue(self.contest.slow) - self.typeComboBox.setCurrentIndex(1 if getattr(self.contest, 'isDxExpedition', False) else 0) + is_dx = getattr(self.contest, 'isDxExpedition', False) + self.typeComboBox.setCurrentIndex(1 if is_dx else 0) + self.nrEntry.setEnabled(not is_dx) if self.contest.mode == RunMode.pileup: self.contestComboBox.setCurrentIndex(0) self.durationComboBox.setCurrentIndex(0) @@ -315,8 +318,9 @@ def writeSummary(self,filename): with open(filename,mode,encoding='utf8') as f: if self.contest.savesummary == 2: f.write("\n") + mode_str = "DX Expedition" if getattr(self.contest, 'isDxExpedition', False) else "Contest" s = (self.contest.call + " " + _translate("RunApp","cwsim summary") - + " " + datetime.datetime.now().strftime("%c") + "\n") + + " (" + mode_str + ") " + datetime.datetime.now().strftime("%c") + "\n") f.write(s) sec = int(self.contest.seconds) h = sec//3600 @@ -396,7 +400,8 @@ def writeSummary(self,filename): chk = self.logTable.item(i,cols-1).text() if chk == "NIL": nil.append(self.logTable.item(i,1).text()) - if chk.count("NR") != 0 or chk.count("RST") != 0: + is_dx = getattr(self.contest, 'isDxExpedition', False) + if chk.count("RST") != 0 or (not is_dx and chk.count("NR") != 0): ex.append((self.logTable.item(i,1).text(), self.logTable.item(i,2).text(),chk)) if chk == "QSY": @@ -787,13 +792,15 @@ def period(self): def semicolon(self): self.sendMsg(StationMessage.HisCall) - self.sendMsg(StationMessage.NR) + if not getattr(self.contest, 'isDxExpedition', False): + self.sendMsg(StationMessage.NR) def f1(self): self.sendMsg(StationMessage.CQ) def f2(self): - self.sendMsg(StationMessage.NR) + if not getattr(self.contest, 'isDxExpedition', False): + self.sendMsg(StationMessage.NR) def f3(self): self.sendMsg(StationMessage.TU) @@ -822,6 +829,76 @@ def f8(self): def entrytabs(self): self.tr = self.entryTabs.currentIndex() == 1 + def space(self): + if self.tr: return + self._mustAdvance = False + foc = QtWidgets.QApplication.focusWidget() + is_dx = getattr(self.contest, 'isDxExpedition', False) + if foc is self.callEntry: + if self._rst == '': + self.rstEntry.setText('599') + else: + self.rstEntry.deselect() + if is_dx: + self.rstEntry.setFocus() + else: + self.nrEntry.setFocus() + elif foc in [self.rstEntry, self.nrEntry]: + self.callEntry.setFocus() + else: + self.callEntry.setFocus() + + def tab(self): + foc = QtWidgets.QApplication.focusWidget() + is_dx = getattr(self.contest, 'isDxExpedition', False) + if foc is self.callEntry: + self.rstEntry.setFocus() + if len(self._rst) == 3: + self.rstEntry.setSelection(1,1) + elif foc == self.rstEntry: + if is_dx: + self.callEntry.setFocus() + i = self._hiscall.find('?') + if i>= 0: + self.callEntry.setSelection(i,1) + else: + self.callEntry.deselect() + self.callEntry.end(False) + else: + self.nrEntry.setFocus() + self.nrEntry.deselect() + elif foc == self.nrEntry: + self.callEntry.setFocus() + i = self._hiscall.find('?') + if i>= 0: + self.callEntry.setSelection(i,1) + else: + self.callEntry.deselect() + self.callEntry.end(False) + elif foc is self.trCallEntry: + self.trExchangeEntry.setFocus() + elif foc is self.trExchangeEntry: + self.trCallEntry.setFocus() + + def backtab(self): + foc = QtWidgets.QApplication.focusWidget() + is_dx = getattr(self.contest, 'isDxExpedition', False) + if foc is self.rstEntry: + self.callEntry.setFocus() + self.callEntry.end(False) + elif foc is self.nrEntry: + self.rstEntry.setFocus() + if len(self._rst) == 3: + self.rstEntry.setSelection(1,1) + elif foc is self.callEntry: + if is_dx: + self.rstEntry.setFocus() + if len(self._rst) == 3: + self.rstEntry.setSelection(1,1) + else: + self.nrEntry.setFocus() + self.nrEntry.deselect() + def enter(self): if self.tr: self.trCallEntry.setText(self._hiscall) @@ -829,20 +906,30 @@ def enter(self): if self._hiscall == '': self.sendMsg(StationMessage.CQ) else: + dx_exp = getattr(self.contest, 'isDxExpedition', False) c = self._callsent n = self._nrsent r = self._nr != "" - if (not c) or ((not n) and (not r)): - self.sendMsg(StationMessage.HisCall) - if not n: - self.sendMsg(StationMessage.NR) - if n and not r: - self.sendMsg(StationMessage.Qm) - if r and (c or n): - self.sendMsg(StationMessage.TU) - self.saveQso() + + if dx_exp: + if not c: + self.sendMsg(StationMessage.HisCall) + self.sendMsg(StationMessage.NR) # Sends RST in DX mode + else: + self.sendMsg(StationMessage.TU) + self.saveQso() else: - self._mustAdvance = True + if (not c) or ((not n) and (not r)): + self.sendMsg(StationMessage.HisCall) + if not n: + self.sendMsg(StationMessage.NR) + if n and not r: + self.sendMsg(StationMessage.Qm) + if r and (c or n): + self.sendMsg(StationMessage.TU) + self.saveQso() + else: + self._mustAdvance = True def qsy(self): self.qsysig.emit() @@ -875,12 +962,11 @@ def advanceslot(self): self.rstEntry.setText('599') else: self.rstEntry.deselect() - if self._hiscall.find('?') == -1: + if self._hiscall.find('?') == -1 and not getattr(self.contest, 'isDxExpedition', False): self.nrEntry.setFocus() else: self.callEntry.setFocus() self._mustAdvance = False - def lastQso(self): self.lastqsosig.emit() @@ -965,18 +1051,18 @@ def saveQso(self): time.sleep(0) #yield #check needed if period or equivalent typed before QSO info set up dx_exp = getattr(self.contest, 'isDxExpedition', False) + if self._rst == "": + self._rst = "599" if dx_exp: - if not (self._hiscall and self._rst): + if not self._hiscall: return else: - if not (self._hiscall and self._nr and self._rst): + if not (self._hiscall and self._nr): return self._nrsent = False self._callsent = False self._rawQsoCount += 1 h,m,s = self.contest.time() - if self._rst == "": - self._rst = "599" self._lastLog = [self._hiscall, int(self._nr) if self._nr else 0, int(self._rst)] time.sleep(0) #yield rawPfx = self.prefix.getPrefix(self._hiscall) @@ -1064,6 +1150,9 @@ def wipe(self): self.rstEntry.setText("") self.nrEntry.setText("") self.callEntry.setFocus() + self._hiscall = "" + self._rst = "" + self._nr = "" self._callsent = False self._nrsent = False @@ -1125,41 +1214,6 @@ def downarrow(self): else: self.ritdown() - def space(self): - if self.tr: return - self._mustAdvance = False - foc = QtWidgets.QApplication.focusWidget() - if foc in [self.callEntry, self.rstEntry]: - if self._rst == '': - self.rstEntry.setText('599') - else: - self.rstEntry.deselect() - self.nrEntry.setFocus() - else: - self.callEntry.setFocus() - - def tab(self): - foc = QtWidgets.QApplication.focusWidget() - if foc is self.callEntry: - self.rstEntry.setFocus() - if len(self._rst) == 3: - self.rstEntry.setSelection(1,1) - elif foc == self.rstEntry: - self.nrEntry.setFocus() - self.nrEntry.deselect() - elif foc == self.nrEntry: - self.callEntry.setFocus() - i = self._hiscall.find('?') - if i>= 0: - self.callEntry.setSelection(i,1) - else: - self.callEntry.deselect() - self.callEntry.end(False) - elif foc is self.trCallEntry: - self.trExchangeEntry.setFocus() - elif foc is self.trExchangeEntry: - self.trCallEntry.setFocus() - def close(self): if not os.path.exists(self.defaultini) or self.contest.saveini != 0: self.contest.writeConfig(self.defaultini) @@ -1169,6 +1223,10 @@ def close(self): if __name__ == "__main__": import locale locale.setlocale(locale.LC_ALL,"") + print("Welcome to Cwsim - Python CW Simulator") + print("Author: Kevin E. Schmidt, W9CF") + print("Version: Testing version") + print("Date: giovedì 26 marzo 2026") app = QApplication(sys.argv) translator = QtCore.QTranslator() if getattr(sys,'frozen',False): diff --git a/python/dxoper.py b/python/dxoper.py index a61be11..9669f28 100644 --- a/python/dxoper.py +++ b/python/dxoper.py @@ -47,7 +47,7 @@ class DxOperator(): FULL_PATIENCE = 5 def __init__(self,rng,minutes=0,cqstn=None,call=None,skills=2, s2bfac=11025/512,lids=True,rptProb=0.1,wpm=40,fast=1.1,slow=0.9, - isSingle=False,state=Os.NeedPrevEnd): + isSingle=False,isDxExpedition=False,state=Os.NeedPrevEnd): """ Arguments rng: numpy random number generator @@ -57,6 +57,7 @@ def __init__(self,rng,minutes=0,cqstn=None,call=None,skills=2, call: My station's call skills: My skills isSingle: True if RunMode is single calls + isDxExpedition: True if in DX Expedition mode state: Initial operator state """ self._rng = rng @@ -69,6 +70,7 @@ def __init__(self,rng,minutes=0,cqstn=None,call=None,skills=2, self.repeatCnt= None self._minutes = minutes self._isSingle = isSingle + self.isDxExpedition = isDxExpedition self._lids = lids self._wpm = wpm self._slow = slow @@ -152,7 +154,7 @@ def ismycall(self): for y in range(1,len(c0)+1): d = m[x-1,y-1] if c[x-1] != c0[y-1]: d += 1 - m[x,y] = np.min([m[x,y-1],m[x-1,y]+1,d]) + m[x,y] = np.min([m[x,y-1],m[x-1,y],d]) else: for y in range(1,len(c0)+1): m[x,y] = np.min([m[x,y-1],m[x-1,y],m[x-1,y-1]]) @@ -276,24 +278,21 @@ def getReply(self): elif self.state == Os.NeedCall: r1 = self._rng.random() # Morserunner's probabilities are 0.5, 0.5*0.25 if r1 < 0.5: - res = StationMessage.DeMyCall if self.isDxExpedition else StationMessage.DeMyCallNr1 + res = StationMessage.DeMyCallNr1 elif r1 < 0.625: - res = StationMessage.DeMyCall if self.isDxExpedition else StationMessage.DeMyCallNr2 + res = StationMessage.DeMyCallNr2 else: - res = StationMessage.MyCall if self.isDxExpedition else StationMessage.MyCallNr2 + res = StationMessage.MyCallNr2 elif self.state == Os.NeedCallNr: if self._rng.random() < 0.5: res = StationMessage.DeMyCall1 else: res = StationMessage.DeMyCall2 else: #NeedEnd - if self.isDxExpedition: - res = StationMessage.TU - elif (self.patience == (DxOperator.FULL_PATIENCE-1) + if (self.patience == (DxOperator.FULL_PATIENCE-1) or self._rng.random() < 0.9): res = StationMessage.R_NR else: res = StationMessage.R_NR2 return res - diff --git a/python/dxstation.py b/python/dxstation.py index 9206617..f6fa0a1 100644 --- a/python/dxstation.py +++ b/python/dxstation.py @@ -29,7 +29,7 @@ def __init__(self,rng,keyer,callList,cqstn,minutes=0, rptProb=0.1,fast=1.1,slow=0.9, straightKeyProb=0.25, isSingle=False,isDxExpedition=False,bufsize=512,rate=11025): - super().__init__(rng,keyer,bufsize=bufsize,rate=rate) + super().__init__(rng,keyer,bufsize=bufsize,rate=rate,isDxExpedition=isDxExpedition) if self._rng.random() < straightKeyProb: while True: l = self._rng.integers(low=20, high=43) diff --git a/python/mystation.py b/python/mystation.py index 87852a3..3ecf64d 100644 --- a/python/mystation.py +++ b/python/mystation.py @@ -19,8 +19,8 @@ from station import StationMessage class MyStation(station.Station): - def __init__(self,rng,keyer,contest,myCall,pitch,wpm,bufsize=512,rate=11025): - super().__init__(rng,keyer,bufsize=bufsize,rate=rate) + def __init__(self,rng,keyer,contest,myCall,pitch,wpm,bufsize=512,rate=11025,isDxExpedition=False): + super().__init__(rng,keyer,bufsize=bufsize,rate=rate,isDxExpedition=isDxExpedition) self._contest = contest self.myCall = myCall self.nr = 1 diff --git a/python/station.py b/python/station.py index abe0eb6..9051081 100644 --- a/python/station.py +++ b/python/station.py @@ -94,7 +94,7 @@ class Station(): StationMessage.AGN: 'AGN' } - def __init__(self,rng,keyer,bufsize=512,rate=11025): + def __init__(self,rng,keyer,bufsize=512,rate=11025,isDxExpedition=False): """ Arguments: rng: a numpy random number generator @@ -115,6 +115,7 @@ def __init__(self,rng,keyer,bufsize=512,rate=11025): self.p = 50 self._rst = 599 self.nr = 1 + self.isDxExpedition = isDxExpedition self.nrWithError = False self.myCall = '' self.hisCall = '' @@ -168,7 +169,11 @@ def sendMsg(self,stationmsg): self.state = StationState.Listening else: self.msgs.append(stationmsg) - self.sendText(Station.msg2txt[stationmsg]) + msg_text = Station.msg2txt[stationmsg] + if self.isDxExpedition: + if stationmsg == StationMessage.CQ: + msg_text = 'CQ DE UP' + self.sendText(msg_text) def tick(self): if self.state == StationState.Sending and self._envelop is None: @@ -181,8 +186,11 @@ def tick(self): self.processEvent(StationEvent.Timeout) def nrAsText(self): - s = '{:d}{:03d}'.format(int(self._rst),int(self.nr)) - if self.nrWithError: + if self.isDxExpedition: + s = '{:d}'.format(int(self._rst)) + else: + s = '{:d}{:03d}'.format(int(self._rst),int(self.nr)) + if self.nrWithError and not self.isDxExpedition: if s[-1] in ['2','3','4','5','6','7']: if self._rng.random() < 0.5: s = '{:d}{:03d}{:s}{:03d}'.format( From 5bcda95bbf9ca83abee4f4c524f064d6e5fa67ea Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Fri, 27 Mar 2026 10:40:49 +0100 Subject: [PATCH 05/22] Implement DX Expedition mode with realistic QSO logic, dynamic speed-up, and localized UI (v1.0.2) --- cwsim.txt | 203 +------------ cwzator_reference.py | 589 -------------------------------------- extract_cwzator.py | 20 -- plan_dx_expedition.md | 41 --- python/add_dx_ui.py | 67 ----- python/add_ui_element.py | 57 ---- python/add_ui_element2.py | 65 ----- python/contest.py | 16 +- python/cwsim.py | 39 ++- python/cwsimgui.ui | 88 +++++- python/dxoper.py | 32 ++- python/dxstation.py | 7 +- python/keyer.py | 114 +++++--- python/mystation.py | 1 + python/station.py | 6 +- python/translate/it_IT.ts | 56 ++++ 16 files changed, 312 insertions(+), 1089 deletions(-) delete mode 100644 cwzator_reference.py delete mode 100644 extract_cwzator.py delete mode 100644 plan_dx_expedition.md delete mode 100644 python/add_dx_ui.py delete mode 100644 python/add_ui_element.py delete mode 100644 python/add_ui_element2.py diff --git a/cwsim.txt b/cwsim.txt index 327f65b..67fc00b 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,194 +1,19 @@ - -IZ4APU Report cwsim 25/03/2026 14:12:23 -Durata 00:02:26 -Durata (QSO) 4 -Velocità CW 26 WPM +IQ4FJ Report cwsim (DX Expedition) 27/03/2026 10:36:52 +Durata 00:10:00 +Durata (QSO) 27 +Velocità CW 32 WPM Condizioni Difficoltà Attività 8 Durata 10 -Punti totali 4 -Prefissi totali 4 -Punteggio totale 16 -Punti verificati 4 -Prefissi verificati 4 -Punteggio verificato 16 -Percentuale d'errore 0.0 +Punti totali 27 +Prefissi totali 25 +Punteggio totale 675 +Punti verificati 24 +Prefissi verificati 23 +Punteggio verificato 552 +Percentuale d'errore 11.1 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 48 -5-9 0 - -IZ4APU Report cwsim 25/03/2026 14:45:11 -Durata 00:00:52 -Durata (QSO) 1 -Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 1 -Prefissi totali 1 -Punteggio totale 1 -Punti verificati 1 -Prefissi verificati 1 -Punteggio verificato 1 -Percentuale d'errore 0.0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 12 -5-9 0 - -IZ4APU Report cwsim 25/03/2026 14:46:17 -Durata 00:00:02 -Durata (QSO) 0 -Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 0 -Prefissi totali 0 -Punteggio totale 0 -Punti verificati 0 -Prefissi verificati 0 -Punteggio verificato 0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 0 -5-9 0 - -P55L Report cwsim (DX Expedition) 26/03/2026 14:00:31 -Durata 00:00:36 -Durata (QSO) 0 -Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 0 -Prefissi totali 0 -Punteggio totale 0 -Punti verificati 0 -Prefissi verificati 0 -Punteggio verificato 0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 0 -5-9 0 - -P55L Report cwsim (DX Expedition) 26/03/2026 14:01:42 -Durata 00:00:15 -Durata (QSO) 0 -Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 0 -Prefissi totali 0 -Punteggio totale 0 -Punti verificati 0 -Prefissi verificati 0 -Punteggio verificato 0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 0 -5-9 0 - -P55L Report cwsim (DX Expedition) 26/03/2026 14:02:28 -Durata 00:00:36 -Durata (QSO) 0 -Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 0 -Prefissi totali 0 -Punteggio totale 0 -Punti verificati 0 -Prefissi verificati 0 -Punteggio verificato 0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 0 -5-9 0 - -P55L Report cwsim (DX Expedition) 26/03/2026 15:36:55 -Durata 00:01:24 -Durata (QSO) 0 -Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 0 -Prefissi totali 0 -Punteggio totale 0 -Punti verificati 0 -Prefissi verificati 0 -Punteggio verificato 0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 0 -5-9 0 - -P55L Report cwsim (DX Expedition) 26/03/2026 15:48:24 -Durata 00:01:54 -Durata (QSO) 0 -Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 0 -Prefissi totali 0 -Punteggio totale 0 -Punti verificati 0 -Prefissi verificati 0 -Punteggio verificato 0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 0 -5-9 0 - -P55L Report cwsim (DX Expedition) 26/03/2026 16:07:52 -Durata 00:03:03 -Durata (QSO) 2 -Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 2 -Prefissi totali 2 -Punteggio totale 4 -Punti verificati 2 -Prefissi verificati 2 -Punteggio verificato 4 -Percentuale d'errore 0.0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 24 -5-9 0 - -P55L Report cwsim (DX Expedition) 26/03/2026 16:12:50 -Durata 00:00:33 -Durata (QSO) 1 -Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 1 -Prefissi totali 1 -Punteggio totale 1 -Punti verificati 1 -Prefissi verificati 1 -Punteggio verificato 1 -Percentuale d'errore 0.0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 12 -5-9 0 - -P55L Report cwsim (DX Expedition) 26/03/2026 16:17:15 -Durata 00:03:07 -Durata (QSO) 10 -Velocità CW 30 WPM -Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 10 -Prefissi totali 10 -Punteggio totale 100 -Punti verificati 8 -Prefissi verificati 8 -Punteggio verificato 64 -Percentuale d'errore 20.0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 120 -5-9 0 +0-4 156 +5-9 168 Nominativi copiati male -YZ9CCU JJ2ONS +HA1TU AI2N IV3UIW diff --git a/cwzator_reference.py b/cwzator_reference.py deleted file mode 100644 index f7502ea..0000000 --- a/cwzator_reference.py +++ /dev/null @@ -1,589 +0,0 @@ -def CWzator(msg, wpm=35, pitch=550, l=30, s=50, p=50, fs=44100, ms=1, vol=0.5, wv=1, sync=False, file=False): - """ - V8.2 di mercoledì 28 maggio 2025 - Gabriele Battaglia (IZ4APU), Claude 3.5, ChatGPT o3-mini-high, Gemini 2.5 Pro - da un'idea originale di Kevin Schmidt W9CF - Genera e riproduce l'audio del codice Morse dal messaggio di testo fornito. - Parameters: - msg (str|int): Messaggio di testo da convertire in Morse. - se == -1 restituisce la mappa morse come dizionario. - wpm (int): Velocità in parole al minuto (range 5-100). - pitch (int): Frequenza in Hz per il tono (range 130-2800). - l (int): Peso per la durata della linea (default 30). - s (int): Peso per la durata degli spazi tra simboli/lettere (default 50). - p (int): Peso per la durata del punto (default 50). - fs (int): Frequenza di campionamento (default 44100 Hz). - ms (int): Durata in millisecondi per i fade-in/out sui toni (default 1). - vol (float): Volume (range 0.0 a 1.0, default 0.5). - wv (int): Tipo d’onda (scipy.signal): 1=Sine(default), 2=Square, 3=Triangle, 4=Sawtooth. - sync (bool): Se True, la funzione aspetta la fine della riproduzione; altrimenti ritorna subito. - file (bool): Se True, salva l’audio in un file WAV. - Returns: - Un oggetto PlaybackHandle e rwpm (velocità effettiva wpm), o (None, None) in caso di errore. - """ - import numpy as np - import sounddevice as sd - import wave - from datetime import datetime - import threading - import sys - from scipy import signal # Importato per le forme d'onda - BLOCK_SIZE = 256 - MORSE_MAP = { - "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":"----.", ".":".-.-.-", "-":"-....-", ",":"--..--", - "?":"..--..", "/":"-..-.", ";":"-.-.-.", "(":"-.--.", "[":"-.--.", - ")":"-.--.-", "]":"-.--.-", "@":".--.-.", "*":"...-.-", "+":".-.-.", - "%":".-...", ":":"---...", "=":"-...-", '"':".-..-.", "'":".----.", - "!":"-.-.--", "$":"...-..-", " ":"", "_":"", - "ò":"---.", "à":".--.-", "ù":"..--", "è":"..-..", - "é":"..-..", "ì":".---."} - if msg==-1: return MORSE_MAP - elif not isinstance(msg, str) or msg == "": print("CWzator Error: msg deve essere una stringa non vuota.", file=sys.stderr); return None, None - if not (isinstance(wpm, int) and 5 <= wpm <= 100): print(f"CWzator Error: wpm ({wpm}) non valido [5-100].", file=sys.stderr); return None, None - if not (isinstance(pitch, int) and 130 <= pitch <= 2800): print(f"CWzator Error: pitch ({pitch}) non valido [130-2000].", file=sys.stderr); return None, None - if not (isinstance(l, int) and 1 <= l <= 100): print(f"CWzator Error: l ({l}) non valido [1-100].", file=sys.stderr); return None, None - if not (isinstance(s, int) and 1 <= s <= 100): print(f"CWzator Error: s ({s}) non valido [1-100].", file=sys.stderr); return None, None - if not (isinstance(p, int) and 1 <= p <= 100): print(f"CWzator Error: p ({p}) non valido [1-100].", file=sys.stderr); return None, None - if not (isinstance(fs, int) and fs > 0): print(f"CWzator Error: fs ({fs}) non valido [>0].", file=sys.stderr); return None, None - if not (isinstance(ms, (int, float)) and ms >= 0): print(f"CWzator Error: ms ({ms}) non valido [>=0].", file=sys.stderr); return None, None - if not (isinstance(vol, (int, float)) and 0.0 <= vol <= 1.0): print(f"CWzator Error: vol ({vol}) non valido [0.0-1.0].", file=sys.stderr); return None, None - if not (isinstance(wv, int) and wv in [1, 2, 3, 4]): print(f"CWzator Error: wv ({wv}) non valido [1-4].", file=sys.stderr); return None, None - # --- Calcolo Durate (con arrotondamento campioni implicito dopo) --- - T = 1.2 / float(wpm) - dot_duration = T * (p / 50.0) - dash_duration = 3.0 * T * (l / 30.0) # Usato 3.0 per float - intra_gap = T * (s / 50.0) - letter_gap = 3.0 * T * (s / 50.0) - word_gap = 7.0 * T * (s / 50.0) - # --- Funzioni Generazione Segmenti (con forme d'onda scipy e arrotondamento) --- - def generate_tone(duration): - # Arrotonda qui per il numero di campioni - N = int(round(fs * duration)) - if N <= 0: return np.array([], dtype=np.int16) # Ritorna array vuoto se durata troppo breve - # Usa float64 per tempo e fase per precisione - t = np.linspace(0, duration, N, endpoint=False, dtype=np.float64) - # Forme d'onda via scipy.signal (output in [-1, 1]) - if wv == 1: # Sine - signal_float = np.sin(2 * np.pi * pitch * t) - elif wv == 2: # Square - signal_float = signal.square(2 * np.pi * pitch * t) - elif wv == 3: # Triangle (width=0.5) - signal_float = signal.sawtooth(2 * np.pi * pitch * t, width=0.5) - else: # Sawtooth (width=1) - signal_float = signal.sawtooth(2 * np.pi * pitch * t, width=1) - signal_float = signal_float.astype(np.float32) # Converti a float32 per audio - # Applica Fade In/Out - fade_samples = int(round(fs * ms / 1000.0)) # Arrotonda campioni fade - # Condizione robusta per sovrapposizione fade - if fade_samples > 0 and fade_samples <= N // 2: - ramp = np.linspace(0, 1, fade_samples, dtype=np.float32) - signal_float[:fade_samples] *= ramp - signal_float[-fade_samples:] *= ramp[::-1] # Usa slicing negativo per l'ultimo pezzo - # Applica volume e converti a int16 - # Clipping prima della conversione int16 - signal_float = np.clip(signal_float * vol, -1.0, 1.0) - return (signal_float * 32767.0).astype(np.int16) - def generate_silence(duration): - # Arrotonda qui per il numero di campioni - N = int(round(fs * duration)) - return np.zeros(N, dtype=np.int16) if N > 0 else np.array([], dtype=np.int16) - # --- Assemblaggio Sequenza (invariato) --- - segments = [] - words = msg.lower().split() - for w_idx, word in enumerate(words): - # Usa una stringa per accumulare le lettere valide invece di una lista - valid_letters = "".join(ch for ch in word if ch in MORSE_MAP) - for l_idx, letter in enumerate(valid_letters): - code = MORSE_MAP.get(letter) # Usa .get() per sicurezza? No, già filtrato. - if not code: continue # Salta se per qualche motivo non c'è codice (non dovrebbe succedere) - for s_idx, symbol in enumerate(code): - if symbol == '.': - segments.append(generate_tone(dot_duration)) - elif symbol == '-': - segments.append(generate_tone(dash_duration)) - # Aggiungi gap intra-simbolo solo se non è l'ultimo simbolo - if s_idx < len(code) - 1: - segments.append(generate_silence(intra_gap)) - # Aggiungi gap tra lettere solo se non è l'ultima lettera - if l_idx < len(valid_letters) - 1: - segments.append(generate_silence(letter_gap)) - # Aggiungi gap tra parole solo se non è l'ultima parola - if w_idx < len(words) - 1: - # Controlla se la parola precedente non era solo spazi o caratteri ignorati - if valid_letters or any(ch in MORSE_MAP for ch in words[w_idx+1]): - segments.append(generate_silence(word_gap)) - # --- Concatenazione e Aggiunta Silenzio Finale --- - audio = np.concatenate(segments) if segments else np.array([], dtype=np.int16) - if audio.size > 0: # Aggiungi solo se c'è audio - silence_samples_end = int(round(fs * 0.005)) # Es. 5ms di silenzio finale - if silence_samples_end > 0: - final_silence = np.zeros(silence_samples_end, dtype=np.int16) - audio = np.concatenate((audio, final_silence)) - # --- Calcolo rwpm (con gestione divisione per zero robusta) --- - rwpm = wpm # Default se pesi standard o nessun elemento contato - if (l, s, p) != (30, 50, 50): - dots = dashes = intra_gaps = letter_gaps = word_gaps = 0 - words_list = msg.lower().split() - processed_letters_count = 0 # Contatore per gestire gaps - for w_idx, w in enumerate(words_list): - current_word_letters = 0 - code_lengths_in_word = [] - for letter in w: - if letter in MORSE_MAP: - code = MORSE_MAP[letter] - if code: # Ignora spazi o caratteri mappati a stringa vuota - dots += code.count('.') - dashes += code.count('-') - code_len = len(code) - if code_len > 1: - intra_gaps += (code_len - 1) - code_lengths_in_word.append(code_len) - current_word_letters += 1 - if current_word_letters > 1: - letter_gaps += (current_word_letters - 1) - processed_letters_count += current_word_letters - # Aggiungi word gap solo se la parola conteneva elementi e non è l'ultima - if current_word_letters > 0 and w_idx < len(words_list) - 1: - # E controlla anche se la parola successiva contiene elementi - if any(ch in MORSE_MAP and MORSE_MAP[ch] for ch in words_list[w_idx+1]): - word_gaps += 1 - # Calcola durate totali (in unità di dot) - # Durata standard: 1 (dot) + 1 (gap) = 2, 3 (dash) + 1 (gap) = 4 - # Gap tra lettere = 3, Gap tra parole = 7 - # L'unità base è la durata del dot standard (T * p/50 dove p=50) - standard_total_units = dots + 3*dashes + intra_gaps + 3*letter_gaps + 7*word_gaps - # Durata attuale con pesi - actual_dot_units = p / 50.0 - actual_dash_units = 3.0 * (l / 30.0) - actual_intra_gap_units = s / 50.0 - actual_letter_gap_units = 3.0 * (s / 50.0) - actual_word_gap_units = 7.0 * (s / 50.0) - actual_total_units = (dots * actual_dot_units) + \ - (dashes * actual_dash_units) + \ - (intra_gaps * actual_intra_gap_units) + \ - (letter_gaps * actual_letter_gap_units) + \ - (word_gaps * actual_word_gap_units) - # Calcola rapporto e rwpm solo se ci sono state durate - if standard_total_units > 0 and actual_total_units > 0: - ratio = actual_total_units / standard_total_units - rwpm = wpm / ratio - elif standard_total_units == 0 and actual_total_units == 0: - rwpm = wpm # Messaggio vuoto, rwpm è uguale a wpm nominale - else: - # Caso anomalo (es. solo spazi?), imposta rwpm a wpm o 0? - # Manteniamo wpm per ora, ma potrebbe essere indice di errore input. - rwpm = wpm - print("CWzator Warning: Calcolo rwpm anomalo, possibile input solo con spazi?", file=sys.stderr) - # --- Classe PlaybackHandle (invariata ma ora riceve audio con silenzio finale) --- - class PlaybackHandle: - def __init__(self, audio_data, sample_rate): - self.audio_data = audio_data - self.sample_rate = sample_rate - self.stream = None - self.is_playing = threading.Event() # Usa Event per thread-safety - self._thread = None # Riferimento al thread - def _playback_target(self): - """Target function per il thread di riproduzione.""" - self.is_playing.set() # Segnala inizio riproduzione - stream = None # Inizializza per blocco finally - try: - with sd.OutputStream( - samplerate=self.sample_rate, channels=1, dtype=np.int16, - blocksize=BLOCK_SIZE, latency='low' - ) as stream: - # Salva riferimento allo stream *dopo* che è stato creato con successo - self.stream = stream - # Scrittura a blocchi, controllando il flag ad ogni blocco - for i in range(0, len(self.audio_data), BLOCK_SIZE): - if not self.is_playing.is_set(): # Controlla l'evento - # print("Debug: Stop richiesto durante la riproduzione.") - stream.stop() # Prova a fermare lo stream corrente - break - block = self.audio_data[i:min(i + BLOCK_SIZE, len(self.audio_data))] - stream.write(block) - # Se il loop finisce normalmente, attendi che lo stream finisca l'output bufferizzato - if self.is_playing.is_set(): - # print("Debug: Loop terminato, attendo stream.close() implicito.") - pass # 'with' gestisce la chiusura e l'attesa implicita - except sd.PortAudioError as pae: - print(f"CWzator Playback PortAudioError: {pae}", file=sys.stderr) - except Exception as e: - print(f"CWzator Playback Error: {e}", file=sys.stderr) - finally: - # print("Debug: Uscita blocco try/finally _playback_target.") - self.is_playing.clear() # Segnala fine riproduzione o errore - self.stream = None # Rilascia riferimento allo stream - def play(self): - """Avvia la riproduzione in un thread separato.""" - if not self.is_playing.is_set() and self.audio_data.size > 0: - # Crea e avvia il thread solo se non sta già suonando e c'è audio - self._thread = threading.Thread(target=self._playback_target) - self._thread.daemon = False # Assicura non-daemon - self._thread.start() - # else: print("Debug: Play chiamato ma già in esecuzione o audio vuoto.") - def wait_done(self): - """Attende la fine della riproduzione corrente.""" - # Attende che l'evento is_playing sia clear O che il thread termini - if self._thread is not None and self._thread.is_alive(): - # print("Debug: wait_done chiamato, joining thread...") - self._thread.join() - # print("Debug: wait_done terminato.") - def stop(self): - """Richiede l'interruzione della riproduzione.""" - # print("Debug: stop richiesto.") - self.is_playing.clear() # Segnala al loop di playback di fermarsi - # Nota: l'interruzione effettiva dipende da quanto velocemente il loop - # controlla l'evento e da quanto tempo impiega stream.stop(). - # Non chiudiamo lo stream qui, il blocco 'with' lo farà. - # --- Creazione Oggetto e Avvio Playback (Logica Originale) --- - play_obj = PlaybackHandle(audio, fs) - # Avvia la riproduzione nel thread interno all'oggetto - play_obj.play() # Il metodo play ora gestisce l'avvio del thread - # --- Salvataggio File (invariato) --- - if file: - filename = f"cwapu Morse recorded at {datetime.now().strftime('%Y%m%d%H%M%S')}.wav" - try: - with wave.open(filename, 'wb') as wf: - wf.setnchannels(1) # Mono - wf.setsampwidth(2) # 16-bit - wf.setframerate(fs) - wf.writeframes(audio.tobytes()) - # print(f"CWzator: Audio salvato in {filename}") - except Exception as e: - print(f"CWzator Error durante salvataggio file: {e}", file=sys.stderr) - # --- Gestione Sync (usa wait_done dell'oggetto) --- - if sync: - play_obj.wait_done() # Usa il metodo dell'oggetto per attendere - # --- Ritorno Oggetto e rwpm --- - return play_obj, rwpm - -class Mazzo: - ''' - V5.2 - settembre 2025 b Gabriele Battaglia & Gemini 2.5 - Classe autocontenuta che rappresenta un mazzo di carte italiano o francese, - con supporto per mazzi multipli, mescolamento, pesca con rimescolamento - automatico degli scarti, e gestione flessibile delle carte. - Non produce output diretto (print), ma restituisce valori o stringhe informative. - ''' - import random - from collections import namedtuple - Carta = namedtuple("Carta", ["id", "nome", "valore", "seme_nome", "seme_id", "desc_breve"]) - _SEMI_FRANCESI = ["Cuori", "Quadri", "Fiori", "Picche"] - _SEMI_ITALIANI = ["Bastoni", "Spade", "Coppe", "Denari"] - _VALORI_FRANCESI = [("Asso", 1)] + [(str(i), i) for i in range(2, 11)] + [("Jack", 11), ("Regina", 12), ("Re", 13)] - _VALORI_ITALIANI = [("Asso", 1)] + [(str(i), i) for i in range(2, 8)] + [("Fante", 8), ("Cavallo", 9), ("Re", 10)] - _VALORI_DESCRIZIONE = {1: 'A', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '0', 11: 'J', 12: 'Q', 13: 'K'} - _SEMI_DESCRIZIONE = {"Cuori": 'C', "Quadri": 'Q', "Fiori": 'F', "Picche": 'P', - "Bastoni": 'B', "Spade": 'S', "Coppe": 'O', "Denari": 'D'} # 'O' per Coppe - def __init__(self, tipo_francese=True, num_mazzi=1): - ''' - Inizializza uno o più mazzi di carte. - Parametri: - - tipo_francese (bool): True per mazzo francese (default), False per mazzo italiano. - - num_mazzi (int): Numero di mazzi da includere (default 1). Deve essere >= 1. - ''' - if not isinstance(num_mazzi, int) or num_mazzi < 1: - raise ValueError("Il numero di mazzi deve essere un intero maggiore o uguale a 1.") - self.tipo_francese = tipo_francese - self.num_mazzi = num_mazzi - # Liste per tracciare lo stato delle carte - self.carte = [] # Mazzo principale da cui pescare - self.scarti = [] # Pila degli scarti, possono essere rimescolati - self.scarti_permanenti = [] # Carte rimosse permanentemente - self._costruisci_mazzo() - def _costruisci_mazzo(self): - ''' - (Metodo privato) Costruisce il mazzo di carte in base al tipo e al numero di mazzi. - ''' - self.carte = [] # Resetta il mazzo - semi = self._SEMI_FRANCESI if self.tipo_francese else self._SEMI_ITALIANI - valori = self._VALORI_FRANCESI if self.tipo_francese else self._VALORI_ITALIANI - id_carta_counter = 1 - for _ in range(self.num_mazzi): - for id_seme, nome_seme in enumerate(semi, 1): - # Correzione: L'ID seme per mazzi italiani dovrebbe partire da 5 per distinguerli? - # No, l'ID seme è relativo al tipo di mazzo (1-4 per entrambi), - # il nome_seme è ciò che li distingue. Manteniamo 1-4. - seme_id_effettivo = id_seme - if not self.tipo_francese: - # Se si volesse un ID globale unico (1-4 Francese, 5-8 Italiano) - # seme_id_effettivo = id_seme + 4 # Questa è un'opzione di design, ma la lasciamo 1-4 per ora - pass # Manteniamo 1-4 come da codice originale - for nome_valore, valore_num in valori: - desc_val = self._VALORI_DESCRIZIONE.get(valore_num, '?') - desc_seme = self._SEMI_DESCRIZIONE.get(nome_seme, '?') - desc_breve = f"{desc_val}{desc_seme}" - nome_completo = f"{nome_valore} di {nome_seme}" - # Usiamo la definizione di Carta interna alla classe - carta = self.Carta(id=id_carta_counter, - nome=nome_completo, - valore=valore_num, - seme_nome=nome_seme, - seme_id=seme_id_effettivo, - desc_breve=desc_breve) - self.carte.append(carta) - id_carta_counter += 1 - def mescola_mazzo(self): - ''' - Mescola le carte nel mazzo principale (self.carte). - Non restituisce nulla. - ''' - if not self.carte: - return # Non fare nulla se il mazzo è vuoto - self.random.shuffle(self.carte) - def pesca(self, quante=1): - ''' - Pesca carte dal mazzo principale. Se le carte nel mazzo non sono sufficienti, - rimescola automaticamente gli scarti prima di pescare. - Le carte pescate vengono spostate nella lista 'pescate'. - Parametri: - - quante (int): Numero di carte da pescare (default 1). - Ritorna: - - list[Carta]: Lista delle carte pescate. Può contenere meno carte di 'quante' - se il mazzo e gli scarti combinati non sono sufficienti. - ''' - if quante < 0: - raise ValueError("Il numero di carte da pescare deve essere non negativo.") - if quante == 0: - return [] - # NUOVA LOGICA: Se le carte nel mazzo sono meno di quelle richieste, rimescola gli scarti. - if len(self.carte) < quante and self.scarti: - print("\n--- Carte insufficienti nel mazzo. Rimescolo gli scarti... ---") # Feedback utile per il giocatore - self.carte.extend(self.scarti) - self.scarti = [] - self.mescola_mazzo() - print(f"--- Rimescolamento completato. Carte nel mazzo: {len(self.carte)} ---") - # Ora procedi con la pesca - num_da_pescare = min(quante, len(self.carte)) - carte_pescate_ora = [] - if num_da_pescare > 0: - for _ in range(num_da_pescare): - carte_pescate_ora.append(self.carte.pop()) - return carte_pescate_ora - def scarta_carte(self, carte_da_scartare): - ''' - Aggiunge una lista di carte alla pila degli scarti. - Parametri: - - carte_da_scartare (list[Carta]): Lista di oggetti Carta da spostare negli scarti. - ''' - if not carte_da_scartare: - return - self.scarti.extend(carte_da_scartare) - def rimescola_scarti(self, include_pescate=False): - ''' - Rimette le carte dalla pila degli scarti nel mazzo principale e mescola. - Opzionalmente, può includere anche le carte attualmente pescate. - Non reintegra le carte scartate permanentemente. - Parametri: - - include_pescate (bool): Se True, anche le carte in self.pescate sono rimesse (default False). - Ritorna: - - str: Messaggio che riepiloga l'operazione. - ''' - carte_da_reintegrare = [] - msg_parts = [] - num_scarti = len(self.scarti) - if num_scarti > 0: - carte_da_reintegrare.extend(self.scarti) - self.scarti = [] - msg_parts.append(f"{num_scarti} scarti reintegrati.") - else: - msg_parts.append("Nessuno scarto da reintegrare.") - num_pescate = len(self.pescate) - if include_pescate: - if num_pescate > 0: - carte_da_reintegrare.extend(self.pescate) - self.pescate = [] - msg_parts.append(f"{num_pescate} carte pescate reintegrate.") - else: - msg_parts.append("Nessuna carta pescata da reintegrare.") - if not carte_da_reintegrare: - return "Nessuna carta da rimescolare. " + " ".join(msg_parts) - self.carte.extend(carte_da_reintegrare) - self.mescola_mazzo() - msg_parts.append(f"Mazzo ora contiene {len(self.carte)} carte.") - return " ".join(msg_parts) - def _rimuovi_carte_da_lista(self, lista_sorgente, condizione, destinazione, nome_destinazione): - ''' Funzione helper per rimuovere carte da una lista in base a una condizione. ''' - carte_da_mantenere = [] - carte_rimosse = [] - for carta in lista_sorgente: - if condizione(carta): - carte_rimosse.append(carta) - else: - carte_da_mantenere.append(carta) - if carte_rimosse: - destinazione.extend(carte_rimosse) - # Modifica la lista originale inplace - lista_sorgente[:] = carte_da_mantenere - return carte_rimosse - def rimuovi_semi(self, semi_id_da_rimuovere, permanente=False): - ''' - Rimuove dal mazzo principale (self.carte) tutte le carte con i semi specificati. - Le carte rimosse vengono spostate negli scarti temporanei o permanenti. - Parametri: - - semi_id_da_rimuovere (list[int]): Lista di ID numerici dei semi da rimuovere. - - permanente (bool): Se True, sposta in scarti_permanenti, altrimenti in scarti (default False). - Ritorna: - - int: Numero di carte rimosse dal mazzo principale. - ''' - destinazione = self.scarti_permanenti if permanente else self.scarti - nome_dest = "permanenti" if permanente else "temporanei" - condizione = lambda carta: carta.seme_id in semi_id_da_rimuovere - carte_rimosse = self._rimuovi_carte_da_lista(self.carte, condizione, destinazione, nome_dest) - return len(carte_rimosse) - def rimuovi_valori(self, valori_da_rimuovere, permanente=True): - ''' - Rimuove dal mazzo principale (self.carte) tutte le carte con i valori specificati. - Le carte rimosse vengono spostate negli scarti permanenti o temporanei. - Parametri: - - valori_da_rimuovere (list[int]): Lista di valori numerici da rimuovere. - - permanente (bool): Se True, sposta in scarti_permanenti (default), altrimenti in scarti. - Ritorna: - - int: Numero di carte rimosse dal mazzo principale. - ''' - destinazione = self.scarti_permanenti if permanente else self.scarti - nome_dest = "permanenti" if permanente else "temporanei" - condizione = lambda carta: carta.valore in valori_da_rimuovere - carte_rimosse = self._rimuovi_carte_da_lista(self.carte, condizione, destinazione, nome_dest) - return len(carte_rimosse) - def aggiungi_jolly(self, quanti_per_mazzo=2): - ''' - Aggiunge jolly al mazzo principale fino a raggiungere il numero corretto - per ogni mazzo originale (quanti_per_mazzo * num_mazzi). - Funziona solo per mazzi di tipo francese. Jolly esistenti non vengono duplicati. - Parametri: - - quanti_per_mazzo (int): Numero di jolly desiderato per ciascun mazzo originale (default 2). - Ritorna: - - str: Messaggio che indica quanti jolly sono stati aggiunti o se erano già presenti. - ''' - if not self.tipo_francese: - return "I jolly possono essere aggiunti solo ai mazzi di tipo francese." - if quanti_per_mazzo < 0: - # Non ha senso avere un numero negativo di jolly per mazzo - return "Numero di jolly per mazzo non valido (deve essere >= 0)." - - # Calcola il numero totale di jolly che dovrebbero esserci - jolly_attesi_totali = self.num_mazzi * quanti_per_mazzo - # Controlla quanti jolly esistono già in *tutte* le liste - all_cards = self.carte + self.pescate + self.scarti + self.scarti_permanenti - jolly_esistenti_count = sum(1 for c in all_cards if c.nome == "Jolly") - # Determina quanti jolly mancano (se ce ne sono) - jolly_da_aggiungere = jolly_attesi_totali - jolly_esistenti_count - if jolly_da_aggiungere <= 0: - # Se non ne mancano o ce ne sono addirittura di più (improbabile ma gestito) - return f"Nessun nuovo jolly aggiunto (numero richiesto: {jolly_attesi_totali}, già presenti: {jolly_esistenti_count})." - # Se dobbiamo aggiungere jolly: - # Trova l'ID massimo attuale per continuare la sequenza - max_id = 0 - if all_cards: - ids = [c.id for c in all_cards if c.id is not None] - if ids: - max_id = max(ids) - jolly_aggiunti_count = 0 - for i in range(jolly_da_aggiungere): - jolly_id = max_id + 1 + i - # Crea il jolly e aggiungilo al mazzo principale - jolly = self.Carta(id=jolly_id, nome="Jolly", valore=None, seme_nome="N/A", seme_id=0, desc_breve="XY") - self.carte.append(jolly) - jolly_aggiunti_count += 1 - # Aggiorna max_id per il prossimo ciclo (se ce n'è più di uno) - max_id = jolly_id - if jolly_aggiunti_count > 0: - return f"Aggiunti {jolly_aggiunti_count} jolly al mazzo principale." - else: - # Questo caso non dovrebbe verificarsi data la logica precedente, ma per sicurezza - return "Nessun nuovo jolly aggiunto." - def rimuovi_jolly(self, permanente=False): - ''' - Rimuove tutti i jolly dalle pile modificabili (mazzo, pescate, e scarti se permanente=True) - e li sposta nella destinazione appropriata (scarti temporanei o permanenti). - Parametri: - - permanente (bool): Se True, sposta in scarti_permanenti e pulisce anche gli scarti temporanei. - Se False, sposta solo in scarti temporanei. - Ritorna: - - str: Messaggio che indica quanti jolly unici sono stati rimossi e dove sono stati spostati. - ''' - jolly_rimossi_total_obj = [] # Lista per collezionare gli oggetti jolly rimossi - destinazione = self.scarti_permanenti if permanente else self.scarti - tipo_destinazione = "permanenti" if permanente else "temporanei" - condizione = lambda carta: carta.nome == "Jolly" - # Helper per evitare codice duplicato e gestire la collezione degli oggetti - def _processa_lista(lista_sorgente): - carte_rimosse = self._rimuovi_carte_da_lista(lista_sorgente, condizione, destinazione, tipo_destinazione) - jolly_rimossi_total_obj.extend(carte_rimosse) - # Rimuove da self.carte - _processa_lista(self.carte) - # Rimuove da self.pescate - _processa_lista(self.pescate) - # Rimuove da self.scarti SOLO SE la destinazione NON è self.scarti - # Questo previene che gli elementi appena aggiunti a self.scarti vengano rimossi di nuovo. - if permanente: - _processa_lista(self.scarti) # Pulisce gli scarti temporanei spostando i jolly in quelli permanenti - # Calcola quanti jolly unici sono stati effettivamente spostati - # Utile se per errore un jolly fosse presente in più liste (non dovrebbe accadere) - num_rimossi_unici = len({j.id for j in jolly_rimossi_total_obj}) - if num_rimossi_unici > 0: - return f"Rimossi {num_rimossi_unici} jolly unici. Spostati negli scarti {tipo_destinazione}." - else: - return "Nessun jolly trovato da rimuovere." - def _rimuovi_carte_da_lista(self, lista_sorgente, condizione, destinazione, nome_destinazione): - ''' Funzione helper per rimuovere carte da una lista in base a una condizione. ''' - carte_da_mantenere = [] - carte_rimosse = [] - for carta in lista_sorgente: - if condizione(carta): - carte_rimosse.append(carta) - else: - carte_da_mantenere.append(carta) - if carte_rimosse: - # Aggiunge gli elementi rimossi alla lista di destinazione - destinazione.extend(carte_rimosse) - # Modifica la lista originale inplace rimuovendo gli elementi - lista_sorgente[:] = carte_da_mantenere - # Ritorna la lista degli elementi rimossi - return carte_rimosse - def stato_mazzo(self): - ''' Ritorna una stringa che riepiloga lo stato attuale del mazzo. ''' - return (f"Mazzo: {len(self.carte)} carte | " - f"Scarti: {len(self.scarti)} carte | " - f"Scarti Permanenti: {len(self.scarti_permanenti)} carte") - def __len__(self): - ''' Ritorna il numero di carte attualmente nel mazzo principale (self.carte). ''' - return len(self.carte) - def __str__(self): - ''' Rappresentazione stringa dell'oggetto Mazzo (mostra lo stato). ''' - return self.stato_mazzo() - def mostra_carte(self, lista='mazzo'): - ''' - Restituisce una stringa con le descrizioni brevi delle carte - in una specifica lista (mazzo, pescate, scarti, permanenti). - Parametri: - - lista (str): Nome della lista ('mazzo', 'pescate', 'scarti', 'permanenti'). - Ritorna: - - str: Stringa formattata con le carte o messaggio di lista vuota/non valida. - ''' - target_lista_ref = None - nome_lista = "" - if lista == 'mazzo': - target_lista_ref = self.carte - nome_lista = "Mazzo Principale" - elif lista == 'pescate': - target_lista_ref = self.pescate - nome_lista = "Carte Pescate" - elif lista == 'scarti': - target_lista_ref = self.scarti - nome_lista = "Pila Scarti" - elif lista == 'permanenti': - target_lista_ref = self.scarti_permanenti - nome_lista = "Scarti Permanenti" - else: - return "Lista non valida. Scegli tra: 'mazzo', 'pescate', 'scarti', 'permanenti'." - if not target_lista_ref: - return f"Nessuna carta nella lista '{nome_lista}'." - # Usa la lista referenziata per ottenere le carte - return f"{nome_lista} ({len(target_lista_ref)}): " + ", ".join([c.desc_breve for c in target_lista_ref]) diff --git a/extract_cwzator.py b/extract_cwzator.py deleted file mode 100644 index cbd7152..0000000 --- a/extract_cwzator.py +++ /dev/null @@ -1,20 +0,0 @@ -import re - -with open(r'e:\git\Mine\GBUtils\gbutils.py', 'r', encoding='utf-8') as f: - content = f.read() - -# Find the start of the CWzator function -start_idx = content.find('def CWzator(') -if start_idx != -1: - # Find the next def to mark the end, or end of file - next_def_idx = content.find('\ndef ', start_idx + 1) - if next_def_idx == -1: - extracted = content[start_idx:] - else: - extracted = content[start_idx:next_def_idx] - - with open('cwzator_reference.py', 'w', encoding='utf-8') as f2: - f2.write(extracted) - print("Estrazione completata con successo.") -else: - print("Funzione CWzator non trovata.") diff --git a/plan_dx_expedition.md b/plan_dx_expedition.md deleted file mode 100644 index 4e162c2..0000000 --- a/plan_dx_expedition.md +++ /dev/null @@ -1,41 +0,0 @@ -# Piano di Implementazione: Modalità DX Expedition - -L'obiettivo è introdurre in `cwsim` la modalità "DX Expedition" come alternativa alla modalità "Contest". Nella modalità DX Expedition, lo scambio dei numeri progressivi (NR) viene omesso e il QSO si conclude con la sola verifica del nominativo (Call) e del rapporto (RST). - -Ecco i passaggi dettagliati per implementare questa nuova funzionalità: - -## 1. Modifiche all'Interfaccia Grafica e Configurazione (COMPLETATO) -* **`cwsimgui.ui`**: - * Aggiunto un nuovo elemento per la scelta tra "Contest" e "DX Expedition" (il `typeComboBox`). -* **`cwsim.py` (Gestione GUI)**: - * Collegato il nuovo selettore a una variabile per aggiornare il file di configurazione (`self.contest.isDxExpedition`). -* **`contest.py` (Configurazione)**: - * Aggiunto un flag booleano `isDxExpedition` alla classe `Contest` (con supporto I/O config). - -## 2. Modifiche alla Macchina a Stati dei Bot (Stazioni DX) (COMPLETATO) -Il cuore del comportamento delle stazioni chiamanti risiede in `dxoper.py` e `dxstation.py`. Attualmente, la macchina a stati si aspetta obbligatoriamente un NR. -* **`dxstation.py`**: - * Passato il parametro `isDxExpedition` dalla classe `Contest` giù fino a `DxStation` e al suo `DxOperator`. -* **`dxoper.py` (Classe `DxOperator`)**: - * Modificato il metodo `msgReceived()` (che analizza cosa hai trasmesso tu): se `isDxExpedition` è vera, la macchina a stati si ritiene soddisfatta saltando la richiesta del numero progressivo e passando a `NeedEnd`. - * Modificato il metodo `getReply()`: omesso l'invio del proprio NR se `isDxExpedition` è attiva. - -## 3. Logica di Validazione del QSO dell'Utente (`cwsim.py`) (COMPLETATO) -Il motore principale di log deve adattarsi all'assenza dell'NR. -* **`saveQso()`**: - * Rimossa l'obbligatorietà dell'NR se `isDxExpedition` è attiva. - * Le colonne "Sent" e "Recv" ora mostrano solo l'RST in modalità DX Expedition. -* **`checkQso()`**: - * Disabilitato il controllo dell'NR per la verifica del punto se la modalità DX Expedition è attiva. -* **Motore di Pressione Tasti (`enter()` e `;`)**: - * Modificato `enter()`: ora in modalità DX Expedition, premere invio invia TU e salva il QSO subito dopo aver trasmesso il call del corrispondente. - * Inibito l'invio dell'NR tramite tasto `F2` o `;` quando in modalità DX Expedition. - -## 4. Aggiornamento delle Statistiche Finali (COMPLETATO) -* **`writeSummary()` in `cwsim.py`**: - * L'intestazione del file TXT prodotto ora specifica tra parentesi se la sessione era "(Contest)" o "(DX Expedition)". - * Nella sezione degli errori di scambio ("Exchanges miscopied"), gli errori relativi all'NR vengono ignorati se la modalità DX Expedition è attiva, evitando segnalazioni errate. - ---- -**Implementazione completata.** La modalità DX Expedition è ora pienamente operativa, integrata nella GUI, salvata nelle impostazioni e rispettata sia dai Bot che dalla logica di validazione del log. - diff --git a/python/add_dx_ui.py b/python/add_dx_ui.py deleted file mode 100644 index 5310d26..0000000 --- a/python/add_dx_ui.py +++ /dev/null @@ -1,67 +0,0 @@ -import xml.etree.ElementTree as ET -import sys - -tree = ET.parse('cwsimgui.ui') -root = tree.getroot() - -# Find contestBox gridLayout_5 to add the typeComboBox -for layout in root.iter('layout'): - if layout.get('name') == 'gridLayout_5': - names = [widget.get('name') for widget in layout.iter('widget') if widget.get('name')] - if 'typeComboBox' in names: - break - - # We have: - # row 0, col 0: contestComboBox (Pileup/Single) - # row 0, col 1: durationComboBox (Min/QSO) - # row 0, col 2: activityLabel - # row 0, col 3: startStopButton - # Let's shift things or add to a new row. Adding a new combobox in a new place: row 1, col 0 - - item_combo = ET.Element('item', attrib={'row': '1', 'column': '0', 'colspan': '2'}) - widget_combo = ET.SubElement(item_combo, 'widget', attrib={'class': 'QComboBox', 'name': 'typeComboBox'}) - - prop_focus = ET.SubElement(widget_combo, 'property', attrib={'name': 'focusPolicy'}) - enum_focus = ET.SubElement(prop_focus, 'enum') - enum_focus.text = 'Qt::TabFocus' - - prop_tt = ET.SubElement(widget_combo, 'property', attrib={'name': 'toolTip'}) - string_tt = ET.SubElement(prop_tt, 'string') - string_tt.text = 'Choose operating mode' - - prop_an = ET.SubElement(widget_combo, 'property', attrib={'name': 'accessibleName'}) - string_an = ET.SubElement(prop_an, 'string') - string_an.text = 'Operating Mode' - - # Item 1: Contest - item1 = ET.SubElement(widget_combo, 'item') - prop_text1 = ET.SubElement(item1, 'property', attrib={'name': 'text'}) - string_text1 = ET.SubElement(prop_text1, 'string') - string_text1.text = 'Contest' - - # Item 2: DX Expedition - item2 = ET.SubElement(widget_combo, 'item') - prop_text2 = ET.SubElement(item2, 'property', attrib={'name': 'text'}) - string_text2 = ET.SubElement(prop_text2, 'string') - string_text2.text = 'DX Expedition' - - layout.append(item_combo) - - break - -# Insert tabstop just after contestComboBox -tabstops = root.find('.//tabstops') -if tabstops is not None: - insert_idx = -1 - for i, ts in enumerate(list(tabstops)): - if ts.text == 'contestComboBox': - insert_idx = i + 1 - break - - if insert_idx != -1: - new_ts = ET.Element('tabstop') - new_ts.text = 'typeComboBox' - tabstops.insert(insert_idx, new_ts) - -tree.write('cwsimgui.ui', encoding='utf-8', xml_declaration=True) -print("Successfully added typeComboBox to cwsimgui.ui") diff --git a/python/add_ui_element.py b/python/add_ui_element.py deleted file mode 100644 index b172e2c..0000000 --- a/python/add_ui_element.py +++ /dev/null @@ -1,57 +0,0 @@ -import xml.etree.ElementTree as ET -import sys - -tree = ET.parse('cwsimgui.ui') -root = tree.getroot() - -for layout in root.iter('layout'): - if layout.get('name') == 'gridLayout_6': - - # Check if already added - names = [widget.get('name') for widget in layout.iter('widget') if widget.get('name')] - if 'straightKeyLabel' in names: - break - - # gridLayout_6 has rows 0 to 3 used (from grep). Let's add to row 4, column 0 and 1. - item_label = ET.Element('item', attrib={'row': '4', 'column': '0'}) - widget_label = ET.SubElement(item_label, 'widget', attrib={'class': 'QLabel', 'name': 'straightKeyLabel'}) - prop_text = ET.SubElement(widget_label, 'property', attrib={'name': 'text'}) - string_text = ET.SubElement(prop_text, 'string') - string_text.text = 'Straight Key %' - layout.append(item_label) - - item_spin = ET.Element('item', attrib={'row': '4', 'column': '1'}) - widget_spin = ET.SubElement(item_spin, 'widget', attrib={'class': 'QDoubleSpinBox', 'name': 'straightKeyProbSpinBox'}) - - prop_focus = ET.SubElement(widget_spin, 'property', attrib={'name': 'focusPolicy'}) - enum_focus = ET.SubElement(prop_focus, 'enum') - enum_focus.text = 'Qt::StrongFocus' - - prop_tt = ET.SubElement(widget_spin, 'property', attrib={'name': 'toolTip'}) - string_tt = ET.SubElement(prop_tt, 'string') - string_tt.text = 'Probability of stations using a straight key (0 to 1)' - - prop_an = ET.SubElement(widget_spin, 'property', attrib={'name': 'accessibleName'}) - string_an = ET.SubElement(prop_an, 'string') - string_an.text = 'Straight Key probability' - - prop_max = ET.SubElement(widget_spin, 'property', attrib={'name': 'maximum'}) - double_max = ET.SubElement(prop_max, 'double') - double_max.text = '1.000000000000000' - - prop_step = ET.SubElement(widget_spin, 'property', attrib={'name': 'singleStep'}) - double_step = ET.SubElement(prop_step, 'double') - double_step.text = '0.010000000000000' - - layout.append(item_spin) - - # Add to tabstops - tabstops = root.find('.//tabstops') - if tabstops is not None: - ts = ET.SubElement(tabstops, 'tabstop') - ts.text = 'straightKeyProbSpinBox' - - break - -tree.write('cwsimgui.ui', encoding='utf-8', xml_declaration=True) -print("Successfully updated cwsimgui.ui") diff --git a/python/add_ui_element2.py b/python/add_ui_element2.py deleted file mode 100644 index 67a2c0b..0000000 --- a/python/add_ui_element2.py +++ /dev/null @@ -1,65 +0,0 @@ -import xml.etree.ElementTree as ET -import sys - -tree = ET.parse('cwsimgui.ui') -root = tree.getroot() - -# Find gridLayout_6 and append our widgets -for layout in root.iter('layout'): - if layout.get('name') == 'gridLayout_6': - - names = [widget.get('name') for widget in layout.iter('widget') if widget.get('name')] - if 'straightKeyLabel' in names: - break - - item_label = ET.Element('item', attrib={'row': '4', 'column': '0'}) - widget_label = ET.SubElement(item_label, 'widget', attrib={'class': 'QLabel', 'name': 'straightKeyLabel'}) - prop_text = ET.SubElement(widget_label, 'property', attrib={'name': 'text'}) - string_text = ET.SubElement(prop_text, 'string') - string_text.text = 'Straight Key %' - layout.append(item_label) - - item_spin = ET.Element('item', attrib={'row': '4', 'column': '1'}) - widget_spin = ET.SubElement(item_spin, 'widget', attrib={'class': 'QDoubleSpinBox', 'name': 'straightKeyProbSpinBox'}) - - prop_focus = ET.SubElement(widget_spin, 'property', attrib={'name': 'focusPolicy'}) - enum_focus = ET.SubElement(prop_focus, 'enum') - enum_focus.text = 'Qt::StrongFocus' - - prop_tt = ET.SubElement(widget_spin, 'property', attrib={'name': 'toolTip'}) - string_tt = ET.SubElement(prop_tt, 'string') - string_tt.text = 'Probability of stations using a straight key (0 to 1)' - - prop_an = ET.SubElement(widget_spin, 'property', attrib={'name': 'accessibleName'}) - string_an = ET.SubElement(prop_an, 'string') - string_an.text = 'Straight Key probability' - - prop_max = ET.SubElement(widget_spin, 'property', attrib={'name': 'maximum'}) - double_max = ET.SubElement(prop_max, 'double') - double_max.text = '1.000000000000000' - - prop_step = ET.SubElement(widget_spin, 'property', attrib={'name': 'singleStep'}) - double_step = ET.SubElement(prop_step, 'double') - double_step.text = '0.010000000000000' - - layout.append(item_spin) - - break - -# Insert tabstop just after lidsCheck -tabstops = root.find('.//tabstops') -if tabstops is not None: - # Find index of lidsCheck - insert_idx = -1 - for i, ts in enumerate(list(tabstops)): - if ts.text == 'lidsCheck': - insert_idx = i + 1 - break - - if insert_idx != -1: - new_ts = ET.Element('tabstop') - new_ts.text = 'straightKeyProbSpinBox' - tabstops.insert(insert_idx, new_ts) - -tree.write('cwsimgui.ui', encoding='utf-8', xml_declaration=True) -print("Successfully updated cwsimgui.ui") diff --git a/python/contest.py b/python/contest.py index 5ebd6b2..2de7a74 100644 --- a/python/contest.py +++ b/python/contest.py @@ -88,6 +88,9 @@ def __init__(self,rng,inifile=None): self.savesummary = 1 self.isDxExpedition = False self.straightKeyProb = 0.25 + self.myRstSpeedUp = 0.20 + self.dxRstSpeedUp = 0.15 + self.dxRstProb = 0.15 self.fontsize = 12 # not used self._qskdecayfactor = 1.0/(self._rate*self.qskdecaytime) @@ -286,7 +289,9 @@ def getAudio(self,outdata,nf,tinfo,status): flutterProb=self.flutterProb, rptProb=self.rptProb,fast=self.fast,slow=self.slow, straightKeyProb=self.straightKeyProb, - isSingle=True,isDxExpedition=self.isDxExpedition,bufsize=self._bufsize,rate=self._rate) + isSingle=True,isDxExpedition=self.isDxExpedition, + dxRstSpeedUp=self.dxRstSpeedUp, dxRstProb=self.dxRstProb, + bufsize=self._bufsize,rate=self._rate) self.stations.append(s) s.processEvent(StationEvent.MeFinished) @@ -332,7 +337,9 @@ def onMeFinishedSending(self): flutterProb=self.flutterProb, rptProb=self.rptProb,fast=self.fast,slow=self.slow, straightKeyProb=self.straightKeyProb, - isSingle=False,isDxExpedition=self.isDxExpedition,bufsize=self._bufsize,rate=self._rate)) + isSingle=False,isDxExpedition=self.isDxExpedition, + dxRstSpeedUp=self.dxRstSpeedUp, dxRstProb=self.dxRstProb, + bufsize=self._bufsize,rate=self._rate)) for s in self.stations: s.processEvent(StationEvent.MeFinished) @@ -410,6 +417,9 @@ def readConfig(self,filename): self.lidRstProb = float(conditionsdict['lidrstprob']) self.lidNrProb = float(conditionsdict['lidnrprob']) self.rptProb = float(conditionsdict['rptprob']) + self.myRstSpeedUp = float(conditionsdict.get('myrstspeedup', '0.20')) + self.dxRstSpeedUp = float(conditionsdict.get('dxrstspeedup', '0.15')) + self.dxRstProb = float(conditionsdict.get('dxrstprob', '0.15')) contestdict = dict(p['Contest']) self.duration = int(contestdict['duration']) self.mode = eval(contestdict['mode']) @@ -433,7 +443,7 @@ def writeConfig(self,filename): p.set('Station',i,str(eval('self.'+i))) p.add_section('Conditions') for i in ['qrn','qrm','tqrm','qsb','flutter','qsy','lids','straightKeyProb','activity' - ,'lidRstProb','lidNrProb','rptProb','flutterProb']: + ,'lidRstProb','lidNrProb','rptProb','flutterProb','myRstSpeedUp','dxRstSpeedUp','dxRstProb']: p.set('Conditions',i,str(eval('self.'+i))) p.add_section('Contest') for i in ['duration','mode','savewave','saveini','savesummary','isDxExpedition']: diff --git a/python/cwsim.py b/python/cwsim.py index 5c82e99..334a6b6 100755 --- a/python/cwsim.py +++ b/python/cwsim.py @@ -60,6 +60,9 @@ def validate(self,string,pos): acceptable, string , pos = super().validate(string,pos) return (acceptable, string.upper(), pos) +VERSION = "1.0.2" +RELEASE_DATE = "venerdì 27 marzo 2026" + class RunApp(QtWidgets.QMainWindow,cwsimgui.Ui_CwsimMainWindow): advancesig = QtCore.pyqtSignal() @@ -175,6 +178,9 @@ def __init__(self,parent=None): self.straightKeyProbSpinBox.valueChanged.connect(self.straightKeyProb) self.rptProbSpinBox.valueChanged.connect(self.rptProb) self.flutterProbSpinBox.valueChanged.connect(self.flutterProb) + self.myRstSpeedUpSpinBox.valueChanged.connect(self.myRstSpeedUp) + self.dxRstSpeedUpSpinBox.valueChanged.connect(self.dxRstSpeedUp) + self.dxRstProbSpinBox.valueChanged.connect(self.dxRstProb) self.fastSpinBox.valueChanged.connect(self.fast) self.slowSpinBox.valueChanged.connect(self.slow) self.trExchangeEntry.setEnabled(False) @@ -227,6 +233,9 @@ def __init__(self,parent=None): self._goodPfxs = set() self.prefix = Prefix() self.nrchecked = 0 + welcome_msg = _translate("RunApp", "Welcome to Cwsim {version} ({date}) by Kevin Schmidt (W9CF) and Gabriele Battaglia (IZ4APU)").format(version=VERSION, date=RELEASE_DATE) + self.logTable.insertRow(0) + self.logTable.setItem(0, 1, QTableWidgetItem(welcome_msg)) def syncGui(self): self.action_Update_Default_Configuration_on_Exit.setChecked( @@ -257,6 +266,9 @@ def syncGui(self): self.lidRstProbSpinBox.setValue(self.contest.lidRstProb) self.lidNrProbSpinBox.setValue(self.contest.lidNrProb) self.straightKeyProbSpinBox.setValue(self.contest.straightKeyProb) + self.myRstSpeedUpSpinBox.setValue(self.contest.myRstSpeedUp) + self.dxRstSpeedUpSpinBox.setValue(self.contest.dxRstSpeedUp) + self.dxRstProbSpinBox.setValue(self.contest.dxRstProb) self.rptProbSpinBox.setValue(self.contest.rptProb) self.flutterProbSpinBox.setValue(self.contest.flutterProb) self.fastSpinBox.setValue(self.contest.fast) @@ -601,14 +613,15 @@ def startStop(self): sc.setEnabled(True) def about(self): - version = "Testing version" + version = VERSION msg = """ - Python CW Simulator {} + Python CW Simulator {} ({}) Copyright 2022, Kevin E. Schmidt, W9CF, w9cf@arrl.net + Authors: Kevin Schmidt (W9CF) and Gabriele Battaglia (IZ4APU) Based on and derivative of Morse Runner Copyright 2004-2006, Alex Shovkoplyas, VE3NEA - ve3nea@dxatlast.com""".format(version) + ve3nea@dxatlast.com""".format(version, RELEASE_DATE) QtWidgets.QMessageBox.about(self,"CW Simulator",msg) def shortcutHelp(self): @@ -747,6 +760,17 @@ def lidNrProb(self,s): def straightKeyProb(self,s): self.contest.straightKeyProb = s + def myRstSpeedUp(self,s): + self.contest.myRstSpeedUp = s + if self.contest.me is not None: + self.contest.me.myRstSpeedUp = s + + def dxRstSpeedUp(self,s): + self.contest.dxRstSpeedUp = s + + def dxRstProb(self,s): + self.contest.dxRstProb = s + def rptProb(self,s): self.contest.rptProb = s @@ -1223,10 +1247,6 @@ def close(self): if __name__ == "__main__": import locale locale.setlocale(locale.LC_ALL,"") - print("Welcome to Cwsim - Python CW Simulator") - print("Author: Kevin E. Schmidt, W9CF") - print("Version: Testing version") - print("Date: giovedì 26 marzo 2026") app = QApplication(sys.argv) translator = QtCore.QTranslator() if getattr(sys,'frozen',False): @@ -1241,6 +1261,11 @@ def close(self): tdir = os.path.join(tdir,'translate') translator.load(tfile,tdir) app.installTranslator(translator) + _translate = QtCore.QCoreApplication.translate + print(_translate("RunApp", "Welcome to Cwsim - Python CW Simulator")) + print(_translate("RunApp", "Authors: Kevin Schmidt (W9CF) and Gabriele Battaglia (IZ4APU)")) + print(_translate("RunApp", "Version: {version}").format(version=VERSION)) + print(_translate("RunApp", "Date: {date}").format(date=RELEASE_DATE)) form = RunApp() form.show() app.exec() diff --git a/python/cwsimgui.ui b/python/cwsimgui.ui index 75f930e..4bd5f45 100644 --- a/python/cwsimgui.ui +++ b/python/cwsimgui.ui @@ -810,7 +810,93 @@ - Straight Key %Qt::StrongFocusProbability of stations using a straight key (0 to 1)Straight Key probability1.0000000000000000.010000000000000 + + + + Straight Key % + + + + + + + My RST Speed Up % + + + + + + + Bot RST Speed Up % + + + + + + + Bot Speed Up Prob % + + + + + + + Qt::StrongFocus + + + Probability of stations using a straight key (0 to 1) + + + Straight Key probability + + + 1.000000000000000 + + + 0.010000000000000 + + + + + + + My RST speed increase (0 to 1) + + + 1.000000000000000 + + + 0.010000000000000 + + + + + + + Bot RST speed increase (0 to 1) + + + 1.000000000000000 + + + 0.010000000000000 + + + + + + + Probability of bots using speed up (0 to 1) + + + 1.000000000000000 + + + 0.010000000000000 + + + + diff --git a/python/dxoper.py b/python/dxoper.py index 9669f28..5450f00 100644 --- a/python/dxoper.py +++ b/python/dxoper.py @@ -77,6 +77,7 @@ def __init__(self,rng,minutes=0,cqstn=None,call=None,skills=2, self._fast = fast self._rptProb = rptProb self._s2bfac = s2bfac + self.rstSent = False def getSendDelay(self): """ @@ -266,7 +267,7 @@ def getReply(self): return reply message. """ if self.state in [Os.NeedPrevEnd, Os.Done, Os.Failed]: - res = StationMessage.noMsg + res = StationMessage.NoMsg elif self.state == Os.NeedQso: res = StationMessage.MyCall elif self.state == Os.NeedNr: @@ -289,10 +290,31 @@ def getReply(self): else: res = StationMessage.DeMyCall2 else: #NeedEnd - if (self.patience == (DxOperator.FULL_PATIENCE-1) - or self._rng.random() < 0.9): - res = StationMessage.R_NR + if self.isDxExpedition: + if not self.rstSent: + res = StationMessage.R_NR # Send RST if not sent yet + else: + # RST already sent, send final greetings + r = self._rng.random() + if r < 0.4: + res = StationMessage.TU + elif r < 0.6: + res = StationMessage.NoMsg + elif r < 0.8: + res = StationMessage.NoMsg # Silence is common + else: + res = StationMessage.TU else: - res = StationMessage.R_NR2 + if (self.patience == (DxOperator.FULL_PATIENCE-1) + or self._rng.random() < 0.9): + res = StationMessage.R_NR + else: + res = StationMessage.R_NR2 + + # Track if RST was sent in this reply + if res in [StationMessage.R_NR, StationMessage.R_NR2, + StationMessage.DeMyCallNr1, StationMessage.DeMyCallNr2, + StationMessage.MyCallNr2, StationMessage.NR]: + self.rstSent = True return res diff --git a/python/dxstation.py b/python/dxstation.py index f6fa0a1..57e1c30 100644 --- a/python/dxstation.py +++ b/python/dxstation.py @@ -28,8 +28,11 @@ def __init__(self,rng,keyer,callList,cqstn,minutes=0, lids=True,lidNrProb=0.1,lidRstProb=0.03,qsb=True,flutterProb=0.3, rptProb=0.1,fast=1.1,slow=0.9, straightKeyProb=0.25, - isSingle=False,isDxExpedition=False,bufsize=512,rate=11025): + isSingle=False,isDxExpedition=False, + dxRstSpeedUp=0.15, dxRstProb=0.15, + bufsize=512,rate=11025): super().__init__(rng,keyer,bufsize=bufsize,rate=rate,isDxExpedition=isDxExpedition) + self.myRstSpeedUp = dxRstSpeedUp if self._rng.random() < straightKeyProb: while True: l = self._rng.integers(low=20, high=43) @@ -62,6 +65,8 @@ def __init__(self,rng,keyer,callList,cqstn,minutes=0, self.nrWithError = lids and (self._rng.random() < lidNrProb) self.wpm = self.oper.getWpm() self.nr = self.oper.getNr() + self.nrChecked = 0 + self.speedUpRst = isDxExpedition and (self._rng.random() < dxRstProb) if lids and self._rng.random() < lidRstProb: self._rst = 559+10*self._rng.integers(4) else: diff --git a/python/keyer.py b/python/keyer.py index 9cdc885..37574ba 100644 --- a/python/keyer.py +++ b/python/keyer.py @@ -34,7 +34,7 @@ class Keyer(): "?":"..--..", "/":"-..-.", ";":"-.-.-.", "(":"-.--.", "[":"-.--.", ")":"-.--.-", "]":"-.--.-", "@":".--.-.", "*":"...-.-", "+":".-.-.", "%":".-...", ":":"---...", "=":"-...-", '"':".-..-.", "'":".----.", - "!":"---.", "$":"...-..-"," ":"", "_":"" + "!":"---.", "$":"...-..-"," ":"", "_":"", ">":"", "<":"" }) def __init__(self,rate=11025,bufsize=512,risetime=0.005): @@ -72,14 +72,20 @@ def encode(self,txt): string encoding for morse dits and dahs """ s = "" - for i in range(len(txt)-1): - s += Keyer._morse[txt[i]] + " " - s += Keyer._morse[txt[len(txt)-1]] + for i in range(len(txt)): + char = txt[i] + if char in [">", "<"]: + s += char + elif char in Keyer._morse: + s += Keyer._morse[char] + # Add space only if not the last char and next is not a speed marker + if i < len(txt) - 1 and txt[i+1] not in [">", "<"]: + s += " " if s != "": s += "~" return s - def getenvelop(self,msg,wpm,l=30,s=50,p=50): + def getenvelop(self,msg,wpm,l=30,s=50,p=50,speed_up_factor=0.20): """ Arguments msg: morse encoding of dits and dahs @@ -87,52 +93,74 @@ def getenvelop(self,msg,wpm,l=30,s=50,p=50): l: dash weight (default 30) s: space weight (default 50) p: dot weight (default 50) + speed_up_factor: how much to increase speed (0 to 1) Returns keying envelop for audio samples """ nr = len(self.rise) - T_samples = 1.2 * self.rate / wpm - dot_on = int(np.rint(T_samples * (p / 50.0))) - dash_on = int(np.rint(3.0 * T_samples * (l / 30.0))) - intra_off = int(np.rint(T_samples * (s / 50.0))) - letter_gap_added = int(np.rint(3.0 * T_samples * (s / 50.0))) - intra_off - pad_added = int(np.rint(T_samples)) - # Calculate total length + def get_params(current_wpm): + T = 1.2 * self.rate / current_wpm + d_on = int(np.rint(T * (p / 50.0))) + da_on = int(np.rint(3.0 * T * (l / 30.0))) + i_off = int(np.rint(T * (s / 50.0))) + l_gap = int(np.rint(3.0 * T * (s / 50.0))) - i_off + pad = int(np.rint(T)) + return d_on, da_on, i_off, l_gap, pad + + # First pass: Calculate total length total_samples = 0 - for i in range(len(msg)): - if msg[i] == '.': - total_samples += dot_on + intra_off - elif msg[i] == '-': - total_samples += dash_on + intra_off - elif msg[i] == ' ': - total_samples += letter_gap_added - nr - elif msg[i] == '~': - total_samples += pad_added - nr + cur_wpm = wpm + for char in msg: + if char == '>': + cur_wpm = wpm * (1.0 + speed_up_factor) + continue + elif char == '<': + cur_wpm = wpm + continue + + d_on, da_on, i_off, l_gap, pad = get_params(cur_wpm) + if char == '.': + total_samples += d_on + i_off + elif char == '-': + total_samples += da_on + i_off + elif char == ' ': + total_samples += l_gap + elif char == '~': + total_samples += pad - n = int(self._bufsize*np.ceil((total_samples+nr+max(dot_on,dash_on))/self._bufsize)) + n = int(self._bufsize*np.ceil((total_samples + nr + 100)/self._bufsize)) env = np.zeros(n,dtype=np.float32) - dit = np.ones(nr+dot_on,dtype=np.float32) - dit[:nr] = self.rise - dit[dot_on:] = self.fall - - dah = np.ones(nr+dash_on,dtype=np.float32) - dah[:nr] = self.rise - dah[dash_on:] = self.fall - k = 0 - for i in range(len(msg)): - if msg[i] == '.': - if k+len(dit) <= n: - env[k:k+len(dit)] = dit - k += dot_on + intra_off - elif msg[i] == '-': - if k+len(dah) <= n: - env[k:k+len(dah)] = dah - k += dash_on + intra_off - elif msg[i] == ' ': - k += letter_gap_added - nr - elif msg[i] == '~': - k += pad_added - nr + cur_wpm = wpm + for char in msg: + if char == '>': + cur_wpm = wpm * (1.0 + speed_up_factor) + continue + elif char == '<': + cur_wpm = wpm + continue + + d_on, da_on, i_off, l_gap, pad = get_params(cur_wpm) + + if char == '.': + pulse = np.ones(nr+d_on,dtype=np.float32) + pulse[:nr] = self.rise + pulse[d_on:] = self.fall + if k+len(pulse) <= n: + env[k:k+len(pulse)] = pulse + k += d_on + i_off + elif char == '-': + pulse = np.ones(nr+da_on,dtype=np.float32) + pulse[:nr] = self.rise + pulse[da_on:] = self.fall + if k+len(pulse) <= n: + env[k:k+len(pulse)] = pulse + k += da_on + i_off + elif char == ' ': + k += l_gap + elif char == '~': + k += pad + return env diff --git a/python/mystation.py b/python/mystation.py index 3ecf64d..58ac2c9 100644 --- a/python/mystation.py +++ b/python/mystation.py @@ -27,6 +27,7 @@ def __init__(self,rng,keyer,contest,myCall,pitch,wpm,bufsize=512,rate=11025,isDx self._rst = 599 self.pitch = pitch self.wpm = wpm + self.speedUpRst = isDxExpedition self._amplitude = 1.0 self._pieces = [] self.app = None diff --git a/python/station.py b/python/station.py index 9051081..9b7e4b3 100644 --- a/python/station.py +++ b/python/station.py @@ -116,6 +116,8 @@ def __init__(self,rng,keyer,bufsize=512,rate=11025,isDxExpedition=False): self._rst = 599 self.nr = 1 self.isDxExpedition = isDxExpedition + self.speedUpRst = False + self.myRstSpeedUp = 0.20 self.nrWithError = False self.myCall = '' self.hisCall = '' @@ -158,7 +160,7 @@ def sendText(self,msg): else: self._msgtext = msg s = self._keyer.encode(self._msgtext.lower()) - self._envelop = self._keyer.getenvelop(s,self.wpm,self.l,self.s,self.p)*self._amplitude + self._envelop = self._keyer.getenvelop(s,self.wpm,self.l,self.s,self.p,speed_up_factor=self.myRstSpeedUp)*self._amplitude self.state = StationState.Sending self._timeout = NEVER @@ -190,6 +192,8 @@ def nrAsText(self): s = '{:d}'.format(int(self._rst)) else: s = '{:d}{:03d}'.format(int(self._rst),int(self.nr)) + if self.speedUpRst: + s = '>' + s + '<' if self.nrWithError and not self.isDxExpedition: if s[-1] in ['2','3','4','5','6','7']: if self._rng.random() < 0.5: diff --git a/python/translate/it_IT.ts b/python/translate/it_IT.ts index abe7530..4bef0ef 100644 --- a/python/translate/it_IT.ts +++ b/python/translate/it_IT.ts @@ -698,6 +698,42 @@ Duration (QSOs) Durata (QSO) + + Straight Key % + Tasto verticale % + + + My RST Speed Up % + Aumento velocità mio RST % + + + Bot RST Speed Up % + Aumento velocità RST Bot % + + + Bot Speed Up Prob % + Prob. aumento velocità Bot % + + + Straight Key probability + Probabilità tasto verticale + + + Probability of stations using a straight key (0 to 1) + Probabilità che le stazioni usino un tasto verticale (da 0 a 1) + + + My RST speed increase (0 to 1) + Incremento velocità del mio RST (da 0 a 1) + + + Bot RST speed increase (0 to 1) + Incremento velocità dell'RST dei Bot (da 0 a 1) + + + Probability of bots using speed up (0 to 1) + Probabilità che i bot usino la velocità aumentata (da 0 a 1) + MplCanvas @@ -889,6 +925,26 @@ QSOs/Hour for each 5 minute interval QSO per ora, suddiviso in intervalli da 5 minuti + + Welcome to Cwsim {version} ({date}) by Kevin Schmidt (W9CF) and Gabriele Battaglia (IZ4APU) + Benvenuti in Cwsim {version} ({date}) di Kevin Schmidt (W9CF) e Gabriele Battaglia (IZ4APU) + + + Welcome to Cwsim - Python CW Simulator + Benvenuti in Cwsim - Simulatore CW in Python + + + Authors: Kevin Schmidt (W9CF) and Gabriele Battaglia (IZ4APU) + Autori: Kevin Schmidt (W9CF) e Gabriele Battaglia (IZ4APU) + + + Version: {version} + Versione: {version} + + + Date: {date} + Data: {date} + Calls miscopied From c11c96e30edfb61c138f76bb821addc7401b918a Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Fri, 27 Mar 2026 12:55:10 +0100 Subject: [PATCH 06/22] new file: plan.md --- plan.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..610dd67 --- /dev/null +++ b/plan.md @@ -0,0 +1,14 @@ +# Piano di Sviluppo Cwsim - Prossime Versioni + +## UI & Accessibilità +- [ ] **Revisione Grid Layout Parametri**: Verificare il posizionamento dei nuovi parametri DX (Straight Key, My RST Speed-up, ecc.). Attualmente NVDA li riporta in fondo alla lista. +- [ ] **Tab Order**: Ottimizzare l'ordine dei tab affinché ogni Label sia seguita immediatamente dalla sua SpinBox, migliorando l'esperienza per utenti con screen reader. +- [ ] **Verifica Visiva**: Confermare se il layout è corretto anche visivamente o se i nuovi widget sono stati accodati in modo disordinato. + +## Report & Logica DX +- [ ] **Pulizia Report in Modalità Expedition**: In modalità DX Expedition, rimuovere o nascondere i riferimenti ai "progressivi" (numeri seriali) nel report finale (`cwsim.txt`), dato che lo scambio prevede solo l'RST. +- [ ] **RST Validation**: Assicurarsi che la validazione dei dati nel report rifletta correttamente lo scambio 5NN unico della modalità DX. + +## Manutenzione +- [ ] **Compilazione Traduzioni**: Produrre il file `.qm` aggiornato non appena l'ambiente dispone di `lrelease` o caricarlo manualmente. +- [ ] **Sincronizzazione Versioning**: Coordinarsi con Kevin per la numerazione ufficiale post-1.0.2. From 59dec09a73768183ebd4f063e357b27f1d3c2cc7 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 2 Apr 2026 07:57:01 +0200 Subject: [PATCH 07/22] modified: cwsim.txt --- cwsim.txt | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index 67fc00b..f25bb0e 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,19 +1,22 @@ -IQ4FJ Report cwsim (DX Expedition) 27/03/2026 10:36:52 -Durata 00:10:00 -Durata (QSO) 27 -Velocità CW 32 WPM +IQ4PG Report cwsim (DX Expedition) 01/04/2026 20:54:34 +Durata 00:13:18 +Durata (QSO) 23 +Velocità CW 26 WPM Condizioni Difficoltà -Attività 8 -Durata 10 -Punti totali 27 -Prefissi totali 25 -Punteggio totale 675 -Punti verificati 24 -Prefissi verificati 23 -Punteggio verificato 552 -Percentuale d'errore 11.1 +Attività 14 +Durata 14 +Punti totali 23 +Prefissi totali 21 +Punteggio totale 483 +Punti verificati 19 +Prefissi verificati 18 +Punteggio verificato 342 +Percentuale d'errore 17.4 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 156 -5-9 168 +0-4 48 +5-9 144 +10-14 84 Nominativi copiati male -HA1TU AI2N IV3UIW +R30 R30RL JH2JCQ +Progressivi copiati male +K2DS copiato 599 era RST 589 From 5c87488fe98bee38d00131706389e8535e0f6cf5 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 2 Apr 2026 12:22:43 +0200 Subject: [PATCH 08/22] pulizia codice eliminazione qt5 --- README.md | 2 +- python/Makefile | 5 +---- python/audioprocess.py | 1 - python/contest.py | 8 +++----- python/cwsim.py | 13 ++++--------- python/keyer.py | 2 -- python/mplwidget.py | 9 ++------- python/mystation.py | 2 +- python/trlineedit.py | 5 +---- requirements_qt5.txt | 17 ----------------- 10 files changed, 13 insertions(+), 51 deletions(-) delete mode 100644 requirements_qt5.txt diff --git a/README.md b/README.md index 2862ad6..4217abe 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ the commands I used to install the needed packages: ``` git clone https://github.com/w9cf/cwsim.git sudo apt-get install python3-matplotlib -sudo apt-get install python3-pyqt5 +sudo apt-get install python3-pyqt6 sudo apt-get install libportaudio2 pip install pip --upgrade --user python3 -m pip install numpy==1.23 --user diff --git a/python/Makefile b/python/Makefile index 80f3815..965726a 100644 --- a/python/Makefile +++ b/python/Makefile @@ -1,11 +1,8 @@ -.PHONY: pyqt6 pyqt5 cwsim.exe +.PHONY: pyqt6 cwsim.exe cwsimgui.py: cwsimgui.ui pyuic6 -i 3 -o cwsimgui.py cwsimgui.ui -pyqt5: - pyuic5 -i 3 -o cwsimgui.py cwsimgui.ui - pyqt6: pyuic6 -i 3 -o cwsimgui.py cwsimgui.ui diff --git a/python/audioprocess.py b/python/audioprocess.py index 5060f37..01c81fd 100644 --- a/python/audioprocess.py +++ b/python/audioprocess.py @@ -14,7 +14,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys import numpy as np class movavg(): diff --git a/python/contest.py b/python/contest.py index 9d765f7..a8d7678 100644 --- a/python/contest.py +++ b/python/contest.py @@ -26,8 +26,6 @@ from mystation import MyStation from dxoper import Os import queue -import wave -import time import sys import os import errno @@ -137,7 +135,7 @@ def bandwidth(self): def bandwidth(self,bandwidth): self._bandwidth = np.max([np.min([round(bandwidth/50)*50,600]),100]) navg = int(np.rint(0.7*self._rate/self._bandwidth)) - self._fgain = np.sqrt(500/self._bandwidth); + self._fgain = np.sqrt(500/self._bandwidth) self._m1 = movavg(self._bufsize,navg,dtype=np.complex128) self._m2 = movavg(self._bufsize,navg,dtype=np.complex128) self._m3 = movavg(self._bufsize,navg,dtype=np.complex128) @@ -231,7 +229,7 @@ def getAudio(self,outdata,nf,tinfo,status): if (self.qsy and isinstance(s,DxStation) and s.oper.state != Os.Done and s.called): self.q.put(s.myCall) - if not (self.me.app is None): + if self.me.app is not None: self.me.app.qsy() self.stations.remove(s) elif s.state == StationState.Sending: @@ -271,7 +269,7 @@ def getAudio(self,outdata,nf,tinfo,status): if isinstance(s,DxStation): if (s.oper.state == Os.Done): self.q.put(s.dataToLastQso()) - if not (self.me.app is None): + if self.me.app is not None: self.me.app.lastQso() # (trueCall, trueRst, trueNr) = s.dataToLastQso() # print("contest Correct Info ",trueCall,trueRst,trueNr) diff --git a/python/cwsim.py b/python/cwsim.py index 208b573..52cc18f 100755 --- a/python/cwsim.py +++ b/python/cwsim.py @@ -16,15 +16,10 @@ # See https://www.gnu.org/licenses/ for GPL licensing information. # import os -try: - from PyQt6 import QtCore, QtGui, QtWidgets - from PyQt6.QtWidgets import QApplication, QTableWidgetItem - from PyQt6.QtGui import QShortcut - from PyQt6.uic import compileUi -except ImportError: - from PyQt5 import QtCore, QtGui, QtWidgets - from PyQt5.QtWidgets import QApplication, QTableWidgetItem, QShortcut - from PyQt5.uic import compileUi +from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtWidgets import QApplication, QTableWidgetItem +from PyQt6.QtGui import QShortcut +from PyQt6.uic import compileUi try: uifilename = os.path.join(os.path.dirname(__file__),"cwsimgui.ui") diff --git a/python/keyer.py b/python/keyer.py index 18df9a8..7bb6809 100644 --- a/python/keyer.py +++ b/python/keyer.py @@ -16,8 +16,6 @@ # along with this program. If not, see . import numpy as np import math -import configparser -import os class Keyer(): """ diff --git a/python/mplwidget.py b/python/mplwidget.py index 40b4454..a3bbabe 100644 --- a/python/mplwidget.py +++ b/python/mplwidget.py @@ -14,15 +14,10 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -try: - from PyQt6 import QtWidgets - from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as Canvas -except ImportError: - from PyQt5 import QtWidgets - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as Canvas +from PyQt6 import QtWidgets +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as Canvas from matplotlib.figure import Figure -import matplotlib import numpy as np class MplCanvas(Canvas): diff --git a/python/mystation.py b/python/mystation.py index 1c2d604..d74eed4 100644 --- a/python/mystation.py +++ b/python/mystation.py @@ -71,7 +71,7 @@ def getBuffer(self): self._pieces.pop(0) if len(self._pieces) > 0: self.sendNextPiece() - if not (self.app is None): + if self.app is not None: self.app.advance() return buf diff --git a/python/trlineedit.py b/python/trlineedit.py index 164737b..cd9f26a 100644 --- a/python/trlineedit.py +++ b/python/trlineedit.py @@ -14,10 +14,7 @@ # # See https://www.gnu.org/licenses/ for GPL licensing information. # -try: - from PyQt6 import QtWidgets, QtCore -except ImportError: - from PyQt5 import QtWidgets, QtCore +from PyQt6 import QtWidgets, QtCore class TrLineEdit(QtWidgets.QLineEdit): diff --git a/requirements_qt5.txt b/requirements_qt5.txt deleted file mode 100644 index 2cbe9fa..0000000 --- a/requirements_qt5.txt +++ /dev/null @@ -1,17 +0,0 @@ -cffi>=1.15.1 -cycler>=0.11.0 -fonttools>=4.34.4 -kiwisolver>=1.4.4 -matplotlib>=3.5.2 -numpy>=1.23.1 -packaging>=21.3 -Pillow>=9.2.0 -pycparser>=2.21 -pyparsing>=3.0.9 -PyQt5>=5.15.7 -PyQt5-Qt5>=5.15.2 -PyQt5-sip>=12.11.0 -python-dateutil>=2.8.2 -pyxdg>=0.28 -six>=1.16.0 -sounddevice>=0.4.4 From de3a929861dd80a9356210066646956a440ba95c Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Fri, 3 Apr 2026 13:50:16 +0200 Subject: [PATCH 09/22] modified: cwsim.txt --- cwsim.txt | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index f25bb0e..f0cd2a0 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,22 +1,22 @@ -IQ4PG Report cwsim (DX Expedition) 01/04/2026 20:54:34 -Durata 00:13:18 -Durata (QSO) 23 -Velocità CW 26 WPM -Condizioni Difficoltà +IQ4PG Report cwsim (DX Expedition) 03/04/2026 00:17:21 +Durata 00:15:00 +Durata (QSO) 51 +Velocità CW 36 WPM +Condizioni QSY Difficoltà Attività 14 -Durata 14 -Punti totali 23 -Prefissi totali 21 -Punteggio totale 483 -Punti verificati 19 -Prefissi verificati 18 -Punteggio verificato 342 -Percentuale d'errore 17.4 +Durata 15 +Punti totali 51 +Prefissi totali 49 +Punteggio totale 2499 +Punti verificati 45 +Prefissi verificati 44 +Punteggio verificato 1980 +Percentuale d'errore 11.8 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 48 -5-9 144 -10-14 84 +0-4 168 +5-9 240 +10-14 204 Nominativi copiati male -R30 R30RL JH2JCQ -Progressivi copiati male -K2DS copiato 599 era RST 589 +KE8URW UA3IAJ K5LLW WD4OJW W8EOP DF2KV +Stazioni che hanno abbandonato senza terminare il QSO +WA9THI N3EMZ N4AYV PD3MR ES4RD R7HF KF7HIL From b59682d513874a8daac4493dca7a7a353d2ecd29 Mon Sep 17 00:00:00 2001 From: Gabriele Battaglia Date: Sun, 5 Apr 2026 10:51:53 +0200 Subject: [PATCH 10/22] modified: cwsim.txt --- cwsim.txt | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index f0cd2a0..6c273a2 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,22 +1,21 @@ -IQ4PG Report cwsim (DX Expedition) 03/04/2026 00:17:21 -Durata 00:15:00 -Durata (QSO) 51 -Velocità CW 36 WPM -Condizioni QSY Difficoltà -Attività 14 -Durata 15 -Punti totali 51 -Prefissi totali 49 -Punteggio totale 2499 -Punti verificati 45 -Prefissi verificati 44 -Punteggio verificato 1980 -Percentuale d'errore 11.8 +IZ4APU Report cwsim (Contest) 05/04/2026 10:35:55 +Durata 00:01:31 +Durata (QSO) 1 +Velocità CW 28 WPM +Condizioni Difficoltà +Attività 8 +Durata 30 +Punti totali 1 +Prefissi totali 1 +Punteggio totale 1 +Punti verificati 1 +Prefissi verificati 1 +Punteggio verificato 1 +Percentuale d'errore 0.0 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 168 -5-9 240 -10-14 204 -Nominativi copiati male -KE8URW UA3IAJ K5LLW WD4OJW W8EOP DF2KV -Stazioni che hanno abbandonato senza terminare il QSO -WA9THI N3EMZ N4AYV PD3MR ES4RD R7HF KF7HIL +0-4 12 +5-9 0 +10-14 0 +15-19 0 +20-24 0 +25-29 0 From e325dbca6b02e3a007437a16a70470770adaf668 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 9 Apr 2026 07:50:05 +0200 Subject: [PATCH 11/22] modified: cwsim.txt --- cwsim.txt | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index 6c273a2..85083d4 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,21 +1,24 @@ -IZ4APU Report cwsim (Contest) 05/04/2026 10:35:55 -Durata 00:01:31 -Durata (QSO) 1 +VU2BK Report cwsim (DX Expedition) 08/04/2026 11:51:46 +Durata 00:15:00 +Durata (QSO) 44 Velocità CW 28 WPM -Condizioni Difficoltà -Attività 8 -Durata 30 -Punti totali 1 -Prefissi totali 1 -Punteggio totale 1 -Punti verificati 1 -Prefissi verificati 1 -Punteggio verificato 1 -Percentuale d'errore 0.0 +Condizioni QSY Difficoltà +Attività 14 +Durata 15 +Punti totali 44 +Prefissi totali 42 +Punteggio totale 1848 +Punti verificati 41 +Prefissi verificati 39 +Punteggio verificato 1599 +Percentuale d'errore 6.8 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 12 -5-9 0 -10-14 0 -15-19 0 -20-24 0 -25-29 0 +0-4 156 +5-9 168 +10-14 204 +Progressivi copiati male +SP3MKS copiato 599 era RST 589 +K9AWM copiato 599 era RST 559 +R2ASY copiato 599 era RST 579 +Stazioni che hanno abbandonato senza terminare il QSO +N1XQX YC0UI W9CSA BG8KST K3JSJ From ff5e399baed92817fb078647d586c94047257c83 Mon Sep 17 00:00:00 2001 From: Gabriele Battaglia Date: Thu, 9 Apr 2026 13:52:26 +0200 Subject: [PATCH 12/22] modified: cwsim.txt --- cwsim.txt | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/cwsim.txt b/cwsim.txt index 85083d4..ff1daa6 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -22,3 +22,72 @@ K9AWM copiato 599 era RST 559 R2ASY copiato 599 era RST 579 Stazioni che hanno abbandonato senza terminare il QSO N1XQX YC0UI W9CSA BG8KST K3JSJ + +P55S Report cwsim (Contest) 09/04/2026 10:21:47 +Durata 00:00:24 +Durata (QSO) 1 +Velocità CW 30 WPM +Condizioni QSY Difficoltà +Attività 8 +Durata 15 +Punti totali 1 +Prefissi totali 1 +Punteggio totale 1 +Punti verificati 1 +Prefissi verificati 1 +Punteggio verificato 1 +Percentuale d'errore 0.0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 12 +5-9 0 +10-14 0 + +P55S Report cwsim (DX Expedition) 09/04/2026 10:22:46 +Durata 00:00:39 +Durata (QSO) 1 +Velocità CW 30 WPM +Condizioni QSY Difficoltà +Attività 8 +Durata 15 +Punti totali 1 +Prefissi totali 1 +Punteggio totale 1 +Punti verificati 1 +Prefissi verificati 1 +Punteggio verificato 1 +Percentuale d'errore 0.0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 12 +5-9 0 +10-14 0 +Stazioni che hanno abbandonato senza terminare il QSO +R5FY + +P55S Report cwsim (DX Expedition) 09/04/2026 10:38:19 +Durata 00:15:00 +Durata (QSO) 48 +Velocità CW 32 WPM +Condizioni QSY Difficoltà +Attività 8 +Durata 15 +Punti totali 49 +Prefissi totali 46 +Punteggio totale 2254 +Punti verificati 41 +Prefissi verificati 40 +Punteggio verificato 1640 +Percentuale d'errore 16.3 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 204 +5-9 204 +10-14 180 +Nominativi copiati male +W1/LU9ESN HB9TIS DC5CS +Progressivi copiati male +KI5EE copiato 599 era RST 569 +DJ2MX copiato 599 era RST 589 +LN3C copiato 599 era RST 579 +SN8J copiato 599 era RST 559 +VE3XN copiato 599 era RST 579 +Stazioni che hanno abbandonato senza terminare il QSO +AA1SE JA1BWD K6TQ WB1ADY W6DMR EB7HQE From 22ff7ab4d46be610263edfc55523cfc722b408a2 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 16 Apr 2026 07:37:15 +0200 Subject: [PATCH 13/22] modified: cwsim.txt --- cwsim.txt | 97 ++++++++----------------------------------------------- 1 file changed, 13 insertions(+), 84 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index ff1daa6..613fbf4 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,93 +1,22 @@ -VU2BK Report cwsim (DX Expedition) 08/04/2026 11:51:46 -Durata 00:15:00 -Durata (QSO) 44 -Velocità CW 28 WPM +SV4VV/A Report cwsim (DX Expedition) 13/04/2026 11:53:00 +Durata 00:14:40 +Durata (QSO) 45 +Velocità CW 30 WPM Condizioni QSY Difficoltà Attività 14 Durata 15 -Punti totali 44 -Prefissi totali 42 -Punteggio totale 1848 -Punti verificati 41 -Prefissi verificati 39 -Punteggio verificato 1599 -Percentuale d'errore 6.8 +Punti totali 45 +Prefissi totali 44 +Punteggio totale 1980 +Punti verificati 43 +Prefissi verificati 42 +Punteggio verificato 1806 +Percentuale d'errore 4.4 QSO per ora, suddiviso in intervalli da 5 minuti 0-4 156 -5-9 168 -10-14 204 -Progressivi copiati male -SP3MKS copiato 599 era RST 589 -K9AWM copiato 599 era RST 559 -R2ASY copiato 599 era RST 579 -Stazioni che hanno abbandonato senza terminare il QSO -N1XQX YC0UI W9CSA BG8KST K3JSJ - -P55S Report cwsim (Contest) 09/04/2026 10:21:47 -Durata 00:00:24 -Durata (QSO) 1 -Velocità CW 30 WPM -Condizioni QSY Difficoltà -Attività 8 -Durata 15 -Punti totali 1 -Prefissi totali 1 -Punteggio totale 1 -Punti verificati 1 -Prefissi verificati 1 -Punteggio verificato 1 -Percentuale d'errore 0.0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 12 -5-9 0 -10-14 0 - -P55S Report cwsim (DX Expedition) 09/04/2026 10:22:46 -Durata 00:00:39 -Durata (QSO) 1 -Velocità CW 30 WPM -Condizioni QSY Difficoltà -Attività 8 -Durata 15 -Punti totali 1 -Prefissi totali 1 -Punteggio totale 1 -Punti verificati 1 -Prefissi verificati 1 -Punteggio verificato 1 -Percentuale d'errore 0.0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 12 -5-9 0 -10-14 0 -Stazioni che hanno abbandonato senza terminare il QSO -R5FY - -P55S Report cwsim (DX Expedition) 09/04/2026 10:38:19 -Durata 00:15:00 -Durata (QSO) 48 -Velocità CW 32 WPM -Condizioni QSY Difficoltà -Attività 8 -Durata 15 -Punti totali 49 -Prefissi totali 46 -Punteggio totale 2254 -Punti verificati 41 -Prefissi verificati 40 -Punteggio verificato 1640 -Percentuale d'errore 16.3 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 204 5-9 204 10-14 180 Nominativi copiati male -W1/LU9ESN HB9TIS DC5CS -Progressivi copiati male -KI5EE copiato 599 era RST 569 -DJ2MX copiato 599 era RST 589 -LN3C copiato 599 era RST 579 -SN8J copiato 599 era RST 559 -VE3XN copiato 599 era RST 579 +JN1VFU K2QD Stazioni che hanno abbandonato senza terminare il QSO -AA1SE JA1BWD K6TQ WB1ADY W6DMR EB7HQE +N8HWV VE2CWI From 567eccee1096d809980588c86a11110be1025abc Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 23 Apr 2026 07:55:49 +0200 Subject: [PATCH 14/22] modified: cwsim.txt --- cwsim.txt | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index 613fbf4..6e9a08c 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,22 +1,23 @@ -SV4VV/A Report cwsim (DX Expedition) 13/04/2026 11:53:00 -Durata 00:14:40 -Durata (QSO) 45 +P55L Report cwsim (DX Expedition) 22/04/2026 15:37:21 +Durata 00:15:00 +Durata (QSO) 48 Velocità CW 30 WPM Condizioni QSY Difficoltà Attività 14 Durata 15 -Punti totali 45 -Prefissi totali 44 -Punteggio totale 1980 -Punti verificati 43 -Prefissi verificati 42 -Punteggio verificato 1806 -Percentuale d'errore 4.4 +Punti totali 48 +Prefissi totali 43 +Punteggio totale 2064 +Punti verificati 44 +Prefissi verificati 40 +Punteggio verificato 1760 +Percentuale d'errore 8.3 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 156 -5-9 204 -10-14 180 +0-4 192 +5-9 180 +10-14 204 Nominativi copiati male -JN1VFU K2QD +KL4OD PD7S W6PHQ HB9SS Stazioni che hanno abbandonato senza terminare il QSO -N8HWV VE2CWI +TM6Y ZS9Z KI5DDO Z68EE N4JR W9ILY PC9DB 4A2MAX +SQ6PLD From 5b605fab9c7c7a67142f47d32605cd49c08b2239 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Mon, 4 May 2026 15:01:16 +0200 Subject: [PATCH 15/22] Release 0.9.0 Beta --- python/cwsim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/cwsim.py b/python/cwsim.py index 783ed52..ecef6da 100755 --- a/python/cwsim.py +++ b/python/cwsim.py @@ -55,8 +55,8 @@ def validate(self,string,pos): acceptable, string , pos = super().validate(string,pos) return (acceptable, string.upper(), pos) -VERSION = "1.0.2" -RELEASE_DATE = "venerdì 27 marzo 2026" +VERSION = "0.9.0 Beta" +RELEASE_DATE = "May 4, 2026" class RunApp(QtWidgets.QMainWindow,cwsimgui.Ui_CwsimMainWindow): From e22d7fd324f718cb5624b2ad70c21cd1cb637b48 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Fri, 8 May 2026 13:56:58 +0200 Subject: [PATCH 16/22] modified: cwsim.txt --- cwsim.txt | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index 6e9a08c..12f6cfe 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,23 +1,20 @@ -P55L Report cwsim (DX Expedition) 22/04/2026 15:37:21 -Durata 00:15:00 -Durata (QSO) 48 +P55L Report cwsim (DX Expedition) 08/05/2026 10:30:36 +Durata 00:04:13 +Durata (QSO) 18 Velocità CW 30 WPM Condizioni QSY Difficoltà Attività 14 Durata 15 -Punti totali 48 -Prefissi totali 43 -Punteggio totale 2064 -Punti verificati 44 -Prefissi verificati 40 -Punteggio verificato 1760 -Percentuale d'errore 8.3 +Punti totali 18 +Prefissi totali 18 +Punteggio totale 324 +Punti verificati 17 +Prefissi verificati 17 +Punteggio verificato 289 +Percentuale d'errore 5.6 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 192 -5-9 180 -10-14 204 -Nominativi copiati male -KL4OD PD7S W6PHQ HB9SS -Stazioni che hanno abbandonato senza terminare il QSO -TM6Y ZS9Z KI5DDO Z68EE N4JR W9ILY PC9DB 4A2MAX -SQ6PLD +0-4 216 +5-9 0 +10-14 0 +Progressivi copiati male +VE3EUS copiato 599 era RST 559 From b5b46f6c089b412aa11a739fe7489f1eeab9c1aa Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 14 May 2026 07:48:02 +0200 Subject: [PATCH 17/22] modified: cwsim.txt --- cwsim.txt | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index 12f6cfe..718d1fa 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,20 +1,22 @@ -P55L Report cwsim (DX Expedition) 08/05/2026 10:30:36 -Durata 00:04:13 -Durata (QSO) 18 +P55L Report cwsim (DX Expedition) 14/05/2026 07:45:15 +Durata 00:07:20 +Durata (QSO) 25 Velocità CW 30 WPM Condizioni QSY Difficoltà Attività 14 Durata 15 -Punti totali 18 -Prefissi totali 18 -Punteggio totale 324 -Punti verificati 17 -Prefissi verificati 17 -Punteggio verificato 289 -Percentuale d'errore 5.6 +Punti totali 25 +Prefissi totali 25 +Punteggio totale 625 +Punti verificati 24 +Prefissi verificati 24 +Punteggio verificato 576 +Percentuale d'errore 4.0 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 216 -5-9 0 +0-4 180 +5-9 120 10-14 0 -Progressivi copiati male -VE3EUS copiato 599 era RST 559 +Nominativi copiati male +JS1ZZV +Stazioni che hanno abbandonato senza terminare il QSO +JH1GZV CA2EIH From 8e3270584bff2e1afc8df9751f89b531d8592269 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 21 May 2026 07:57:14 +0200 Subject: [PATCH 18/22] modified: cwsim.txt --- cwsim.txt | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index 718d1fa..b75a40f 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,22 +1,22 @@ -P55L Report cwsim (DX Expedition) 14/05/2026 07:45:15 -Durata 00:07:20 -Durata (QSO) 25 -Velocità CW 30 WPM +P55L Report cwsim (DX Expedition) 18/05/2026 17:46:23 +Durata 00:10:30 +Durata (QSO) 35 +Velocità CW 32 WPM Condizioni QSY Difficoltà Attività 14 Durata 15 -Punti totali 25 -Prefissi totali 25 -Punteggio totale 625 -Punti verificati 24 -Prefissi verificati 24 -Punteggio verificato 576 -Percentuale d'errore 4.0 +Punti totali 37 +Prefissi totali 33 +Punteggio totale 1221 +Punti verificati 30 +Prefissi verificati 28 +Punteggio verificato 840 +Percentuale d'errore 18.9 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 180 -5-9 120 -10-14 0 +0-4 252 +5-9 180 +10-14 12 Nominativi copiati male -JS1ZZV +DL2DCK K9PY/6 R4VAL SU2WYX YD3BFD CQ8Y PY1KNZ Stazioni che hanno abbandonato senza terminare il QSO -JH1GZV CA2EIH +IV3RYX From 1712152d4ba3bd6472912e4a0418b52c6ffc86b5 Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 28 May 2026 07:55:53 +0200 Subject: [PATCH 19/22] modified: cwsim.txt --- cwsim.txt | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index b75a40f..31f22a7 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,22 +1,22 @@ -P55L Report cwsim (DX Expedition) 18/05/2026 17:46:23 -Durata 00:10:30 -Durata (QSO) 35 +P55L Report cwsim (DX Expedition) 27/05/2026 12:47:27 +Durata 00:12:45 +Durata (QSO) 37 Velocità CW 32 WPM Condizioni QSY Difficoltà Attività 14 Durata 15 Punti totali 37 -Prefissi totali 33 -Punteggio totale 1221 -Punti verificati 30 -Prefissi verificati 28 -Punteggio verificato 840 -Percentuale d'errore 18.9 +Prefissi totali 35 +Punteggio totale 1295 +Punti verificati 34 +Prefissi verificati 32 +Punteggio verificato 1088 +Percentuale d'errore 8.1 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 252 +0-4 156 5-9 180 -10-14 12 +10-14 108 Nominativi copiati male -DL2DCK K9PY/6 R4VAL SU2WYX YD3BFD CQ8Y PY1KNZ +DK0PJ F6KKS DL2FDR Stazioni che hanno abbandonato senza terminare il QSO -IV3RYX +UA3RBP DG0AM WB8ASI YD1CAS N6NCW DM4G From 32d98aa633d0e2ebb4b6b86c193e3279b7ca7e34 Mon Sep 17 00:00:00 2001 From: Gabriele Battaglia Date: Thu, 28 May 2026 16:17:43 +0200 Subject: [PATCH 20/22] modified: cwsim.txt --- cwsim.txt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/cwsim.txt b/cwsim.txt index 31f22a7..63f8439 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -20,3 +20,26 @@ Nominativi copiati male DK0PJ F6KKS DL2FDR Stazioni che hanno abbandonato senza terminare il QSO UA3RBP DG0AM WB8ASI YD1CAS N6NCW DM4G + +P55S Report cwsim (DX Expedition) 28/05/2026 12:14:20 +Durata 00:02:51 +Durata (QSO) 9 +Velocità CW 32 WPM +Condizioni QSY Difficoltà +Attività 8 +Durata 15 +Punti totali 10 +Prefissi totali 9 +Punteggio totale 90 +Punti verificati 8 +Prefissi verificati 8 +Punteggio verificato 64 +Percentuale d'errore 20.0 +QSO per ora, suddiviso in intervalli da 5 minuti +0-4 120 +5-9 0 +10-14 0 +Nominativi copiati male +PY4OM AQ6ZEZ +Stazioni che hanno abbandonato senza terminare il QSO +UX8IX SQ6MIZ From 95520ec44022f05d1964c6b9163643b2a7bb522a Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Sat, 6 Jun 2026 06:51:38 +0200 Subject: [PATCH 21/22] modified: cwsim.txt --- cwsim.txt | 53 +++++++++++++++-------------------------------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index 63f8439..9222885 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,45 +1,22 @@ -P55L Report cwsim (DX Expedition) 27/05/2026 12:47:27 -Durata 00:12:45 -Durata (QSO) 37 +P55L Report cwsim (DX Expedition) 04/06/2026 17:34:09 +Durata 00:15:00 +Durata (QSO) 49 Velocità CW 32 WPM Condizioni QSY Difficoltà Attività 14 Durata 15 -Punti totali 37 -Prefissi totali 35 -Punteggio totale 1295 -Punti verificati 34 -Prefissi verificati 32 -Punteggio verificato 1088 -Percentuale d'errore 8.1 +Punti totali 50 +Prefissi totali 46 +Punteggio totale 2300 +Punti verificati 42 +Prefissi verificati 39 +Punteggio verificato 1638 +Percentuale d'errore 16.0 QSO per ora, suddiviso in intervalli da 5 minuti -0-4 156 -5-9 180 -10-14 108 +0-4 216 +5-9 264 +10-14 120 Nominativi copiati male -DK0PJ F6KKS DL2FDR +DL5ZD N5US G9N JH4OYB W4SI PU2PND PD0KS BD8AGF Stazioni che hanno abbandonato senza terminare il QSO -UA3RBP DG0AM WB8ASI YD1CAS N6NCW DM4G - -P55S Report cwsim (DX Expedition) 28/05/2026 12:14:20 -Durata 00:02:51 -Durata (QSO) 9 -Velocità CW 32 WPM -Condizioni QSY Difficoltà -Attività 8 -Durata 15 -Punti totali 10 -Prefissi totali 9 -Punteggio totale 90 -Punti verificati 8 -Prefissi verificati 8 -Punteggio verificato 64 -Percentuale d'errore 20.0 -QSO per ora, suddiviso in intervalli da 5 minuti -0-4 120 -5-9 0 -10-14 0 -Nominativi copiati male -PY4OM AQ6ZEZ -Stazioni che hanno abbandonato senza terminare il QSO -UX8IX SQ6MIZ +S53SL SN8V YB1HDF TM23RUGB DG3BZ WA5SNL From cf9eb09555c24f5208b47c8994c306eb874b3daf Mon Sep 17 00:00:00 2001 From: GabrieleBattaglia Date: Thu, 25 Jun 2026 07:45:53 +0200 Subject: [PATCH 22/22] modified: cwsim.txt --- cwsim.txt | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/cwsim.txt b/cwsim.txt index 9222885..c42cc70 100644 --- a/cwsim.txt +++ b/cwsim.txt @@ -1,22 +1,22 @@ -P55L Report cwsim (DX Expedition) 04/06/2026 17:34:09 -Durata 00:15:00 -Durata (QSO) 49 +P55L Report cwsim (DX Expedition) 24/06/2026 20:04:46 +Durata 00:09:41 +Durata (QSO) 36 Velocità CW 32 WPM Condizioni QSY Difficoltà Attività 14 Durata 15 -Punti totali 50 -Prefissi totali 46 -Punteggio totale 2300 -Punti verificati 42 -Prefissi verificati 39 -Punteggio verificato 1638 -Percentuale d'errore 16.0 +Punti totali 36 +Prefissi totali 34 +Punteggio totale 1224 +Punti verificati 34 +Prefissi verificati 32 +Punteggio verificato 1088 +Percentuale d'errore 5.6 QSO per ora, suddiviso in intervalli da 5 minuti 0-4 216 -5-9 264 -10-14 120 +5-9 216 +10-14 0 Nominativi copiati male -DL5ZD N5US G9N JH4OYB W4SI PU2PND PD0KS BD8AGF +N1QEY PD9RW Stazioni che hanno abbandonato senza terminare il QSO -S53SL SN8V YB1HDF TM23RUGB DG3BZ WA5SNL +SE6N W3PRL