Author Archives: eloranta

Antenna comparison using dual RX FT8

Using the two independent receivers on my Elecraft K4D, I can compare the performance of two receive antennas. RX1 is set to antenna 1 (inverted L) and RX2 to beverage on ground (BOG; 160′ long pointed towards EU). Then putting the radio in diversity mode, the output is set up such that RX1 audio appears on the left stereo channel and RX2 on the right channel. On the computer I run two independent WSJT-X instances which read the audio from the left and right channels. Note that the second WSJT-X must be started with the -r option to avoid conflict with the first instance. After running the set up for a couple of hours, I then collect the relevant data from the corresponding ALL.TXT files (.local/share/WSJT-X*) and feed them to the following program to analyze.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CTY_DAT "cty.dat" // cty file
#define FREQ_TOL 3 // Hz
// #define REQUIRE_BOTH // Requre decoded line to be in both files

typedef struct {
int date;
int time;
int db;
int freq;
char call[25];
} log_entry;

#define MAX_PREFIX 32768
#define MAX_PREFLEN 16

char prefixes[MAX_PREFIX][MAX_PREFLEN];
int nprefixes = 0;

#ifdef CTY_DAT
void read_cty(char *file) {

FILE *fp;
char buf[512];
int i, j, prev_j, state = 0;

if(!(fp = fopen(CTY_DAT, "r"))) {
fprintf(stderr, "Can't open %s.\n", file);
exit(1);
}
while (1) {
if(fgets(buf, sizeof(buf), fp) == NULL) break;
if(buf[0] == '#') continue;
prev_j = 0;
switch(state) {
case 0:
// no relevant info on this line
state = 1;
break;
case 1:
for (j = 0; ; j++) {
if(buf[j] == ' ' || buf[j] == '=') {
prev_j = j + 1;
continue;
}
if(buf[j] == ',' || buf[j] == ';' || buf[j] == '(' || buf[j] == '[') {
if(nprefixes == MAX_PREFIX) {
fprintf(stderr, "Increase MAX_PREFIX.\n");
exit(1);
}
strncpy(prefixes[nprefixes], &buf[prev_j], j - prev_j);
prefixes[nprefixes][j - prev_j + 1] = '\0';
// printf("%d %s\n", nprefixes, prefixes[nprefixes]);fflush(stdout);
nprefixes++;
if(buf[j] == '(') {
for ( ; buf[j] != ')'; j++);
j++;
}
if(buf[j] == '[') {
for ( ; buf[j] != ']'; j++);
j++;
}
prev_j = j + 1;
}
if(buf[j] == ';') {
state = 0;
break;
}
if(buf[j] == ',' && buf[j+1] == '\r') {
state = 1;
break;
}
}
}
}
fprintf(stderr, "Number of prefixes read = %d\n", nprefixes);
fclose(fp);
}
#endif

int check_prefix(char *call) { // 0 = not wanted, 1 = wanted

int i;

for (i = 0; i < nprefixes; i++) {
if(!strncmp(prefixes[i], call, strlen(prefixes[i]))) return 1;
}
return 0;
}

void read_log(char *file, log_entry *log, int *nbr) {

*nbr = 0;
char tmp1[64], tmp2[64], tmp3[64], tmp4[4], buf[256];
FILE *fp;

if(!(fp = fopen(file, "r"))) {
fprintf(stderr, "Can't open file %s\n", file);
exit(1);
}
while(1) {
tmp1[0] = tmp2[0] = tmp3[0] = tmp4[0] = '\0';
if(fgets(buf, sizeof(buf), fp) == NULL) break;
sscanf(buf, "%d_%d %*f %*s %*s %d %*f %d %s %s %s %s", &(log[*nbr].date),
&(log[*nbr].time), &(log[*nbr].db), &(log[*nbr].freq), tmp1, tmp2, tmp3, tmp4);
if(tmp4[0] == '\0' || tmp4[0] == 'a') strcpy(log[*nbr].call, tmp2);
else strcpy(log[*nbr].call, tmp3);
#ifdef CTY_DAT
if(check_prefix(log[*nbr].call)) *nbr += 1; // otherwise ignore
#else
*nbr += 1;
#endif
}
fclose(fp);
}

