Showing posts with label ham radio. Show all posts
Showing posts with label ham radio. Show all posts

Sunday, June 8, 2025

FlexRadio gets Rotary Phone Dial Interface

  

-----

The Flex Radio 6400 is an amazing state of the art ham radio transceiver with so many modern bells and whistles than no normal human could ever get around to using them all.  However, this can also put off many olde tyme hams that resist changes from the Golden Age of Radio.  In an effort to ease this transition we had to create the Drifty Flex application.  As helpful as this has been, there was still the common complaint that "Dammit, real radios have knobs!".   So, to assist again with the transition to Flex Radio we launched a new project.

-----

One fantastic feature Flex provides is access to the rig API.  This has allowed for many amazing third party programs to be developed and also allows any user to create their on applications.  This is exactly what we did here.    The Python script below monitors a FT232RL FTDI USB to RS232 Converter to detect pulses from the rotary dial phone.  Those pulse are interpreted as numbers to build a frequency for the FlexRadio to tune to.   After the frequency is "dialed in" the Python program sends the API command to the rig to change to that frequency.   The hardware and software are pretty straight forward.

-----

Hook it up like this:

----- 

Run the script and this is what you get this:

A convenient, satisfying, and familiar analog input for your modern radio.

-----

#
#  FlexRadio Dial Up Interface
#  www.WhiskeyTangloHotel.Com
#  June 2025#
#
# Use an old Rotary Dial phone to change frequency on your FlexRadio.
# Program runs under Window with Python3.
#
# Uses FT232RL FTDI Module [USB to TTL serial] with a 10K resistor.
#
#                       +5V from FTDI VCC pin
#                             |
#                             |
#                         [ 10kΩ ]
#                             |
#                             +-------------------+
#                             |                   |
#                         FTDI CTS pin       Rotary Dial Switch
#                                                (Normally closed,
#                             |                  opens during pulse)
#                             |
#                            GND <-------------+
#                         (FTDI GND pin)       |
#                                              |
#                         GND <----------------+
#                   (Rotary phone body ground or second switch terminal)

import serial
import time
import socket
import sys
import msvcrt  

# >>>>> Rotary Pulse Detection Settings.  Set to your COM port <<<<<
ser = serial.Serial('COM21', baudrate=9600, timeout=1)
prev_cts = ser.cts
pulse_count = 0
number = ""
last_pulse_time = None
last_activity_time = None

# >>>>> FlexRadio Control Settings <<<<<
DEBUG = "OFF"   # Turn on to show vebose comments that could be helpful for debugging
IP_ADDRESS = "192.168.1.2"  # <<< Replace with your FlexRadio IP address
PORT = 4992
MODE = "CW"
BANDWIDTH_HZ = 2800
SLICE_INDEX = 0  # 0 = Slice A

print("Ready:  Dial in the frequency...")

def send_cmd(sock, cmd, sequence):
    sock.send(cmd.encode())
    sequence += 1
    time.sleep(0.5)
    response = sock.recv(4096).decode()
    return response, sequence

try:
    while True:
        cts = ser.cts
        current_time = time.time()

        # Detect falling edge pulse from rotary dial
        if prev_cts and not cts:
            pulse_count += 1
            last_pulse_time = current_time
            last_activity_time = current_time

        # Build freq number if input detected between 0.25 and 3 seconds
        if pulse_count > 0 and last_pulse_time:
            if 0.250 <= (current_time - last_pulse_time) <= 3:
                digit = pulse_count if pulse_count != 10 else 0
                number += str(digit)
                print(f"Digit entered: {digit} | Frequency so far: {number}")
                pulse_count = 0
                last_pulse_time = None

        # If >3 sec since last pulse, treat number as complete
        if number and last_activity_time and (current_time - last_activity_time) > 3.0:
            print(f"\nTuning to Frequency: {number}")

            # Format number for FlexRadio
            FrequencyString = number
            try:
                FREQUENCY_HZ = int(float(FrequencyString) / 1000 * 1_000_000)
                if DEBUG == "ON":
                    print(f"Prepared settings:")
                    print(f"  Frequency: {FREQUENCY_HZ / 1_000_000:.3f} MHz")
                    print(f"  Mode: {MODE}")
                    print(f"  Filter Bandwidth: {BANDWIDTH_HZ} Hz")
                    print(f"  Slice: {SLICE_INDEX}")
            except ValueError:
                print("Invalid frequency input. Expected format like '14114.000'.")
                number = ""
                continue

            # Send to FlexRadio automatically, no keyboard interaction
            sock = None
            client_handle = None
            sequence = 1
            try:
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.settimeout(5)
                sock.connect((IP_ADDRESS, PORT))
                if DEBUG == "ON":
                    print(f"\nConnected to FlexRadio at {IP_ADDRESS}:{PORT}")
                response = sock.recv(4096).decode()
                for line in response.splitlines():
                    if line.startswith("H"):
                        client_handle = line[1:]
                        if DEBUG == "ON":
                            print(f"Client handle assigned: {client_handle}")
                if not client_handle and DEBUG == "ON":
                    print("Warning: No client handle received.")
            except Exception as e:
                print(f"Connection failed: {e}")
                sock = None

            if sock and client_handle:
                resp, sequence = send_cmd(sock, f"C{sequence}|slice list\n", sequence)
                if DEBUG == "ON":
                    print(f"Slice list response:\n{resp}")

                freq_mhz = FREQUENCY_HZ / 1_000_000
                send_cmd(sock, f"C{sequence}|slice tune {SLICE_INDEX} {freq_mhz:.6f}\n", sequence)
                send_cmd(sock, f"C{sequence}|filter set {SLICE_INDEX} low=0 high={BANDWIDTH_HZ}\n", sequence)
                send_cmd(sock, f"C{sequence}|transmit set slice={SLICE_INDEX}\n", sequence)
                send_cmd(sock, f"C{sequence}|client disconnect {client_handle}\n", sequence)

                sock.close()
                print("Radio settings applied successfully!!!")
                print(" ")
                print(" ")
                print("Ready to detect rotary dial pulses for frequency...")

            # Reset for next number
            number = ""
            pulse_count = 0
            last_pulse_time = None
            last_activity_time = None

        prev_cts = cts
        time.sleep(0.01)

