PSK Reporter, comparing two receiving stations

You can zoom into the map by clicking the magnifying glass icon.

import cartopy
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import shapely.geometry as sgeom
import cartopy.io.shapereader as shpreader
import matplotlib.pyplot as plt
import numpy as np
import re
import datetime

def maiden2lonlat(maiden: str):
    n = len(maiden)
    maiden = maiden.lower()
    aaa = ord('a')
    lon = -180.0
    lat = -90.0

    lon += (ord(maiden[0])-aaa)*20.0
    lat += (ord(maiden[1])-aaa)*10.0
    lon += int(maiden[2])*2.0
    lat += int(maiden[3])*1.0
    if n >= 6:
        lon += (ord(maiden[4])-aaa) * 5.0/60.0
        lat += (ord(maiden[5])-aaa) * 2.5/60.0
    if n >= 8:
        lon += int(maiden[6]) * 5.0/600.0
        lat += int(maiden[7]) * 2.5/600.0
    return lon, lat
 
def parse_adif(fn):
    raw = re.split('<eor>|<eoh>',open(fn).read() )
    raw.pop(0)
    raw.pop()
    logbook =[]
    for record in raw:
        qso = {}
        tags = re.findall('<(.*?):\d+.*?>([^<\t\n\r\f\v]+)',record)
        for tag in tags:
            qso[tag[0]] = tag[1]
        logbook.append(qso)    
    return logbook

def mydatetime(date, time):
    dt = datetime.datetime(int(date[0:4]), int(date[4:6]), int(date[6:8]), \
                           int(time[0:2]), int(time[2:4]), int(time[4:6]))
    return dt

 
fnames = ['station1.adif', 'station2.adif']

dt0 = datetime.datetime(2001,  1,  1,  0, 0 ,  0)
dt9 = datetime.datetime(2099, 12, 31, 23, 59, 59)

fig = plt.figure(figsize=(16, 9))

fcount = 0
for fn in fnames:
    x = []
    y = []
    r = []
    c = []
    log = parse_adif(fn)
    scount = 0
    for qso in log:
        if ('GRIDSQUARE' in qso):
            dt = mydatetime(qso['QSO_DATE'], qso['TIME_ON'])
            mytime = qso['TIME_ON']
            myhour = float(mytime[0:2])
            if dt >= dt0 and dt <=dt9:
                grid = qso['GRIDSQUARE']
                mylon, mylat = maiden2lonlat(grid)
                if ('APP_PSKREP_SNR' in qso):
                    snr = float(qso['APP_PSKREP_SNR'])
                    print(fcount, scount, grid, mylon, mylat, snr, mytime, myhour)
                    x.append(mylon)
                    y.append(mylat)
                    r.append(50.0+2.0*snr)
                    c.append(myhour/24.0)
                    scount += 1


    cent_lon = 152.5
    ax = fig.add_subplot(2, 3, 1+3*fcount, projection=ccrs.PlateCarree(central_longitude=cent_lon))
    ax.stock_img()
    ax.gridlines()
    ax.coastlines()
    ax.scatter(np.subtract(x,cent_lon), y, c=c, s=r, cmap='hsv', alpha=0.7)

    cent_lon = 139.7
    cent_lat = 35.7
    ax = fig.add_subplot(2, 3, 2+3*fcount, projection=ccrs.AzimuthalEquidistant(central_longitude=cent_lon, central_latitude=cent_lat))
    ax.stock_img()
    ax.gridlines()
    ax.coastlines()
    ax.scatter(x, y, c=c, s=r, cmap='hsv', alpha=0.7, transform=ccrs.Geodetic())

    ax = fig.add_subplot(2, 3, 3+3*fcount, projection=ccrs.PlateCarree())
    ax.stock_img()
    ax.coastlines()
    ax.set_extent([-130.0, -66.5, 20.0, 50.0], ccrs.Geodetic())
    shapename = 'admin_1_states_provinces_lakes_shp'
    states_shp = shpreader.natural_earth(resolution='110m', category='cultural', name=shapename)

    for state in shpreader.Reader(states_shp).geometries():
        facecolor = [0.9375, 0.9375, 0.859375]
        edgecolor = 'black'
        ax.add_geometries([state], ccrs.PlateCarree(),
                          facecolor=facecolor, edgecolor=edgecolor, alpha=0.1)

    ax.scatter(x, y, c=c, s=r, cmap='hsv', alpha=0.9)
    fcount += 1

plt.show()

PSK Reporter and your own map (2)

This may look better if you live in Japan.

cent_lon = 152.5
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=cent_lon))
x.append(mylon-cent_lon)