int main(int argc, char **argv) {

log_entry log1[2048], log2[2048];
int i, j, n1, n2;

if(argc != 3) {
fprintf(stderr, "Usage: ft8cmp ALL1.DAT ALL2.DAT\n");
exit(1);
}

#ifdef CTY_DAT
read_cty(CTY_DAT);
#endif

read_log(argv[1], log1, &n1);
fprintf(stderr, "%d lines read from %s (unwanted prefixed excluded).\n", n1, argv[1]);
read_log(argv[2], log2, &n2);
fprintf(stderr, "%d lines read from %s (unwanted prefixed excluded).\n", n2, argv[2]);

// search for every ALL1.TXT entries
for(i = 0; i < n1; i++) {
for (j = 0; j < n2; j++)
if(log1[i].date == log2[j].date && log1[i].time == log2[j].time &&
abs(log1[i].freq - log2[j].freq) <= FREQ_TOL && !strcmp(log1[i].call, log2[j].call)) {
printf("# %s (%d-%d)\n", log1[i].call, log1[i].date, log1[i].time);
printf("%d %d\n", log1[i].db, log2[j].db);
break;
}
#ifndef REQUIRE_BOTH
if(j && j == n2) {
printf("# %s (%d-%d)\n", log1[i].call, log1[i].date, log1[i].time);
printf("%d -26\n", log1[i].db); // not received in ALL2.TXT
}
#endif
}

#ifndef REQUIRE_BOTH
// search for every ALL2.TXT entries - only entries that are missing in ALL1.TXT
for(j = 0; j < n2; j++) {
for (i = 0; i < n1; i++)
if(log1[i].date == log2[j].date && log1[i].time == log2[j].time &&
abs(log1[i].freq - log2[j].freq) <= FREQ_TOL && !strcmp(log1[i].call, log2[j].call))
break;
if(i && i == n1) {
printf("# %s (%d-%d)\n", log2[j].call, log2[j].date, log2[j].time);
printf("-26 %d\n", log2[j].db); // not received in ALL1.TXT
}
}
#endif
}

The cty.dat file above is the AD1C cty.dat ASCII file that includes the countries of interest for the analysis. In my case I excluded the US and Canada (removed them from the cty.dat file), so the test focused on DX receive. Since BOG is directional, I should also exclude DX stations that are not in the direction of the BOG (to be done). Note that the program compares data obtained from the two receivers during the exact same time slice. This eliminates the QSB and other effects that could play a significant role if different time slices were used in the comparison. The two RXs are identical, so the RX bias is minimal. This could be tested by reversing the roles of the two RXs. Of course, the test yields the data within the 15 second FT8 transmission cycle but both antennas have the exact same change of decoding the signals during that time slice.

The first test was on 80 m FT8 (evening time; band open to EU). The x-axis is the decoded signal strength in dB from the RX that was connected to the inverted L and y-axis signal strength from the RX connected to the BOG. The red line shows where inv L and BOG would be equally good in receiving. Points representing a single decode above the red line has BOG obtaining better signal to noise (S/N) where as below it inverted L has better S/N. Results from collecting data for a couple of hours is shown below.

Well, BOG seems to work (very short test!!!) but another thing that this highlights the fact that running two independent WSJT-X processes with different types of antennas can improve FT8 receive a lot. There were many instances where BOG was able to decode but not inverted L and vice versa. The stations that BOG could not copy were mostly in South America (BOG was oriented towards EU). The “BOG only copy” stations on the other hand were in EU.

Below is another comparison between 60 m dipole and the same BOG as above (band open to EU):

On 60 m band BOG beat dipole hands down. There were many stations that BOG heard but dipole did not. And dipole was not able to hear anything that BOG did not.

NOTE: While this can still be used, multi program (see another blog post) can collect the statistics for direct visualization.

QK4 on Arch Linux AUR repository

I wrote PKGBUILD script for QK4 (Elecraft K4 remote software) and submitted that to the AUR repository. Arch Linux users can install QK4 with yay as follows:

$ yay -S qk4-git

It will create the proper menu / icon entries & decrease pipewire latency so that CW can be used. To update to the latest version use:

$ yay --devel -Suyy

Antenna widget in gtk+

There is no documentation on libqt6c -> decided to switch to gtk+. Cleaned up the code a bit too.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <gtk/gtk.h>

// Define the antenna names
#define ANTENNA1 "6m beam"
#define ANTENNA2 "WARC beam"
#define ANTENNA3 "20-15-10 stack"
#define ANTENNA4 "40m beam"
#define ANTENNA5 "No antenna"
#define ANTENNA6 "No antenna"
#define ANTENNA7 "No antenna"
#define ANTENNA8 "No antenna"
#define ANTENNA9 "No antenna"
#define ANTENNA10 "No antenna"
#define ANTENNA11 "No antenna"
#define ANTENNA12 "No antenna"
#define MAX_ANTENNAS 12