except KeyboardInterrupt:
    print("\nBye for now...")

finally:
    ser.close()

-----

Wednesday, May 21, 2025

Is your Reverse Beacon (RBN) Spotting Station Up?

  

-----

The Reverse Beacon Network (RBN) is a system of volunteer-operated CW (Morse Code) skimmers that listen to ham radio bands and automatically report any CW CQ signals they hear.    These reports include callsign, signal strength, location, speed (WPM), etc. and are then relayed to a central server.  So who cares?  Well, if you are a ham radio CW operator this allows you to see where your signals are reaching, identify other operators, monitor band conditions, and a whole lot more.   See the RBN site for details.

-----

If you are running or monitoring a RBN skimmer/reporting station knowing if the system is reporting information (spots) to the central service is information you want.   Most of these stations are "just running happily in the background" and the operator running the station may not know it is down.  So...

-----

We created a Python script to send an email if a skimmer you own or monitor is down.   The code below will send an email alert if no spots have been reported in 60 minutes for a particular spotter's callsign.  It allows control of the bands, WPM, distance, and other filtering options through the RBN URL that the program interrogates.  

-----

# Python3 script to monitor a RBN CW Spotting Station
# and report with email if that station is down.
# Programs assume FireFox browser installed somewhere  
# Program assumes Gmail acct for sending email.

# WhiskeyTangoHotel.Com
# MAY 2025

import time
from selenium import webdriver
# Assumes Firefox is installed somewhere
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
import smtplib
from email.mime.text import MIMEText

# --- CONFIGURATION and SETUP ---
CHECK_INTERVAL_MINUTES = 60    # How often to check
# Below is the RBN URL used.   You can change bands, etc.  Make certain the age is less than the CHECK_INTERVAL_MINUTES variable
# The call sign in the WEBPAGE_URL and the SEARCH_TERM variable must be the same!!!
WEBPAGE_URL = "https://www.reversebeacon.net/main.php?rows=100&max_age=45,minutes&bands=160,80,60,40,30,20,17,15,12,10,6&modes=cw&spotter_call=K5TR&hide=distance_km,mode,time"
SEARCH_TERM = "K5TR"   # What RBN Spotting station call sign do you want to monitor for uptime?

# Your email settings
EMAIL_FROM = "yourgmailname@gmail.com"
EMAIL_TO = ["rx_address_1@domain.com" , "rx_address_2@domail.net"]  # etc, etc for more email addresses
EMAIL_SUBJECT = "RBN Spot Alert by WhiskeyTangoHotel.Com"
EMAIL_BODY_TEMPLATE = "ALERT: Only {count} occurrences found on the Reverse Beacon Network."

# Gmail credentials
GMAIL_USERNAME = "yourgmailname@gmail.com"
GMAIL_APP_PASSWORD = "abcd efgh ijkl mnop"  # NEVER EVER let your GMAIL_APP_PASSWORD into the wild

def check_count():
    options = Options()
    options.add_argument("--headless")
    service = Service()
    driver = webdriver.Firefox(service=service, options=options)
    driver.get(WEBPAGE_URL)
    time.sleep(5)  # Don't shorten this
    page_source = driver.page_source
    driver.quit()
    return page_source.count(SEARCH_TERM)

def send_email(count):  # Gmail sending stuff
    msg = MIMEText(EMAIL_BODY_TEMPLATE.format(count=count))
    msg["Subject"] = EMAIL_SUBJECT
    msg["From"] = EMAIL_FROM
    msg["To"] = ", ".join(EMAIL_TO)  # Displayed in the email header

    with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:  
        smtp.login(GMAIL_USERNAME, GMAIL_APP_PASSWORD)
        smtp.sendmail(EMAIL_FROM, EMAIL_TO, msg.as_string())

def main_loop():
    while True:  # Loop until the Dallas Cowboys win a Super Bowl
        count = check_count()
        timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
        print(f"[{timestamp}] Found {count} occurrence(s) of K5TR.")
        
        if count <= 1:   # Set to <=1 unless testing.  If this condition is met then send email.
            send_email(count)
            print("Email sent.")

        for minute in range(CHECK_INTERVAL_MINUTES, 0, -1):  # Print status message.
            time_now = time.strftime('%Y-%m-%d %H:%M:%S')
            print(f"[{time_now}] Next {SEARCH_TERM} RBN Spotter check in {minute} minutes...")
            #print(f"  Next check in {minute} minutes...")
            time.sleep(60)
        print()  # Move to next line after countdown

