Posted on :: Tags: , , ,

Introduction

The previous posts in this series (1,2,3) have all been abstract and mostly focused on math, but now we're going to talk about something much more concrete: WSJT-X and the role it can play in a DoTDoA system. As usual, I'm assuming that most readers are licensed hams so I don't need to explain what WSJT-X is. Instead, I'll focus on how it's FT8 functionality can be leveraged.

What can WSJT-X do for us?

Internally, the WSJT-X program determines the times at which each message begins, to an accuracy of about 5 milliseconds. This is not a trivial task and involves a lot of signal processing, yet WSJT-X performs this analysis for every readable message sent in a 3kHz sub-band. Since we can depend on the availability of these start times, we are able to substantially simplify the math of DoDToA. In fact, the math in previous posts involved only subtraction and distance calculations, whereas a more general discussion would require understanding of fourier transforms, convolutions, etc.

Example data flow and processing

Any given WSJT-X instance produces a stream of decode information which can be logged. For our purposes, we can ignore most of that decode info (SNR, freq offset into band, etc), and just keep the "DT" column (which I'll show here in milliseconds), a value that identifies the FT8 transmission period, and the sender's sign and location extracted from the "Message" column. Below we see such data for one period as produced by WSJT-X processing audio from the W3HFU WebSDR at FM19MQ:

RECEIVER LOG

 signR    locR     period      dt    signS     locS
----------------------------------------------------
"W3HFU"  "FM19MQ"  1727844420  440  "EA5FD"   "IM99"
"W3HFU"  "FM19MQ"  1727844420  370  "EA5HM"   "IM99"
"W3HFU"  "FM19MQ"  1727844420  480  "EC7DWP"  "IM87"
"W3HFU"  "FM19MQ"  1727844420  395  "ES2AJ"   "KO29"
"W3HFU"  "FM19MQ"  1727844420  360  "K8TE"    "DM65"
"W3HFU"  "FM19MQ"  1727844420 -090  "KO6EDH"  "DM13"
"W3HFU"  "FM19MQ"  1727844420  365  "LZ1JZ"   "KN22"
"W3HFU"  "FM19MQ"  1727844420  315  "LZ6LZ"   "KN33"
"W3HFU"  "FM19MQ"  1727844420  395  "NE1V"    "FN42"
"W3HFU"  "FM19MQ"  1727844420  455  "NY6C"    "CM88"
"W3HFU"  "FM19MQ"  1727844420  295  "R7CD"    "KN94"
"W3HFU"  "FM19MQ"  1727844420  215  "RN8C"    "MO06"
"W3HFU"  "FM19MQ"  1727844420  530  "RX3DQX"  "KO94"
"W3HFU"  "FM19MQ"  1727844420  335  "W6SPB"   "DM12"
"W3HFU"  "FM19MQ"  1727844420  400  "W7KEG"   "CN84"
"W3HFU"  "FM19MQ"  1727844420  345  "YO6PPX"  "KN26"
"W3HFU"  "FM19MQ"  1727844420  355  "ZD7CTO"  "IH74"
"W3HFU"  "FM19MQ"  1727844420  360  "ZL1VAH"  "RF72"

Each row says that W3HFU spotted a message from a sending station at a specific location, and that reception of that message began at time "DT" during the period beginning at 1727844420 (a Unix time).

We're all used to seeing a list of spots from WSJT-X, but things get more interesting when we compare these spots to another set of spots collected during the same period by another WSJT-X instance processing audio from the VE5BMS WebSDR at DO51RD:

RECEIVER A LOG                                            RECEIVER B LOG

 signR    locR     period      dt    signS     locS        signR    locR     period      dt    signS     locS
------------------------------------------------------    ------------------------------------------------------
"W3HFU"  "FM19MQ"  1727844420  440  "EA5FD"   "IM99"      "VE5BMS"  "DO51RD"  1727844420  370  "EA5FD"   "IM99"
"W3HFU"  "FM19MQ"  1727844420  370  "EA5HM"   "IM99"         x         x           x       x      x        x
"W3HFU"  "FM19MQ"  1727844420  480  "EC7DWP"  "IM87"         x         x           x       x      x        x
"W3HFU"  "FM19MQ"  1727844420  395  "ES2AJ"   "KO29"      "VE5BMS"  "DO51RD"  1727844420  315  "ES2AJ"   "KO29"
"W3HFU"  "FM19MQ"  1727844420  360  "K8TE"    "DM65"      "VE5BMS"  "DO51RD"  1727844420  290  "K8TE"    "DM65"
   x        x           x       x      x        x         "VE5BMS"  "DO51RD"  1727844420  625  "KA6VKP"  "DM03"
   x        x           x       x      x        x         "VE5BMS"  "DO51RD"  1727844420  250  "KD2YQS"  "FN20"
"W3HFU"  "FM19MQ"  1727844420 -090  "KO6EDH"  "DM13"      "VE5BMS"  "DO51RD"  1727844420 -170  "KO6EDH"  "DM13"
"W3HFU"  "FM19MQ"  1727844420  365  "LZ1JZ"   "KN22"      "VE5BMS"  "DO51RD"  1727844420  295  "LZ1JZ"   "KN22"
"W3HFU"  "FM19MQ"  1727844420  315  "LZ6LZ"   "KN33"      "VE5BMS"  "DO51RD"  1727844420  240  "LZ6LZ"   "KN33"
"W3HFU"  "FM19MQ"  1727844420  395  "NE1V"    "FN42"      "VE5BMS"  "DO51RD"  1727844420  305  "NE1V"    "FN42"
"W3HFU"  "FM19MQ"  1727844420  455  "NY6C"    "CM88"      "VE5BMS"  "DO51RD"  1727844420  365  "NY6C"    "CM88"
"W3HFU"  "FM19MQ"  1727844420  295  "R7CD"    "KN94"      "VE5BMS"  "DO51RD"  1727844420  220  "R7CD"    "KN94"
"W3HFU"  "FM19MQ"  1727844420  215  "RN8C"    "MO06"      "VE5BMS"  "DO51RD"  1727844420  135  "RN8C"    "MO06"
"W3HFU"  "FM19MQ"  1727844420  530  "RX3DQX"  "KO94"      "VE5BMS"  "DO51RD"  1727844420  455  "RX3DQX"  "KO94"
"W3HFU"  "FM19MQ"       x       x      x        x         "VE5BMS"  "DO51RD"  1727844420 -045  "W4IMD"   "EM84"
"W3HFU"  "FM19MQ"       x       x      x        x         "VE5BMS"  "DO51RD"  1727844420  330  "W4VG"    "FM18"
"W3HFU"  "FM19MQ"  1727844420  335  "W6SPB"   "DM12"      "VE5BMS"  "DO51RD"  1727844420  245  "W6SPB"   "DM12"
"W3HFU"  "FM19MQ"  1727844420  400  "W7KEG"   "CN84"         x         x           x       x      x        x
"W3HFU"  "FM19MQ"       x       x      x        x         "VE5BMS"  "DO51RD"  1727844420 -080  "W8OTJ"   "EM88"
"W3HFU"  "FM19MQ"  1727844420  345  "YO6PPX"  "KN26"      "VE5BMS"  "DO51RD"  1727844420  265  "YO6PPX"  "KN26"
"W3HFU"  "FM19MQ"  1727844420  355  "ZD7CTO"  "IH74"         x         x           x       x      x        x
"W3HFU"  "FM19MQ"  1727844420  360  "ZL1VAH"  "RF72"      "VE5BMS"  "DO51RD"  1727844420  275  "ZL1VAH"  "RF72"

There's a lot of overlap. If we elminate the rows that don't have data from both receiving stations we're left with what I call a list of "cospots":

COSPOTS

 signA   locA    |  signB    locB    | period     |  signS    locS   dtA  dtB
-----------------+-------------------+------------+---------------------------
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "EA5FD"  "IM99"  440  370
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "ES2AJ"  "KO29"  395  315
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "K8TE"   "DM65"  360  290
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "KO6EDH" "DM13" -090 -170
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "LZ1JZ"  "KN22"  365  295
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "LZ6LZ"  "KN33"  315  240
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "NE1V"   "FN42"  395  305
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "NY6C"   "CM88"  455  365
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "R7CD"   "KN94"  295  220
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "RN8C"   "MO06"  215  135
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "RX3DQX" "KO94"  530  455
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "W6SPB"  "DM12"  335  245
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "YO6PPX" "KN26"  345  265
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | 1727844420 | "ZL1VAH" "RF72"  360  275

Each "cospot" row specifies a sending station and the times at which it was cospotted by two receiving stations.

Now imagine that LZ6LZ is the sending station whose location I'd like to verify, so it will play the role of $S_U$ while the other 13 senders take turns playing the $S_K$ role. By pairing the $S_U$ cospot with each of the $S_K$ cospots, we get a list of what I call "double cospots". Note that since there are now four different DT values of interest, I'm switching to my usual $T_{uv}$ notation.

DOUBLE COSPOTS

 signA   locA    |  signB    locB    |  signU   locU   tUA  tUB |  signK    locK   tKA  tKB | ΔmKU
-----------------+-------------------+--------------------------+---------------------------+-----
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "EA5FD"  "IM99"  370  440 |  -5
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "ES2AJ"  "KO29"  315  395 |   5
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "K8TE"   "DM65"  290  360 |  -5
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "KO6EDH" "DM13" -170  -90 |   5
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "LZ1JZ"  "KN22"  295  365 |  -5
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "NE1V"   "FN42"  305  395 |  15
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "NY6C"   "CM88"  365  455 |  15
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "R7CD"   "KN94"  220  295 |   0
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "RN8C"   "MO06"  135  215 |   5
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "RX3DQX" "KO94"  455  530 |   0
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "W6SPB"  "DM12"  245  335 |  15
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "YO6PPX" "KN26"  265  345 |   5
"W3HFU" "FM19MQ" | "VE5BMS" "DO51RD" | "LZ6LZ" "KN33"  240  315 | "ZL1VAH" "RF72"  275  360 |  10

The last column is calculated from $(T_{UA} - T_{KA}) - (T_{UB} - T_{KB})$. Recalling the DoTDoA equation, $M_U - M_K$ is calculated the same way so $ΔM_{KU}$ is just an alias for that difference and it follows that, for each row:

  • $ M_U = M_{K} + ΔM_{KU}$

So, we have a set of $M_U$ values which can statistically analyzed to give an indication of which branch $S_U$ is on.

WSJT-X modifications

Some readers may have noticed that this post shows WSJT-X DT values to the nearest 5 milliseconds while WSJT-X's GUI, logs, and UDP messages all present this data rounded to one-tenth of a second. Modifications were required to expose the 5 msec resolution but before we discuss them you need to know that the "WSJT-X experience" that you are familiar with is actually provided by two different executables:

  • jt9 is the lower-level executable, written in Fortran, which does the signal processing.
  • wsjt-x is the higher-level executable, written in C++, which does everything else. It uses a jt9 process behind the scenes to perform signal processing.

The modification that needs to be done is in jt9. The file decoder.f90 contains a subroutine named ft8_decoded that writes various information to stdout whenever a FT8 message is decoded. The code in question appears below:

     write(*,1001) params%nutc, snr, dt, nint(freq), decoded0, annot
1001 format(i6.6, i4, f5.1, i5, ' ~ ', 1x, a37, 1x, a2)

The first line specifies some values which are to be written to stdout and the second line specifies how those values should be formatted. The format for the "dt" value is "f5.1" meaning that it will be treated as a floating point value, rounded to 1 decimal place, and space-padded to 5 characters. If we change dt's format to "f7.3", we'll get two more decimal places and that's all we need to expose the full internal resolution.

Unfortunately, this change to jt9 can potentially create problems for the higher-level wjst-x executable which assumes that the output of jt9 has a very specific form. I've avoided this issue by using KK5JY's ft8modem instead. It's a command-line alternative to the wsjt-x executable but it uses the same jt9 for signal processing, without depending on overly-specific details of jt9's output format.

Conclusion

While it certainly wasn't part of the original vision when WSJT-X was created, it's amazing how easily the program can be used by those of us who are interested in experimenting with (Do)TDoA to determine or verify the location of FT8 transmitters. For contrast, see this post by Panoradio SDR which discusses a more general approach to TDoA without synchronized clocks and you'll see the sort of signal processing we're able to avoid by leveraging WSJT-X.

In the next post I'll describe a complete proof-of-concept that adds web-based SDRs and a relatively simple Julia program on top of everything we've discussed so far.

73s