From 8047517d014c3fc1b26b828abdcab8e04f8c29b3 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Mon, 13 Feb 2023 08:20:22 -0800 Subject: [PATCH 01/23] WIP first commit - CID working well enough to raise a NED - but does not raise changeCallsignDialog --- radiolog.py | 66 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/radiolog.py b/radiolog.py index fa82add..3aa14b7 100644 --- a/radiolog.py +++ b/radiolog.py @@ -1743,11 +1743,28 @@ def fsCheck(self): else: if isWaiting: rprint(" DATA IS WAITING!!!") + valid=False tmpData=comPortTry.read(comPortTry.inWaiting()).decode("utf-8") if '\x02I' in tmpData or tmpData=='\x020\x03' or tmpData=='\x021\x03' or tmpData.startswith('\x02$PKL'): rprint(" VALID FLEETSYNC DATA!!!") + valid=True + elif '\x02gI' in tmpData: + rprint(' VALID NEXEDGE NXDN DATA!!!') + valid=True + # NEXEDGE format (e.g. for ID 03001; NXDN has no concept of fleet:device - just 5-decimal-digit unit ID, max=65536 (4 hex characters)) + # BOT CID: ☻gI1U03001U03001♥ + # EOT CID: ☻gI0U03001U03001♥ + # ☻gI - preamble (\x02gI) + # BOT/EOT - BOT=1, EOT=0 + # U##### - U followed by unit ID (5 decimal digits) + # repeat U##### + # ♥ - postamble (\x03) + # GPS: same as with fleetsync, but PKNSH instead of PKLSH; arrives immediately after BOT CID rather than EOT CID + else: + rprint(" but not valid fleetsync data. Scan continues...") + rprint(str(tmpData)) + if valid: self.fsBuffer=self.fsBuffer+tmpData - if not self.firstComPortFound: self.firstComPort=comPortTry # pass the actual open com port object, to keep it open self.firstComPortFound=True @@ -1757,9 +1774,6 @@ def fsCheck(self): self.secondComPortFound=True self.fsLatestComPort=self.secondComPort self.comPortTryList.remove(comPortTry) # and remove the good com port from the list of ports to try going forward - else: - rprint(" but not valid fleetsync data. Scan continues...") - rprint(str(tmpData)) else: if comLog: rprint(" no data") @@ -1863,6 +1877,7 @@ def fsParse(self): sec=time.time() fleet=None dev=None + uid=None # the line delimeters are literal backslash then n, rather than standard \n for line in self.fsBuffer.split('\n'): rprint(" line:"+line) @@ -2102,7 +2117,23 @@ def fsParse(self): self.sendPendingGet() # while getString will be non-empty if this bump had GPS, it may still have the default callsign return - rprint("CID detected (not in $PKLSH): fleet="+fleet+" dev="+dev+" callsign="+callsign) + rprint("FleetSync CID detected (not in $PKLSH): fleet="+fleet+" dev="+dev+" callsign="+callsign) + + elif '\x02gI' in line: # NEXEDGE CID - similar to above + packetSet=set(re.findall(r'\x02gI[0-1]U([0-9]{5})U\1\x03',line)) + if len(packetSet)>1: + rprint('NEXEDGE(NXDN) ERROR: data appears garbled; there are two complete but non-identical CID packets. Skipping this message.') + return + if len(packetSet)==0: + rprint('NEXEDGE(NXDN) ERROR: data appears garbled; no complete CID packets were found in the incoming data. Skipping this message.') + return + packet=packetSet.pop() + count=line.count(packet) + uid=packet[0:5] + callsign=self.getCallsign(uid) + + rprint('NEXEDGE CID detected (not in $PKNSH): id='+uid+' callsign='+callsign) + # if any new entry dialogs are already open with 'from' and the # current callsign, and that entry has been edited within the 'continue' time, # update it with the current location if available; @@ -2156,6 +2187,10 @@ def fsParse(self): else: self.openNewEntry('fs',callsign,formattedLocString,fleet,dev,origLocString) self.sendPendingGet() + elif uid: + if not found: + self.openNewEntry('nex',callsign,formattedLocString,uid,None,origLocString) + self.sendPendingGet() def sendPendingGet(self,suffix=""): # NOTE that requests.get can cause a blocking delay; so, do it AFTER spawning the newEntryDialog @@ -2428,12 +2463,21 @@ def fsSaveLookup(self): warn.raise_() warn.exec_() - def getCallsign(self,fleet,dev): - entry=[element for element in self.fsLookup if (str(element[0])==str(fleet) and str(element[1])==str(dev))] - if len(entry)!=1 or len(entry[0])!=3: # no match - return "KW-"+str(fleet)+"-"+str(dev) + def getCallsign(self,fleetOrUid,dev=None,treatAsFleet=100): + if len(fleetOrUid)>4: # 5 characters - must be NEXEDGE + uid=fleetOrUid + entry=[element for element in self.fsLookup if (str(element[0])==str(treatAsFleet) and str(element[1])==str(uid[:4]))] + if len(entry)!=1 or len(entry[0])!=3: # no match + return "KW-NXDN-"+str(uid) + else: + return entry[0][2] else: - return entry[0][2] + fleet=fleetOrUid + entry=[element for element in self.fsLookup if (str(element[0])==str(fleet) and str(element[1])==str(dev))] + if len(entry)!=1 or len(entry[0])!=3: # no match + return "KW-"+str(fleet)+"-"+str(dev) + else: + return entry[0][2] def getFleetDev(self,callsign): entry=[element for element in self.fsLookup if (element[2]==callsign)] @@ -4097,6 +4141,8 @@ def openNewEntry(self,key=None,callsign=None,formattedLocString=None,fleet=None, self.newEntryWindow.ui.tabWidget.setCurrentIndex(1) self.newEntryWidget.ui.to_fromField.setCurrentIndex(0) self.newEntryWidget.ui.messageField.setFocus() + elif key=='nex': # spawned by NEXEDGE (Kenwood NXDN); let addTab determine focus + pass else: # some other keyboard key - assume it's the start of the team name self.newEntryWidget.ui.to_fromField.setCurrentIndex(0) # need to 'burp' the focus to prevent two blinking cursors From bdf619ac7dfa73fbfd30fd29c7273ef67a13379f Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Wed, 15 Feb 2023 07:18:24 -0800 Subject: [PATCH 02/23] WIP - add notes for sendText and pollGPS outgoing data formats, determined from radtext and realterm - allow string argument for getCallsign --- radiolog.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/radiolog.py b/radiolog.py index 3aa14b7..a33517c 100644 --- a/radiolog.py +++ b/radiolog.py @@ -2464,11 +2464,11 @@ def fsSaveLookup(self): warn.exec_() def getCallsign(self,fleetOrUid,dev=None,treatAsFleet=100): - if len(fleetOrUid)>4: # 5 characters - must be NEXEDGE + if isinstance(fleetOrUid,str) and len(fleetOrUid)>4: # 5 characters - must be NEXEDGE uid=fleetOrUid - entry=[element for element in self.fsLookup if (str(element[0])==str(treatAsFleet) and str(element[1])==str(uid[:4]))] + entry=[element for element in self.fsLookup if (str(element[0])==str(treatAsFleet) and str(element[1])==uid[:4])] if len(entry)!=1 or len(entry[0])!=3: # no match - return "KW-NXDN-"+str(uid) + return "KW-NXDN-"+uid else: return entry[0][2] else: @@ -4907,12 +4907,14 @@ def tabContextMenu(self,pos): # # # - 02 hex (ascii smiley face) + # for NEXEDGE, insert 67 hex (lowecase g) after 02 # - indicates max possible message length, though the plain text message is not padded to that length # 46 hex (ascii F) - corresponds to 'S' (Short - 48 characters) # 47 hex (ascii G) - corresponds to both 'L' (Long - 1024 characters) and 'X' (Extra-long - 4096 characters) # if you send COM port data with message body longer than that limit, the mobile will not transmit - # - plain-text three-digit fleet ID (000 for broadcast) - # - plain-text four-digit device ID (0000 for broadcast) + # + # FleetSync: - plain-text three-digit fleet ID and four-digit device ID (0000000 for broadcast) + # NEXEDGE: U - U followed by five-digit unit ID # - plain-text message # UNUSED: - plain-text two-digit decimal sequence identifier - increments with each send - probably not relevant # NOTE: sequnce is generated by radtext, but, it shows up as part of the message body on the @@ -4921,8 +4923,12 @@ def tabContextMenu(self,pos): # - 03 hex (ascii heart) # # examples: - # broadcast 'test' (short): 02 46 30 30 30 30 30 30 30 74 65 73 74 32 39 03 F0000000test28 (sequence=28) - # 100:1002 'test' (short): 02 46 31 30 30 31 30 30 32 74 65 73 74 33 31 03 F1001002test31 (sequence=31) + # FleetSync: + # broadcast 'test' (short): 02 46 30 30 30 30 30 30 30 74 65 73 74 32 39 03 F0000000test28 (sequence=28) + # 100:1002 'test' (short): 02 46 31 30 30 31 30 30 32 74 65 73 74 33 31 03 F1001002test31 (sequence=31) + # NEXEDGE: + # 03001 'test' (short): 02 67 46 55 30 33 30 30 31 74 65 73 74 31 31 03 gFU03001test10 (sequence=10) + def sendText(self,fleetOrListOrAll,device=None,message=None): self.fsTimedOut=False @@ -5059,15 +5065,23 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): # # # - 02 hex (ascii smiley face) - # - 52 33 hex (ascii R3) - # - plain-text three-digit fleet ID (000 for broadcast) - # - plain-text four-digit device ID (0000 for broadcast) + # for NEXEDGE, insert 67 hex (lowecase g) after 02 + # + # 52 33 hex (ascii R3) + # + # FleetSync: - plain-text three-digit fleet ID and four-digit device ID (0000000 for broadcast) + # NEXEDGE: U - U followed by five-digit unit ID # UNUSED - see sendText notes - - plain-text two-digit decimal sequence identifier - increments with each send - probably not relevant # - 03 hex (ascii heart) # examples: - # poll 100:1001: 02 52 33 31 30 30 31 30 30 31 32 35 03 R3100100120 (sequence=20) - # poll 100:1002: 02 52 33 31 30 30 31 30 30 32 32 37 03 R3100100221 (sequence=21) + # FleetSync: + # poll 100:1001: 02 52 33 31 30 30 31 30 30 31 32 35 03 R3100100120 (sequence=20) + # poll 100:1002: 02 52 33 31 30 30 31 30 30 32 32 37 03 R3100100221 (sequence=21) + # NEXEDGE: + # poll 03001: 02 67 52 33 55 30 33 30 30 31 30 36 03 gR3U0300108 (sequence=08) + + def pollGPS(self,fleet,device): if self.fsShowChannelWarning: From 0ba600a34866d6147a593c7aed24f04e0ca04c17 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 17 Feb 2023 14:49:32 -0800 Subject: [PATCH 03/23] WIP - fsLookup flow is working enough to save and recognize callsigns from NEXEDGE ID - start work on enforcing strings (not integers) throughout for fleet, id, and uid --- designer/changeCallsignDialog.ui | 22 ++--- radiolog.py | 149 ++++++++++++++++++++----------- ui/changeCallsignDialog_ui.py | 58 ++++++------ 3 files changed, 138 insertions(+), 91 deletions(-) diff --git a/designer/changeCallsignDialog.ui b/designer/changeCallsignDialog.ui index 401d221..f030348 100644 --- a/designer/changeCallsignDialog.ui +++ b/designer/changeCallsignDialog.ui @@ -40,7 +40,7 @@ QDialogButtonBox::Cancel|QDialogButtonBox::Ok - + false @@ -48,7 +48,7 @@ 210 20 - 111 + 141 41 @@ -62,15 +62,15 @@ true - + false - 390 + 380 20 - 131 + 141 41 @@ -128,12 +128,12 @@ false - + 220 60 - 55 + 101 21 @@ -147,12 +147,12 @@ Fleet - + - 400 + 390 60 - 61 + 111 21 @@ -210,7 +210,7 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + 10 diff --git a/radiolog.py b/radiolog.py index a33517c..b0ce976 100644 --- a/radiolog.py +++ b/radiolog.py @@ -2189,7 +2189,7 @@ def fsParse(self): self.sendPendingGet() elif uid: if not found: - self.openNewEntry('nex',callsign,formattedLocString,uid,None,origLocString) + self.openNewEntry('nex',callsign,formattedLocString,None,uid,origLocString) self.sendPendingGet() def sendPendingGet(self,suffix=""): @@ -2224,26 +2224,34 @@ def sendPendingGet(self,suffix=""): # if callsign is specified, update the callsign but not the time; # if callsign is not specified, udpate the time but not the callsign; # if the entry does not yet exist, add it - def fsLogUpdate(self,fleet,dev,callsign=False,bump=False): + def fsLogUpdate(self,fleet=None,dev=None,uid=None,callsign=False,bump=False): # row structure: [fleet,dev,callsign,filtered,last_received,com port] # don't process the dummy default entry if callsign=='Default': return - try: - fleet=int(fleet) - dev=int(dev) - except: - rprint('ERROR in call to fsLogUpdate: fleet and dev must both be integers or integer-strings: fleet='+str(fleet)+' dev='+str(dev)) + if not (fleet and dev) or uid: + rprint('ERROR in call to fsLogUpdate: either fleet and dev must be specified, or uid must be specified.') return + # # this clause is dead code now, since enforcing that fleet and dev are always strings throughout the code + # if fleet and dev: # fleetsync, not nexedge + # try: + # fleet=int(fleet) + # dev=int(dev) + # except: + # rprint('ERROR in call to fsLogUpdate: fleet and dev must both be integers or integer-strings: fleet='+str(fleet)+' dev='+str(dev)) + # return com='' if self.fsLatestComPort: com=str(self.fsLatestComPort.name) if com!='': # suppress printing on initial log population during fsLoadLookup - rprint("updating fsLog: fleet="+str(fleet)+" dev="+str(dev)+" callsign="+(callsign or "")+" COM port="+com) + if fleet and dev: + rprint("updating fsLog (fleetsync): fleet="+fleet+" dev="+dev+" callsign="+(callsign or "")+" COM port="+com) + elif uid: + rprint("updating fsLog (nexedge): user id = "+uid+" callsign="+(callsign or "")+" COM port="+com) found=False t=time.strftime("%a %H:%M:%S") for row in self.fsLog: - if row[0]==fleet and row[1]==dev: + if (row[0]==fleet and row[1]==dev) or (row[0]=='' and row[1]==uid): found=True if callsign: row[2]=callsign @@ -2254,7 +2262,10 @@ def fsLogUpdate(self,fleet,dev,callsign=False,bump=False): row[6]+=1 if not found: # always update callsign - it may have changed since creation - self.fsLog.append([fleet,dev,self.getCallsign(fleet,dev),False,t,com,int(bump)]) + if fleet and dev: # fleetsync + self.fsLog.append([fleet,dev,self.getCallsign(fleet,dev),False,t,com,int(bump)]) + elif uid: # nexedge + self.fsLog.append(['',uid,self.getCallsign(uid),False,t,com,int(bump)]) # rprint(self.fsLog) # if self.fsFilterDialog.ui.tableView: self.fsFilterDialog.ui.tableView.model().layoutChanged.emit() @@ -2340,7 +2351,7 @@ def fsLoadLookup(self,startupFlag=False,fsFileName=None,hideWarnings=False): self.fsLookup=[] csvReader=csv.reader(fsFile) usedCallsignList=[] # keep track of used callsigns to check for duplicates afterwards - usedFleetDevPairList=[] # keep track of used fleet-dev pairs to check for duplicates afterwards + usedIDPairList=[] # keep track of used fleet-dev pairs (or blank-and-unit-ID pairs for NEXEDGE) to check for duplicates afterwards duplicateCallsignsAllowed=False for row in csvReader: # rprint('row:'+str(row)) @@ -2381,13 +2392,13 @@ def fsLoadLookup(self,startupFlag=False,fsFileName=None,hideWarnings=False): self.fsLookup.append(row) # self.fsLogUpdate(row[0],row[1],row[2]) usedCallsignList.append(cs) - usedFleetDevPairList.append(str(row[0])+':'+str(row[1])) + usedIDPairList.append(str(row[0])+':'+str(row[1])) else: # rprint(' adding row: '+str(row)) self.fsLookup.append(row) # self.fsLogUpdate(row[0],row[1],row[2]) usedCallsignList.append(row[2]) - usedFleetDevPairList.append(str(row[0])+':'+str(row[1])) + usedIDPairList.append(str(row[0])+':'+str(row[1])) # rprint('reading done') if not duplicateCallsignsAllowed and len(set(usedCallsignList))!=len(usedCallsignList): seen=set() @@ -2403,14 +2414,14 @@ def fsLoadLookup(self,startupFlag=False,fsFileName=None,hideWarnings=False): self.fsMsgBox.show() self.fsMsgBox.raise_() self.fsMsgBox.exec_() # modal - if len(set(usedFleetDevPairList))!=len(usedFleetDevPairList): + if len(set(usedIDPairList))!=len(usedIDPairList): seen=set() - duplicatedFleetDevPairs=[x for x in usedFleetDevPairList if x in seen or seen.add(x)] - n=len(duplicatedFleetDevPairs) + duplicatedIDPairs=[x for x in usedIDPairList if x in seen or seen.add(x)] + n=len(duplicatedIDPairs) if n>3: - duplicatedFleetDevPairs=duplicatedFleetDevPairs[0:3]+['(and '+str(n-3)+' more)'] - msg='Fleet-and-device pairs are repeated in the FleetSync lookup file\n\n'+fsFullPath+'\n\nFor this session, the last definition will be used, overwriting definitions that appear earlier in the file. Please correct the file soon.\n\n' - msg+='Repeated pairs:\n\n'+str(duplicatedFleetDevPairs).replace('[','').replace(']','').replace("'","").replace(', (',' (') + duplicatedIDPairs=duplicatedIDPairs[0:3]+['(and '+str(n-3)+' more)'] + msg='Device ID pairs are repeated in the FleetSync lookup file\n\n'+fsFullPath+'\n\nFor this session, the last definition will be used, overwriting definitions that appear earlier in the file. Please correct the file soon.\n\n' + msg+='Repeated pairs:\n\n'+str(duplicatedIDPairs).replace('[','').replace(']','').replace("'","").replace(', (',' (') rprint('FleetSync Table Warning:'+msg) self.fsMsgBox=QMessageBox(QMessageBox.Warning,"FleetSync Table Warning",msg, QMessageBox.Close,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) @@ -2463,28 +2474,37 @@ def fsSaveLookup(self): warn.raise_() warn.exec_() - def getCallsign(self,fleetOrUid,dev=None,treatAsFleet=100): - if isinstance(fleetOrUid,str) and len(fleetOrUid)>4: # 5 characters - must be NEXEDGE + def getCallsign(self,fleetOrUid,dev=None): + if not isinstance(fleetOrUid,str): + rprint('ERROR in call to getCallsign: fleetOrId is not a string.') + return + if dev and not isinstance(dev,str): + rprint('ERROR in call to getCallsign: dev is not a string.') + return + if len(fleetOrUid)==3: # 3 characters - must be fleetsync + fleet=fleetOrUid + entries=[element for element in self.fsLookup if (element[0]==fleet and element[1]==dev)] + if len(entries)!=1 or len(entries[0][0])!=3: # no match + return "KW-"+fleet+"-"+dev + else: + return entries[0][2] + elif len(fleetOrUid)==5: # 5 characters - must be NEXEDGE uid=fleetOrUid - entry=[element for element in self.fsLookup if (str(element[0])==str(treatAsFleet) and str(element[1])==uid[:4])] - if len(entry)!=1 or len(entry[0])!=3: # no match + entries=[element for element in self.fsLookup if element[1]==uid] + if len(entries)!=1 or entries[0][0]!='': # no match return "KW-NXDN-"+uid else: - return entry[0][2] + return entries[0][2] else: - fleet=fleetOrUid - entry=[element for element in self.fsLookup if (str(element[0])==str(fleet) and str(element[1])==str(dev))] - if len(entry)!=1 or len(entry[0])!=3: # no match - return "KW-"+str(fleet)+"-"+str(dev) - else: - return entry[0][2] + rprint('ERROR in call to getCallsign: first argument must be 3 characters (FleetSync) or 5 characters (NEXEDGE): "'+fleetOrUid+'"') - def getFleetDev(self,callsign): - entry=[element for element in self.fsLookup if (element[2]==callsign)] - if len(entry)!=1: # no match - return False - else: - return [entry[0][0],entry[0][1]] + # not called from anywhere + # def getIdFromCallsign(self,callsign): + # entry=[element for element in self.fsLookup if (element[2]==callsign)] + # if len(entry)!=1: # no match + # return False + # else: + # return [entry[0][0],entry[0][1]] # for nexEdge, the first value def testConvertCoords(self): coordsTestList=[ @@ -6135,8 +6155,10 @@ def openChangeCallsignDialog(self): # problem: changeCallsignDialog does not stay on top of newEntryWindow! # only open the dialog if the newEntryWidget was created from an incoming fleetSync ID # (it has no meaning for hotkey-opened newEntryWidgets) + rprint('openChangeCallsignDialog: self.fleet='+str(self.fleet)+' self.dev='+str(self.dev)) self.needsChangeCallsign=False - if self.fleet: + # for fleetsync, fleet and dev will both have values; for nexedge, fleet will be None but dev will have a value + if self.fleet or self.dev: try: #482: wrap in try/except, since a quick 'esc' will close the NED, deleting teamField, # before this code executes since it is called from a singleshot timer @@ -7645,7 +7667,7 @@ def accept(self): class changeCallsignDialog(QDialog,Ui_changeCallsignDialog): openDialogCount=0 - def __init__(self,parent,callsign,fleet,device): + def __init__(self,parent,callsign,fleet=None,device=None): QDialog.__init__(self) self.ui=Ui_changeCallsignDialog() self.ui.setupUi(self) @@ -7654,13 +7676,20 @@ def __init__(self,parent,callsign,fleet,device): self.setAttribute(Qt.WA_DeleteOnClose) self.parent=parent self.currentCallsign=callsign - self.fleet=int(fleet) - self.device=int(device) + self.fleet=fleet + self.device=device - rprint("openChangeCallsignDialog called. fleet="+str(self.fleet)+" dev="+str(self.device)) - - self.ui.fleetField.setText(fleet) - self.ui.deviceField.setText(device) + rprint("changeCallsignDialog created. fleet="+str(self.fleet)+" dev="+str(self.device)) + + if fleet: # fleetsync + self.ui.idField1.setText(str(fleet)) + self.ui.idField2.setText(str(device)) + else: # nexedge + self.ui.idLabel.setText('NEXEDGE ID') + self.ui.idLabel1.setText('Unit ID') + self.ui.idField1.setText(str(device)) + self.ui.idLabel2.setVisible(False) + self.ui.idField2.setVisible(False) self.ui.currentCallsignField.setText(callsign) self.ui.newCallsignField.setFocus() self.ui.newCallsignField.setText("Team ") @@ -7681,7 +7710,7 @@ def __init__(self,parent,callsign,fleet,device): self.ui.newCallsignField.setCompleter(self.completer) def fsFilterConfirm(self): - really=QMessageBox(QMessageBox.Warning,"Please Confirm","Filter (ignore) future incoming messages\n from this FleetSync device?", + really=QMessageBox(QMessageBox.Warning,"Please Confirm","Filter (ignore) future incoming messages\n from this device?", QMessageBox.Yes|QMessageBox.No,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) if really.exec_()==QMessageBox.No: self.close() @@ -7694,24 +7723,43 @@ def fsFilterConfirm(self): def accept(self): found=False - fleet=self.ui.fleetField.text() - dev=self.ui.deviceField.text() + id1=self.ui.idField1.text() + id2=self.ui.idField2.text() + fleet='' + dev='' + uid='' + if id2: # fleetsync + fleet=id1 + dev=id2 + rprint('accept: FleetSync fleet='+str(fleet)+' dev='+str(dev)) + else: + uid=id1 + rprint('accept: NEXEDGE uid='+str(uid)) # fix #459 (and other places in the code): remove all leading and trailing spaces, and change all chains of spaces to one space newCallsign=re.sub(r' +',r' ',self.ui.newCallsignField.text()).strip() # change existing device entry if found, otherwise add a new entry for n in range(len(self.parent.parent.fsLookup)): entry=self.parent.parent.fsLookup[n] - if entry[0]==fleet and entry[1]==dev: + if (entry[0]==fleet and entry[1]==dev) or (entry[1]==uid): found=True self.parent.parent.fsLookup[n][2]=newCallsign if not found: - self.parent.parent.fsLookup.append([fleet,dev,newCallsign]) + if fleet and dev: # fleetsync + self.parent.parent.fsLookup.append([fleet,dev,newCallsign]) + else: # nexedge + self.parent.parent.fsLookup.append(['',uid,newCallsign]) + rprint('fsLookup after CCD:'+str(self.parent.parent.fsLookup)) # set the current radio log entry teamField also self.parent.ui.teamField.setText(newCallsign) # save the updated table (filename is set at the same times that csvFilename is set) self.parent.parent.fsSaveLookup() # change the callsign in fsLog - self.parent.parent.fsLogUpdate(int(fleet),int(dev),newCallsign) + if id2: # fleetsync + self.parent.parent.fsLogUpdate(fleet,dev,newCallsign) + rprint("New callsign pairing created from FleetSync: fleet="+fleet+" dev="+dev+" callsign="+newCallsign) + else: # nexedge + self.parent.parent.fsLogUpdate(None,uid,newCallsign) + rprint("New callsign pairing created from NEXEDGE: unit ID = "+uid+" callsign="+newCallsign) # finally, pass the 'accept' signal on up the tree as usual changeCallsignDialog.openDialogCount-=1 self.parent.parent.sendPendingGet(newCallsign) @@ -7719,7 +7767,6 @@ def accept(self): # the same as the new entry, as determined by addTab self.parent.parent.newEntryWindow.ui.tabWidget.currentWidget().ui.messageField.setFocus() super(changeCallsignDialog,self).accept() - rprint("New callsign pairing created: fleet="+fleet+" dev="+dev+" callsign="+newCallsign) class clickableWidget(QWidget): diff --git a/ui/changeCallsignDialog_ui.py b/ui/changeCallsignDialog_ui.py index be85cda..39e084b 100644 --- a/ui/changeCallsignDialog_ui.py +++ b/ui/changeCallsignDialog_ui.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'designer\changeCallsignDialog.ui' +# Form implementation generated from reading ui file 'C:\Users\caver\Documents\GitHub\radiolog\designer\changeCallsignDialog.ui' # # Created by: PyQt5 UI code generator 5.15.6 # @@ -27,24 +27,24 @@ def setupUi(self, changeCallsignDialog): self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") - self.fleetField = QtWidgets.QLineEdit(changeCallsignDialog) - self.fleetField.setEnabled(False) - self.fleetField.setGeometry(QtCore.QRect(210, 20, 111, 41)) + self.idField1 = QtWidgets.QLineEdit(changeCallsignDialog) + self.idField1.setEnabled(False) + self.idField1.setGeometry(QtCore.QRect(210, 20, 141, 41)) font = QtGui.QFont() font.setFamily("Segoe UI") font.setPointSize(16) - self.fleetField.setFont(font) - self.fleetField.setReadOnly(True) - self.fleetField.setObjectName("fleetField") - self.deviceField = QtWidgets.QLineEdit(changeCallsignDialog) - self.deviceField.setEnabled(False) - self.deviceField.setGeometry(QtCore.QRect(390, 20, 131, 41)) + self.idField1.setFont(font) + self.idField1.setReadOnly(True) + self.idField1.setObjectName("idField1") + self.idField2 = QtWidgets.QLineEdit(changeCallsignDialog) + self.idField2.setEnabled(False) + self.idField2.setGeometry(QtCore.QRect(380, 20, 141, 41)) font = QtGui.QFont() font.setFamily("Segoe UI") font.setPointSize(16) - self.deviceField.setFont(font) - self.deviceField.setReadOnly(True) - self.deviceField.setObjectName("deviceField") + self.idField2.setFont(font) + self.idField2.setReadOnly(True) + self.idField2.setObjectName("idField2") self.currentCallsignField = QtWidgets.QLineEdit(changeCallsignDialog) self.currentCallsignField.setEnabled(False) self.currentCallsignField.setGeometry(QtCore.QRect(210, 100, 311, 41)) @@ -63,20 +63,20 @@ def setupUi(self, changeCallsignDialog): self.newCallsignField.setPlaceholderText("") self.newCallsignField.setClearButtonEnabled(False) self.newCallsignField.setObjectName("newCallsignField") - self.label = QtWidgets.QLabel(changeCallsignDialog) - self.label.setGeometry(QtCore.QRect(220, 60, 55, 21)) + self.idLabel1 = QtWidgets.QLabel(changeCallsignDialog) + self.idLabel1.setGeometry(QtCore.QRect(220, 60, 101, 21)) font = QtGui.QFont() font.setFamily("Segoe UI") font.setPointSize(12) - self.label.setFont(font) - self.label.setObjectName("label") - self.label_2 = QtWidgets.QLabel(changeCallsignDialog) - self.label_2.setGeometry(QtCore.QRect(400, 60, 61, 21)) + self.idLabel1.setFont(font) + self.idLabel1.setObjectName("idLabel1") + self.idLabel2 = QtWidgets.QLabel(changeCallsignDialog) + self.idLabel2.setGeometry(QtCore.QRect(390, 60, 111, 21)) font = QtGui.QFont() font.setFamily("Segoe UI") font.setPointSize(12) - self.label_2.setFont(font) - self.label_2.setObjectName("label_2") + self.idLabel2.setFont(font) + self.idLabel2.setObjectName("idLabel2") self.label_3 = QtWidgets.QLabel(changeCallsignDialog) self.label_3.setGeometry(QtCore.QRect(10, 100, 191, 41)) font = QtGui.QFont() @@ -93,14 +93,14 @@ def setupUi(self, changeCallsignDialog): self.label_4.setFont(font) self.label_4.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.label_4.setObjectName("label_4") - self.label_5 = QtWidgets.QLabel(changeCallsignDialog) - self.label_5.setGeometry(QtCore.QRect(10, 20, 191, 41)) + self.idLabel = QtWidgets.QLabel(changeCallsignDialog) + self.idLabel.setGeometry(QtCore.QRect(10, 20, 191, 41)) font = QtGui.QFont() font.setFamily("Segoe UI") font.setPointSize(16) - self.label_5.setFont(font) - self.label_5.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_5.setObjectName("label_5") + self.idLabel.setFont(font) + self.idLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.idLabel.setObjectName("idLabel") self.fsFilterButton = QtWidgets.QToolButton(changeCallsignDialog) self.fsFilterButton.setGeometry(QtCore.QRect(476, 227, 47, 46)) self.fsFilterButton.setText("...") @@ -118,9 +118,9 @@ def setupUi(self, changeCallsignDialog): def retranslateUi(self, changeCallsignDialog): _translate = QtCore.QCoreApplication.translate changeCallsignDialog.setWindowTitle(_translate("changeCallsignDialog", "Change Callsign")) - self.label.setText(_translate("changeCallsignDialog", "Fleet")) - self.label_2.setText(_translate("changeCallsignDialog", "Device")) + self.idLabel1.setText(_translate("changeCallsignDialog", "Fleet")) + self.idLabel2.setText(_translate("changeCallsignDialog", "Device")) self.label_3.setText(_translate("changeCallsignDialog", "Current Callsign")) self.label_4.setText(_translate("changeCallsignDialog", "New Callsign")) - self.label_5.setText(_translate("changeCallsignDialog", "FleetSync ID")) + self.idLabel.setText(_translate("changeCallsignDialog", "FleetSync ID")) import radiolog_ui_rc From 1d609f94e110eb400a57620965c31d1172772edc Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 17 Feb 2023 15:58:03 -0800 Subject: [PATCH 04/23] WIP - filters not working yet, but, some initial refactoring done --- radiolog.py | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/radiolog.py b/radiolog.py index b0ce976..e6971d4 100644 --- a/radiolog.py +++ b/radiolog.py @@ -1568,11 +1568,11 @@ def fsFilterBlink(self,state): else: self.ui.fsFilterButton.setStyleSheet("QToolButton { }") - def fsFilterEdit(self,fleet,dev,state=True): + def fsFilterEdit(self,fleetOrBlank,devOrUid,state=True): # rprint("editing filter for "+str(fleet)+" "+str(dev)) for row in self.fsLog: # rprint("row:"+str(row)) - if row[0]==fleet and row[1]==dev: + if row[0]==fleetOrBlank and row[1]==devOrUid: # rprint("found") row[3]=state self.fsBuildTooltip() @@ -1614,16 +1614,22 @@ def fsGetTeamDevices(self,extTeamName): # rprint('returning '+str(rval)) return rval - def fsFilteredCallDisplay(self,state="off",fleet=0,dev=0,callsign=''): - if state=="on": - self.ui.incidentNameLabel.setText("Incoming FS filtered:\n"+callsign+" ("+str(fleet)+":"+str(dev)+")") - self.ui.incidentNameLabel.setStyleSheet("background-color:#ff5050;color:white;font-size:"+str(int(self.limitedFontSize*3/4))+"pt") - elif state=="bump": - self.ui.incidentNameLabel.setText("Mic bump filtered:\n"+callsign+" ("+str(fleet)+":"+str(dev)+")") - self.ui.incidentNameLabel.setStyleSheet("background-color:#5050ff;color:white;font-size:"+str(int(self.limitedFontSize*3/4))+"pt") + def fsFilteredCallDisplay(self,state='off',fleetOrBlank='',devOrUid='',callsign=''): + if fleetOrBlank: # fleetsync + typeStr='FleetSync' + idStr=fleetOrBlank+':'+devOrUid + else: #nexedge + typeStr='NEXEDGE' + idStr=devOrUid + if state=='on': + self.ui.incidentNameLabel.setText(typeStr+' filtered:\n'+callsign+' ('+idStr+')') + self.ui.incidentNameLabel.setStyleSheet('background-color:#ff5050;color:white;font-size:'+str(int(self.limitedFontSize*3/4))+'pt') + elif state=='bump': + self.ui.incidentNameLabel.setText('Mic bump filtered:\n'+callsign+' ('+idStr+')') + self.ui.incidentNameLabel.setStyleSheet('background-color:#5050ff;color:white;font-size:'+str(int(self.limitedFontSize*3/4))+'pt') else: self.ui.incidentNameLabel.setText(self.incidentName) - self.ui.incidentNameLabel.setStyleSheet("background-color:none;color:black;font-size:"+str(self.limitedFontSize)+"pt") + self.ui.incidentNameLabel.setStyleSheet('background-color:none;color:black;font-size:'+str(self.limitedFontSize)+'pt') def fsCheckBoxCB(self): # 0 = unchecked / empty: mute fleetsync completely @@ -2271,16 +2277,20 @@ def fsLogUpdate(self,fleet=None,dev=None,uid=None,callsign=False,bump=False): self.fsFilterDialog.ui.tableView.model().layoutChanged.emit() self.fsBuildTeamFilterDict() - def fsGetLatestComPort(self,fleet,device): + def fsGetLatestComPort(self,fleetOrBlank,devOrUid): rprint('fsLog:'+str(self.fsLog)) - log=[x for x in self.fsLog if x[0:2]==[int(fleet),int(device)]] + if fleetOrBlank: + idStr=fleetOrBlank+':'+devOrUid + else: + idStr=devOrUid + log=[x for x in self.fsLog if x[0:2]==[fleetOrBlank,devOrUid]] if len(log)==1: comPortName=log[0][5] elif len(log)>1: - rprint('WARNING: there are multiple fsLog entries for '+str(fleet)+':'+str(device)) + rprint('WARNING: there are multiple fsLog entries for '+idStr) comPortName=log[0][5] else: - rprint('WARNING: '+str(fleet)+':'+str(device)+' has no fsLog entry so it probably has not been heard from yet') + rprint('WARNING: '+idStr+' has no fsLog entry so it probably has not been heard from yet') comPortName=None # rprint('returning '+str(comPortName)) if self.firstComPort and self.firstComPort.name==comPortName: @@ -2304,7 +2314,7 @@ def fsBuildTooltip(self): tt='No devices are currently being filtered.
(left-click to edit)
' self.ui.fsFilterButton.setToolTip(tt) - def fsIsFiltered(self,fleet,dev): + def fsIsFiltered(self,fleetOrBlank,devOrUid): # rprint("checking fsFilter: fleet="+str(fleet)+" dev="+str(dev)) # disable fsValidFleetList checking to allow arbitrary fleets; this # idea is probably obsolete @@ -2314,7 +2324,7 @@ def fsIsFiltered(self,fleet,dev): # return True # if the fleet is valid, check for filtered device ID for row in self.fsLog: - if row[0]==fleet and row[1]==dev and row[3]==True: + if row[0]==fleetOrBlank and row[1]==devOrUid and row[3]==True: # rprint(" device is fitlered; returning True") return True # rprint("not filtered; returning False") From ddfa5a37cfeafd5feb18ce6e57f45b85810a5c9a Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 17 Feb 2023 17:54:51 -0800 Subject: [PATCH 05/23] WIP filter table and team tab context menu are showing nexedge devices, but, fitlering a nexedge device doesn't prevent a NED --- radiolog.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/radiolog.py b/radiolog.py index e6971d4..4ad26ea 100644 --- a/radiolog.py +++ b/radiolog.py @@ -2119,7 +2119,7 @@ def fsParse(self): self.fsFilteredCallDisplay() # blank for a tenth of a second in case of repeated bumps QTimer.singleShot(200,lambda:self.fsFilteredCallDisplay('bump',fleet,dev,callsign)) QTimer.singleShot(5000,self.fsFilteredCallDisplay) # no arguments will clear the display - self.fsLogUpdate(int(fleet),int(dev),bump=True) + self.fsLogUpdate(fleet=fleet,dev=dev,bump=True) self.sendPendingGet() # while getString will be non-empty if this bump had GPS, it may still have the default callsign return @@ -2184,10 +2184,10 @@ def fsParse(self): except: pass if fleet and dev: - self.fsLogUpdate(int(fleet),int(dev)) + self.fsLogUpdate(fleet=fleet,dev=dev) # only open a new entry widget if the fleet/dev is not being filtered if not found: - if self.fsIsFiltered(int(fleet),int(dev)): + if self.fsIsFiltered(fleet,dev): self.fsFilteredCallDisplay("on",fleet,dev,callsign) QTimer.singleShot(5000,self.fsFilteredCallDisplay) # no arguments will clear the display else: @@ -2235,8 +2235,9 @@ def fsLogUpdate(self,fleet=None,dev=None,uid=None,callsign=False,bump=False): # don't process the dummy default entry if callsign=='Default': return - if not (fleet and dev) or uid: + if not ((fleet and dev) or uid): rprint('ERROR in call to fsLogUpdate: either fleet and dev must be specified, or uid must be specified.') + rprint(' fleet='+str(fleet)+' dev='+str(dev)+' uid='+str(uid)) return # # this clause is dead code now, since enforcing that fleet and dev are always strings throughout the code # if fleet and dev: # fleetsync, not nexedge @@ -2272,7 +2273,7 @@ def fsLogUpdate(self,fleet=None,dev=None,uid=None,callsign=False,bump=False): self.fsLog.append([fleet,dev,self.getCallsign(fleet,dev),False,t,com,int(bump)]) elif uid: # nexedge self.fsLog.append(['',uid,self.getCallsign(uid),False,t,com,int(bump)]) -# rprint(self.fsLog) + rprint('fsLog after fsLogUpdate:'+str(self.fsLog)) # if self.fsFilterDialog.ui.tableView: self.fsFilterDialog.ui.tableView.model().layoutChanged.emit() self.fsBuildTeamFilterDict() @@ -7758,17 +7759,19 @@ def accept(self): self.parent.parent.fsLookup.append([fleet,dev,newCallsign]) else: # nexedge self.parent.parent.fsLookup.append(['',uid,newCallsign]) - rprint('fsLookup after CCD:'+str(self.parent.parent.fsLookup)) + # rprint('fsLookup after CCD:'+str(self.parent.parent.fsLookup)) # set the current radio log entry teamField also self.parent.ui.teamField.setText(newCallsign) # save the updated table (filename is set at the same times that csvFilename is set) self.parent.parent.fsSaveLookup() # change the callsign in fsLog if id2: # fleetsync - self.parent.parent.fsLogUpdate(fleet,dev,newCallsign) + rprint('calling fsLogUpdate for fleetsync') + self.parent.parent.fsLogUpdate(fleet=fleet,dev=dev,callsign=newCallsign) rprint("New callsign pairing created from FleetSync: fleet="+fleet+" dev="+dev+" callsign="+newCallsign) else: # nexedge - self.parent.parent.fsLogUpdate(None,uid,newCallsign) + rprint('calling fsLogUpdate for nexedge') + self.parent.parent.fsLogUpdate(uid=uid,callsign=newCallsign) rprint("New callsign pairing created from NEXEDGE: unit ID = "+uid+" callsign="+newCallsign) # finally, pass the 'accept' signal on up the tree as usual changeCallsignDialog.openDialogCount-=1 From 86609880c3f7be2253de9c58c7c73c6f9175cd35 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 17 Feb 2023 19:43:13 -0800 Subject: [PATCH 06/23] WIP - correctly filter calls from NEXEDGE devices per filter settings - clean up context menu to show NEXEDGE devices as 'NX:#####' --- designer/fsFilterDialog.ui | 4 ++-- radiolog.py | 38 +++++++++++++++++++++----------------- ui/fsFilterDialog_ui.py | 4 ++-- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/designer/fsFilterDialog.ui b/designer/fsFilterDialog.ui index 1077958..e52b526 100644 --- a/designer/fsFilterDialog.ui +++ b/designer/fsFilterDialog.ui @@ -17,7 +17,7 @@
- Radio Log - FleetSync Filter Setup + RadioLog - FleetSync / NEXEDGE Filter Setup false @@ -34,7 +34,7 @@ - FleetSync Filter Setup + FleetSync / NEXEDGE Filter Setup Qt::AlignCenter diff --git a/radiolog.py b/radiolog.py index 4ad26ea..fb73dbd 100644 --- a/radiolog.py +++ b/radiolog.py @@ -2195,7 +2195,11 @@ def fsParse(self): self.sendPendingGet() elif uid: if not found: - self.openNewEntry('nex',callsign,formattedLocString,None,uid,origLocString) + if self.fsIsFiltered('',uid): + self.fsFilteredCallDisplay('on','',uid) + QTimer.singleShot(5000,self.fsFilteredCallDisplay) # no arguments will clear the display + else: + self.openNewEntry('nex',callsign,formattedLocString,None,uid,origLocString) self.sendPendingGet() def sendPendingGet(self,suffix=""): @@ -4750,40 +4754,40 @@ def tabContextMenu(self,pos): fsToggleAllAction=False # initialize, so the action checker does not die fsSendTextToAllAction=False # initialize, so the action checker does not die if len(devices)>0: - fsMenu=menu.addMenu("FleetSync...") + fsMenu=menu.addMenu('FleetSync/NEXEDGE...') menu.addSeparator() if self.enableSendText: if len(devices)>1: - fsSendTextToAllAction=fsMenu.addAction("Send text message to all "+niceTeamName+" devices") + fsSendTextToAllAction=fsMenu.addAction('Send text message to all '+niceTeamName+' devices') fsMenu.addSeparator() for device in devices: - key=str(device[0])+":"+str(device[1]) - fsMenu.addAction("Send text message to "+key).setData([device[0],device[1],'SendText']) + key=(device[0] or 'NX')+':'+device[1] + fsMenu.addAction('Send text message to '+key).setData([device[0],device[1],'SendText']) fsMenu.addSeparator() if self.enablePollGPS: for device in devices: - key=str(device[0])+":"+str(device[1]) - fsMenu.addAction("Request location from "+key).setData([device[0],device[1],'PollGPS']) + key=(device[0] or 'NX')+':'+device[1] + fsMenu.addAction('Request location from '+key).setData([device[0],device[1],'PollGPS']) fsMenu.addSeparator() if len(devices)>1: if teamFSFilterDict[extTeamName]==2: - fsToggleAllAction=fsMenu.addAction("Unfilter all "+niceTeamName+" devices") + fsToggleAllAction=fsMenu.addAction('Unfilter all '+niceTeamName+' devices') else: - fsToggleAllAction=fsMenu.addAction("Filter all "+niceTeamName+" devices") + fsToggleAllAction=fsMenu.addAction('Filter all '+niceTeamName+' devices') fsMenu.addSeparator() for device in devices: - key=str(device[0])+":"+str(device[1]) + key=(device[0] or 'NX')+':'+device[1] if self.fsIsFiltered(device[0],device[1]): - fsMenu.addAction("Unfilter calls from "+key).setData([device[0],device[1],'unfilter']) + fsMenu.addAction('Unfilter calls from '+key).setData([device[0],device[1],'unfilter']) else: - fsMenu.addAction("Filter calls from "+key).setData([device[0],device[1],'filter']) + fsMenu.addAction('Filter calls from '+key).setData([device[0],device[1],'filter']) else: - key=str(devices[0][0])+":"+str(devices[0][1]) + key=(devices[0][0] or 'NX')+':'+devices[0][1] if teamFSFilterDict[extTeamName]==2: - fsToggleAllAction=fsMenu.addAction("Unfilter calls from "+niceTeamName+" ("+key+")") + fsToggleAllAction=fsMenu.addAction('Unfilter calls from '+niceTeamName+' ('+key+')') else: - fsToggleAllAction=fsMenu.addAction("Filter calls from "+niceTeamName+" ("+key+")") - deleteTeamTabAction=menu.addAction("Hide tab for "+str(niceTeamName)) + fsToggleAllAction=fsMenu.addAction('Filter calls from '+niceTeamName+' ('+key+')') + deleteTeamTabAction=menu.addAction('Hide tab for '+str(niceTeamName)) # action handlers action=menu.exec_(self.ui.tabWidget.tabBar().mapToGlobal(pos)) @@ -4793,7 +4797,7 @@ def tabContextMenu(self,pos): self.openNewEntry('tab',str(niceTeamName)) self.newEntryWidget.ui.to_fromField.setCurrentIndex(1) elif action==printTeamLogAction: - rprint("printing team log for "+str(niceTeamName)) + rprint('printing team log for '+str(niceTeamName)) self.printLog(self.opPeriod,str(niceTeamName)) self.radioLogNeedsPrint=True # since only one log has been printed; need to enhance this elif action==deleteTeamTabAction: diff --git a/ui/fsFilterDialog_ui.py b/ui/fsFilterDialog_ui.py index 8649e42..3cf12bb 100644 --- a/ui/fsFilterDialog_ui.py +++ b/ui/fsFilterDialog_ui.py @@ -83,7 +83,7 @@ def setupUi(self, fsFilterDialog): def retranslateUi(self, fsFilterDialog): _translate = QtCore.QCoreApplication.translate - fsFilterDialog.setWindowTitle(_translate("fsFilterDialog", "Radio Log - FleetSync Filter Setup")) - self.label.setText(_translate("fsFilterDialog", "FleetSync Filter Setup")) + fsFilterDialog.setWindowTitle(_translate("fsFilterDialog", "RadioLog - FleetSync / NEXEDGE Filter Setup")) + self.label.setText(_translate("fsFilterDialog", "FleetSync / NEXEDGE Filter Setup")) self.label_2.setText(_translate("fsFilterDialog", "- No New Entry dialog will be created for filtered devices.")) self.label_3.setText(_translate("fsFilterDialog", "- Click in the \'Filtered?\' column to toggle a device\'s filter.")) From a53ba0404398a9c21486ad2bcbd062577752fb3b Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Sun, 19 Feb 2023 06:26:42 -0800 Subject: [PATCH 07/23] WIP more complete refactor in fsCheck and fsParse; untested --- radiolog.py | 146 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 48 deletions(-) diff --git a/radiolog.py b/radiolog.py index fb73dbd..d16cf19 100644 --- a/radiolog.py +++ b/radiolog.py @@ -1653,23 +1653,24 @@ def fsCheckBoxCB(self): self.ui.incidentNameLabel.setText(self.incidentName) self.ui.incidentNameLabel.setStyleSheet("background-color:none;color:black;font-size:"+str(self.limitedFontSize)+"pt;") - # FleetSync - check for pending data + # FleetSync / NEXEDGE - check for pending data # - check for pending data at regular interval (from timer) # (it's important to check for ID-only lines, since handhelds with no - # GPS mic will not send $PKLSH but we still want to spawn a new entry - # dialog; and, $PKLSH isn't necessarily sent on BOT (Beginning of Transmission) anyway) - # (for 1 second interval, ID-only is always processed separately from $PKLSH; + # GPS mic will not send $PKLSH / $PKNSH but we still want to spawn a new entry + # dialog; and, $PKLSH / $PKNSH isn't necessarily sent on BOT (Beginning of Transmission) anyway) + # (for 1 second interval, ID-only is always processed separately from $PKLSH/ $PKNSH; # even for 2 second interval, sometimes ID is processed separately. - # So, if PKLSH is processed, add coords to any rececntly-spawned - # 'from' new entry dialog for the same fleetsync id.) + # So, if $PKLSH / $PKNSH is processed, add coords to any rececntly-spawned + # 'from' new entry dialog for the same fleetsync / nexedge id.) # - append any pending data to fsBuffer - # - if a clean end of fleetsync transmission is included in this round of data, + # - if a clean end of transmission packet is included in this round of data, # spawn a new entry window (unless one is already open with 'from' - # and the same fleetsync fleet and ID) with any geographic data + # and the same fleetsync / nexedge ID) with any geographic data # from the current fsBuffer, then empty the fsBuffer # NOTE that the data coming in is bytes b'1234' but we want string; use bytes.decode('utf-8') to convert, - # after which /x02 will show up as a happy face and /x03 will show up as a heart on standard ASCII display. + # after which /x02 (a.k.a. STX a.k.a. Start of Text) will show up as a happy face + # and /x03 (a.k.a. ETX a.k.a. End of Text) will show up as a heart on standard ASCII display. def fsCheck(self): if self.fsAwaitingResponse: @@ -1689,33 +1690,42 @@ def fsCheck(self): values[0]=time.strftime("%H%M") values[6]=time.time() [f,dev]=self.fsAwaitingResponse[0:2] - callsignText=self.getCallsign(f,dev) + if f: # fleetsync + callsignText=self.getCallsign(f,dev) + h='FLEETSYNC' + idStr=f+':'+dev + else: # nexedge + uid=dev + callsignText=self.getCallsign(uid) + h='NEXEDGE' + idStr=uid values[2]=str(callsignText) if callsignText: callsignText='('+callsignText+')' else: callsignText='(no callsign)' if self.fsAwaitingResponse[2]=='Location request sent': - values[3]='FLEETSYNC: No response received for location request from '+str(f)+':'+str(dev)+' '+callsignText + values[3]=h+': No response received for location request from '+idStr+' '+callsignText elif self.fsAwaitingResponse[2]=='Text message sent': msg=self.fsAwaitingResponse[4] values[1]='TO' - values[3]='FLEETSYNC: Text message sent to '+str(f)+':'+str(dev)+' '+callsignText+' but delivery was NOT confirmed: "'+msg+'"' + values[3]=h+': Text message sent to '+idStr+' '+callsignText+' but delivery was NOT confirmed: "'+msg+'"' else: - values[3]='FLEETSYNC: Timeout after unknown command type "'+self.fsAwaitingResponse[2]+'"' + values[3]=h+': Timeout after unknown command type "'+self.fsAwaitingResponse[2]+'"' + self.newEntry(values) - msg='No FleetSync response: unable to confirm that the message was received by the target device(s).' + msg='No '+h+' response: unable to confirm that the message was received by the target device(s).' if self.fsAwaitingResponse[2]=='Location request sent': msg+='\n\nThis could happen after a location request for one of several reasons:\n - The radio in question was off\n - The radio in question was on, but not set to this channel\n - The radio in question was on and set to this channel, but had no GPS fix' self.fsAwaitingResponse=None # clear the flag if len(self.fsSendList)==1: - box=QMessageBox(QMessageBox.Critical,'FleetSync timeout',msg, + box=QMessageBox(QMessageBox.Critical,h+' timeout',msg, QMessageBox.Close,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) box.open() box.raise_() box.exec_() else: - self.fsResponseMessage+='\n\n'+str(f)+':'+str(dev)+' '+callsignText+':'+msg + self.fsResponseMessage+='\n\n'+idStr+' '+callsignText+':'+msg else: remaining=self.fsAwaitingResponseTimeout-self.fsAwaitingResponse[3] suffix='' @@ -1767,7 +1777,7 @@ def fsCheck(self): # ♥ - postamble (\x03) # GPS: same as with fleetsync, but PKNSH instead of PKLSH; arrives immediately after BOT CID rather than EOT CID else: - rprint(" but not valid fleetsync data. Scan continues...") + rprint(" but not valid FleetSync or NEXEDGE data. Scan continues...") rprint(str(tmpData)) if valid: self.fsBuffer=self.fsBuffer+tmpData @@ -1896,7 +1906,7 @@ def fsParse(self): # the next lines will include $PKLSH etc. In this case, there is no action to take; # just move on to the next line. if self.fsAwaitingResponse and self.fsAwaitingResponse[2]=='Text message sent': - [f,dev]=self.fsAwaitingResponse[0:2] + [fleet,dev]=self.fsAwaitingResponse[0:2] msg=self.fsAwaitingResponse[4] self.fsAwaitingResponse=None # clear the flag before closing the messagebox try: @@ -1910,22 +1920,30 @@ def fsParse(self): values=["" for n in range(10)] values[0]=time.strftime("%H%M") values[1]='TO' - if int(f)==0 and int(dev)==0: + if int(dev)==0: recipient='all devices' values[2]='ALL' else: - callsignText=self.getCallsign(f,dev) + if fleet: # fleetsync + callsignText=self.getCallsign(fleet,dev) + idStr=fleet+':'+dev + h='FLEETSYNC' + else: # nexedge + uid=dev + callsignText=self.getCallsign(uid) + idStr=uid + h='NEXEDGE' values[2]=str(callsignText) if callsignText: callsignText='('+callsignText+')' else: callsignText='(no callsign)' - recipient=str(f)+':'+str(dev)+' '+callsignText + recipient=idStr+' '+callsignText suffix=' and delivery was confirmed' - values[3]='FLEETSYNC: Text message sent to '+recipient+suffix+': "'+msg+'"' + values[3]=h+': Text message sent to '+recipient+suffix+': "'+msg+'"' values[6]=time.time() self.newEntry(values) - rprint('FLEETSYNC: Text message sent to '+recipient+suffix) + rprint(h+': Text message sent to '+recipient+suffix) return if line=='\x021\x03': # failure response if self.fsAwaitingResponse: @@ -1935,15 +1953,23 @@ def fsParse(self): # [time,to_from,team,message,self.formattedLocString,status,self.sec,self.fleet,self.dev,self.origLocString] values=["" for n in range(10)] values[0]=time.strftime("%H%M") - [f,dev]=self.fsAwaitingResponse[0:2] - callsignText=self.getCallsign(f,dev) + [fleet,dev]=self.fsAwaitingResponse[0:2] + if fleet: # fleetsync + callsignText=self.getCallsign(fleet,dev) + idStr=fleet+':'+dev + h='FLEETSYNC' + else: # nexedge + uid=dev + callsignText=self.getCallsign(uid) + idStr=uid + h='NEXEDGE' values[2]=str(callsignText) if callsignText: callsignText='('+callsignText+')' else: callsignText='(no callsign)' - recipient=str(f)+':'+str(dev)+' '+callsignText - values[3]='FLEETSYNC: NO RESPONSE from '+recipient + recipient=idStr+' '+callsignText + values[3]=h+': NO RESPONSE from '+recipient values[6]=time.time() self.newEntry(values) self.fsFailedFlag=True @@ -1952,15 +1978,30 @@ def fsParse(self): except: pass self.fsAwaitingResponse=None # clear the flag - rprint('FLEETSYNC: NO RESPONSE from '+recipient) + rprint(h+': NO RESPONSE from '+recipient) return - if '$PKLSH' in line: + if '$PKLSH' in line or '$PKNSH' in line: # handle fleetsync and nexedge in this 'if' clause lineParse=line.split(',') + header=lineParse[0] # $PKLSH or $PKNSH # if CID packet(s) came before $PKLSH on the same line, that's OK since they don't have any commas - if len(lineParse)==10: - [pklsh,nval,nstr,wval,wstr,utc,valid,fleet,dev,chksum]=lineParse - callsign=self.getCallsign(fleet,dev) - rprint("$PKLSH detected containing CID: fleet="+fleet+" dev="+dev+" --> callsign="+callsign) + if (header=='$PKLSH' and len(lineParse)==10) or (header=='$PKNSH' and len(lineParse==9)): + if header=='$PKLSH': # fleetsync + [header,nval,nstr,wval,wstr,utc,valid,fleet,dev,chksum]=lineParse + uid='' + callsign=self.getCallsign(fleet,dev) + idStr=fleet+':'+dev + h='FLEETSYNC' + rprint("$PKLSH (FleetSync) detected containing CID: fleet="+fleet+" dev="+dev+" --> callsign="+callsign) + else: # nexedge + [header,nval,nstr,wval,wstr,utc,valid,uid,chksum]=lineParse + fleet='' + dev='' + uid=uid[1:] # get rid of the leading 'U' + callsign=self.getCallsign(uid) + idStr=uid + h='NEXEDGE' + rprint("$PKNSH (NEXEDGE) detected containing CID: Unit ID = "+uid+" --> callsign="+callsign) + # OLD RADIOS (2180): # unusual PKLSH lines seen from log files: # $PKLSH,2913.1141,N,,,175302,A,100,2016,*7A - no data for west - this caused @@ -1982,6 +2023,11 @@ def fsParse(self): # - if valid=='A' - as with old radios # so: # if valid=='A': # only process if there is a GPS lock + # + # $PKNSH (NEXEDGE equivalent of $PKLSH) - has one less comma-delimited token than $PKLSH + # U01001 = unit ID 01001 + # $PKNSH,3916.1154,N,12101.6008,W,123456,A,U01001,*4C + if valid!='Z': # process regardless of GPS lock locList=[nval,nstr,wval,wstr] origLocString='|'.join(locList) # don't use comma, that would conflict with CSV delimeter @@ -2007,18 +2053,22 @@ def fsParse(self): # but deviceID can be any text; use the callsign to get useful names in sarsoft if callsign.startswith("KW-"): # did not find a good callsign; use the device number in the GET request - devTxt=dev + devTxt=dev or uid else: # found a good callsign; use the callsign in the GET request devTxt=callsign - self.getString="http://"+self.sarsoftServerName+":8080/rest/location/update/position?lat="+str(lat)+"&lng="+str(lon)+"&id=FLEET:"+fleet+"-" + # for sending locator updates, assume fleet 100 for now - this may be dead code soon - see #598 + self.getString="http://"+self.sarsoftServerName+":8080/rest/location/update/position?lat="+str(lat)+"&lng="+str(lon)+"&id=FLEET:"+(fleet or '100')+"-" # if callsign = "Radio ..." then leave the getString ending with hyphen for now, as a sign to defer # sending until accept of change callsign dialog, or closeEvent of newEntryWidget, whichever comes first; # otherwise, append the callsign now, as a sign to send immediately + + # TODO: change this hardcode to deal with other default device names - see #635 if not devTxt.startswith("Radio "): self.getString=self.getString+devTxt + # was this a response to a location request for this device? - if self.fsAwaitingResponse and [int(fleet),int(dev)]==[int(x) for x in self.fsAwaitingResponse[0:2]]: + if self.fsAwaitingResponse and [fleet,dev]==[x for x in self.fsAwaitingResponse[0:2]]: try: self.fsAwaitingResponseMessageBox.close() except: @@ -2037,13 +2087,13 @@ def fsParse(self): else: prefix='UNKNOWN RESPONSE CODE "'+str(valid)+'"' values[4]='!'+values[4]+'!' - callsignText=self.getCallsign(fleet,dev) - values[2]=callsignText or '' - if callsignText: - callsignText='('+callsignText+')' + # callsignText=self.getCallsign(fleet,dev) + values[2]=callsign or '' + if callsign: + callsignText='('+callsign+')' else: callsignText='(no callsign)' - values[3]='FLEETSYNC LOCATION REQUEST: '+prefix+' from device '+str(fleet)+':'+str(dev)+' '+callsignText + values[3]=h+' LOCATION REQUEST: '+prefix+' from device '+idStr+' '+callsignText values[6]=time.time() self.newEntry(values) rprint(values[3]) @@ -2054,26 +2104,26 @@ def fsParse(self): self.fsAwaitingResponseMessageBox.show() self.fsAwaitingResponseMessageBox.raise_() self.fsAwaitingResponseMessageBox.exec_() - return # done processing this FS traffic - don't spawn a new entry dialog + return # done processing this traffic - don't spawn a new entry dialog else: - rprint("INVALID location string parsed from $PKLSH: '"+origLocString+"'") + rprint('INVALID location string parsed from '+header+': "'+origLocString+'"') origLocString='INVALID' formattedLocString='INVALID' elif valid=='Z': origLocString='NO FIX' formattedLocString='NO FIX' elif valid=='V': - rprint("WARNING status character parsed from $PKLSH; check the GPS mic attached to that radio") + rprint('WARNING status character parsed from '+header+'; check the GPS mic attached to that radio') origLocString='WARNING' formattedLocString='WARNING' else: origLocString='UNDEFINED' formattedLocString='UNDEFINED' else: - rprint("Parsed line contained "+str(len(lineParse))+" tokens instead of the expected 10; skipping.") + rprint("Parsed "+header+" line contained "+str(len(lineParse))+" tokens instead of the expected 10 (FleetSync) or 9 (NEXEDGE); skipping.") origLocString='BAD DATA' formattedLocString='BAD DATA' - if '\x02I' in line: # 'if' rather than 'elif' means that self.getString is available to send to sartopo + if '\x02I' in line: # fleetsync CID - 'if' rather than 'elif' means that self.getString is available to send to sartopo # caller ID lines look like " I110040021004002" (first character is \x02, may show as a space) # " I" is a prefix, n is either 1 (BOT) or 0 (EOT) # the next three characters (100 above) are the fleet# @@ -2128,10 +2178,10 @@ def fsParse(self): elif '\x02gI' in line: # NEXEDGE CID - similar to above packetSet=set(re.findall(r'\x02gI[0-1]U([0-9]{5})U\1\x03',line)) if len(packetSet)>1: - rprint('NEXEDGE(NXDN) ERROR: data appears garbled; there are two complete but non-identical CID packets. Skipping this message.') + rprint('NEXEDGE ERROR: data appears garbled; there are two complete but non-identical CID packets. Skipping this message.') return if len(packetSet)==0: - rprint('NEXEDGE(NXDN) ERROR: data appears garbled; no complete CID packets were found in the incoming data. Skipping this message.') + rprint('NEXEDGE ERROR: data appears garbled; no complete CID packets were found in the incoming data. Skipping this message.') return packet=packetSet.pop() count=line.count(packet) From 780ea0aef861dd1303fd07c60adb0489f0718796 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Mon, 20 Feb 2023 20:29:02 -0800 Subject: [PATCH 08/23] WIP cleanup GPS location parsing code, to correctly check number of tokens for fleetsync vs nexedge, and to accept CID STX thru ETX as part of the first token (using 'in' rather than '==') --- radiolog.py | 258 +++++++++++++++++++++++++++------------------------- 1 file changed, 133 insertions(+), 125 deletions(-) diff --git a/radiolog.py b/radiolog.py index d16cf19..a313c95 100644 --- a/radiolog.py +++ b/radiolog.py @@ -1675,7 +1675,7 @@ def fsCheckBoxCB(self): def fsCheck(self): if self.fsAwaitingResponse: if self.fsAwaitingResponse[3]>=self.fsAwaitingResponseTimeout: - rprint('Fleetsync timed out awaiting response') + rprint('Timed out awaiting FleetSync or NEXEDGE response') self.fsFailedFlag=True try: self.fsTimedOut=True @@ -1982,17 +1982,24 @@ def fsParse(self): return if '$PKLSH' in line or '$PKNSH' in line: # handle fleetsync and nexedge in this 'if' clause lineParse=line.split(',') - header=lineParse[0] # $PKLSH or $PKNSH + header=lineParse[0] # $PKLSH or $PKNSH, possibly following CID STX thru ETX + # rprint('header:'+header+' tokens:'+str(len(lineParse))) # if CID packet(s) came before $PKLSH on the same line, that's OK since they don't have any commas - if (header=='$PKLSH' and len(lineParse)==10) or (header=='$PKNSH' and len(lineParse==9)): - if header=='$PKLSH': # fleetsync + if '$PKLSH' in header: # fleetsync + if len(lineParse)==10: [header,nval,nstr,wval,wstr,utc,valid,fleet,dev,chksum]=lineParse uid='' callsign=self.getCallsign(fleet,dev) idStr=fleet+':'+dev h='FLEETSYNC' rprint("$PKLSH (FleetSync) detected containing CID: fleet="+fleet+" dev="+dev+" --> callsign="+callsign) - else: # nexedge + else: + rprint("Parsed $PKLSH line contained "+str(len(lineParse))+" tokens instead of the expected 10 tokens; skipping.") + origLocString='BAD DATA' + formattedLocString='BAD DATA' + continue + elif '$PKNSH' in header: # nexedge + if len(lineParse)==9: [header,nval,nstr,wval,wstr,utc,valid,uid,chksum]=lineParse fleet='' dev='' @@ -2001,128 +2008,129 @@ def fsParse(self): idStr=uid h='NEXEDGE' rprint("$PKNSH (NEXEDGE) detected containing CID: Unit ID = "+uid+" --> callsign="+callsign) - - # OLD RADIOS (2180): - # unusual PKLSH lines seen from log files: - # $PKLSH,2913.1141,N,,,175302,A,100,2016,*7A - no data for west - this caused - # parsing error "ValueError: invalid literal for int() with base 10: ''" - # $PKLSH,3851.3330,N,09447.9417,W,012212,V,100,1202,*23 - what's 'V'? - # in standard NMEA sentences, status 'V' = 'warning'. Dead GPS mic? - # we should flag these to the user somehow; note, the coordinates are - # for the Garmin factory in Olathe, KS - # - if valid=='V' set coord field to 'WARNING', do not attempt to parse, and carry on - # - if valid=='A' and coords are incomplete or otherwise invalid, set coord field - # to 'INVALID', do not attempt to parse, and carry on - # - # NEW RADIOS (NX5200): - # $PKLSH can contain status 'V' if it had a GPS lock before but does not currently, - # in which case the real coodinates of the last known lock will be included. - # If this happens, we do want to see the coordinates in the entry body, but we do not want - # to update the sartopo locator. - # - iv valid=='V', append the formatted string with an asterisk, but do not update the locator - # - if valid=='A' - as with old radios - # so: - # if valid=='A': # only process if there is a GPS lock - # - # $PKNSH (NEXEDGE equivalent of $PKLSH) - has one less comma-delimited token than $PKLSH - # U01001 = unit ID 01001 - # $PKNSH,3916.1154,N,12101.6008,W,123456,A,U01001,*4C - - if valid!='Z': # process regardless of GPS lock - locList=[nval,nstr,wval,wstr] - origLocString='|'.join(locList) # don't use comma, that would conflict with CSV delimeter - validated=True - try: - float(nval) - except ValueError: - validated=False - try: - float(wval) - except ValueError: - validated=False - validated=validated and nstr in ['N','S'] and wstr in ['W','E'] - if validated: - rprint("Valid location string:'"+origLocString+"'") - formattedLocString=self.convertCoords(locList,self.datum,self.coordFormat) - rprint("Formatted location string:'"+formattedLocString+"'") - [lat,lon]=self.convertCoords(locList,targetDatum="WGS84",targetFormat="D.dList") - rprint("WGS84 lat="+str(lat)+" lon="+str(lon)) - if valid=='A': # don't update the locator if valid=='V' - # sarsoft requires &id=FLEET:- - # fleet# must match the locatorGroup fleet number in sarsoft - # but deviceID can be any text; use the callsign to get useful names in sarsoft - if callsign.startswith("KW-"): - # did not find a good callsign; use the device number in the GET request - devTxt=dev or uid - else: - # found a good callsign; use the callsign in the GET request - devTxt=callsign - # for sending locator updates, assume fleet 100 for now - this may be dead code soon - see #598 - self.getString="http://"+self.sarsoftServerName+":8080/rest/location/update/position?lat="+str(lat)+"&lng="+str(lon)+"&id=FLEET:"+(fleet or '100')+"-" - # if callsign = "Radio ..." then leave the getString ending with hyphen for now, as a sign to defer - # sending until accept of change callsign dialog, or closeEvent of newEntryWidget, whichever comes first; - # otherwise, append the callsign now, as a sign to send immediately - - # TODO: change this hardcode to deal with other default device names - see #635 - if not devTxt.startswith("Radio "): - self.getString=self.getString+devTxt - - # was this a response to a location request for this device? - if self.fsAwaitingResponse and [fleet,dev]==[x for x in self.fsAwaitingResponse[0:2]]: - try: - self.fsAwaitingResponseMessageBox.close() - except: - pass - # values format for adding a new entry: - # [time,to_from,team,message,self.formattedLocString,status,self.sec,self.fleet,self.dev,self.origLocString] - values=["" for n in range(10)] - values[0]=time.strftime("%H%M") - values[1]='FROM' - values[4]=formattedLocString - if valid=='A': - prefix='SUCCESSFUL RESPONSE' - elif valid=='V': - prefix='RESPONSE WITH WARNING CODE (probably indicates a stale GPS lock)' - values[4]='*'+values[4]+'*' - else: - prefix='UNKNOWN RESPONSE CODE "'+str(valid)+'"' - values[4]='!'+values[4]+'!' - # callsignText=self.getCallsign(fleet,dev) - values[2]=callsign or '' - if callsign: - callsignText='('+callsign+')' - else: - callsignText='(no callsign)' - values[3]=h+' LOCATION REQUEST: '+prefix+' from device '+idStr+' '+callsignText - values[6]=time.time() - self.newEntry(values) - rprint(values[3]) - t=self.fsAwaitingResponse[2] - self.fsAwaitingResponse=None # clear the flag - self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.Information,t,values[3]+':\n\n'+formattedLocString+'\n\nNew entry created with response coordinates.', - QMessageBox.Ok,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) - self.fsAwaitingResponseMessageBox.show() - self.fsAwaitingResponseMessageBox.raise_() - self.fsAwaitingResponseMessageBox.exec_() - return # done processing this traffic - don't spawn a new entry dialog - else: - rprint('INVALID location string parsed from '+header+': "'+origLocString+'"') - origLocString='INVALID' - formattedLocString='INVALID' - elif valid=='Z': - origLocString='NO FIX' - formattedLocString='NO FIX' - elif valid=='V': - rprint('WARNING status character parsed from '+header+'; check the GPS mic attached to that radio') - origLocString='WARNING' - formattedLocString='WARNING' else: - origLocString='UNDEFINED' - formattedLocString='UNDEFINED' + rprint("Parsed $PKNSH line contained "+str(len(lineParse))+" tokens instead of the expected 9 tokens; skipping.") + origLocString='BAD DATA' + formattedLocString='BAD DATA' + continue + + # OLD RADIOS (2180): + # unusual PKLSH lines seen from log files: + # $PKLSH,2913.1141,N,,,175302,A,100,2016,*7A - no data for west - this caused + # parsing error "ValueError: invalid literal for int() with base 10: ''" + # $PKLSH,3851.3330,N,09447.9417,W,012212,V,100,1202,*23 - what's 'V'? + # in standard NMEA sentences, status 'V' = 'warning'. Dead GPS mic? + # we should flag these to the user somehow; note, the coordinates are + # for the Garmin factory in Olathe, KS + # - if valid=='V' set coord field to 'WARNING', do not attempt to parse, and carry on + # - if valid=='A' and coords are incomplete or otherwise invalid, set coord field + # to 'INVALID', do not attempt to parse, and carry on + # + # NEW RADIOS (NX5200): + # $PKLSH can contain status 'V' if it had a GPS lock before but does not currently, + # in which case the real coodinates of the last known lock will be included. + # If this happens, we do want to see the coordinates in the entry body, but we do not want + # to update the sartopo locator. + # - iv valid=='V', append the formatted string with an asterisk, but do not update the locator + # - if valid=='A' - as with old radios + # so: + # if valid=='A': # only process if there is a GPS lock + # + # $PKNSH (NEXEDGE equivalent of $PKLSH) - has one less comma-delimited token than $PKLSH + # U01001 = unit ID 01001 + # $PKNSH,3916.1154,N,12101.6008,W,123456,A,U01001,*4C + + if valid!='Z': # process regardless of GPS lock + locList=[nval,nstr,wval,wstr] + origLocString='|'.join(locList) # don't use comma, that would conflict with CSV delimeter + validated=True + try: + float(nval) + except ValueError: + validated=False + try: + float(wval) + except ValueError: + validated=False + validated=validated and nstr in ['N','S'] and wstr in ['W','E'] + if validated: + rprint("Valid location string:'"+origLocString+"'") + formattedLocString=self.convertCoords(locList,self.datum,self.coordFormat) + rprint("Formatted location string:'"+formattedLocString+"'") + [lat,lon]=self.convertCoords(locList,targetDatum="WGS84",targetFormat="D.dList") + rprint("WGS84 lat="+str(lat)+" lon="+str(lon)) + if valid=='A': # don't update the locator if valid=='V' + # sarsoft requires &id=FLEET:- + # fleet# must match the locatorGroup fleet number in sarsoft + # but deviceID can be any text; use the callsign to get useful names in sarsoft + if callsign.startswith("KW-"): + # did not find a good callsign; use the device number in the GET request + devTxt=dev or uid + else: + # found a good callsign; use the callsign in the GET request + devTxt=callsign + # for sending locator updates, assume fleet 100 for now - this may be dead code soon - see #598 + self.getString="http://"+self.sarsoftServerName+":8080/rest/location/update/position?lat="+str(lat)+"&lng="+str(lon)+"&id=FLEET:"+(fleet or '100')+"-" + # if callsign = "Radio ..." then leave the getString ending with hyphen for now, as a sign to defer + # sending until accept of change callsign dialog, or closeEvent of newEntryWidget, whichever comes first; + # otherwise, append the callsign now, as a sign to send immediately + + # TODO: change this hardcode to deal with other default device names - see #635 + if not devTxt.startswith("Radio "): + self.getString=self.getString+devTxt + + # was this a response to a location request for this device? + if self.fsAwaitingResponse and [fleet,dev]==[x for x in self.fsAwaitingResponse[0:2]]: + try: + self.fsAwaitingResponseMessageBox.close() + except: + pass + # values format for adding a new entry: + # [time,to_from,team,message,self.formattedLocString,status,self.sec,self.fleet,self.dev,self.origLocString] + values=["" for n in range(10)] + values[0]=time.strftime("%H%M") + values[1]='FROM' + values[4]=formattedLocString + if valid=='A': + prefix='SUCCESSFUL RESPONSE' + elif valid=='V': + prefix='RESPONSE WITH WARNING CODE (probably indicates a stale GPS lock)' + values[4]='*'+values[4]+'*' + else: + prefix='UNKNOWN RESPONSE CODE "'+str(valid)+'"' + values[4]='!'+values[4]+'!' + # callsignText=self.getCallsign(fleet,dev) + values[2]=callsign or '' + if callsign: + callsignText='('+callsign+')' + else: + callsignText='(no callsign)' + values[3]=h+' LOCATION REQUEST: '+prefix+' from device '+idStr+' '+callsignText + values[6]=time.time() + self.newEntry(values) + rprint(values[3]) + t=self.fsAwaitingResponse[2] + self.fsAwaitingResponse=None # clear the flag + self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.Information,t,values[3]+':\n\n'+formattedLocString+'\n\nNew entry created with response coordinates.', + QMessageBox.Ok,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) + self.fsAwaitingResponseMessageBox.show() + self.fsAwaitingResponseMessageBox.raise_() + self.fsAwaitingResponseMessageBox.exec_() + return # done processing this traffic - don't spawn a new entry dialog + else: + rprint('INVALID location string parsed from '+header+': "'+origLocString+'"') + origLocString='INVALID' + formattedLocString='INVALID' + elif valid=='Z': + origLocString='NO FIX' + formattedLocString='NO FIX' + elif valid=='V': + rprint('WARNING status character parsed from '+header+'; check the GPS mic attached to that radio') + origLocString='WARNING' + formattedLocString='WARNING' else: - rprint("Parsed "+header+" line contained "+str(len(lineParse))+" tokens instead of the expected 10 (FleetSync) or 9 (NEXEDGE); skipping.") - origLocString='BAD DATA' - formattedLocString='BAD DATA' + origLocString='UNDEFINED' + formattedLocString='UNDEFINED' if '\x02I' in line: # fleetsync CID - 'if' rather than 'elif' means that self.getString is available to send to sartopo # caller ID lines look like " I110040021004002" (first character is \x02, may show as a space) # " I" is a prefix, n is either 1 (BOT) or 0 (EOT) From 1efa8814ca86454badc9b2563659d0be73e8874b Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Mon, 20 Feb 2023 20:48:30 -0800 Subject: [PATCH 09/23] WIP pollGPS edits (untested) --- radiolog.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/radiolog.py b/radiolog.py index a313c95..76a995b 100644 --- a/radiolog.py +++ b/radiolog.py @@ -5176,18 +5176,22 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): - def pollGPS(self,fleet,device): + def pollGPS(self,fleet='',device=None): if self.fsShowChannelWarning: - m='WARNING: You are about to send FleetSync data burst noise on one or both mobile radios.\n\nMake sure that neither radio is set to any law or fire channel, or any other channel where FleetSync data bursts would cause problems.' - box=QMessageBox(QMessageBox.Warning,'FleetSync Channel Warning',m, + m='WARNING: You are about to send FleetSync or NEXEDGE data burst noise on one or both mobile radios.\n\nMake sure that neither radio is set to any law or fire channel, or any other channel where FleetSync data bursts would cause problems.' + box=QMessageBox(QMessageBox.Warning,'FleetSync / NEXEDGE Channel Warning',m, QMessageBox.Ok|QMessageBox.Cancel,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) box.show() box.raise_() box.exec_() if box.clickedButton().text()=='Cancel': return - rprint('polling GPS for fleet='+str(fleet)+' device='+str(device)) - d='\x02\x52\x33'+str(fleet)+str(device)+'\x03' + if fleet: # fleetsync + rprint('polling GPS for fleet='+str(fleet)+' device='+str(device)) + d='\x02\x52\x33'+str(fleet)+str(device)+'\x03' + else: # nexedge + rprint('polling GPS for NEXEDGE unit ID = '+str(device)) + d='\x02g\x52\x33U'+str(device)+'\x03' rprint('com data: '+str(d)) self.fsTimedOut=False self.fsFailedFlag=False @@ -5203,7 +5207,16 @@ def pollGPS(self,fleet,device): fsFirstPortToTry.write(d.encode()) self.fsAwaitingResponse=[fleet,device,'Location request sent',0] [f,dev,t]=self.fsAwaitingResponse[0:3] - self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.NoIcon,t,t+' to '+str(f)+':'+str(dev)+' on preferred COM port; awaiting response up to '+str(self.fsAwaitingResponseTimeout)+' seconds...', + if f: # fleetsync + idStr=f+':'+dev + h='FleetSync' + callsignText=self.getCallsign(f,dev) + else: # nexedge + idStr=dev + h='NEXEDGE' + uid=dev + callsignText=self.getCallsign(uid) + self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.NoIcon,t,t+' to '+idStr+' on preferred COM port; awaiting response up to '+str(self.fsAwaitingResponseTimeout)+' seconds...', QMessageBox.Abort,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) self.fsAwaitingResponseMessageBox.show() self.fsAwaitingResponseMessageBox.raise_() @@ -5214,13 +5227,12 @@ def pollGPS(self,fleet,device): # [time,to_from,team,message,self.formattedLocString,status,self.sec,self.fleet,self.dev,self.origLocString] values=["" for n in range(10)] values[0]=time.strftime("%H%M") - callsignText=self.getCallsign(f,dev) values[2]=str(callsignText) if callsignText: callsignText='('+callsignText+')' else: callsignText='(no callsign)' - values[3]='FLEETSYNC: GPS location request set to '+str(f)+':'+str(dev)+' '+callsignText+' but radiolog operator clicked Abort before response was received' + values[3]=h+': GPS location request set to '+idStr+' '+callsignText+' but radiolog operator clicked Abort before response was received' values[6]=time.time() self.newEntry(values) if self.fsFailedFlag: # timed out, or, got a '1' response @@ -5232,7 +5244,7 @@ def pollGPS(self,fleet,device): # rprint('5: fsThereWillBeAnotherTry='+str(self.fsThereWillBeAnotherTry)) self.fsSecondPortToTry.write(d.encode()) self.fsAwaitingResponse[3]=0 # reset the timer - self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.NoIcon,t,t+' to '+str(f)+':'+str(dev)+' on alternate COM port; awaiting response up to '+str(self.fsAwaitingResponseTimeout)+' seconds...', + self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.NoIcon,t,t+' to '+idStr+' on alternate COM port; awaiting response up to '+str(self.fsAwaitingResponseTimeout)+' seconds...', QMessageBox.Abort,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) self.fsAwaitingResponseMessageBox.show() self.fsAwaitingResponseMessageBox.raise_() @@ -5243,13 +5255,12 @@ def pollGPS(self,fleet,device): # [time,to_from,team,message,self.formattedLocString,status,self.sec,self.fleet,self.dev,self.origLocString] values=["" for n in range(10)] values[0]=time.strftime("%H%M") - callsignText=self.getCallsign(f,dev) values[2]=str(callsignText) if callsignText: callsignText='('+callsignText+')' else: callsignText='(no callsign)' - values[3]='FLEETSYNC: GPS location request set to '+str(f)+':'+str(dev)+' '+callsignText+' but radiolog operator clicked Abort before response was received' + values[3]=h+': GPS location request set to '+idStr+' '+callsignText+' but radiolog operator clicked Abort before response was received' values[6]=time.time() self.newEntry(values) if self.fsFailedFlag: # timed out, or, got a '1' response From 5a19436cbf299d3d64dc996600ff864c71ab45c2 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 23 Jun 2023 10:42:31 -0700 Subject: [PATCH 10/23] cleanup getCallsign indent errors indent errors were caused by using the online conflict resolution tool --- radiolog.py | 64 +++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/radiolog.py b/radiolog.py index 0c92d06..963f18c 100644 --- a/radiolog.py +++ b/radiolog.py @@ -2601,41 +2601,43 @@ def getCallsign(self,fleetOrUid,dev=None): rprint('ERROR in call to getCallsign: dev is not a string.') return matches=[] + if len(fleetOrUid)==3: # 3 characters - must be fleetsync + rprint('getCallsign called for FleetSync fleet='+str(fleetOrUid)+' dev='+str(dev)) fleet=fleetOrUid - rprint('getCallsign called for fleet='+str(fleet)+' dev='+str(dev)) - for entry in self.fsLookup: - if int(entry[0])==int(fleet) and int(entry[1])==int(dev): - # check each potential match against existing matches before adding to the list of matches - found=False - for match in matches: - if int(match[0])==int(fleet) and int(match[1])==int(dev) and match[2].lower().replace(' ','')==entry[2].lower().replace(' ',''): - found=True - if not found: - matches.append(entry) -# matches=[element for element in self.fsLookup if (element[0]==fleet and element[1]==dev)] - rprint('found matching entry/entries:'+str(matches)) - if len(matches)!=1 or len(matches[0][0])!=3: # no match - return "KW-"+fleet+"-"+dev - else: - return matches[0][2] + for entry in self.fsLookup: + if int(entry[0])==int(fleet) and int(entry[1])==int(dev): + # check each potential match against existing matches before adding to the list of matches + found=False + for match in matches: + if int(match[0])==int(fleet) and int(match[1])==int(dev) and match[2].lower().replace(' ','')==entry[2].lower().replace(' ',''): + found=True + if not found: + matches.append(entry) + # matches=[element for element in self.fsLookup if (element[0]==fleet and element[1]==dev)] + rprint('found matching entry/entries:'+str(matches)) + if len(matches)!=1 or len(matches[0][0])!=3: # no match + return "KW-"+fleet+"-"+dev + else: + return matches[0][2] + elif len(fleetOrUid)==5: # 5 characters - must be NEXEDGE uid=fleetOrUid - rprint('getCallsign called for uid='+str(uid)) - for entry in self.fsLookup: - if int(entry[1])==uid: - # check each potential match against existing matches before adding to the list of matches - found=False - for match in matches: - if str(match[1])==str(uid) and match[2].lower().replace(' ','')==entry[2].lower().replace(' ',''): - found=True - if not found: - matches.append(entry) -# matches=[element for element in self.fsLookup if element[1]==uid] - if len(matches)!=1 or matches[0][0]!='': # no match - return "KW-NXDN-"+uid - else: - return matches[0][2] + rprint('getCallsign called for NXDN UID='+str(uid)) + for entry in self.fsLookup: + if int(entry[1])==uid: + # check each potential match against existing matches before adding to the list of matches + found=False + for match in matches: + if str(match[1])==str(uid) and match[2].lower().replace(' ','')==entry[2].lower().replace(' ',''): + found=True + if not found: + matches.append(entry) + # matches=[element for element in self.fsLookup if element[1]==uid] + if len(matches)!=1 or matches[0][0]!='': # no match + return "KW-NXDN-"+uid + else: + return matches[0][2] else: rprint('ERROR in call to getCallsign: first argument must be 3 characters (FleetSync) or 5 characters (NEXEDGE): "'+fleetOrUid+'"') From f2e24f9c958e870023873ed2e80f451860e1e3c3 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 23 Jun 2023 11:38:53 -0700 Subject: [PATCH 11/23] add total count arg in fsLog for NXDN --- radiolog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radiolog.py b/radiolog.py index 963f18c..a0aaf60 100644 --- a/radiolog.py +++ b/radiolog.py @@ -2380,7 +2380,7 @@ def fsLogUpdate(self,fleet=None,dev=None,uid=None,callsign=False,bump=False): if fleet and dev: # fleetsync self.fsLog.append([fleet,dev,self.getCallsign(fleet,dev),False,t,com,int(bump),0]) elif uid: # nexedge - self.fsLog.append(['',uid,self.getCallsign(uid),False,t,com,int(bump)]) + self.fsLog.append(['',uid,self.getCallsign(uid),False,t,com,int(bump),0]) # rprint('fsLog after fsLogUpdate:'+str(self.fsLog)) # if self.fsFilterDialog.ui.tableView: self.fsFilterDialog.ui.tableView.model().layoutChanged.emit() From ccf4631e28b91bea8db2801ac0f31c062ba04242 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 23 Jun 2023 11:52:46 -0700 Subject: [PATCH 12/23] cleanup fsLoadLookup to use all-strings for fleet/uid and dev this was already done in the nxdn branch before merging 3.7.0 into it, but got clobbered during that merge process --- radiolog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/radiolog.py b/radiolog.py index a0aaf60..17f94e1 100644 --- a/radiolog.py +++ b/radiolog.py @@ -2506,15 +2506,15 @@ def fsLoadLookup(self,startupFlag=False,fsFileName=None,hideWarnings=False): for id in range(int(firstLast[0]),int(firstLast[1])+1): r=eval(callsignExpr) cs=callsign.replace('[]',str(r)) - row=[int(fleet),int(id),cs] + row=[str(fleet),str(id),cs] # rprint(' adding evaluated row: '+str(row)) self.fsLookup.append(row) # self.fsLogUpdate(row[0],row[1],row[2]) usedCallsignList.append(cs) - usedIDPairList.append(str(row[0])+':'+str(row[1])) + usedIDPairList.append(row[0]+':'+row[1]) else: # rprint(' adding row: '+str(row)) - self.fsLookup.append([int(fleet),int(idOrRange),callsign]) + self.fsLookup.append([str(fleet),str(idOrRange),callsign]) # self.fsLogUpdate(row[0],row[1],row[2]) usedCallsignList.append(row[2]) usedIDPairList.append(str(row[0])+':'+str(row[1])) From 6a3e891b137ff3b45b5e47078ae5ea5828a5c85b Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 23 Jun 2023 12:11:00 -0700 Subject: [PATCH 13/23] cleanup #655 use strings instead of int to check for first non-mic-bump call --- radiolog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radiolog.py b/radiolog.py index 17f94e1..b2ceba7 100644 --- a/radiolog.py +++ b/radiolog.py @@ -4344,7 +4344,7 @@ def openNewEntry(self,key=None,callsign=None,formattedLocString=None,fleet=None, self.newEntryWidget.ui.teamField.setFocus() self.newEntryWidget.ui.teamField.selectAll() # i[7] = total call count; i[6] = mic bump count; we want to look at the total non-bump count, i[7]-i[6] - if fleet and dev and len([i for i in self.fsLog if i[0]==int(fleet) and i[1]==int(dev) and (i[7]-i[6])<2])>0: # this is the device's first non-mic-bump call + if fleet and dev and len([i for i in self.fsLog if i[0]==str(fleet) and i[1]==str(dev) and (i[7]-i[6])<2])>0: # this is the device's first non-mic-bump call rprint('First non-mic-bump call from this device.') found=False for i in self.CCD1List: From ba938b066f41c2f615ab944957dbe90bd6adfcac Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 23 Jun 2023 16:37:02 -0700 Subject: [PATCH 14/23] hopefully final cleanup from merge commit trivial changes - comment typo, and changeCallsignDialog idLabel setting --- radiolog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/radiolog.py b/radiolog.py index b2ceba7..449eb92 100644 --- a/radiolog.py +++ b/radiolog.py @@ -6807,7 +6807,7 @@ def messageTextChanged(self): # gets called after every keystroke or button pres if box.exec_()==QMessageBox.Yes: self.quickTextClueAction() #642 - to make sure the operator sees the clue dialog, move it to the same location - # as the 'looks like a clue' pupop (hardcode to 50px above and left, no less than 10,10) + # as the 'looks like a clue' popup (hardcode to 50px above and left, no less than 10,10) self.newClueDialog.move(max(10,boxX-50),max(10,boxY-50)) self.newClueDialog.ui.descriptionField.setPlainText(self.ui.messageField.text()) # move cursor to end since it doesn't happen automatically @@ -7889,6 +7889,7 @@ def __init__(self,parent,callsign,fleet=None,device=None): rprint("changeCallsignDialog created. fleet="+str(self.fleet)+" dev="+str(self.device)) if fleet: # fleetsync + self.ui.idLabel.setText('FleetSync ID') self.ui.idField1.setText(str(fleet)) self.ui.idField2.setText(str(device)) else: # nexedge From 807312fe0a586b946b7ef99c9d01cda906e203b3 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Sun, 25 Jun 2023 17:27:44 -0700 Subject: [PATCH 15/23] cleanup from merge-commit - fixed indent issues in getCallsign that resulted in a return value of None (both FS and NXDN) when id no callsign is found - raise exception if a line in the radiolog csv doesn't have enough columns; this will cause the file to be treated as corrupt and the next .bak file will be used instead --- radiolog.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/radiolog.py b/radiolog.py index 449eb92..cccb5d5 100644 --- a/radiolog.py +++ b/radiolog.py @@ -2615,11 +2615,14 @@ def getCallsign(self,fleetOrUid,dev=None): if not found: matches.append(entry) # matches=[element for element in self.fsLookup if (element[0]==fleet and element[1]==dev)] - rprint('found matching entry/entries:'+str(matches)) - if len(matches)!=1 or len(matches[0][0])!=3: # no match - return "KW-"+fleet+"-"+dev - else: - return matches[0][2] + if len(matches)>0: + rprint(' found matching entry/entries:'+str(matches)) + else: + rprint(' no matches') + if len(matches)!=1 or len(matches[0][0])!=3: # no match + return "KW-"+fleet+"-"+dev + else: + return matches[0][2] elif len(fleetOrUid)==5: # 5 characters - must be NEXEDGE uid=fleetOrUid @@ -2634,10 +2637,14 @@ def getCallsign(self,fleetOrUid,dev=None): if not found: matches.append(entry) # matches=[element for element in self.fsLookup if element[1]==uid] - if len(matches)!=1 or matches[0][0]!='': # no match - return "KW-NXDN-"+uid - else: - return matches[0][2] + if len(matches)>0: + rprint(' found matching entry/entries:'+str(matches)) + else: + rprint(' no matches') + if len(matches)!=1 or matches[0][0]!='': # no match + return "KW-NXDN-"+uid + else: + return matches[0][2] else: rprint('ERROR in call to getCallsign: first argument must be 3 characters (FleetSync) or 5 characters (NEXEDGE): "'+fleetOrUid+'"') @@ -2955,6 +2962,7 @@ def printLog(self,opPeriod,teams=False): if self.useOperatorLogin: operatorImageFile=os.path.join(iconsDir,'user_icon_80px.png') if os.path.isfile(operatorImageFile): + rprint('operator image file found: '+operatorImageFile) headers.append(Image(operatorImageFile,width=0.16*inch,height=0.16*inch)) else: rprint('operator image file not found: '+operatorImageFile) @@ -4032,6 +4040,8 @@ def load(self,fileName=None,bakAttempt=0): rprint("normalized loaded incident name: '"+self.incidentNameNormalized+"'") self.ui.incidentNameLabel.setText(self.incidentName) if not row[0].startswith('#'): # prune comment lines + if len(row)<9: + raise Exception('Row does not contain enough columns; the file may be corrupted.\n File:'+fName+'\n Row:'+str(row)) totalEntries=totalEntries+1 progressBox.setMaximum(totalEntries+14) progressBox.setValue(1) @@ -4056,7 +4066,7 @@ def load(self,fileName=None,bakAttempt=0): loadedRadioLog=[] i=0 for row in csvReader: - if not row[0].startswith('#'): # prune comment lines + if not row[0].startswith('#') and len(row)>9: # prune comment lines and lines with not enough elements row[6]=float(row[6]) # convert epoch seconds back to float, for sorting row += [''] * (colCount-len(row)) # pad the row up to 10 or 11 elements if needed, to avoid index errors elsewhere loadedRadioLog.append(row) From c383a417ccf102599ad85227604118f4a98efd89 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Wed, 28 Jun 2023 06:53:17 -0700 Subject: [PATCH 16/23] more cleanup after merge-commit - add passive mic bump filtering for NXDN - add call to fsLogUpdate for NXDN on non-mic-bump calls - add callsign to fsFilteredCallDisplay call for NXDN - allow blank dev handling in getCallsign (for NXDN) - add first-non-mic-bump-call raise-CCD handling for NXDN --- radiolog.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/radiolog.py b/radiolog.py index cccb5d5..0764dc6 100644 --- a/radiolog.py +++ b/radiolog.py @@ -2188,6 +2188,7 @@ def fsParse(self): # even for a mic bump, the second packet is identical to the first, so set() will only have one member; # if set length != 1 then we know there's garbled data and there's nothing else we can do here packetSet=set(re.findall('\x02I[0-1]([0-9]{6,19})\x03',line)) + # rprint('FleetSync packetSet: '+str(list(packetSet))) if len(packetSet)>1: rprint('FLEETSYNC ERROR: data appears garbled; there are two complete but non-identical CID packets. Skipping this message.') return @@ -2197,7 +2198,7 @@ def fsParse(self): packet=packetSet.pop() count=line.count(packet) # rprint('packet:'+str(packet)) - # rprint('packet count on this line:'+str(count)) + # rprint('packet count on this line: '+str(count)) # 2. within a well-defined packed, the 7-digit fid (fleet&ID) should begin at index 0 (first character) fid=packet[0:7] # returns indices 0 thru 6 = 7 digits @@ -2213,7 +2214,7 @@ def fsParse(self): # passive mic bump filter: if BOT and EOT packets are in the same line, return without opening a new dialog if count>1: - rprint(' Mic bump filtered from '+callsign) + rprint(' Mic bump filtered from '+callsign+' (FleetSync)') self.fsFilteredCallDisplay() # blank for a tenth of a second in case of repeated bumps QTimer.singleShot(200,lambda:self.fsFilteredCallDisplay('bump',fleet,dev,callsign)) QTimer.singleShot(5000,self.fsFilteredCallDisplay) # no arguments will clear the display @@ -2224,7 +2225,10 @@ def fsParse(self): rprint("FleetSync CID detected (not in $PKLSH): fleet="+fleet+" dev="+dev+" callsign="+callsign) elif '\x02gI' in line: # NEXEDGE CID - similar to above - packetSet=set(re.findall(r'\x02gI[0-1]U([0-9]{5})U\1\x03',line)) + match=re.findall('\x02gI[0-1](U\d{5}U\d{5})\x03',line) + # rprint('match:'+str(match)) + packetSet=set(match) + # rprint('NXDN packetSet: '+str(list(packetSet))) if len(packetSet)>1: rprint('NEXEDGE ERROR: data appears garbled; there are two complete but non-identical CID packets. Skipping this message.') return @@ -2233,9 +2237,21 @@ def fsParse(self): return packet=packetSet.pop() count=line.count(packet) - uid=packet[0:5] + # rprint('packet:'+str(packet)) + # rprint('packet count on this line: '+str(count)) + uid=packet[1:6] # 'U' not included - this is a 5-character string of integers callsign=self.getCallsign(uid) + # passive mic bump filter: if BOT and EOT packets are in the same line, return without opening a new dialog + if count>1: + rprint(' Mic bump filtered from '+callsign+' (NXDN)') + self.fsFilteredCallDisplay() # blank for a tenth of a second in case of repeated bumps + QTimer.singleShot(200,lambda:self.fsFilteredCallDisplay('bump',None,uid,callsign)) + QTimer.singleShot(5000,self.fsFilteredCallDisplay) # no arguments will clear the display + self.fsLogUpdate(uid=uid,bump=True) + self.sendPendingGet() # while getString will be non-empty if this bump had GPS, it may still have the default callsign + return + rprint('NEXEDGE CID detected (not in $PKNSH): id='+uid+' callsign='+callsign) # if any new entry dialogs are already open with 'from' and the @@ -2296,9 +2312,12 @@ def fsParse(self): self.openNewEntry('fs',callsign,formattedLocString,fleet,dev,origLocString) self.sendPendingGet() elif uid: + self.fsLogUpdate(uid=uid) + # only open a new entry widget if none is alredy open within the continue time, + # and the fleet/dev is not being filtered if not found: if self.fsIsFiltered('',uid): - self.fsFilteredCallDisplay('on','',uid) + self.fsFilteredCallDisplay('on','',uid,callsign) QTimer.singleShot(5000,self.fsFilteredCallDisplay) # no arguments will clear the display else: self.openNewEntry('nex',callsign,formattedLocString,None,uid,origLocString) @@ -2606,7 +2625,7 @@ def getCallsign(self,fleetOrUid,dev=None): rprint('getCallsign called for FleetSync fleet='+str(fleetOrUid)+' dev='+str(dev)) fleet=fleetOrUid for entry in self.fsLookup: - if int(entry[0])==int(fleet) and int(entry[1])==int(dev): + if entry[0] and int(entry[0])==int(fleet) and entry[1] and int(entry[1])==int(dev): # check each potential match against existing matches before adding to the list of matches found=False for match in matches: @@ -2628,7 +2647,7 @@ def getCallsign(self,fleetOrUid,dev=None): uid=fleetOrUid rprint('getCallsign called for NXDN UID='+str(uid)) for entry in self.fsLookup: - if int(entry[1])==uid: + if entry[1]==uid: # check each potential match against existing matches before adding to the list of matches found=False for match in matches: @@ -4353,8 +4372,11 @@ def openNewEntry(self,key=None,callsign=None,formattedLocString=None,fleet=None, if callsign[0:3]=='KW-': self.newEntryWidget.ui.teamField.setFocus() self.newEntryWidget.ui.teamField.selectAll() + rprint('fsLog:') + rprint(str(self.fsLog)) # i[7] = total call count; i[6] = mic bump count; we want to look at the total non-bump count, i[7]-i[6] - if fleet and dev and len([i for i in self.fsLog if i[0]==str(fleet) and i[1]==str(dev) and (i[7]-i[6])<2])>0: # this is the device's first non-mic-bump call + if (fleet and dev and len([i for i in self.fsLog if i[0]==str(fleet) and i[1]==str(dev) and (i[7]-i[6])<2])>0) or \ + (dev and not fleet and len([i for i in self.fsLog if i[0]=='' and i[1]==str(dev) and (i[7]-i[6])<2])>0) : # this is the device's first non-mic-bump call rprint('First non-mic-bump call from this device.') found=False for i in self.CCD1List: From 3006ab14c54d7a83d186c3a3776002d2bb223cb9 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Wed, 28 Jun 2023 06:54:04 -0700 Subject: [PATCH 17/23] more cleanup - comments only - comment out a few print lines --- radiolog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radiolog.py b/radiolog.py index 0764dc6..b820811 100644 --- a/radiolog.py +++ b/radiolog.py @@ -4372,8 +4372,8 @@ def openNewEntry(self,key=None,callsign=None,formattedLocString=None,fleet=None, if callsign[0:3]=='KW-': self.newEntryWidget.ui.teamField.setFocus() self.newEntryWidget.ui.teamField.selectAll() - rprint('fsLog:') - rprint(str(self.fsLog)) + # rprint('fsLog:') + # rprint(str(self.fsLog)) # i[7] = total call count; i[6] = mic bump count; we want to look at the total non-bump count, i[7]-i[6] if (fleet and dev and len([i for i in self.fsLog if i[0]==str(fleet) and i[1]==str(dev) and (i[7]-i[6])<2])>0) or \ (dev and not fleet and len([i for i in self.fsLog if i[0]=='' and i[1]==str(dev) and (i[7]-i[6])<2])>0) : # this is the device's first non-mic-bump call From bd4c7e3fec267b3f60e30d8d5bab6c354644e658 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Thu, 29 Jun 2023 21:05:06 -0700 Subject: [PATCH 18/23] WIP - all NXDN features appear to be working some cleanup still needed, to be followed by full QA - see github issue comments --- radiolog.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/radiolog.py b/radiolog.py index b820811..e0e9bcd 100644 --- a/radiolog.py +++ b/radiolog.py @@ -5151,6 +5151,7 @@ def tabContextMenu(self,pos): def sendText(self,fleetOrListOrAll,device=None,message=None): + rprint('sendText called: fleetOrListOrAll='+str(fleetOrListOrAll)+' device='+str(device)+' message='+str(message)) self.fsTimedOut=False self.fsResponseMessage='' broadcast=False @@ -5178,7 +5179,8 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): if broadcast: # portable radios will not attempt to send acknowledgement for broadcast rprint('broadcasting text message to all devices') - d='\x02\x460000000'+timestamp+' '+message+'\x03' + # d='\x02\x460000000'+timestamp+' '+message+'\x03' + d='\x02gFG00000'+timestamp+' '+message+'\x03' rprint('com data: '+str(d)) suffix=' using one mobile radio' self.firstComPort.write(d.encode()) @@ -5200,20 +5202,25 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): box.exec_() else: # recipient portable will send acknowledgement when fleet and device ase specified - for [fleet,device] in self.fsSendList: + for [fleetOrNone,device] in self.fsSendList: # values format for adding a new entry: # [time,to_from,team,message,self.formattedLocString,status,self.sec,self.fleet,self.dev,self.origLocString] values=["" for n in range(10)] - callsignText=self.getCallsign(fleet,device) + if fleetOrNone: # fleetsync + callsignText=self.getCallsign(fleetOrNone,device) + rprint('sending FleetSync text message to fleet='+str(fleet)+' device='+str(device)+' '+callsignText) + d='\x02F'+str(fleetOrNone)+str(device)+timestamp+' '+message+'\x03' + else: # NXDN + callsignText=self.getCallsign(device,None) + rprint('sending NXDN text message to device='+str(device)+' '+callsignText) + d='\x02gFU'+str(device)+timestamp+' '+message+'\x03' values[2]=str(callsignText) if callsignText: callsignText='('+callsignText+')' else: callsignText='(no callsign)' - rprint('sending text message to fleet='+str(fleet)+' device='+str(device)+' '+callsignText) - d='\x02\x46'+str(fleet)+str(device)+timestamp+' '+message+'\x03' rprint('com data: '+str(d)) - fsFirstPortToTry=self.fsGetLatestComPort(fleet,device) or self.firstComPort + fsFirstPortToTry=self.fsGetLatestComPort(fleetOrNone,device) or self.firstComPort if fsFirstPortToTry==self.firstComPort: self.fsSecondPortToTry=self.secondComPort # could be None; inst var so fsCheck can see it else: @@ -5224,7 +5231,7 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): # rprint('1: fsThereWillBeAnotherTry='+str(self.fsThereWillBeAnotherTry)) fsFirstPortToTry.write(d.encode()) # if self.fsSendData(d,fsFirstPortToTry): - self.fsAwaitingResponse=[fleet,device,'Text message sent',0,message] + self.fsAwaitingResponse=[fleetOrNone,device,'Text message sent',0,message] [f,dev,t]=self.fsAwaitingResponse[0:3] self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.NoIcon,t,t+' to '+str(f)+':'+str(dev)+' on preferred COM port; awaiting response for '+str(self.fsAwaitingResponseTimeout)+' more seconds...', QMessageBox.Abort,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) From a10e53ad8bc4288f9cd75ee5e171a59c44dbad0a Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 30 Jun 2023 07:31:58 -0700 Subject: [PATCH 19/23] various cleanup of printed messages - use NEXEDGE rather than NXDN everywhere except default callsign where space is at a premium - show NEXEDGE vs FLEETSYNC / FleetSync in entries and dialogs as appropriate --- radiolog.py | 51 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/radiolog.py b/radiolog.py index e0e9bcd..8682852 100644 --- a/radiolog.py +++ b/radiolog.py @@ -1805,7 +1805,7 @@ def fsCheck(self): rprint(" VALID FLEETSYNC DATA!!!") valid=True elif '\x02gI' in tmpData: - rprint(' VALID NEXEDGE NXDN DATA!!!') + rprint(' VALID NEXEDGE DATA!!!') valid=True # NEXEDGE format (e.g. for ID 03001; NXDN has no concept of fleet:device - just 5-decimal-digit unit ID, max=65536 (4 hex characters)) # BOT CID: ☻gI1U03001U03001♥ @@ -2244,7 +2244,7 @@ def fsParse(self): # passive mic bump filter: if BOT and EOT packets are in the same line, return without opening a new dialog if count>1: - rprint(' Mic bump filtered from '+callsign+' (NXDN)') + rprint(' Mic bump filtered from '+callsign+' (NEXEDGE)') self.fsFilteredCallDisplay() # blank for a tenth of a second in case of repeated bumps QTimer.singleShot(200,lambda:self.fsFilteredCallDisplay('bump',None,uid,callsign)) QTimer.singleShot(5000,self.fsFilteredCallDisplay) # no arguments will clear the display @@ -2645,7 +2645,7 @@ def getCallsign(self,fleetOrUid,dev=None): elif len(fleetOrUid)==5: # 5 characters - must be NEXEDGE uid=fleetOrUid - rprint('getCallsign called for NXDN UID='+str(uid)) + rprint('getCallsign called for NEXEDGE UID='+str(uid)) for entry in self.fsLookup: if entry[1]==uid: # check each potential match against existing matches before adding to the list of matches @@ -4993,7 +4993,10 @@ def tabContextMenu(self,pos): self.fsFilterEdit(d[0],d[1],d[2].lower()=='filter') self.fsBuildTeamFilterDict() elif d[2]=='SendText': - callsignText=self.getCallsign(d[0],d[1]) + if d[0]: # fleetsync + callsignText=self.getCallsign(d[0],d[1]) + else: # nxdn + callsignText=self.getCallsign(d[1],None) if callsignText: callsignText='('+callsignText+')' else: @@ -5179,8 +5182,8 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): if broadcast: # portable radios will not attempt to send acknowledgement for broadcast rprint('broadcasting text message to all devices') - # d='\x02\x460000000'+timestamp+' '+message+'\x03' - d='\x02gFG00000'+timestamp+' '+message+'\x03' + # d='\x02F0000000'+timestamp+' '+message+'\x03' # fleetsync + d='\x02gFG00000'+timestamp+' '+message+'\x03' # nexedge rprint('com data: '+str(d)) suffix=' using one mobile radio' self.firstComPort.write(d.encode()) @@ -5203,6 +5206,7 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): else: # recipient portable will send acknowledgement when fleet and device ase specified for [fleetOrNone,device] in self.fsSendList: + aborted=False # values format for adding a new entry: # [time,to_from,team,message,self.formattedLocString,status,self.sec,self.fleet,self.dev,self.origLocString] values=["" for n in range(10)] @@ -5212,7 +5216,7 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): d='\x02F'+str(fleetOrNone)+str(device)+timestamp+' '+message+'\x03' else: # NXDN callsignText=self.getCallsign(device,None) - rprint('sending NXDN text message to device='+str(device)+' '+callsignText) + rprint('sending NEXEDGE text message to device='+str(device)+' '+callsignText) d='\x02gFU'+str(device)+timestamp+' '+message+'\x03' values[2]=str(callsignText) if callsignText: @@ -5233,19 +5237,28 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): # if self.fsSendData(d,fsFirstPortToTry): self.fsAwaitingResponse=[fleetOrNone,device,'Text message sent',0,message] [f,dev,t]=self.fsAwaitingResponse[0:3] - self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.NoIcon,t,t+' to '+str(f)+':'+str(dev)+' on preferred COM port; awaiting response for '+str(self.fsAwaitingResponseTimeout)+' more seconds...', + if f: + h='FLEETSYNC' + h2='FleetSync' + sep=':' + else: + h='NEXEDGE' + h2='NEXEDGE' + sep='' + self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.NoIcon,h2+' '+t,h2+' '+t+' to '+str(f)+sep+str(dev)+' on preferred COM port; awaiting response for '+str(self.fsAwaitingResponseTimeout)+' more seconds...', QMessageBox.Abort,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) self.fsAwaitingResponseMessageBox.show() self.fsAwaitingResponseMessageBox.raise_() self.fsAwaitingResponseMessageBox.exec_() # add a log entry when Abort is pressed if self.fsAwaitingResponse and not self.fsTimedOut: + aborted=True values[0]=time.strftime("%H%M") values[1]='TO' - values[3]='FLEETSYNC: Text message sent to '+str(f)+':'+str(dev)+' '+callsignText+' but radiolog operator clicked Abort before delivery could be confirmed: "'+str(message)+'"' + values[3]=h+': Text message sent to '+str(f)+sep+str(dev)+' '+callsignText+' but radiolog operator clicked Abort before delivery could be confirmed: "'+str(message)+'"' values[6]=time.time() self.newEntry(values) - self.fsResponseMessage+='\n\n'+str(f)+':'+str(dev)+' '+callsignText+': radiolog operator clicked Abort before delivery could be confirmed' + self.fsResponseMessage+='\n\n'+str(f)+sep+str(dev)+' '+callsignText+': radiolog operator clicked Abort before delivery could be confirmed' if self.fsFailedFlag: # timed out, or, got a '1' response if self.fsSecondPortToTry: rprint('failed on preferred COM port; sending on alternate COM port') @@ -5255,7 +5268,7 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): self.fsThereWillBeAnotherTry=False # rprint('2: fsThereWillBeAnotherTry='+str(self.fsThereWillBeAnotherTry)) self.fsAwaitingResponse[3]=0 # reset the timer - self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.NoIcon,t,t+' to '+str(f)+':'+str(dev)+' on alternate COM port; awaiting response up to '+str(self.fsAwaitingResponseTimeout)+' seconds...', + self.fsAwaitingResponseMessageBox=QMessageBox(QMessageBox.NoIcon,t,t+' to '+str(f)+sep+str(dev)+' on alternate COM port; awaiting response up to '+str(self.fsAwaitingResponseTimeout)+' seconds...', QMessageBox.Abort,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) self.fsAwaitingResponseMessageBox.show() self.fsAwaitingResponseMessageBox.raise_() @@ -5264,23 +5277,23 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): if self.fsAwaitingResponse and not self.fsTimedOut: values[0]=time.strftime("%H%M") values[1]='TO' - values[3]='FLEETSYNC: Text message sent to '+str(f)+':'+str(dev)+' '+callsignText+' but radiolog operator clicked Abort before delivery could be confirmed: "'+str(message)+'"' + values[3]=h+': Text message sent to '+str(f)+sep+str(dev)+' '+callsignText+' but radiolog operator clicked Abort before delivery could be confirmed: "'+str(message)+'"' values[6]=time.time() self.newEntry(values) - self.fsResponseMessage+='\n\n'+str(f)+':'+str(dev)+' '+callsignText+': radiolog operator clicked Abort before delivery could be confirmed' + self.fsResponseMessage+='\n\n'+str(f)+sep+str(dev)+' '+callsignText+': radiolog operator clicked Abort before delivery could be confirmed' if self.fsFailedFlag: # timed out, or, got a '1' response rprint('failed on alternate COM port: message delivery not confirmed') else: rprint('apparently successful on alternate COM port') - self.fsResponseMessage+='\n\n'+str(f)+':'+str(dev)+' '+callsignText+': delivery confirmed' + self.fsResponseMessage+='\n\n'+str(f)+sep+str(dev)+' '+callsignText+': delivery confirmed' else: rprint('failed on preferred COM port; no alternate COM port available') - else: + elif not aborted: rprint('apparently successful on preferred COM port') - self.fsResponseMessage+='\n\n'+str(f)+':'+str(dev)+' '+callsignText+': delivery confirmed' + self.fsResponseMessage+='\n\n'+str(f)+sep+str(dev)+' '+callsignText+': delivery confirmed' self.fsAwaitingResponse=None # clear the flag - this will happen after the messagebox is closed (due to valid response, or timeout in fsCheck, or Abort clicked) if self.fsResponseMessage: - box=QMessageBox(QMessageBox.Information,'FleetSync Response Summary','FleetSync response summary:'+self.fsResponseMessage, + box=QMessageBox(QMessageBox.Information,h2+' Response Summary',h2+' response summary:'+self.fsResponseMessage, QMessageBox.Close,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) box.open() box.raise_() @@ -7890,11 +7903,15 @@ def __init__(self,parent,fdcList): # fdcList is a list of [fleet,dev,callsignTex self.ui.messageField.setValidator(QRegExpValidator(QRegExp('.{1,36}'),self.ui.messageField)) if len(fdcList)==1: [fleet,device,callsignText]=fdcList[0] + if not fleet: + fleet='NEXEDGE' self.ui.theLabel.setText('Message for '+str(fleet)+':'+str(device)+' '+str(callsignText)+':') else: label='Message for multiple radios:' for fdc in fdcList: [fleet,device,callsignText]=fdc + if not fleet: + fleet='NEXEDGE' label+='\n '+str(fleet)+':'+str(device)+' '+str(callsignText) self.ui.theLabel.setText(label) From d38fb9dd95ab6051ba5c1d33f7f48a9b1047b8f2 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Fri, 30 Jun 2023 10:00:21 -0700 Subject: [PATCH 20/23] more cleanup of text dialogs - call getCallsign correctly for NXDN for multiple devices - same as recent NXDN edit for single device - add a warning message about possible lengthy delay when sending to multiple specific radios --- radiolog.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/radiolog.py b/radiolog.py index 8682852..20de8d7 100644 --- a/radiolog.py +++ b/radiolog.py @@ -1765,7 +1765,7 @@ def fsCheck(self): box.raise_() box.exec_() else: - self.fsResponseMessage+='\n\n'+idStr+' '+callsignText+':'+msg + self.fsResponseMessage+='\n\n'+idStr+' '+callsignText+': '+msg else: remaining=self.fsAwaitingResponseTimeout-self.fsAwaitingResponse[3] suffix='' @@ -5017,7 +5017,10 @@ def tabContextMenu(self,pos): elif action==fsSendTextToAllAction: theList=[] for device in self.fsGetTeamDevices(extTeamName): - callsignText=self.getCallsign(device[0],device[1]) + if device[0]: # fleetsync + callsignText=self.getCallsign(device[0],device[1]) + else: # nxdn + callsignText=self.getCallsign(device[1],None) if callsignText: callsignText='('+callsignText+')' else: @@ -7907,7 +7910,7 @@ def __init__(self,parent,fdcList): # fdcList is a list of [fleet,dev,callsignTex fleet='NEXEDGE' self.ui.theLabel.setText('Message for '+str(fleet)+':'+str(device)+' '+str(callsignText)+':') else: - label='Message for multiple radios:' + label='Message for multiple radios:\n\n(NOTE: This could take a while: the system will wait up to '+str(self.parent.fsAwaitingResponseTimeout)+' seconds for an acknowledge response from each radio. If each acknowledge is quick, this might just take a second or so per radio. Consider whether this is what you really want to do.)\n' for fdc in fdcList: [fleet,device,callsignText]=fdc if not fleet: From 4b8727c3313ab147d669a8493b91b4f860a5796d Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Sat, 1 Jul 2023 12:30:31 -0700 Subject: [PATCH 21/23] fleetsync send console updates to handle NXDN - add FleetSync/NEXEDGE radio buttons, disabled for Send to All add vertical line under Send to All, to clarify that FleetSync vs NEXEDGE radio button doesn't apply to 'Send to All' - set validators, modify enable state of fields and labels, etc as a function of radio buttons and checkbox - all in one function updateGUI --- designer/fsSendDialog.ui | 100 ++++++++++++++++++++++++++++++++------- radiolog.py | 46 +++++++++++------- ui/fsSendDialog_ui.py | 77 +++++++++++++++++++++--------- 3 files changed, 167 insertions(+), 56 deletions(-) diff --git a/designer/fsSendDialog.ui b/designer/fsSendDialog.ui index 176adc4..e5ec882 100644 --- a/designer/fsSendDialog.ui +++ b/designer/fsSendDialog.ui @@ -142,14 +142,63 @@ - + + + Qt::Horizontal + + + + + + + + + 0 + + + + + + Segoe UI + 12 + + + + FleetSync + + + true + + + buttonGroup_3 + + + + + + + + Segoe UI + 12 + + + + NEXEDGE + + + buttonGroup_3 + + + + + 0 - + Segoe UI @@ -223,7 +272,7 @@ QLayout::SetDefaultConstraint - + Segoe UI @@ -242,7 +291,7 @@ - + Segoe UI @@ -322,8 +371,8 @@ reject() - 304 - 320 + 315 + 374 286 @@ -335,15 +384,15 @@ sendTextRadioButton toggled(bool) fsSendDialog - functionChanged() + updateGUI() - 259 - 47 + 306 + 28 - 315 - 93 + 386 + 42 @@ -351,15 +400,31 @@ sendToAllCheckbox toggled(bool) fsSendDialog - sendAllCheckboxChanged() + updateGUI() - 219 - 136 + 251 + 117 - 315 - 140 + 383 + 115 + + + + + fsRadioButton + toggled(bool) + fsSendDialog + updateGUI() + + + 59 + 168 + + + 111 + 217 @@ -367,8 +432,11 @@ functionChanged() sendAllCheckboxChanged() + fsNxRadioButtonChanged() + updateGUI() + diff --git a/radiolog.py b/radiolog.py index 20de8d7..36761d0 100644 --- a/radiolog.py +++ b/radiolog.py @@ -7809,30 +7809,42 @@ def __init__(self,parent): # need to connect apply signal by hand https://stackoverflow.com/a/35444005 btn=self.ui.buttonBox.button(QDialogButtonBox.Apply) btn.clicked.connect(self.apply) - self.ui.fleetField.setValidator(QRegExpValidator(QRegExp('[1-9][0-9][0-9]'),self.ui.fleetField)) # 36 character max length - see sendText notes self.ui.messageField.setValidator(QRegExpValidator(QRegExp('.{1,36}'),self.ui.messageField)) - self.functionChanged() # to set device validator + self.updateGUI() # to set device validator - def functionChanged(self): + def updateGUI(self): sendText=self.ui.sendTextRadioButton.isChecked() + sendAll=self.ui.sendToAllCheckbox.isChecked() + fs=self.ui.fsRadioButton.isChecked() self.ui.sendToAllCheckbox.setEnabled(sendText) - self.ui.messageField.setEnabled(sendText) - # if sending, a comma-or-space-delimited list of device IDs can be specified; - # if polling GPS, only one device ID can be specified + self.ui.fsRadioButton.setEnabled(not sendAll) + self.ui.nxRadioButton.setEnabled(not sendAll) + self.ui.fleetField.setEnabled(fs and not sendAll) + self.ui.fleetLabel.setEnabled(fs and not sendAll) + self.ui.deviceField.setEnabled(not sendAll) + self.ui.deviceLabel.setEnabled(not sendAll) + digitRE='[0-9]' + if fs: + deviceDigits=4 + firstDigitRE=digitRE + else: + deviceDigits=5 + firstDigitRE='[1-9]' + deviceSuffix='(s)' + if not sendText: + deviceSuffix='' + self.ui.deviceLabel.setText(str(deviceDigits)+'-digit Device ID'+deviceSuffix) + # allow multiple devices when sending text, but just one when polling GPS + coreRE=firstDigitRE+(digitRE*(deviceDigits-1)) # '[1-9][0-9][0-9][0-9]' or '[0-9][0-9][0-9][0-9][0-9]' if sendText: - self.ui.deviceLabel.setText('Device ID(s)') - self.ui.deviceField.setValidator(QRegExpValidator(QRegExp('^([1-9][0-9][0-9][0-9][, ]?)*$'),self.ui.deviceField)) + devValRE='^('+coreRE+'[, ]?)*$' else: - self.ui.sendToAllCheckbox.setChecked(False) # this should automatically enable fleet/device fields - self.ui.deviceField.setText('') # easier than overriding fixup() in a custom validator - self.ui.deviceLabel.setText('Device ID') - self.ui.deviceField.setValidator(QRegExpValidator(QRegExp('[1-9][0-9][0-9][0-9]'),self.ui.deviceField)) - - def sendAllCheckboxChanged(self): - sendAll=self.ui.sendToAllCheckbox.isChecked() - self.ui.fleetField.setEnabled(not sendAll) - self.ui.deviceField.setEnabled(not sendAll) + devValRE=coreRE + self.ui.deviceField.setValidator(QRegExpValidator(QRegExp(devValRE),self.ui.deviceField)) + self.ui.messageField.setEnabled(sendText) + self.ui.messageLabel1.setEnabled(sendText) + self.ui.messageLabel2.setEnabled(sendText) def apply(self): if not self.parent.firstComPort: diff --git a/ui/fsSendDialog_ui.py b/ui/fsSendDialog_ui.py index 3de4c64..e82dd21 100644 --- a/ui/fsSendDialog_ui.py +++ b/ui/fsSendDialog_ui.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'fsSendDialog.ui' +# Form implementation generated from reading ui file 'C:\Users\caver\Documents\GitHub\radiolog\designer\fsSendDialog.ui' # # Created by: PyQt5 UI code generator 5.15.6 # @@ -69,19 +69,47 @@ def setupUi(self, fsSendDialog): spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_3.addItem(spacerItem3) self.verticalLayout_5.addLayout(self.horizontalLayout_3) + self.line_2 = QtWidgets.QFrame(fsSendDialog) + self.line_2.setFrameShape(QtWidgets.QFrame.HLine) + self.line_2.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line_2.setObjectName("line_2") + self.verticalLayout_5.addWidget(self.line_2) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") + self.verticalLayout_7 = QtWidgets.QVBoxLayout() + self.verticalLayout_7.setSpacing(0) + self.verticalLayout_7.setObjectName("verticalLayout_7") + self.fsRadioButton = QtWidgets.QRadioButton(fsSendDialog) + font = QtGui.QFont() + font.setFamily("Segoe UI") + font.setPointSize(12) + self.fsRadioButton.setFont(font) + self.fsRadioButton.setChecked(True) + self.fsRadioButton.setObjectName("fsRadioButton") + self.buttonGroup_3 = QtWidgets.QButtonGroup(fsSendDialog) + self.buttonGroup_3.setObjectName("buttonGroup_3") + self.buttonGroup_3.addButton(self.fsRadioButton) + self.verticalLayout_7.addWidget(self.fsRadioButton) + self.nxRadioButton = QtWidgets.QRadioButton(fsSendDialog) + font = QtGui.QFont() + font.setFamily("Segoe UI") + font.setPointSize(12) + self.nxRadioButton.setFont(font) + self.nxRadioButton.setObjectName("nxRadioButton") + self.buttonGroup_3.addButton(self.nxRadioButton) + self.verticalLayout_7.addWidget(self.nxRadioButton) + self.horizontalLayout.addLayout(self.verticalLayout_7) self.verticalLayout_2 = QtWidgets.QVBoxLayout() self.verticalLayout_2.setSpacing(0) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.label = QtWidgets.QLabel(fsSendDialog) + self.fleetLabel = QtWidgets.QLabel(fsSendDialog) font = QtGui.QFont() font.setFamily("Segoe UI") font.setPointSize(12) - self.label.setFont(font) - self.label.setStyleSheet("margin-bottom:-2") - self.label.setObjectName("label") - self.verticalLayout_2.addWidget(self.label) + self.fleetLabel.setFont(font) + self.fleetLabel.setStyleSheet("margin-bottom:-2") + self.fleetLabel.setObjectName("fleetLabel") + self.verticalLayout_2.addWidget(self.fleetLabel) self.fleetField = QtWidgets.QLineEdit(fsSendDialog) font = QtGui.QFont() font.setFamily("Segoe UI") @@ -110,29 +138,29 @@ def setupUi(self, fsSendDialog): self.deviceField.setObjectName("deviceField") self.verticalLayout_3.addWidget(self.deviceField) self.horizontalLayout.addLayout(self.verticalLayout_3) - self.horizontalLayout.setStretch(0, 1) - self.horizontalLayout.setStretch(1, 2) + self.horizontalLayout.setStretch(1, 1) + self.horizontalLayout.setStretch(2, 2) self.verticalLayout_5.addLayout(self.horizontalLayout) self.verticalLayout_4 = QtWidgets.QVBoxLayout() self.verticalLayout_4.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) self.verticalLayout_4.setSpacing(0) self.verticalLayout_4.setObjectName("verticalLayout_4") - self.label_3 = QtWidgets.QLabel(fsSendDialog) + self.messageLabel1 = QtWidgets.QLabel(fsSendDialog) font = QtGui.QFont() font.setFamily("Segoe UI") font.setPointSize(12) - self.label_3.setFont(font) - self.label_3.setStyleSheet("margin-bottom:-2") - self.label_3.setWordWrap(True) - self.label_3.setObjectName("label_3") - self.verticalLayout_4.addWidget(self.label_3) - self.label_4 = QtWidgets.QLabel(fsSendDialog) + self.messageLabel1.setFont(font) + self.messageLabel1.setStyleSheet("margin-bottom:-2") + self.messageLabel1.setWordWrap(True) + self.messageLabel1.setObjectName("messageLabel1") + self.verticalLayout_4.addWidget(self.messageLabel1) + self.messageLabel2 = QtWidgets.QLabel(fsSendDialog) font = QtGui.QFont() font.setFamily("Segoe UI") font.setPointSize(9) - self.label_4.setFont(font) - self.label_4.setObjectName("label_4") - self.verticalLayout_4.addWidget(self.label_4) + self.messageLabel2.setFont(font) + self.messageLabel2.setObjectName("messageLabel2") + self.verticalLayout_4.addWidget(self.messageLabel2) self.messageField = QtWidgets.QLineEdit(fsSendDialog) font = QtGui.QFont() font.setFamily("Segoe UI") @@ -163,8 +191,9 @@ def setupUi(self, fsSendDialog): self.retranslateUi(fsSendDialog) self.buttonBox.rejected.connect(fsSendDialog.reject) # type: ignore - self.sendTextRadioButton.toggled['bool'].connect(fsSendDialog.functionChanged) # type: ignore - self.sendToAllCheckbox.toggled['bool'].connect(fsSendDialog.sendAllCheckboxChanged) # type: ignore + self.sendTextRadioButton.toggled['bool'].connect(fsSendDialog.updateGUI) # type: ignore + self.sendToAllCheckbox.toggled['bool'].connect(fsSendDialog.updateGUI) # type: ignore + self.fsRadioButton.toggled['bool'].connect(fsSendDialog.updateGUI) # type: ignore QtCore.QMetaObject.connectSlotsByName(fsSendDialog) fsSendDialog.setTabOrder(self.sendTextRadioButton, self.pollGPSRadioButton) fsSendDialog.setTabOrder(self.pollGPSRadioButton, self.sendToAllCheckbox) @@ -178,8 +207,10 @@ def retranslateUi(self, fsSendDialog): self.sendTextRadioButton.setText(_translate("fsSendDialog", "Send Text Message")) self.pollGPSRadioButton.setText(_translate("fsSendDialog", "Get Radio Location")) self.sendToAllCheckbox.setText(_translate("fsSendDialog", "Send to All")) - self.label.setText(_translate("fsSendDialog", "Fleet ID")) + self.fsRadioButton.setText(_translate("fsSendDialog", "FleetSync")) + self.nxRadioButton.setText(_translate("fsSendDialog", "NEXEDGE")) + self.fleetLabel.setText(_translate("fsSendDialog", "Fleet ID")) self.deviceLabel.setText(_translate("fsSendDialog", "Device ID(s)")) - self.label_3.setText(_translate("fsSendDialog", "Message")) - self.label_4.setText(_translate("fsSendDialog", " 36 characters max; date and time automatically included")) + self.messageLabel1.setText(_translate("fsSendDialog", "Message")) + self.messageLabel2.setText(_translate("fsSendDialog", " 36 characters max; date and time automatically included")) self.label_2.setText(_translate("fsSendDialog", "Note: any of these functions (except \'Send to All\') can also be done by right-clicking a Team Tab")) From b15dfd8572deb8c5fc946e43bb86ef3a9a24fae8 Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Sat, 1 Jul 2023 12:38:02 -0700 Subject: [PATCH 22/23] fleetsync send console updateGUI cleanup account for the case where Send to All was checked when the first radio button is changed from 'send message' to 'poll for location' --- radiolog.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/radiolog.py b/radiolog.py index 36761d0..d872bd9 100644 --- a/radiolog.py +++ b/radiolog.py @@ -7817,13 +7817,14 @@ def updateGUI(self): sendText=self.ui.sendTextRadioButton.isChecked() sendAll=self.ui.sendToAllCheckbox.isChecked() fs=self.ui.fsRadioButton.isChecked() + IDNeeded=not sendText or not sendAll self.ui.sendToAllCheckbox.setEnabled(sendText) - self.ui.fsRadioButton.setEnabled(not sendAll) - self.ui.nxRadioButton.setEnabled(not sendAll) - self.ui.fleetField.setEnabled(fs and not sendAll) - self.ui.fleetLabel.setEnabled(fs and not sendAll) - self.ui.deviceField.setEnabled(not sendAll) - self.ui.deviceLabel.setEnabled(not sendAll) + self.ui.fsRadioButton.setEnabled(IDNeeded) + self.ui.nxRadioButton.setEnabled(IDNeeded) + self.ui.fleetField.setEnabled(fs and IDNeeded) + self.ui.fleetLabel.setEnabled(fs and IDNeeded) + self.ui.deviceField.setEnabled(IDNeeded) + self.ui.deviceLabel.setEnabled(IDNeeded) digitRE='[0-9]' if fs: deviceDigits=4 From e8fe9616a514da83788fcdbfc229708677dfbebb Mon Sep 17 00:00:00 2001 From: Tom Grundy Date: Tue, 11 Jul 2023 17:53:45 -0700 Subject: [PATCH 23/23] send text to all: fleetsync then nexedge --- radiolog.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/radiolog.py b/radiolog.py index d872bd9..5d97fa8 100644 --- a/radiolog.py +++ b/radiolog.py @@ -5185,15 +5185,22 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): if broadcast: # portable radios will not attempt to send acknowledgement for broadcast rprint('broadcasting text message to all devices') - # d='\x02F0000000'+timestamp+' '+message+'\x03' # fleetsync - d='\x02gFG00000'+timestamp+' '+message+'\x03' # nexedge - rprint('com data: '+str(d)) - suffix=' using one mobile radio' - self.firstComPort.write(d.encode()) - if self.secondComPort: - time.sleep(3) # yes, we do want a blocking sleep - suffix=' using two mobile radios' - self.secondComPort.write(d.encode()) + # - send fleetsync to all com ports, then nexedge to all com ports + # - wait 2 seconds between each send - arbitrary amount of time to let + # the mobile radio(s) recover between transmissions + d_fs='\x02F0000000'+timestamp+' '+message+'\x03' # fleetsync + d_nx='\x02gFG00000'+timestamp+' '+message+'\x03' # nexedge + stringList=[d_fs,d_nx] + for d in stringList: + rprint('com data: '+str(d)) + suffix=' using one mobile radio' + self.firstComPort.write(d.encode()) + if self.secondComPort: + time.sleep(2) # yes, we do want a blocking sleep + suffix=' using two mobile radios' + self.secondComPort.write(d.encode()) + if d!=stringList[-1]: + time.sleep(2) # values format for adding a new entry: # [time,to_from,team,message,self.formattedLocString,status,self.sec,self.fleet,self.dev,self.origLocString] values=["" for n in range(10)] @@ -5201,7 +5208,7 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): values[3]='TEXT MESSAGE SENT TO ALL DEVICES'+suffix+': "'+str(message)+'"' values[6]=time.time() self.newEntry(values) - box=QMessageBox(QMessageBox.Information,'FleetSync Broadcast Sent',values[3]+'\n\nNo confirmation signal is expected. This only indicates that instructions were sent from the computer to the mobile radio, and is not a guarantee that the message was actually transmitted.', + box=QMessageBox(QMessageBox.Information,'FleetSync & NEXEDGE Broadcast Sent',values[3]+'\n\nNo confirmation signal is expected. This only indicates that instructions were sent from the computer to the mobile radio, and is not a guarantee that the message was actually transmitted.', QMessageBox.Close,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint) box.show() box.raise_() @@ -5215,7 +5222,7 @@ def sendText(self,fleetOrListOrAll,device=None,message=None): values=["" for n in range(10)] if fleetOrNone: # fleetsync callsignText=self.getCallsign(fleetOrNone,device) - rprint('sending FleetSync text message to fleet='+str(fleet)+' device='+str(device)+' '+callsignText) + rprint('sending FleetSync text message to fleet='+str(fleetOrNone)+' device='+str(device)+' '+callsignText) d='\x02F'+str(fleetOrNone)+str(device)+timestamp+' '+message+'\x03' else: # NXDN callsignText=self.getCallsign(device,None)