// Device for BM5
#define DEVICE "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_B003ISA1-if00-port0"

// Update every 100 msec
#define UPDATE 100

char *antennas[] = {ANTENNA1, ANTENNA2, ANTENNA3, ANTENNA4, ANTENNA5, ANTENNA6,
ANTENNA7, ANTENNA8, ANTENNA9, ANTENNA10, ANTENNA11, ANTENNA12};

#define MAX_SM 11
char *stack_match[] = {"No antenna", "Top antenna", "Mid antenna", "Top+mid antennas",
"Bottom antenna", "Top+bot antennas", "Mid+bot antennas",
"All antennas", "AUX", "Unknown", "Phase"};

int fd;
GtkWidget *ant, *sm;

struct packet {
unsigned char cmd;
unsigned char dst;
unsigned char src;
unsigned char channel;
int len;
unsigned char data[256];
};

int read_untilcr(int fd, char *buf, int maxlen) {

int pos = 0, bytes_rd;

while(1) {
if((bytes_rd = read(fd, buf + pos, 1)) < 0) return -1;
if(bytes_rd == 0) {
sleep(1); // could be less than 1sec
continue;
}
if(!pos && (*buf == '\r' || *buf == '\n')) continue;
if(pos == maxlen) return -1;
if(buf[pos] == '\r') break;
pos++;
}
buf[pos] = '\0';
return pos;
}

int parse_packet(char *str, struct packet *packet) {

memset((void *) packet, 0, sizeof(struct packet));
if(sscanf(str, "[T%2xR%2x%2xH%2xM%[^]]", &(packet->cmd), &(packet->dst), &(packet->src), &(packet->channel), &(packet->data)) != 5) return -1;
return 0;
}

void set_tty() {

struct termios param;

if(tcgetattr(fd, &param) < 0) {
fprintf(stderr, "Status get failed.\n");
exit(1);
}

param.c_cflag &= ~PARENB;
param.c_cflag &= ~CSTOPB;
param.c_cflag |= CS8;
param.c_cflag &= ~CRTSCTS;
param.c_cflag |= CREAD | CLOCAL;
param.c_lflag &= ~ICANON;
param.c_lflag &= ~ECHO;
param.c_lflag &= ~ECHOE;
param.c_lflag &= ~ECHONL;
param.c_lflag &= ~ISIG;
param.c_iflag &= ~(IXON | IXOFF | IXANY);
param.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL);
param.c_oflag &= ~OPOST;
param.c_oflag &= ~ONLCR;
cfsetispeed(&param, B57600);
cfsetospeed(&param, B57600);

if(tcsetattr(fd, TCSANOW, &param) < 0) {
fprintf(stderr, "Status set failed.\n");
exit(1);
}
}

static gboolean update_band_info(void *asd) {

int len, antenna, relay;
struct packet packet;
char buf[512], tr = 0;

if((len = read_untilcr(fd, buf, sizeof(buf))) < 0) {
fprintf(stderr, "Read error.\n");
exit(1);
}

if(parse_packet(buf, &packet) < 0) {
printf("Incomplete packet -- skipping.\n");
return 1;
}

switch(packet.cmd) {
case 0xc4: // C4 = Bandmaster 5 status
if(sscanf(packet.data, "0%1d01EEEEEEEEEEEE%4d", &antenna, &relay) != 2) {
printf("Error extracting antenna & relay information.");
antenna = 99;
}
if(antenna > 0 && antenna < MAX_ANTENNAS)
sprintf(buf, "<span color='blue'>%s (relays = %d)</span>", antennas[antenna-1], relay);
else
sprintf(buf, "<span color='red'>Unknown</span>");
gtk_label_set_markup(GTK_LABEL(ant), buf);
break;
case 0x45: // Stack match
sscanf(packet.data, "%2x", &antenna);
if(antenna >= 128) {
antenna -= 128; // transmitting
tr = 1;
}
if(antenna >= 0 && antenna < MAX_SM)
sprintf(buf, "<span color='blue'>%s (%s)</span>", stack_match[antenna], tr?"TX":"RX");
else
strcpy(buf, "<span color='red'>Unknown state</span>");
gtk_label_set_markup(GTK_LABEL(sm), buf);
break;
case 0x13: // decoder data
printf("Decoder data ignored.\n");
break;
default:
printf("Unknown packet (%x) - ignored.\n", packet.cmd);
}
return 1;
}

void closewin(GtkWidget *win, gpointer data) {

gtk_main_quit();
}