The color shows the time of the day, and the dot size SNR.

The colormap “hsv” is used. The leftmost part if for 00Z, and the rightmost for 23Z.

cent_lon = 139.7
cent_lat = 35.7
ax = fig.add_subplot(1, 1, 1, projection=ccrs.AzimuthalEquidistant(central_longitude=cent_lon, central_latitude=cent_lat))
x.append(mylon)
ax.scatter(x, y, c=c, s=r, cmap='hsv', alpha=0.7, transform=ccrs.Geodetic())

ax.set_extent([-135, -66.5, 20, 55], ccrs.Geodetic())
shapename = 'admin_1_states_provinces_lakes_shp'
states_shp = shpreader.natural_earth(resolution='110m', category='cultural', name=shapename)

for state in shpreader.Reader(states_shp).geometries():
    facecolor = [0.9375, 0.9375, 0.859375]
    edgecolor = 'black'
    ax.add_geometries([state], ccrs.PlateCarree(),      facecolor=facecolor, edgecolor=edgecolor, alpha=0.1)

PSK Reporter and your own map

The size and the color of a dot shows the SNR of a received FT8 signal.

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import numpy as np
import re
import datetime

def maiden2lonlat(maiden: str):
    n = len(maiden)
    maiden = maiden.lower()
    print(maiden, n)
    aaa = ord('a')
    lon = -180.0
    lat = -90.0

    lon += (ord(maiden[0])-aaa)*20.0
    lat += (ord(maiden[1])-aaa)*10.0
    lon += int(maiden[2])*2.0
    lat += int(maiden[3])*1.0
    if n >= 6:
        lon += (ord(maiden[4])-aaa) * 5.0/60.0
        lat += (ord(maiden[5])-aaa) * 2.5/60.0
    if n >= 8:
        lon += int(maiden[6]) * 5.0/600.0
        lat += int(maiden[7]) * 2.5/600.0
    return lon, lat


ax = plt.axes(projection=ccrs.PlateCarree())
ax.stock_img()
 
fnames = ['station1.adif']
 
def parse_adif(fn):
    raw = re.split('<eor>|<eoh>',open(fn).read() )
    raw.pop(0)
    raw.pop()
    logbook =[]
    for record in raw:
        qso = {}
        tags = re.findall('<(.*?):\d+.*?>([^<\t\n\r\f\v]+)',record)
        for tag in tags:
            qso[tag[0]] = tag[1]
        logbook.append(qso)    
    return logbook

def mydatetime(date, time):
    dt = datetime.datetime(int(date[0:4]), int(date[4:6]), int(date[6:8]), \
                           int(time[0:2]), int(time[2:4]), int(time[4:6]))
    return dt

dt0 = datetime.datetime(2001,  1,  1,  0, 0 ,  0)
dt9 = datetime.datetime(2099, 12, 31, 23, 59, 59)

x = []
y = []
r = []
c = []
fcount = 0
for fn in fnames:
    log = parse_adif(fn)
    scount = 0
    for qso in log:
        if ('GRIDSQUARE' in qso):
            dt = mydatetime(qso['QSO_DATE'], qso['TIME_ON'])
            if dt >= dt0 and dt <=dt9:
                grid = qso['GRIDSQUARE']
                mylon, mylat = maiden2lonlat(grid)
                if ('APP_PSKREP_SNR' in qso):
                    snr = float(qso['APP_PSKREP_SNR'])
                    print(fcount, scount, grid, mylon, mylat, snr)
                    x.append(mylon)
                    y.append(mylat)
                    r.append(50.0+2.0*snr)
                    c.append(-snr*30.0)
                    scount += 1
    fcount += 1
plt.scatter(x, y, c=c, s=r, cmap='hsv', alpha=0.5)
plt.show()

In the above figure, the colors show the time of the day.

A colormap named “jet” is employed.

The leftmost part corresponds to 00Z, and the rightmost 23Z.

PSK Reporter and Cartopy

Cartopy is a Python package designed for geospatial data processing in order to produce maps and other geospatial data analyses.

Starting in 2016, Basemap came under new management. The Cartopy project will replace Basemap, but it hasn’t yet implemented all of Basemap’s features. All new software development should try to use Cartopy whenever possible, and existing software should start the process of switching over to use Cartopy.

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import numpy as np

ax = plt.axes(projection=ccrs.PlateCarree())
ax.stock_img()

n=50
x = 360.0 * np.random.rand(n) - 180.0
y = 120.0 * np.random.rand(n) -  60.0
r =  50.0 * np.random.rand(n) +  20.0

