From 44e4bc3a3e288965ae57b65c9df84d1d974b266d Mon Sep 17 00:00:00 2001 From: Mike Brashler Date: Mon, 13 May 2024 10:05:27 -0700 Subject: [PATCH 1/4] Add comments to existing code, DxOperator, TOperatorState --- DxOper.pas | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Station.pas | 5 ++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/DxOper.pas b/DxOper.pas index 3b6f758..6b6009b 100644 --- a/DxOper.pas +++ b/DxOper.pas @@ -14,6 +14,66 @@ interface FULL_PATIENCE = 5; type + { + TOperatorState represents the various states of an independent DxStation/ + DxOperator object. During a pile-up, multiple DxStation objects will exist. + These states represent the operational state of each unique station within + a simulated QSO. + + Each state follows the back and forth transmissions between the user and + an indiviual DxOperator. Remember that DxStation and DxOperator objects + are the simulated stations within the MR simulation. + + State Description + NORMAL FLOW... + osNeedPrevEnd Starting point. This is the initial operator state for a + newly created DxStation. The station will wait for the + completion of any prior QSO's as indicated by either + the user's next CQ call or a 'TU' message. + osNeedQso The DxOperator is waiting for their Dx callsign to be sent + by user. This state begins after the user has either called + CQ or finished the prior QSO by sending a 'TU' message. + For RunMode rmSingle, the CQ message is assumed and + the DxOperator immediately goes into this state + (expecting their callsign to be sent by user). + Typical response msg: send DxStation's callsign. + osNeedNr DxOperator is waiting for user's exchange. + DxOperator has received the user's callsign and is now + waiting to receive the user's exchange. + Typical response: send DxStation's exchange. + osNeedEnd DxStation is waiting for 'TU' from user. + User's call and exchange have been received. + Typical response msg: send DxStation's exchange. + osDone DxOperator has received a 'TU' from the user. + This QSO is now considered complete and can be logged. + + SPECIAL CASES (timeouts, call/exchange copy errors, random events)... + osFailed This QSO has failed. Reasons for failure include: + - DxStation is created and waits for the User to call their + callsign. If the user does not call them within a given + timeframe, the DxOperator will loose Patience and stop + sending their callsign. This is a form of caller ghosting + where the DxOperator gives up due to lack of patience + (occurs whenever Patience decrements to zero). + - user sends a msgNIL, which forces the QSO to fail. + - user sends a msgB4, stating that they had a prior QSO. + osNeedCall DxStation is expecting their call to be corrected by the + user. This state is entered when user sends a partially- + correct callsign. This DxOperator will wait for the correct + call to be sent before sending its Exchange. + The logic also appears to support the fact the user's + exchange (NR) has already been copied by this DxStation. + Once corrected, we should send 'R '. + Typical response msg: send DxStation's callsign + osNeedCallNr DxStation is expecting both their callsign and Exchange + to be sent by user. + This state is entered when the DxStation receives a + partially-correct callsign from the user. In this case, + the QSO advances from osNeedQso to osNeedCallNr. + Once the correct callsign is received, the next state will + be osNeedNr. + Typical response msg: DxStation's callsign + } TOperatorState = (osNeedPrevEnd, osNeedQso, osNeedNr, osNeedCall, osNeedCallNr, osNeedEnd, osDone, osFailed); @@ -27,7 +87,11 @@ TDxOperator = class public Call: string; Skills: integer; - Patience: integer; + Patience: integer; // Number of times operator will retry before leaving. + // Decremented to zero upon each evTimeout. + // When it reaches zero, the operator will ghost and its + // TDxOperator.State set to osFailed. + // Patience is increased with calls to MorePatience. RepeatCnt: integer; State: TOperatorState; function GetSendDelay: integer; @@ -106,6 +170,11 @@ function TDxOperator.GetName: string; end; +{ + Returns the amount of time to wait for a reply after sending a transmission. + This is in units of block counts. A new block is fetched by the audio + system as needed to keep the audio stream full (See TContest.GetAudio). +} function TDxOperator.GetReplyTimeout: integer; begin if RunMode = rmHst then @@ -116,7 +185,11 @@ function TDxOperator.GetReplyTimeout: integer; end; - +{ + DecPatience is typically called after an evTimeout event. + The TDxOperator.Patience value is decremented down to zero. + When this count reaches zero, the DxStation is deleted from the simulation. +} procedure TDxOperator.DecPatience; begin if State = osDone then Exit; @@ -126,9 +199,34 @@ procedure TDxOperator.DecPatience; end; +{ + Calling this function will set the new State and compute a new Patience + value to represent how patient this operator will be while waiting for + a subsequent transmission from the user. + + SetState will: + - set the operator State - See TOperatorState. + - set Patience value - represents operator patience while waiting + for response from user. + - For osNeedQso, Patience is set to a random value using a + Rayleigh distribution within the range of [1, 14] retries, + with a Mean value of 4. + - For all other states, Patience is set to 5. + + This function is typically called by TDxOperator.MsgReceived() whenever + new TStationMessages are being sent to this DxStation/DxOperator by the + simulation engine. +} procedure TDxOperator.SetState(AState: TOperatorState); begin State := AState; + + { + Patience, set below, represents how long a station will stay around to + complete a QSO with the user. FULL_PATIENCE = 5. Patience is the number of + TimeOut events to occur before this station will disappear. + A TimeOut is typically in the range of 3-6 seconds (See GetReplyTimeout). + } if AState = osNeedQso then Patience := Round(RndRayleigh(4)) else Patience := FULL_PATIENCE; diff --git a/Station.pas b/Station.pas index e933cd5..4ebced3 100644 --- a/Station.pas +++ b/Station.pas @@ -48,7 +48,10 @@ TStation = class (TCollectionItem) procedure SetPitch(const Value: integer); protected SendPos: integer; - TimeOut: integer; + TimeOut: integer; // remaining Ticks until evTimeout occurs. + // A Tick occurs whenever an audio block is requested. + // TStation.Tick() calls ProcessEvent(evTimeout) whenever + // Timeout decrements to zero. NrWithError: boolean; R1 : Single; // holds a Random number; used in NrAsText procedure Init; From 0be0a159218aaad08d3d77741b4c5a6a562ca17b Mon Sep 17 00:00:00 2001 From: Mike Brashler Date: Mon, 13 May 2024 10:16:51 -0700 Subject: [PATCH 2/4] Fix caller ghosting after entering call and exchange info This is the first of three parts to fix the ghosting issue. Here, one of the root causes of caller ghosting is when the DxOperator.Patience value was not increased after receiving the last transmission from user. Thus, it would timeout sooner than expected. To fix this problem, the Patience value increased by calling TDxOperator.MorePatience. This is basically saying that when the DxOperator receives a transmission from the User, they will continue to work the QSO without loosing patience and giving up - thus the notion of "more patience" in introduced. This fixes issue #200. See Issue #200 for more information. --- DxOper.pas | 59 +++++++++++++++++++++++++++++++++++++++++++++--------- DxStn.pas | 4 ++-- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/DxOper.pas b/DxOper.pas index 6b6009b..9f5e8e1 100644 --- a/DxOper.pas +++ b/DxOper.pas @@ -83,6 +83,7 @@ interface TDxOperator = class private procedure DecPatience; + procedure MorePatience(AValue: integer = 0); function IsMyCall: TCallCheckResult; public Call: string; @@ -199,6 +200,31 @@ procedure TDxOperator.DecPatience; end; +{ + MorePatience is called to add additional patience while remaining in the + current state. This will happen when a message is received from the user + without an associated state change. Without adding additional patience, + the DxStation will timeout and disappear from the user in the middle of + an ongoing QSO. + + Parameter AValue is an optional Patience value. + If AValue > 0, Patience is set to this value; + else if RunMode = rmSingle, Patience is set to 4; + otherwise Patience is incremented by 2 (up to maximum of 4). +} +procedure TDxOperator.MorePatience(AValue: integer); +begin + if State = osDone then Exit; + + if AValue > 0 then + Patience := Min(AValue, FULL_PATIENCE) + else if RunMode = rmSingle then + Patience := 4 + else + Patience := Min(Patience + 2, 4); +end; + + { Calling this function will set the new State and compute a new Patience value to represent how patient this operator will be while waiting for @@ -341,6 +367,7 @@ procedure TDxOperator.MsgReceived(AMsg: TStationMessages); mcYes: if State in [osNeedPrevEnd, osNeedQso] then SetState(osNeedNr) else if State = osNeedCallNr then SetState(osNeedNr) + else if State in [osNeedNr, osNeedEnd] then MorePatience else if State = osNeedCall then SetState(osNeedEnd); mcAlmost: @@ -366,26 +393,38 @@ procedure TDxOperator.MsgReceived(AMsg: TStationMessages); osNeedPrevEnd: ; osNeedQso: State := osNeedPrevEnd; osNeedNr: if (Random < 0.9) or (RunMode in [rmHst, rmSingle]) then - SetState(osNeedEnd); - osNeedCall: ; + SetState(osNeedEnd) + else + MorePatience; + osNeedCall: MorePatience; osNeedCallNr: if (Random < 0.9) or (RunMode in [rmHst, rmSingle]) then - SetState(osNeedCall); - osNeedEnd: ; + SetState(osNeedCall) + else + MorePatience; + osNeedEnd: MorePatience; end; if msgTU in AMsg then case State of osNeedPrevEnd: SetState(osNeedQso); - osNeedQso: ; - osNeedNr: ; - osNeedCall: ; - osNeedCallNr: ; + osNeedQso: SetState(osNeedQso); + osNeedNr: State := osDone; // may have exchange (NR) error + osNeedCall: State := osDone; // possible partial call match + osNeedCallNr: SetState(osNeedQso); // start over with new QSO osNeedEnd: State := osDone; end; if msgQm in AMsg then - if (State = osNeedPrevEnd) and (Mainform.Edit1.Text = '') then - SetState(osNeedQso); + begin + case State of + osNeedPrevEnd: if Mainform.Edit1.Text = '' then SetState(osNeedQso); + osNeedQso: ; + osNeedNr: MorePatience; + osNeedCall: MorePatience; + osNeedCallNr: MorePatience; + osNeedEnd: MorePatience; + end; + end; if (not Ini.Lids) and (AMsg = [msgGarbage]) then State := osNeedPrevEnd; diff --git a/DxStn.pas b/DxStn.pas index 01ce226..3556873 100644 --- a/DxStn.pas +++ b/DxStn.pas @@ -135,7 +135,7 @@ procedure TDxStation.ProcessEvent(AEvent: TStationEvent); // during debug, use status bar to show CW stream if BDebugCwDecoder or BDebugGhosting then Mainform.sbar.Caption := - (Format('[%s-Timeout]',[MyCall]) + '; ' + + (Format('[%s-osFailed], Stn deleted',[MyCall]) + '; ' + Mainform.sbar.Caption).Substring(0, 80); Free; Exit; @@ -169,7 +169,7 @@ procedure TDxStation.ProcessEvent(AEvent: TStationEvent); // during debug, use status bar to show CW stream if BDebugCwDecoder or BDebugGhosting then Mainform.sbar.Caption := - (Format('[%s-Failed]',[MyCall]) + '; ' + + (Format('[%s-osFailed, Stn deleted]',[MyCall]) + '; ' + Mainform.sbar.Caption).Substring(0, 80); Free; Exit; From f072b851b9c6df5cd51cef130a7e7b5d90abdb1f Mon Sep 17 00:00:00 2001 From: Mike Brashler Date: Mon, 13 May 2024 10:37:11 -0700 Subject: [PATCH 3/4] Fix ghosting by stations leaving the QSO early after calling them This is the second of three parts to fix the ghosting issue. This fix addresses the second root cause of caller ghosting where a Dx station would disappear almost immediately after being created. This would occur about 10% of the time. The user would enter and send the Dx station's callsign, only to discover that the station goes silent and does not return its exchange information. The root cause for this second problem was traced back to a call to the random function RndRayleigh(4). The result of this call is used to set the value of DxOperator.Patience. Patience represents operator patience, an expression of how patient the Dx operator will be while waiting for a responce from the user. RndRayleigh(4) will return a random integer value with a Mean of 4. The problem is, 11% of the time, this value could be 0 or 1. Given a value of 0 or 1, other logic will cause the DxStation would disappear almost immediately. This was fixed by replacing the RndRayleight(4) call with '2 + RndRayleigh(3)'. This change makes sure that a minimum value of 2 is returned. The new Mean is now 5 instead of 4. This fixes issue #200. See Issue #200 for more information. --- DxOper.pas | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/DxOper.pas b/DxOper.pas index 9f5e8e1..afc430e 100644 --- a/DxOper.pas +++ b/DxOper.pas @@ -252,9 +252,25 @@ procedure TDxOperator.SetState(AState: TOperatorState); complete a QSO with the user. FULL_PATIENCE = 5. Patience is the number of TimeOut events to occur before this station will disappear. A TimeOut is typically in the range of 3-6 seconds (See GetReplyTimeout). + + When entering the osNeedQso state, the original code was setting a Patience + value which would cause a station to disappear quickly after its first + transmission (i.e. sending its callsign). This was caused by the original + RndRayleigh(4) distribution below having a result in the range [0,2] about + 6% of the time. + + In May 2024, this was changed to '3 + RndRayleigh(3)' to keep the + station around long enough for the user to respond to a call. + This fixes the so-called ghosting-problem where stations would disappear + almost immediately after sending their callsign for the first time. + See Issue #200 for additional information. + + 0 + RndRayleigh(4) 0+([1,14], mean 4); value 0|1|2 occurs 6% (ghosting) + 3 + RndRayleigh(3) 3+([1,11], mean 3); [4,14], mean 6; value 4 occurs 2.6% + 3 + RndRayleigh(2) 3+([1, 7], mean 2); [4,10], mean 5; value 4 occurs 11% } if AState = osNeedQso - then Patience := Round(RndRayleigh(4)) + then Patience := 3 + Round(RndRayleigh(3)) else Patience := FULL_PATIENCE; if (AState = osNeedQso) and (not (RunMode in [rmSingle, RmHst])) and (Random < 0.1) From 0c6dfa3b9495d743fa3fb5d23cef1ab828cd6899 Mon Sep 17 00:00:00 2001 From: Mike Brashler Date: Mon, 20 May 2024 16:18:01 -0700 Subject: [PATCH 4/4] Fix problem where ghosted calls are not added to log This is the third of three changes addressing the issue of caller ghosting. A ghosted call was originally handled as a failed QSO after the Dx Station timeout out due to User inactivity. Once failed, it was deleted from the simulation before the QSO was logged by the user. With this fix, DxStation will stop sending and remaining in the set of active stations. This allows the QSO to be logged as usual by the user. This fixes issue #200. See Issue #200 for more information. --- DxOper.pas | 38 +++++++++++++++++++++++++++++++++++--- DxStn.pas | 30 +++++++++++++++++++++++++----- Station.pas | 15 +++++++++++++++ 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/DxOper.pas b/DxOper.pas index afc430e..0457bb2 100644 --- a/DxOper.pas +++ b/DxOper.pas @@ -95,6 +95,7 @@ TDxOperator = class // Patience is increased with calls to MorePatience. RepeatCnt: integer; State: TOperatorState; + function IsGhosting: boolean; function GetSendDelay: integer; function GetReplyTimeout: integer; function GetWpm(out AWpmC : integer) : integer; @@ -114,6 +115,25 @@ implementation { TDxOperator } +{ + The notion of ghosting refers to a DxOperator who has run out of + Patience and is leaving the QSO because the User has failed to respond. + This will occur if the User does not respond or continue to interact with + this DxOperator. A station is considered ghosting whenever Patience = 0. + + When a DxStation is ghosting, it will: + - leaving the QSO because User did not complete QSO + - will not send additional transmissions to the user + - will retain in set of active stations so it can still receive messages + from the user. Most often, it is waiting for the final 'TU' message. + - if 'TU' is received, then the station can be added to the log. +} +function TDxOperator.IsGhosting: boolean; +begin + Result := Patience = 0; +end; + + //Delay before reply, keying speed and exchange number are functions //of the operator's skills @@ -189,14 +209,21 @@ function TDxOperator.GetReplyTimeout: integer; { DecPatience is typically called after an evTimeout event. The TDxOperator.Patience value is decremented down to zero. - When this count reaches zero, the DxStation is deleted from the simulation. + When this count reaches zero, the DxStation will start "ghosting" and + stop transmitting. The ghosting station will remain active so it can + receive final messages from user, logged and deleted from the simulation. } procedure TDxOperator.DecPatience; begin if State = osDone then Exit; - Dec(Patience); - if Patience < 1 then State := osFailed; + if Patience > 0 then + Dec(Patience); + + // starting in v1.85, caller ghosting will occur when a QSO has started, but + // has not yet completed. If the QSO has not yet started, set State=osFailed. + if (Patience < 1) and (State in [osNeedPrevEnd, osNeedQso]) then + State := osFailed; end; @@ -451,6 +478,11 @@ procedure TDxOperator.MsgReceived(AMsg: TStationMessages); function TDxOperator.GetReply: TStationMessage; begin + // A ghosting station (Patience=0) will not send any additional messages + assert(not IsGhosting, 'this should not be called when ghosting'); + if IsGhosting then + Result := msgNone + else case State of osNeedPrevEnd, osDone, osFailed: Result := msgNone; osNeedQso: Result := msgMyCall; diff --git a/DxStn.pas b/DxStn.pas index 3556873..ae7d4c9 100644 --- a/DxStn.pas +++ b/DxStn.pas @@ -140,8 +140,18 @@ procedure TDxStation.ProcessEvent(AEvent: TStationEvent); Free; Exit; end; - State := stPreparingToSend; + + if Oper.IsGhosting then + begin + // if the operator is ghosting, this station will stop transmitting. + // force this station's state into stListening mode so it can + // receive final messages from the operator. + State := stListening; + end + else + State := stPreparingToSend; end; + //preparations to send are done, now send if State = stPreparingToSend then for i:=1 to Oper.RepeatCnt do SendMsg(Oper.GetReply) @@ -173,18 +183,28 @@ procedure TDxStation.ProcessEvent(AEvent: TStationEvent); Mainform.sbar.Caption).Substring(0, 80); Free; Exit; - end - else + end; + + if Oper.IsGhosting then begin + // if the operator is ghosting, this station will stop transmitting. + // force this station's state into stListening mode so it can + // receive final messages from the operator. + State := stListening; + end + else begin TimeOut := Oper.GetSendDelay; //reply or switch to standby - State := stPreparingToSend; + State := stPreparingToSend; + end; end; evMeStarted: //If we are not sending, we can start copying //Cancel timeout, he is replying begin - if State <> stSending then + if State <> stSending then begin + assert(State in [stPreparingToSend, stListening]); State := stCopying; + end; TimeOut := NEVER; end; end; diff --git a/Station.pas b/Station.pas index 4ebced3..5e74724 100644 --- a/Station.pas +++ b/Station.pas @@ -20,6 +20,20 @@ interface msgQrl, msgQrl2, msqQsy, msgAgn); TStationMessages = set of TStationMessage; + + { + TStationState represents the operational states of a Station. + + stListening + Station is waiting for Operator (the User) to send a message. + stCopying + Station is in this state while Operator's message is transmitted. + stPreparingToSend + Station is preparing a new message to be sent to the User. + After a brief delay time, this message is transmitted. + stSending + Station is sending it's message to the User. + } TStationState = (stListening, stCopying, stPreparingToSend, stSending); TStationEvent = (evTimeout, evMsgSent, evMeStarted, evMeFinished); @@ -201,6 +215,7 @@ function TStation.GetBlock: TSingleArray; procedure TStation.SendMsg(AMsg: TStationMessage); begin + assert(State in [stPreparingToSend, stSending, stListening]); if Envelope = nil then Msg := []; if AMsg = msgNone then begin State := stListening; Exit; End; Include(Msg, AMsg);