int main(int argc, char *argv[]) {

GtkWidget *window;
GtkWidget *ant_label, *sm_label, *hbox1, *hbox2, *vbox;

gtk_init(&argc, &argv);
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "Antenna Status");
gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
gtk_window_set_default_size(GTK_WINDOW(window), 350, 60);
g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(closewin), NULL);

ant_label = gtk_label_new("Antenna:");
gtk_label_set_xalign(GTK_LABEL(ant_label), 0.0); // 0 = left, 1 = right
sm_label = gtk_label_new("Stack match:");
gtk_label_set_xalign(GTK_LABEL(sm_label), 0.0);
ant = gtk_label_new("-----");
gtk_label_set_xalign(GTK_LABEL(ant), 0.5);
sm = gtk_label_new("-----");
gtk_label_set_xalign(GTK_LABEL(sm), 0.5);

hbox1 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);

gtk_box_pack_start(GTK_BOX(hbox1), ant_label, TRUE, TRUE, 5);
gtk_box_pack_start(GTK_BOX(hbox1), ant, TRUE, TRUE, 5);

gtk_box_pack_start(GTK_BOX(hbox2), sm_label, TRUE, TRUE, 5);
gtk_box_pack_start(GTK_BOX(hbox2), sm, TRUE, TRUE, 5);

gtk_box_pack_start(GTK_BOX(vbox), hbox1, TRUE, TRUE, 5);
gtk_box_pack_start(GTK_BOX(vbox), hbox2, TRUE, TRUE, 5);
gtk_container_add(GTK_CONTAINER(window), vbox);

if((fd = open(DEVICE, O_RDONLY)) < 0) {
fprintf(stderr, "Can't open device.\n");
exit(1);
}
set_tty();

g_timeout_add(UPDATE, update_band_info, NULL);

gtk_widget_show_all(window);
gtk_main();

close(fd);
return 0;
}

Compile with (the above is in ant.c file): gcc ant.c -o ant -march=native `pkg-config –cflags –libs gtk+-3.0`

Switched from VNC(RFB) to RDP for remote access

In order to migrate from XOrg to Wayland, I switched from x11vnc & TigerVNC combo (uses RFB protocol) to krdp & remmina (uses RDP protocol). Remmina scrolling was kind of slow at first but after discovering the “step size for auto-scroll” setting in the preferences really helped (10 -> 30). Now the krdp is very picky but it seems that android app aRDP works with it once H264 is enabled.

UPDATE: krdb etc. are just to slow and clumsy (can’t start automatically before login), so had to switch back to VNC (x11vnc).

Beverage on ground (BOG) testing

Made 200′ long beverage on ground (BOG) for receiving on 160m and 80m bands:

Feed point connected to 50 Ohm coax (no impedance transformer). Coax shield connected to ground and hot to the BOG wire.

End of the BOG is ground terminated (ground rod) with a 320 Ohm resistor (white).

The antenna is oriented approximately to Bouvet Island (3Y0K). In some cases seems to give better signal to noise as compared to the transmission antenna (inverted L with 45 full length radials).

Bandmaster V info widget

Wrote the following info widget for Bandmaster V (Linux):

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <qt6/libqt6c.h>

// Define the antenna names
#define ANTENNA1 "6m beam"
#define ANTENNA2 "WARC beam"
#define ANTENNA3 "Stack"
#define ANTENNA4 "40m beam"
#define ANTENNA5 "No antenna"
#define ANTENNA6 "No antenna"
#define ANTENNA7 "No antenna"
#define ANTENNA8 "No antenna"
#define ANTENNA9 "No antenna"
#define ANTENNA10 "No antenna"
#define ANTENNA11 "No antenna"
#define ANTENNA12 "No antenna"

#define DEVICE "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_B003ISA1-if00-port0"

int fd;
void *widget, *label;

int read_untilcr(int fd, char *buf, int maxlen) {

int pos = 0, bytes_rd;

while(1) {
if((bytes_rd = read(fd, buf + pos, 1)) < 0) return -1;
if(bytes_rd == 0) {
sleep(1); // could be less than 1sec
continue;
}
if(!pos && (*buf == '\r' || *buf == '\n')) continue;
if(pos == maxlen) return -1;
if(buf[pos] == '\r') break;
pos++;
}
buf[pos] = '\0';
return pos;
}