if __name__ == "__main__":
    main_loop()


-----

Monday, April 28, 2025

VBand interface to Physical Morse Key

-----

What is VBand?  It's a virtual CW Band where you can have a virtual Morse Code QSO as well as practice your CW skills.  It doesn't require a ham radio rig.  It doesn't require a ham radio license.   It's not ham radio, but it is popular and a great place for some none RF fun.

-----

First things first; this problem has been solved over and over.   Our frustration was finding a one stop site that provided clear instructions on how to interface a Morse Key to the VBand website.  Also, there are several VBand interfaces for sell that reports suggest "just don't work".   If you decide DIY is not for you then be safe and make your purchase directly from the VBand store.

----

Bill of Materials and Hookup (~$10USD):

-----
Of course, before anything works you will need to use the Arduino IDE to upload the software below to the Arduino Pro Micro.   
 
//This program acts a USB interface for a CW paddle or straight key for VBand Virtual CW Band.
//Program written by Tony Milluzzi, KD8RTT using Keyboard library example as starting point.

// WhiskeyTangoHotel.com notes APRIL 2025:
//       IDE Board Selection: Ardunio Micro (but my board is labeled Pro Micro)
//       The VBand web address is: https://hamradio.solutions/vband/
//       Select Speed, keyer type, etc. on the VBand website 'Settings" section

#include "Keyboard.h"

//declaring paddle input pins
const int dah = 3;          
const int dit = 2;

int previousdahState = HIGH;
int previousditState = HIGH;


void setup() {
  //declare the inputs as input_pullup
  pinMode(dah, INPUT_PULLUP);  
  pinMode(dit, INPUT_PULLUP);  
  Keyboard.begin();
}

void loop() {
  //checking the state of the inputs
  int dahState = digitalRead(dah);
  int ditState = digitalRead(dit);

 
 //replaces left paddle input with dit using "[" as defined by VBand website
  if (dahState == LOW && previousdahState == HIGH) {
      // and it's currently pressed:
    Keyboard.press(93);
    delay(50);
  }
  if (dahState == HIGH && previousdahState == LOW) {
      // and it's currently released:[[[[]]]]
    Keyboard.release(93);
    delay(50);
  }
 
 //replaces right paddle input with dah using "]" as defined by VBand website
  if (ditState == LOW && previousditState == HIGH) {
      // and it's currently pressed:
    Keyboard.press(91);
    delay(50);
  }
  if (ditState == HIGH && previousditState == LOW) {
      // and it's currently released:
    Keyboard.release(91);
    delay(50);
  }

  previousdahState = dahState;
  previousditState = ditState;
}
 
-----

At this point everything should "just work".  However, there are some VBand setting you can adjust. The settings are obvious, but we will highlight two here:

-----

Provided you have used the Arduino IDE before to upload code the whole project should take only a few minutes.   Enjoy VBand, practice up, and hope to see you on 'real' airwaves.

-----

Monday, October 28, 2024

Tuesday, September 17, 2024

D104 Vintage Mic made Morse (CW) Key

 

 -----

Question: What do you do when a ham radio pal gifts you a vintage Astatic D104 power mic and your only operating mode is CW (Morse code)?

Answer: You rewire the PTT button, add a blue LED and turn the thing into a straight key.

-----

Monday, April 29, 2024

Where is the ISS? [Rasberry Pi version]

  

-----

It all started out simple enough... We were just curious how Orbital Files and Kepler Elements are use to define the current and predicted locations of a satellite.  One thing leads to another and the result was an International Space Station tracker.  Sure, it's been done before but you can learn a few things when you do it yourself.

-----

For example, we wrote the Python3 program on our own and then decided we would see how ChatGPT would handle the problem.   Turns out we were a lot faster at the task than the ChatGPT AI.  ChatGPT would finally get there, but it seemed error prone.  Still, impressively ChatGPT finally got to a solution after a TON of help from us.  But... we did like some of the things ChatGPT did better.  For example, the ChatGPT routine to translate compass degrees to a cardinal direction was better than ours.  So, in addition to learning about Orbital Files and Kelper Elements we learned a little about what ChatGPT is good and "less good" at.

-----

The project updates the location on the ISS for your location every ten seconds.  If the ISS is above the horizon a LED flashes to let you know.  The whole enchilada is contained in a 3D Printed box.

Here's the result and we like it!

-----

The connection of the 16x2 LCD and the indicator LED are simple and pretty obvious from the pin names in the source code below.

-----

For those that may want to duplicate the build below is our Python3 source code:

# ISS Tracker
# Project details at: WhiskeyTangoHotel.Com
#                     APRIL 2024
#
# Raspberry PI 3.  Runs under Python3
#
# Show current Alt and Az of the ISS
# Results are displayed in the terminal and 1602 Line LCD
# LED Blinks when ISS is above horizon.

from datetime import datetime, timedelta
from skyfield.api import Topos, load
import os
import urllib.request
import time
import pytz
import smbus
import RPi.GPIO as GPIO
GPIO.setwarnings(False)
risefall = "Calculating..."   # Is the ISS getting closer or farther from the horizon
seconds_between_screens = 5  # How long to show the position and current time screen