plt.scatter(x, y, c=x, s=r, cmap='hsv', alpha=0.75)
plt.show()

PSK Reporter and Basemap Toolkit

The maps provided by PSK Reporter are somewhat uncontrollable with their sizes and offsets, so I thought I might draw my own maps using Matplotlib Basemap Toolkit from an ADIF file downloadable from the site.

<FREQ:8>7.074812
<MODE:3>FT8
<OPERATOR:6>******
<MY_GRIDSQUARE:6>++++++
<QSO_DATE:8>20181227
<TIME_ON:6>140700
<APP_PSKREP_SNR:3:N>-14
<CALL:6>******
<COUNTRY:13>United States
<DXCC:3>291
<GRIDSQUARE:8>++++++++
<QSO_COMPLETE:3>NIL
<SWL:1>1
<eor>

What I need is a Python library to convert a Maidenhead grid square to a latitude and longitude pair.

WSJT-X and Raspberry Pi 3 revisited

pi@raspberrypi:~ $ ls -l ~/Downloads/
-rw-r--r-- 1 pi pi 9999942 Dec 27 09:33 wsjtx_2.0.0_armhf.deb
pi@raspberrypi:~ $ sudo apt install libqt5multimedia5-plugins libqt5serialport5 libqt5sql5-sqlite libfftw3-single3
pi@raspberrypi:~/Downloads $ sudo dpkg -i wsjtx_2.0.0_armhf.deb
pi@raspberrypi:~ $ cat /etc/modules
snd-aloop
pi@raspberrypi:~ $ sudo modprobe snd_aloop
pi@raspberrypi:~ $ lsmod
Module                  Size  Used by
snd_aloop              24576  4
pi@raspberrypi:~ $ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]
  Subdevices: 7/7
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
card 0: ALSA [bcm2835 ALSA], device 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: Loopback [Loopback], device 0: Loopback PCM [Loopback PCM]
  Subdevices: 8/8
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
  Subdevice #7: subdevice #7
card 1: Loopback [Loopback], device 1: Loopback PCM [Loopback PCM]
  Subdevices: 7/8
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
  Subdevice #7: subdevice #7
pi@raspberrypi:~ $ 

Airspy HF+ and Raspberry Pi 3 revisited

Let’s start from the beginning.

Etcher is a graphical SD card writing tool that works on Mac OS, Linux and Windows.

Mac-mini:~ user1$ ssh pi@raspberrypi.local
pi@raspberrypi.local's password: 
Linux raspberrypi 4.14.79-v7+ #1159 SMP Sun Nov 4 17:50:20 GMT 2018 armv7l
Last login: Wed Dec 26 23:23:15 2018
pi@raspberrypi:~ $ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/root        30G  4.5G   24G  16% /
devtmpfs        460M     0  460M   0% /dev
tmpfs           464M     0  464M   0% /dev/shm
tmpfs           464M   18M  446M   4% /run
tmpfs           5.0M  4.0K  5.0M   1% /run/lock
tmpfs           464M     0  464M   0% /sys/fs/cgroup
/dev/mmcblk0p1   44M   23M   22M  51% /boot
tmpfs            93M     0   93M   0% /run/user/1000
pi@raspberrypi:~ $ sudo apt-get update
Hit:1 http://archive.raspberrypi.org/debian stretch InRelease                              
Hit:2 http://raspbian.raspberrypi.org/raspbian stretch InRelease                           
Reading package lists... Done                      
pi@raspberrypi:~ $ sudo apt-get upgrade
Reading package lists... Done
Building dependency tree       
Reading state information... Done
Calculating upgrade... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
pi@raspberrypi:~ $ sudo apt-get dist-upgrade
Reading package lists... Done
Building dependency tree       
Reading state information... Done
Calculating upgrade... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
pi@raspberrypi:~ $ ls -l ~/Downloads/
total 968
-rw-r--r-- 1 pi pi 984932 Dec 27 02:08 gqrx-sdr-2.11.5-linux-rpi3.tar.xz
pi@raspberrypi:~ $ tar Jxfv gqrx-sdr-2.11.5-linux-rpi3.tar.xz
pi@raspberrypi:~/gqrx-sdr-2.11.5-linux-rpi3 $ sudo apt update
pi@raspberrypi:~/gqrx-sdr-2.11.5-linux-rpi3 $ sudo apt install gnuradio libvolk1-bin libusb-1.0-0 gr-iqbal
pi@raspberrypi:~/gqrx-sdr-2.11.5-linux-rpi3 $ sudo apt install qt5-default libqt5svg5 libportaudio2
pi@raspberrypi:~/gqrx-sdr-2.11.5-linux-rpi3 $ sudo cp ./udev/52-airspyhf.rules /etc/udev/rules.d/
pi@raspberrypi:~ $ dmesg | grep -i airspy
[ 2830.069314] usb 1-1.4: Product: AIRSPY HF+
[ 2830.069323] usb 1-1.4: Manufacturer: www.airspy.com
[ 2830.069331] usb 1-1.4: SerialNumber: AIRSPYHF SN: ..............
pi@raspberrypi:~/gqrx-sdr-2.11.5-linux-rpi3 $ ./gqrx