void set_tty() {

struct termios param;

if(tcgetattr(fd, &param) < 0) {
fprintf(stderr, "Status get failed.\n");
exit(1);
}

param.c_cflag &= ~PARENB;
param.c_cflag &= ~CSTOPB;
param.c_cflag |= CS8;
param.c_cflag &= ~CRTSCTS;
param.c_cflag |= CREAD | CLOCAL;
param.c_lflag &= ~ICANON;
param.c_lflag &= ~ECHO;
param.c_lflag &= ~ECHOE;
param.c_lflag &= ~ECHONL;
param.c_lflag &= ~ISIG;
param.c_iflag &= ~(IXON | IXOFF | IXANY);
param.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL);
param.c_oflag &= ~OPOST;
param.c_oflag &= ~ONLCR;
cfsetispeed(&param, B57600);
cfsetospeed(&param, B57600);

if(tcsetattr(fd, TCSANOW, &param) < 0) {
fprintf(stderr, "Status set failed.\n");
exit(1);
}
}

char *txt_noant = "<b><font color='red'>No antenna.</font></b>",
*txt_40m = "<b><font color='blue'>40m beam.</font></b>",
*txt_warc = "<b><font color='blue'>WARC beam.</font></b>",
*txt_stack = "<b><font color='blue'>Stack.</font></b>",
*txt_6m = "<b><font color='blue'>6m beam.</font></b>";

void update_band_info(void *asd) {

int len, antenna, relay;
char buf[512];

if((len = read_untilcr(fd, buf, sizeof(buf))) < 0) {
fprintf(stderr, "Read error.\n");
exit(1);
}
if(sscanf(buf, "[TC4RFA29H00M0%1d01EEEEEEEEEEEE%4d]", &antenna, &relay) != 2) return; // Some other mesg

switch(antenna) {
case 1:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA1, relay);
q_label_set_text(label, buf);
break;
case 2:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA2, relay);
q_label_set_text(label, buf);
break;
case 3:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA3, relay);
q_label_set_text(label, buf);
break;
case 4:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA4, relay);
q_label_set_text(label, buf);
break;
case 5:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA5, relay);
q_label_set_text(label, buf);
break;
case 6:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA6, relay);
q_label_set_text(label, buf);
break;
case 7:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA7, relay);
q_label_set_text(label, buf);
break;
case 8:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA8, relay);
q_label_set_text(label, buf);
break;
case 9:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA9, relay);
q_label_set_text(label, buf);
break;
case 10:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA10, relay);
q_label_set_text(label, buf);
break;
case 11:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA11, relay);
q_label_set_text(label, buf);
break;
case 12:
sprintf(buf, "<b><font color='blue'>%s (%d)</font></b>", ANTENNA12, relay);
q_label_set_text(label, buf);
break;
default:
q_label_set_text(label, "<b><font color='red'>Unknown antennan.</font></b>");
}
}

int main(int argc, char *argv[]) {

QApplication *app;
QTimer *timer;

if((fd = open(DEVICE, O_RDONLY)) < 0) {
fprintf(stderr, "Can't open device.\n");
exit(1);
}
set_tty();

app = q_application_new(&argc, argv);
widget = q_widget_new2();
timer = q_timer_new2(widget);
q_widget_set_window_title(widget, "Antenna selection");
q_widget_resize(widget, 150, 30);
label = q_label_new5("------------------------------", widget);
q_label_set_alignment(label, QT_ALIGNMENTFLAG_ALIGNCENTER);
q_label_set_text_format(label, QT_TEXTFORMAT_RICHTEXT);
q_timer_start(timer, 100);
q_timer_on_timeout(timer, update_band_info);
q_widget_show(widget);
(void) q_application_exec();
q_widget_delete(widget);
q_application_delete(app);

close(fd);
}

Uses libqt6c (QT bindings for C).

Tower and antenna installation

Some photos from the project.

Time to get started!

Sections of Rohn-55g on the ground.

Bearing installation on the tower.

Theodore inspecting the foundation and the rotor

The first piece of Rohn-55g on the rotor

All 140′ of Rohn-55g up!

Antennas going up!

All done!

Final rotor setup.

CQ WW DX Contest CW 2025

I worked 15m single band (high power, unassisted); about 30 hours. Some issues during the contest:

  • 5 hour power outage on Sunday morning. Of course, there was a great EU opening during that time! G E N E R A T O R next time…
  • hamlib/rigctld keying problems that required frequent restarting of both not1mm and rigctld. Switching to cwdaemon for the next contest.
  • It was cold in the shack! I need to work on the insulation!

But still the contest went much better than last year (… new antennas …): 1344 QSOs, 122 countries, and 35 zones.