# Set up the 16 x 2 Line I2C LCD
I2C_ADDR  = 0x27 # I2C device address
LCD_WIDTH = 16   # Maximum characters per line

LCD_CHR = 1 # Mode - Sending data
LCD_CMD = 0 # Mode - Sending command

LCD_LINE_1 = 0x80 # LCD RAM address for the 1st line
LCD_LINE_2 = 0xC0 # LCD RAM address for the 2nd line

LCD_BACKLIGHT  = 0x08  # On
# LCD_BACKLIGHT = 0x00  # Off

ENABLE = 0b00000100 # Enable bit

# Timing constants
E_PULSE = 0.0005
E_DELAY = 0.0005

# Open I2C interface
bus = smbus.SMBus(1) # Rev 2 Pi uses 1 RasPI(SXSW)

# LED pin to blink if ISS is above horizon
LED_PIN = 17  

def setup_led():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(LED_PIN, GPIO.OUT)

def led_on():
    GPIO.output(LED_PIN, GPIO.HIGH)

def led_off():
    GPIO.output(LED_PIN, GPIO.LOW)

def lcd_init():
    # Initialise display
    lcd_byte(0x33,LCD_CMD) # 110011 Initialise
    lcd_byte(0x32,LCD_CMD) # 110010 Initialise
    lcd_byte(0x06,LCD_CMD) # 000110 Cursor move direction
    lcd_byte(0x0C,LCD_CMD) # 001100 Display On,Cursor Off, Blink Off
    lcd_byte(0x28,LCD_CMD) # 101000 Data length, number of lines, font size
    lcd_byte(0x01,LCD_CMD) # 000001 Clear display
    time.sleep(E_DELAY)

def lcd_byte(bits, mode):
    # Send byte to data pins
    # bits = the data
    # mode = 1 for data
    #        0 for command

    bits_high = mode | (bits & 0xF0) | LCD_BACKLIGHT
    bits_low = mode | ((bits<<4) & 0xF0) | LCD_BACKLIGHT

    # High bits
    bus.write_byte(I2C_ADDR, bits_high)
    lcd_toggle_enable(bits_high)

    # Low bits
    bus.write_byte(I2C_ADDR, bits_low)
    lcd_toggle_enable(bits_low)

def lcd_toggle_enable(bits):
    # Toggle enable
    time.sleep(E_DELAY)
    bus.write_byte(I2C_ADDR, (bits | ENABLE))
    time.sleep(E_PULSE)
    bus.write_byte(I2C_ADDR,(bits & ~ENABLE))
    time.sleep(E_DELAY)

def lcd_string(message,line):
    # Send string to display
    message = message.ljust(LCD_WIDTH," ")
    lcd_byte(line, LCD_CMD)

    for i in range(LCD_WIDTH):
        lcd_byte(ord(message[i]),LCD_CHR)

# We set up to view the ISS with Austin, Texas coordinates
austin = Topos(latitude=30.35307, longitude=-97.85726)

# URL to fetch TLE data
tle_url = 'https://www.celestrak.com/NORAD/elements/stations.txt'

# Let's do self-test on the LCD and the LED
lcd_init()

# Center the text for line 1
alt_text = "ISS TRACKER"
print("ISS TRACKER")
spaces = (LCD_WIDTH - len(alt_text)) // 2
alt_text = " " * spaces + alt_text
lcd_string(alt_text, LCD_LINE_1)

# Flash LED self-test
setup_led()

for _ in range(5):  # Flash LED
    # Center the text for line 2
    az_text = "Starts in " + str(5 - _) + "..."
    print("Starts in " + str(5 - _) + "...")
    spaces = (LCD_WIDTH - len(az_text)) // 2
    az_text = " " * spaces + az_text
    lcd_string(az_text, LCD_LINE_2)
    led_on()
    time.sleep(0.5)  # LED on for 0.5 seconds
    led_off()
    time.sleep(0.5)  # LED off for 0.5 seconds
GPIO.cleanup()

print(" ")

last_altitude = None  # Variable to store the previous altitude