Airspy HF+ and Raspberry Pi 3 (3)

For Web scraping, we need to install a WebDriver and some python related software.

WebDriver is an open source tool for automated testing of webapps across many browsers. It provides capabilities for navigating to web pages, user input, JavaScript execution, and more. ChromeDriver is a standalone server which implements WebDriver’s wire protocol for Chromium.

pi@raspberrypi:~/Zpython3 $ sudo apt-get install chromium-browser
pi@raspberrypi:~/Zpython3 $ sudo apt-get install chromium-chromedriver
pi@raspberrypi:~/Zpython3 $ /usr/lib/chromium-browser/chromium-browser --version
Chromium 65.0.3325.181 
pi@raspberrypi:~/Zpython3 $ /usr/lib/chromium-browser/chromedriver --version
ChromeDriver 2.35 (0)

pi@raspberrypi:~/Zpython3 $ wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh

pi@raspberrypi:~/Zpython3 $ which python3
/home/pi/berryconda3/bin/python3
pi@raspberrypi:~/Zpython3 $ python3 -V
Python 3.6.1

pi@raspberrypi:~/Zpython3 $ pip3 list
Package      Version  
------------ ---------
selenium     3.141.0  
pi@raspberrypi:~/Zpython3 $ pip3 show selenium
Name: selenium
Version: 3.141.0
Summary: Python bindings for Selenium
Home-page: https://github.com/SeleniumHQ/selenium/
Author: UNKNOWN
Author-email: UNKNOWN
License: Apache 2.0
Location: /home/pi/berryconda3/lib/python3.6/site-packages
Requires: urllib3
Required-by: 

pi@raspberrypi:~/Zpython3 $ python3 selenium01.py
import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options

chrome_option = webdriver.ChromeOptions()
chrome_option.add_argument('--headless')
driver = webdriver.Chrome('/usr/lib/chromium-browser/chromedriver', chrome_options=chrome_option)

for irep in range(360):
    driver.get('https://pskreporter.info/pskmap.html?preset&callsign=ja1a&band=6000000-8000000&timerange=86400')
    assert 'Display Reception Reports' in driver.title
    time.sleep(200)
    fname = time.strftime("%a%d%b%Y%H%M%S.png", time.gmtime())
    print(fname)
    driver.save_screenshot(fname)
    time.sleep(100)

driver.quit()

Airspy HF+ and Raspberry Pi 3 (2)

Updated to WSJT-X v2.0.0.

pi@raspberrypi:~ $ lsb_release -a
No LSB modules are available.
Distributor ID:	Raspbian
Description:	Raspbian GNU/Linux 9.6 (stretch)
Release:	9.6
Codename:	stretch
pi@raspberrypi:~ $ uname -a
Linux raspberrypi 4.14.34-v7+ #1110 SMP Mon Apr 16 15:18:51 BST 2018 armv7l GNU/Linux
pi@raspberrypi:~ $ cat /proc/version
Linux version 4.14.34-v7+ (dc4@dc4-XPS13-9333) (gcc version 4.9.3 (crosstool-NG crosstool-ng-1.22.0-88-g8460611)) #1110 SMP Mon Apr 16 15:18:51 BST 2018

PSK Reporter and Web Scraping

The screen is captured every three minutes, and a GIF animation file is created.

import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
 
options = Options()
options.binary_location = '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
options.add_argument('--headless')
driver = webdriver.Chrome('/Users/user1/Downloads/chromedriver', chrome_options=options)
 
for irep in range(10000):
    driver.get('https://pskreporter.info/pskmap.html?preset&callsign=ja1a&band=6000000-8000000&timerange=86400')
    assert 'Display Reception Reports' in driver.title
    time.sleep(120)
    fname = time.strftime("%a%d%b%Y%H%M%S.png", time.gmtime())
    print(fname)
    driver.save_screenshot(fname)
    time.sleep(60)
 
driver.quit()
$ mkdir ./cropped
$ mogrify -crop 800x432+0+128! -path ./cropped *.png
$ convert -delay 1 -loop 0 ./cropped/*.png movie.gif