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/cwsim.txt b/cwsim.txt new file mode 100644 index 0000000..c42cc70 --- /dev/null +++ b/cwsim.txt @@ -0,0 +1,22 @@ +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 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 216 +10-14 0 +Nominativi copiati male +N1QEY PD9RW +Stazioni che hanno abbandonato senza terminare il QSO +SE6N W3PRL 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. 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..44f0d23 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 @@ -86,6 +84,11 @@ def __init__(self,rng,inifile=None): self.savewave = 0 self.saveini = 1 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) @@ -98,7 +101,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) @@ -137,7 +140,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 +234,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 +274,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) @@ -283,10 +286,13 @@ 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, - isSingle=True,bufsize=self._bufsize,rate=self._rate) + straightKeyProb=self.straightKeyProb, + 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) - + # np.savetxt(self.ef,audio) # self.wf.writeframesraw((audio*30000).astype(np.int16)) @@ -328,7 +334,10 @@ 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)) + straightKeyProb=self.straightKeyProb, + 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) @@ -401,16 +410,21 @@ 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']) 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']) 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: @@ -426,10 +440,10 @@ 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' - ,'lidRstProb','lidNrProb','rptProb','flutterProb']: + for i in ['qrn','qrm','tqrm','qsb','flutter','qsy','lids','straightKeyProb','activity' + ,'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']: + 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 208b573..ecef6da 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") @@ -60,6 +55,9 @@ def validate(self,string,pos): acceptable, string , pos = super().validate(string,pos) return (acceptable, string.upper(), pos) +VERSION = "0.9.0 Beta" +RELEASE_DATE = "May 4, 2026" + class RunApp(QtWidgets.QMainWindow,cwsimgui.Ui_CwsimMainWindow): advancesig = QtCore.pyqtSignal() @@ -136,6 +134,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) @@ -171,8 +170,12 @@ 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.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) @@ -184,6 +187,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, @@ -224,6 +228,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( @@ -253,10 +260,17 @@ 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.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) self.slowSpinBox.setValue(self.contest.slow) + 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) @@ -311,8 +325,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 @@ -392,7 +407,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": @@ -592,14 +608,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): @@ -683,6 +700,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() @@ -732,6 +752,20 @@ def lidRstProb(self,s): def lidNrProb(self,s): self.contest.lidNrProb = 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 @@ -777,13 +811,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) @@ -812,6 +848,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) @@ -819,20 +925,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() @@ -865,12 +981,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() @@ -943,23 +1058,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 self._rst == "": + self._rst = "599" + if dx_exp: + if not self._hiscall: + return + else: + 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), 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 @@ -987,8 +1110,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 @@ -1042,6 +1169,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 @@ -1103,41 +1233,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) @@ -1161,6 +1256,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 9ecb078..4bd5f45 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,6 +810,92 @@ + + + + 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 + + + @@ -1303,7 +1389,7 @@ Qt::NoFocus - + @@ -1440,7 +1526,7 @@ - + Qt::TabFocusChoose operating modeOperating ModeContestDX Expedition @@ -1538,11 +1624,11 @@ false - - - - - + + + + + @@ -1621,15 +1707,15 @@ &File - - - - - - - - - + + + + + + + + + @@ -1638,14 +1724,14 @@ &Help - - - + + + - - + + - + toolBar @@ -1779,7 +1865,7 @@ qsyCheck qrmCheck lidsCheck - lidRstProbSpinBox + straightKeyProbSpinBoxlidRstProbSpinBox rptProbSpinBox fastSpinBox qsbCheck @@ -1789,7 +1875,7 @@ flutterProbSpinBox slowSpinBox contestComboBox - startStopButton + typeComboBoxstartStopButton durationComboBox durationSpinBox activitySpinBox @@ -1800,7 +1886,7 @@ trCallEntry trExchangeEntry - + action_Exit @@ -1851,4 +1937,4 @@ - + \ No newline at end of file diff --git a/python/dxoper.py b/python/dxoper.py index a2cef22..5450f00 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,12 +70,14 @@ 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 self._fast = fast self._rptProb = rptProb self._s2bfac = s2bfac + self.rstSent = False def getSendDelay(self): """ @@ -152,7 +155,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]]) @@ -208,12 +211,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: @@ -258,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: @@ -281,11 +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 b9c2e8c..57e1c30 100644 --- a/python/dxstation.py +++ b/python/dxstation.py @@ -27,8 +27,23 @@ 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, - isSingle=False,bufsize=512,rate=11025): - super().__init__(rng,keyer,bufsize=bufsize,rate=rate) + straightKeyProb=0.25, + 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) + 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() @@ -37,7 +52,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, @@ -45,11 +59,14 @@ 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) 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 18df9a8..d7ca91b 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(): """ @@ -34,7 +32,7 @@ class Keyer(): "?":"..--..", "/":"-..-.", ";":"-.-.-.", "(":"-.--.", "[":"-.--.", ")":"-.--.-", "]":"-.--.-", "@":".--.-.", "*":"...-.-", "+":".-.-.", "%":".-...", ":":"---...", "=":"-...-", '"':".-..-.", "'":".----.", - "!":"---.", "$":"...-..-"," ":"", "_":"" + "!":"---.", "$":"...-..-"," ":"", "_":"", ">":"", "<":"" }) def __init__(self,rate=11025,bufsize=512,risetime=0.005): @@ -72,42 +70,95 @@ 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): + def getenvelop(self,msg,wpm,l=30,s=50,p=50,speed_up_factor=0.20): """ 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) + speed_up_factor: how much to increase speed (0 to 1) 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)) + + 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 + 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 + 100)/self._bufsize)) env = np.zeros(n,dtype=np.float32) - dit = np.ones(nr+samples,dtype=np.float32) - dit[:nr] = self.rise - dit[samples:] = self.fall - dah = np.ones(nr+3*samples,dtype=np.float32) - dah[:nr] = self.rise - dah[3*samples:] = self.fall + k = 0 - for i in range(len(msg)): - if msg[i] == '.': - env[k:k+len(dit)] = dit - k += 2*samples - elif msg[i] == '-': - env[k:k+len(dah)] = dah - k += 4*samples - elif msg[i] == ' ': - k += 2*samples-nr - elif msg[i] == '~': - k += samples-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/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..c4f8931 100644 --- a/python/mystation.py +++ b/python/mystation.py @@ -19,14 +19,15 @@ 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 self._rst = 599 self.pitch = pitch self.wpm = wpm + self.speedUpRst = isDxExpedition self._amplitude = 1.0 self._pieces = [] self.app = None @@ -71,7 +72,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 @@ -84,7 +85,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..9b7e4b3 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 @@ -110,8 +110,14 @@ 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.isDxExpedition = isDxExpedition + self.speedUpRst = False + self.myRstSpeedUp = 0.20 self.nrWithError = False self.myCall = '' self.hisCall = '' @@ -154,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._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 @@ -165,7 +171,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: @@ -178,8 +188,13 @@ 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.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: s = '{:d}{:03d}{:s}{:03d}'.format( 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 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 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)