while True:  # Loop forever...
    # Initialise display
    lcd_init()

    # Check if the TLE data file exists
    if not os.path.exists('stations.txt'):
        # If it doesn't exist, download it
        urllib.request.urlretrieve(tle_url, 'stations.txt')

    # Get the last modified time of the TLE data file
    last_modified = datetime.fromtimestamp(os.path.getmtime('stations.txt'))

    # Check if 12 hours have passed since the last update
    if datetime.now() - last_modified > timedelta(hours=12):
        # If it is too old, update the TLE data file
        urllib.request.urlretrieve(tle_url, 'stations.txt')

    # Load the updated ISS TLE data
    satellites = load.tle_file('stations.txt')
    iss = satellites[0]  # Accessing the first element which contains the ISS data

    # Get the current time
    ts = load.timescale()
    current_time = ts.now()

    # Get the position of the ISS relative to Austin, Texas
    difference = iss - austin
    topocentric = difference.at(current_time)
    alt, az, distance = topocentric.altaz()
    
    #alt.degrees = 73  # For debug only.  Comment for normal use
    
    # Print the current time and date in Austin
    austin_time = current_time.astimezone(pytz.timezone('America/Chicago'))
    print(austin_time.strftime("%A"), austin_time.strftime("%m-%d-%Y %H:%M:%S %Z"))
    print("--------------------------------------")
    
    # Print altitude and azimuth of the ISS as integers
    print("ISS altitude: " + str(int(alt.degrees)) + "\u00b0")
    
    
    # Convert azimuth to cardinal compass direction
    compass_directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
    compass_index = round(az.degrees / (360. / len(compass_directions)))
    compass = compass_directions[int(compass_index) % len(compass_directions)]
    #print("Cardinal direction: " + compass)
    print(" ISS azimuth: " + str(int(az.degrees)) + "\u00b0" + " <" + compass + ">")
    
    # Check if the altitude is increasing or decreasing
    if last_altitude is not None:
        if alt.degrees > last_altitude:
            risefall = "RISING"
        elif alt.degrees < last_altitude:
            risefall = "FALLING"
        else:
            risefall = "STABLE"
            
    #risefall = "RISING" # For debug only.  Comment for normal use
            
    print("              " + risefall)
    last_altitude = alt.degrees  # Update the last altitude
    
    print(" ")

    # Display on LCD
    lcd_init()

    # Center the text for line 1
    alt_text = "Alt " + str(int(alt.degrees)) + " deg @"
    spaces = (LCD_WIDTH - len(alt_text)) // 2
    alt_text = " " * spaces + alt_text
    lcd_string(alt_text, LCD_LINE_1)

    # Center the text for line 2
    az_text = str(int(az.degrees)) + " deg <" + compass + ">"
    spaces = (LCD_WIDTH - len(az_text)) // 2
    az_text = " " * spaces + az_text
    lcd_string(az_text, LCD_LINE_2)

    # Flash LED if altitude is positive
    setup_led()
    if alt.degrees > 0:
        for _ in range(seconds_between_screens):  # Flash LED
            led_on()
            time.sleep(0.5)  # LED on for 0.5 seconds
            led_off()
            time.sleep(0.5)  # LED off for 0.5 seconds
    else:
        led_off()
        time.sleep(seconds_between_screens)
    
    # Date/Time on LCD briefly
    lcd_init()
    
    if alt.degrees > 0:
        led_on()  # LED on is ISS UP while Day/Time display

    # Center the text for line 1
    #alt_text = austin_time.strftime("%A")  # Show the DOW
    alt_text = risefall  # Rising or Falling?
    spaces = (LCD_WIDTH - len(alt_text)) // 2
    alt_text = " " * spaces + alt_text
    lcd_string(alt_text, LCD_LINE_1)

    # Center the text for line 2
    az_text = austin_time.strftime("%H:%M:%S %Z")  # Show the local time
    spaces = (LCD_WIDTH - len(az_text)) // 2
    az_text = " " * spaces + az_text
    lcd_string(az_text, LCD_LINE_2)   
    
    # Flash LED if altitude is positive
    setup_led()
    if alt.degrees > 0:
        for _ in range(seconds_between_screens):  # Flash LED
            led_on()
            time.sleep(0.5)  # LED on for 0.5 seconds
            led_off()
            time.sleep(0.5)  # LED off for 0.5 seconds
    else:
        led_off()
        time.sleep(seconds_between_screens)
    
    time.sleep(0.1)  # tiny delay before cleanup to ensure the LED is off
    GPIO.cleanup()
-----

Sunday, March 24, 2024

Headphone Jack turns Ham Radio Morse Key

 

-----

We have featured a few of our DIY ham radio Morse Code Keys here before, but this one is less than a dollar, super portable, and actually works surprisingly well.  I may have found my new hiking key!

-----

Monday, December 4, 2023

Measuring CW Keyer speed on the Flex 6400 and QRP Labs QCX

 

----

We are linking to the Alan W2AEW video above.  Alan's YouTube channel is an excellent resource for ham radio, test and measurement, as well as other electronics knowledge.   The video gave us the idea to check the CW keyer speeds on our most expensive rig and our lowest price rig.  It seemed like a good excuse to use the Liquid Instruments Moku:Lab oscilloscope, plus we were curious. 

----

We won't explain the method, Alan W2AEW does a great job with that in the video.  One different is we used the audio output for our delta(t) measurement.

-----

Here are our results:

We haven't seen the same RBN vs. Rig WPM speed different as Alan pointed out and the table helps explain why.  Sure, the numbers are 'off' at the 25WPM test, but we seldom operate at those CW speeds.

----

FlexRadio 6400: Moku:Lab oscilloscope captures:



-----

QRP Labs QCX: Moku:Lab oscilloscope captures:



-----

Friday, October 27, 2023

Automagically Switch an Idle FlexRadio to WSPR

 

-----

When our FlexRadio is not being used it really should do 'something'.   We typically put it in a WSPR spotting mode via WSJT-X, but sometimes we forget.  This short simple Python script takes care of that.

----

FlexRadio has an open API and we could have just used it to accomplish the project.  However, there is wonderful (and free) program that is always open on the PC running with the FlexRadio SmartSDR software that make things even easier.  We use FRStack for it's API calls and to automatically set things up for WSJT-X when the rig is idle.  NOTE: We use FRStack for other features as well.  IMO it is a must have program to make your Flex more flexible. 

-----

The Python script listed below looks at user defined intervals (in the example it is every 60 minutes) to see if any settings on the rig have changed.  If a setting has changed (frequency, band, mode, volume, filters, etc. etc. etc...) the program assumes the rig is in use and just waits for the next check interval. 


