Table of Contents
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 ajt9
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