-----

If the time interval expires and no settings have been changed the program assumes the rig is idle and switches it to DIGU mode and starts reporting WSPR spots.



-----

# auto_wspr.py  OCT2023
# JUN2026 edits removes Slicemaster dependency
#
# Python script to make the Flex Radio switch to WSPR mode when idle.
#
# Written by:  WhiskeyTangoHotel.Com
#
# Leverages the auto LAUNCH feature of "Slice Master".
# We don't use Slicemaster for Spots.  
# Now prog excepts WSJT-X runs all the time.  Spots by FRSTACK
# https://github.com/K1DBO/slice-master-6000
#
# Leverages WSJT-X for TX/Rx spotting
# https://wsjt.sourceforge.io/wsjtx.html
#
# Leverages FRStack
# https://www.mkcmsoftware.com/download/FRStackWebApiReadme.html#apis

import requests
import urllib.request
import time
from datetime import datetime
import pytz

# Dictionary mapping city names to their respective timezones
city_timezones = {
    "austin": "America/Chicago"
    # Add more cities and their timezones if needed
}

city = "austin"  # Change this to any city you want to get the local time for

# Get the timezone for the given city
timezone = pytz.timezone(city_timezones.get(city.lower()))

# Set idle_max to the number of minutes between checks to see if any rig settings have changed.
# If idle rig then switch to DIGU mode.  DIGU mode will trigger Slice Master to start WSJT-X.

idle_max = 60    # in minutes,  How ofen to check to see if the rig is idle.

# 9 WSPR frequencies listed to cycle through
wspr_bands = [
    ("80m", 3.5686),
    ("40m", 7.0386),
    ("30m", 10.1387),
    ("20m", 14.0956),
    ("17m", 18.1046),
    ("15m", 21.0946),
    ("12m", 24.9246),
    ("10m", 28.1246),
    ("6m", 50.2930)
]

band_index = 0

last_status = 'First_run'
mode = ' '

#print ( 'Entering endless loop.....' )
while True:    
print ('Checking for rig activity every' , idle_max , 'minutes.')
print ('---------------------------------------------------')

# If rig is in DIGU mode then do nothing.  We are already running WXJT
response = requests.get('http://localhost:13522/api/ActiveSlice/MODE?')
response = response.text
mode = response
#print ('MODE is:' , response)
if response == '"DIGU"':

print('Rig already in DIGU mode.')
print('Cycling to next WSPR band.')

# Tune to the next WSPR band
band_name, freq = wspr_bands[band_index]

print(f"Switching to {band_name} WSPR ({freq} MHz)")

urllib.request.urlopen(
f'http://localhost:13522/api/ActiveSlice/FREQ?={freq}'
)

# Advance to the next band for the next idle cycle
band_index = (band_index + 1) % len(wspr_bands)

else:  # not in DIGU mode. Have rig settings changed or is it idle?

response = requests.get('http://localhost:13522/api/ActiveSlice/INFO?')
current_status = response.text

if last_status != current_status:

print('Rig in use. Do not change to DIGU. Do not launch WSJT')

last_status = current_status

else:

print('Rig has been idle for', idle_max, 'minutes.')

# DIGU modes can be LOUD. Adjust the volume to low
urllib.request.urlopen(
'http://localhost:13522/api/ActiveSlice/AUDIOGAIN?=+1'
)

# Switch rig to DIGU mode
urllib.request.urlopen(
'http://localhost:13522/api/ActiveSlice/MODE?=DIGU'
)

# Tune to the first WSPR band after becoming idle
band_name, freq = wspr_bands[band_index]

print(f"Switching to {band_name} WSPR ({freq} MHz)")

urllib.request.urlopen(
f'http://localhost:13522/api/ActiveSlice/FREQ?={freq}'
)

band_index = (band_index + 1) % len(wspr_bands)

# Remember this status so we don't repeatedly think the rig
# has just become idle.
last_status = current_status

for nextcheck in range(idle_max, 0, -1):
response = requests.get('http://localhost:13522/api/ActiveSlice/MODE?')
response = response.text
mode = response
response = requests.get('http://localhost:13522/api/ActiveSlice/FREQ?')
response = response.text
freq = response
local_time = datetime.now(timezone)
#print("-------------------------------")
#print(f"The local time in {city.capitalize()}, Texas is:", local_time.strftime("%Y-%m-%d %H:%M:%S %Z"))
print ('MODE:', mode, "@" , freq, '|', local_time.strftime("%H:%M:%S %Z"), "with",  int(nextcheck) , "minutes until next idle rig check...")
time.sleep(60)   # 60 because one minute increments expected.
print (' ')
# end endless while loop
-----

Saturday, May 27, 2023

HamAlert.Org Integration with Pimoroni Galactic Unicorn

 

-----

The Pimoroni Galactic Unicorn is a cool piece of kit that we have had our eye on for a while.  The item is popular and sells out quickly, but we finally got our hands on one.  Summary:  It's awesome!  After the initial "WOW!" factor diminished we went in search of a project for it.

-----

The answer was obvious.  How many times are you sitting comfortably in your home theater room with the family and you miss a Morse Code CQ from one of your ham radio friends?  That's what we thought, so we set out to solve that inconvenient and irritating problem!

-----

The project needs a WiFi connection for the Pimoroni Galactic Unicorn.  No ham radio or RF access to the ham bands are needed.  

Steps:

    - Buy a Pimoroni Galactic Unicorn and set it up to run MicroPython.

    - Create a HamAlert account and set up some triggers for the ham operators of interest.  You will need to select the "telnet" reporting option in the trigger menu.

    - Copy/Paste our MicroPython source code below into the Pimoroni Galactic Unicorn. We used Thonny as our development environment.  Name the program "main.py" so it will autostart at bootup.

    - Place the rig under that big screen TV in the home theater room.  Simply wait for your movie or favorite show to be interrupted letting you know it's time to drop everything and fire up your ham radio for a QSO!

-----

Here is a live demo of the setup.  What happens is:

    - K5JM, who is in my HamAlert triggers is spotted calling CQ.

    - The Pimoroni Galactic Unicorn is logged into and monitoring the HamAlert telenet server.

    - We parse the response of the HamAlert telnet server and display the information we want on the LED matrix.

    - Of course, we answer K5JM.  Success!!!  Next we return to the home theater room and await the next interruption.


 -----

Source code below:

 '''
Morse Alert
MAY2023
WhiskeyTangoHotel.Com

This program displays your individual hamalert.org telnet CW triggers
onto the display of the Raspberry PI PicoW based Pimoroni Galactic Unicorn.
Thanks HamAlert.Org by Manuel Kasper (HB9DQM) for their telnet service!

CW CQ info shown is "Callsign", "Frequency", and "WPM", but other options are available.
The text is color coded by band.
Scroll speed and total display variables are adjustable.
Alert 'chirp' is adjustable.

A CW beacon, such as WR5U which transmits about every 30 minutes,
may be a suggested hamalert telnet trigger to avoid timeouts from the
hamalert.org server.

NOTE: Does not work with hamalert.org simulated triggers

'''


#Variable set up:
wifi_ssid = "wifi_ssid"
wifi_password =  "wifi_password"

# hamalert.org login info
username = "telnet_username"
password = "telnet_passord"
telnetaddr = "hamalert.org"  # "telnetaddress.com"
port = 7300    # port as a number, not a string

from galactic import GalacticUnicorn
gu = GalacticUnicorn()

# Set up speaker for a sweeping 'chirp' alert
volume = .5 # range is 0 to 1.  0 = For no sound
start_tone = 500 #500 is a good start  Adjust to suit.
end_tone = 1000  #1000 is a good start.  Adjust to suit.
channels = [gu.synth_channel(i) for i in range(1)]

waitmsg = "HamAlert..."
howbright = 0.1 # value range 0.0 to 1.0
dwelltime = 5 # how many seconds to display Callsign, Freq, WPM
tot_time = 60 # how long (seconds) to cycle this info

utc_offset = -5 # we print to screen (not to LEDs the zulu and local time)

import network
import usocket as socket
import time
import utime

from picographics import PicoGraphics, DISPLAY_GALACTIC_UNICORN
graphics = PicoGraphics(display=DISPLAY_GALACTIC_UNICORN)

#Define some colours
BLACK = graphics.create_pen(0, 0, 0)
RED =  graphics.create_pen(255, 0, 0)
YELLOW = graphics.create_pen(255, 255, 0)
GREEN = graphics.create_pen(0, 255, 0)
CYAN =  graphics.create_pen(0, 255, 255)
BLUE =  graphics.create_pen(0, 0, 255)
MAGENTA =  graphics.create_pen(255, 0, 255)
WHITE =  graphics.create_pen(200, 200, 200)
GREY =  graphics.create_pen(100, 100, 100)
DRKGRY =  graphics.create_pen(50, 50, 50)
FREQCOLOR = WHITE  # this is the text color that will change per band
waitmsgcolor =  GREY  # number, not a string

# create a PicoGraphics framebuffer to draw into
graphics = PicoGraphics(display=DISPLAY_GALACTIC_UNICORN)
gu.set_brightness(howbright)

#Create a single wlan object and use as a global for all net calls
wlan = network.WLAN(network.STA_IF)

wlan.active(True)
wlan.connect(wifi_ssid, wifi_password)

# Wait for connect success or failure
max_wait = 100
while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    max_wait -= 1
    wifistat = 'WiFi...' + str(100-max_wait)
    if max_wait == 0:
        wifistat = "WiFi fail"
    print(wifistat)
    width = graphics.measure_text(wifistat, 1)
    startplace = int(float(GalacticUnicorn.WIDTH) - width) / 2
    # clear the graphics object
    graphics.set_pen(BLACK)
    graphics.clear()
    # draw the text
    graphics.set_pen(waitmsgcolor)
    graphics.text(wifistat, round(startplace), 2, -1, 0.55);    
    # update the display
    gu.update(graphics)
    time.sleep(.5)
    
if max_wait > 0:
    print("WIFI OK!")
    width = graphics.measure_text('WIFI OK!', 1)
    startplace = int(float(GalacticUnicorn.WIDTH) - width) / 2
    # clear the graphics object
    graphics.set_pen(BLACK)
    graphics.clear()
    # draw the text
    graphics.set_pen(waitmsgcolor)
    graphics.text('WIFI OK!', round(startplace), 2, -1, 0.55);    
    # update the display
    gu.update(graphics)    
    time.sleep(5)

# Connect to the telnet server
tn = socket.socket()
addr = socket.getaddrinfo(telnetaddr, port)[0][-1]
tn.connect(addr)

# Log in with the username and password to telnet server
tn.send(username + "\r\n")
tn.send(password + "\r\n")

last_spot = utime.time()  # we track/print time between spots

# Read and process the telnet server response
while True:
    data = tn.recv(1024)
    data = data.decode("utf-8")
    print(data)
    
    # Center "waitmsg"
    width = graphics.measure_text(waitmsg, 1)
    startplace = int(float(GalacticUnicorn.WIDTH) - width) / 2
    # clear the graphics object
    graphics.set_pen(BLACK)
    graphics.clear()
    # draw the text
    graphics.set_pen(waitmsgcolor)
    graphics.text(waitmsg, round(startplace), 2, -1, 0.55);    
    # update the display
    gu.update(graphics)

    # Play 'connected' Chirp alert
    for tone in range(start_tone, end_tone):
        channels[0].play_tone(tone, volume)
        gu.play_synth()
        time.sleep(.0009)
    gu.stop_playing()
    
    if "DX de" in data:
        #print(data)
                
        # Split the string into a list of values
        data_list = data.split()
        
        # Assign each value to separate variables
        dx = data_list[0]
        de = data_list[1]
        spotter = data_list[2]
        freq = data_list[3]
        spotted = data_list[4]
        db = data_list[5]
        wpm = data_list[6]
        zulu = data_list[7]

        # Print the values of the variables
        print("HamAlert returns:")
        #print('dx:', dx )
        #print('de:', de)
        #print('spotter:', spotter)
        print('Freq:', freq)
        print('Spotted:', spotted)
        #print('db:', db)
        print('WPM:', wpm)
        #print('zulu:', zulu)
        
        #Convert zulu to local
        hours = int(zulu[:2])
        minutes = int(zulu[2:4])     
        local_hours = hours + utc_offset
        local_minutes = minutes
        # If negative hour fix wraparound
        if local_hours < 0:
            local_hours += 24
        # Format to local timeg
        local_time = "{:02d}:{:02d}".format(local_hours, local_minutes)
        print('Local time:', local_time)
        #print('Minutes last spot:', int( (utime.time() - last_spot)/ 60)  )
        last_spot = utime.time()  # we track/print time between spots
        print('-------------------')
        print
        
        FREQCOLOR = WHITE
        band = float(freq)
        if 24890 <= band <= 24990:
            FREQCOLOR = CYAN # text color for 12 meter spots
        if 18068 <= band <= 18168:
            FREQCOLOR = BLUE # text color for 17 meter spots
        if 14000 <= band <= 14350:
            FREQCOLOR = MAGENTA  # text color for 20 meter spots          
        if 10100 <= band <= 10150:
            FREQCOLOR = RED # text color for 30 meter spots
        if 7000 <= band <= 7300:
            FREQCOLOR = YELLOW  # text color for 40 meter spots                  
        if 3500 <= band <= 4000:
            FREQCOLOR = GREEN # text color for 80 meter spots
        
        for x in range(0, int((tot_time/(dwelltime*3)))):
            # Center position of the text
            width = graphics.measure_text(spotted, 1)
            startplace = int(float(GalacticUnicorn.WIDTH) - width) / 2
            # clear the graphics object
            graphics.set_pen(BLACK)
            graphics.clear()
            # draw the text
            graphics.set_pen(FREQCOLOR)
            graphics.text(spotted, round(startplace), 2, -1, 0.55);
            # update the display
            gu.update(graphics)
            time.sleep(dwelltime)
            
            # Center position of the text
            width = graphics.measure_text(freq, 1)
            startplace = int(float(GalacticUnicorn.WIDTH) - width) / 2
            # clear the graphics object
            graphics.set_pen(BLACK)
            graphics.clear()
            # draw the text
            graphics.set_pen(FREQCOLOR)
            graphics.text(freq, round(startplace), 2, -1, 0.55);
            # update the display
            gu.update(graphics)            
            time.sleep(dwelltime)
            
            # Center position of the text
            width = graphics.measure_text(wpm, 1)
            startplace = int(float(GalacticUnicorn.WIDTH) - width) / 2
            # clear the graphics object
            graphics.set_pen(BLACK)
            graphics.clear()
            # draw the text
            graphics.set_pen(FREQCOLOR)
            graphics.text(wpm, round(startplace), 2, -1, 0.55);
            # update the display
            gu.update(graphics)
            time.sleep(dwelltime)
                
        #print(waitmsg)
       
        # Below if you want time of last spot shown.  This can help ID a telnet timeout.
        # See our reason for having a CW beacon comment at the very top.
        waitmsg = 'Last ' + local_time
        
        # This if you want the your pre-defined waitmsg after a spot
        #width = graphics.measure_text(waitmsg, 1)
        
        width = graphics.measure_text(waitmsg, 1)
        startplace = int(float(GalacticUnicorn.WIDTH) - width) / 2
        # clear the graphics object
        graphics.set_pen(BLACK)
        graphics.clear()
        # draw the text
        graphics.set_pen(waitmsgcolor)
        graphics.text(waitmsg, round(startplace), 2, -1, 0.55);
        # update the display
        gu.update(graphics)
-----