Showing posts with label CW. Show all posts
Showing posts with label CW. 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.

-----

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:



-----

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)
-----


Thursday, March 2, 2023

DIY Morse (CW) Key on the Cheap [Paddle Version]


 Schematics sent on request. HIHI.

-----

Monday, January 16, 2023

DIY Morse (CW) Key on the Cheap

 

Schematics sent on request. HIHI.

-----

Friday, September 16, 2022

Morse Code (CW) Paddle becomes Bug

 

-----

CW or Morse Code is a way ham radio operators send messages with a simple tone of two durations.  It was a must before the days of wireless voice communications and still remains popular because it is a fun challenge.  Like most things, Morse keys range from low cost to expensive.  Generally speaking Morse keys come in three styles: straight key, paddle, and bug.  Each has pros and cons requiring unique skills to operate with the "bug" typically being more expensive and requiring more skill.

To use this rig, you don't need a ham radio license unless you connect to an actually ham radio transmitter; which is kinda the whole point.  My quick ham radio advertisement:  Get a FCC Amateur Radio license; it's not hard and is an extremely interesting hobby.  BTW, learning Morse Code is no longer a requirement.

-----

We wanted to try a Morse bug key, but didn't want one 'forever' and didn't want to spend the money.   Plus like most ham radio operators we think any project is a good project regardless of it's usefulness.   Basically, with a bug key the dahs (dashes) are made manually and the dits (dots) are generated semi automatically via a spring mechanism.  The design is mechanically challenging so we opted for a different method.

-----

The dah (dash) part is easy.  For the semi automatic dash (dit) part the traditional mechanical spring system was replaced with a $10 signal generator kit.  Pressing the dit key powers up a 7404 and a square wave output is passed through the 7404 simply to provide the "umph" for the relay to open and close providing the dits.   The signal generator frequency control allows for an easy way to adjust the sending WPM (words per minute).


 The video shows our very first unpracticed test of the rig.   After a bit of practice we put the key on the air for a few SKCC QSOs.  Fun was had on our end and tolerance was shown by the OPs decoding our sig,

-----

Here are a few pics of the rig on the bench:


 

-----

And here's the schematic:

Of course, not the traditional Morse key bug, but a nice afternoon project diversion.  73!!!

-----

Sunday, May 30, 2021

Remote CW Straight Key [FlexRadio and RemoteHams.Com]

Note:  As presented this project requires a FCC Amateur Radio license.  It's not hard to get and it is a fun and rewarding hobby. Also, learning Morse code is no longer a requirement.

-----

As a remote ham radio operator that enjoys using a manual CW (Morse code) straight key we were presented with two problems:

Since both require slightly different hardware wiring to the PC running the remote rig, we set out to make something that would be a universal connection regardless of which platform we are using. 

-----

The hardware connection is simple and shown below.  It does require a USB/RS-232 adapter and using the RS-232 terminal block break out module shown makes things easier.   There are many example of these two items on Amazon.Com, etc.

For both applications software is needed as well.

-----

For our RemoteHams.Com solution go here.

-----

For the FlexRadio solution you will need to install the excellent and easy to use Remote Keyer Interface software.   Follow the RKI link for the software and you will also find the very helpful RKI message board that I personally proved is helpful and tolerant of 'stupid' questions.  Seriously, the software has great documentation and even though I asked a "RTFM" question I got helpful direction from the forum.   After you install and run the RKI software you will see this screen: 

Make the remote connection to your FlexRadio via your SmartLink login and select the correct COM Port for your (now) USB straight key.  Verify the other options are selected per above screen shot and you should be good to go.

BTW, Remote Keyer Interface software has several other cool features in addition to this simple application.

-----

Summary:   My remote CW manual straight keying problems are solved!

-----

Thursday, May 20, 2021

Morse Code QSO via Vintage Phone

 

-----

As a preteen many many years ago the thought never occurred that the same phone used to plan childhood mischief would some day be used for a CW (Morse code) ham radio QSO.   Ain't life funny...

-----

We found the old Western Electric phone in the attic and wired the switch hook as a straight key.

 


 -----

Then we went to 80m where we heard ham radio operator K5JM and decided to "ring" him up.  The result; success!!!

-----

73!

Wednesday, January 20, 2021

Flex Radio Turned Boat Anchor

Have you ever wanted the nostalgic sound of a vintage ham radio rig only to be stuck with an expensive modern transceiver?

-----

Note:  As presented this project requires a FCC Amateur Radio license.  Amateur Radio is about experimentation.   Even still you must be versed in the band plan, stay in accordance to FCC Amateur Radio rules in FCC Section 97, and probably a few other things.  That said, let's continue....
-----

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.   But, sometimes you long to send out that relaxing and warm sound that could only be achieved from a vintage "boat anchor" radio.  A signature trademark of these rigs is their inability to hold steady on a frequency.  This results in frequency drift.  This 'feature' is lost in modern rigs like the Flex Radio.  So what's a modern ham to do when that vintage sound is desired?

-----

The Flex Radio is a SDR; think powerful computer turned radio.   Flex allows third party applications to interface with the radio to make it even more powerful.   One such program is FRStack .  One of FRStack's many many features makes it easy to use API calls to control and configure the Flex Radio.  We wrote a simple Python script to have the Flex Radio simulate a drifting vintage ham radio during transmit.   We really don't recommend using this application; it's just stupid.  Our goal was to experiment with the APIs when the idea hit us. 

-----

 Here is a demo of the Flex Radio transmitting CW with the DriftyFlex script:

-----

Here is the short DriftyFlex Python script:

# DriftyFlex.py  JAN2021
# Python script to make the Flex Radio drift
# like that vintage rig you always wanted.
#
# WhiskeyTangoHotel.Com

# Simple novelty example of controlling
# a Flex Radio via FRStack

# FRStack is an amazing program with 100s of features that
# are a million times more useful than this stupid example.
# See: https://www.mkcmsoftware.com/Flex/Index

import urllib.request
import time
import random

# Define initial XIT offset XIT (0 is suggested)
XIT = 0

# Define amount of XIT SHIFT (or drift) per loop
SHIFT = 5  # in Hz

# Define MAX_XIT (or drift) so things don't get too out of control
MAX_XIT = 300  # in Hz

# Define how often to simulate a new drift
DELAY = .1   # in seconds

# turn on XIT mode
urllib.request.urlopen('http://localhost:13522/api/ActiveSlice/XIT?param=1')

print ( 'Drifty Flex endlesssssss loop.' )
while True:    
    DRIFT = (random.randint(-SHIFT, SHIFT))   

    NEW_XIT = XIT + DRIFT

    if abs(NEW_XIT) > MAX_XIT:  # Maintain drift constraints
        NEW_XIT = XIT - DRIFT
       
    url = 'http://localhost:13522/api/ActiveSlice/XITFREQ?param=' + str(NEW_XIT)
    urllib.request.urlopen( url )

    print ( 'Current drift is: ', NEW_XIT, ' Hz.' )
    XIT = NEW_XIT
    
    time.sleep(DELAY)
    # end while loop  

-----

Sunday, July 19, 2020

3D Printed Single Lever Paddle/Cootie Morse Code Key


-----
We had originally printed this Morse Code key to operate in "Cootie" mode.  You can read about that here.   It turns out using a Cootie key is fun and easy on the wrist, but.....  learning to operate a Cootie key does take some practice.   That said, the automotive spark plug feeler gauge gave such an effortless feel we decided to make a small modification and create a Single Lever Paddle from the original design.
-----
This mod allows usage with an electronic keyer in three terminal mode.  The two terminals on the left can be shorted to quickly switch into Cootie mode.

Hardware parts list:

-----



-----

Tuesday, July 14, 2020

Send Morse Code (CW) with your Voice

-----
CW or Morse Code is a way ham radio operators send messages with a simple tone of two durations.  It was a must before the days of wireless voice communications and still remains popular because it is a fun challenge. 

There are many online tools that will convert text into Morse Code.  However, a Google AIY Voice Bonnet came into our possession and we thought we could take that another step by creating a "Speech to Morse" translator that would actually send code over the air on the ham radio bands from spoken word.

To use the rig, you don't need a ham radio license unless you connect to an actually ham radio transmitter; which is kinda the whole point.  My quick ham radio advertisement:  Get a FCC Amateur Radio license; it's not hard and is an extremely interesting hobby.  BTW, learning Morse Code is no longer a requirement.
-----
When we were given the AIY Voice Bonnet we really didn't even know what it did.  We knew it needed a Raspberry PI and it could act somewhat like a Google Voice Assistant.   We had a Raspberry PI Zero handy and after some Google search engineering we found a disk image file to get us started.

Like a lot of internet projects APIs can change and documentation can be conflicting, we finally got the RasPI and Voice Bonnet to talk to the web after some effort.  A full blown Google Voice Assistant this thing is NOT; if you want one of those then BUY, don't BUILD.  After leaning into the AIY GitHub for knowledge we were finally rewarded with success.
-----
The connections are easy and just follow the AIY GitHub to wire the ACTIVATE BUTTON. We used IO Pin 16 on the RasPI for the relay control because it wasn't used by the AIY Voice Bonnet.  The AIY Voice Bonnet has four GPIO pins on board, but we could never get them to work.   Here's the summary:
-----
And here is the rig in action.  The Python code to make it run is below.

Demo at 20 WPM:
Demo at 12 WPM:
-----
Thanks and 73!   Here is our Python source code:

#!/usr/bin/env python3

#
#  July  2020
#  Voice to Morse Code Translator
#  WhiskeyTangoHotel.Com
#
# RaspberryPI Zero and AIY Voice Bonnet.
#
# Use Google Voice AIY to convert speech to text.
# Read that text file created and convert the text.
# Turn that text into to Morse Code "." and "-".
# Close a relay to transmit it over the air with a ham radio
#
#
# BTW, CW is fun to learn.
# *  --... / ...--   //  --... / ...--

# Some program set up and Define some variables

import os  # needed to delete the text file after reading
import time  # needed for delays
from datetime import datetime # need to calc run time
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM) # Use BCM I/O board pin numbers

SendingLine = 0  # this is the text file line we will send
StartCW = 0    #  Char position to start sending from the parsed line
EndCW = 0       #  Char position to stop sending from the parsed line

keyer = 16  # Pin where the keyer relay is connected
GPIO.setup(keyer, GPIO.OUT)
GPIO.output(keyer, GPIO.LOW)  # make certain we are not keyed down

# Load the CW database 'dits' and 'dahs' for each Alphanumeric. Boring....
A = ".-"
B = "-..."
C = "-.-."
D = "-.."
E = "."
F = "..-."
G = "--."
H = "...."
I = ".."
J = ".---"
K = "-.-"
L = ".-.."
M = "--"
N = "-."
O = "---"
P = ".--."
Q = "--.-"
R = ".-."
S = "..."
T = "-"
U = "..-"
V = "...-"
W = ".--"
X = "-..-"
Y = "-.--"
Z = "--.."
period = ".-.-.-"
zero = "-----"
one = ".----"
two = "..---"
three = "...--"
four = "....-"
five = "....."
six = "-...."
seven = "--..."
eight = "---.."
nine = "----."

# WPM spacing calculated using the "Paris Standard"
# Per Morse code convention: 1 Dah = 3 Dits, LETTERS are spaced by one Dit, WORDS are spaced by one Dah.

WPM = 20.0    # Don't leave off the ".0" or WPM will be an integer (we want it to float)
Dit = 1.200 / WPM
Dah = Dit * 3.0
between_ditdah_spacing = Dit
between_character_spacing = Dit * 2
between_word_spacing = Dit * 3

"""Calls to Google Assistant GRPC recognizer."""

# We use AIY in the cloud to do the speech to text processing.  The RaspberryPI just can't do it as good.

# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import locale
import logging
import signal
import sys

from aiy.assistant.grpc import AssistantServiceClientWithLed
from aiy.board import Board

def volume(string):
    value = int(string)
    if value < 0 or value > 100:
        raise argparse.ArgumentTypeError('Volume must be in [0...100] range.')
    return value

def locale_language():
    language, _ = locale.getdefaultlocale()
    return language

def main():
    logging.basicConfig(level=logging.DEBUG)
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit(0))

    parser = argparse.ArgumentParser(description='Assistant service example.')
    parser.add_argument('--language', default=locale_language())
    parser.add_argument('--volume', type=volume, default=100)
    args = parser.parse_args()

    with Board() as board:
        assistant = AssistantServiceClientWithLed(board=board,
                                                  volume_percentage=args.volume,
                                                  language_code=args.language)
        while True:
            logging.info('Press button to start conversation...')
            board.button.wait_for_press()
            logging.info('Conversation started!')
            assistant.conversation()

            # Voice to CW routine starts here are the dim RED button is pressed
            # Button will turn bright RED while the rig is listening
            # Button will dim blink RED when ready for next speech input

            #  Read the text file created by the speech to text and get it ready to send as CW

            # Look for "Trigger Lines" in the text file.  Parse and send the line above it
            Trig1 = "Recording stopped."
            Trig2 = "End of audio request detected"
            Trig3 = "Updating conversation state"
            i = -1

            Get_file = "keywhat.txt"  # this is the text file created from AIY that we are going to process

            mylines = []
            with open (Get_file, 'rt') as myfile:    #'rt' means 'read text data'
                for line in myfile:
                    i = i + 1
                    mylines.append(line)
                   
                    # If we find one to the Trigger lines, we have passed the voice text by one line in the file
                    if mylines[i].find(Trig1) and mylines[i].find(Trig2) and mylines[i].find(Trig3)!= -1 :
                        SendingLine = i-1

            print (SendingLine, mylines[SendingLine])
            Send_text = mylines[SendingLine]

            #  Strip out only the text we want to send over the air
            StartCW = int((mylines[SendingLine].find('"')) + 1)
            EndCW = int(len(mylines[SendingLine]) - 3)
            # print StartCW, EndCW    #  used for debug

            SendasCW = Send_text[StartCW:EndCW]  # this is the text sent as CW

            #SendasCW = "test"  # used for debug
            SendasCW = (SendasCW.upper())  # and we always make certain it is all caps

            # We have the text.  Now parse it and send each letter as CW

            print ("Going to send: " + SendasCW + " at " + str(int(WPM)) + " WPM.")
            print ("-------------------------------------------")

            L = len(SendasCW)
            t0 = datetime.now()

            for x in range(0,L):  # Start character converstion to dit and dahs
                    Text_substring = SendasCW[x:x+1]
                    print ("Sending: " + Text_substring)   # for debug
                    if Text_substring == "0"  :  Text_substring = zero
                    if Text_substring == "1"  :  Text_substring = one
                    if Text_substring == "2"  :  Text_substring = two
                    if Text_substring == "3"  :  Text_substring = three
                    if Text_substring == "4"  :  Text_substring = four
                    if Text_substring == "5"  :  Text_substring = five
                    if Text_substring == "6"  :  Text_substring = six
                    if Text_substring == "7"  :  Text_substring = seven
                    if Text_substring == "8"  :  Text_substring = eight
                    if Text_substring == "9"  :  Text_substring = nine
                    if Text_substring == "."  :  Text_substring = period
                    if Text_substring == "A"  :  Text_substring = A
                    if Text_substring == "B"  :  Text_substring = B
                    if Text_substring == "C"  :  Text_substring = C
                    if Text_substring == "D"  :  Text_substring = D
                    if Text_substring == "E"  :  Text_substring = E
                    if Text_substring == "F"  :  Text_substring = F
                    if Text_substring == "G"  :  Text_substring = G
                    if Text_substring == "H"  :  Text_substring = H
                    if Text_substring == "I"  :  Text_substring = I
                    if Text_substring == "J"  :  Text_substring = J
                    if Text_substring == "K"  :  Text_substring = K
                    if Text_substring == "L"  :  Text_substring = L
                    if Text_substring == "M"  :  Text_substring = M
                    if Text_substring == "N"  :  Text_substring = N
                    if Text_substring == "O"  :  Text_substring = O
                    if Text_substring == "P"  :  Text_substring = P
                    if Text_substring == "Q"  :  Text_substring = Q
                    if Text_substring == "R"  :  Text_substring = R
                    if Text_substring == "S"  :  Text_substring = S
                    if Text_substring == "T"  :  Text_substring = T
                    if Text_substring == "U"  :  Text_substring = U
                    if Text_substring == "V"  :  Text_substring = V
                    if Text_substring == "W"  :  Text_substring = W
                    if Text_substring == "X"  :  Text_substring = X
                    if Text_substring == "Y"  :  Text_substring = Y
                    if Text_substring == "Z"  :  Text_substring = Z

                    #  Now send the dit/dahs for the Self Test Callsign Character
                    CW_length = int(len(Text_substring))

                    time.sleep(between_character_spacing) # a delay between characters

                    for j in range(0, CW_length):     # Send tones and blink LED in CW

                            Dit_or_Dah = Text_substring[j:j+1]

                            # HIGH closes a relay connect to the ham radio.  LOW opens the relay
                            # This relay closure is the same as pushing down on a straight key

                            if (Dit_or_Dah) == "."  :    # process a Dit
                                    print (Dit_or_Dah)
                                    GPIO.output(keyer, GPIO.HIGH)
                                    time.sleep(Dit) # a delay of "1 Dit" between dit/dahs in a character
                                    GPIO.output(keyer, GPIO.LOW)  #make certain we are not keyed down
                                    time.sleep(between_ditdah_spacing) # a delay bewteen characters
                                    # end if process a Dit

                            if (Dit_or_Dah) == "-"  :    # process a Dah
                                    print (Dit_or_Dah)
                                    GPIO.output(keyer, GPIO.HIGH)
                                    time.sleep(Dah) # a delay of "1 Dit" between dit/dahs in a character
                                    GPIO.output(keyer, GPIO.LOW)  #make certain we are not keyed down
                                    time.sleep(between_ditdah_spacing) # a delay bewteen characters
                                    # end if process a Dah

                            if (Dit_or_Dah) == " "  :    # process a SPACE (delay between words)
                                    print ("<SPACE>")
                                    GPIO.output(keyer, GPIO.LOW)  #make double certain we are not keyed down
                                    time.sleep(between_word_spacing) # a delay of "3 Dit" between words
                                    # end if process a SPACE


            GPIO.output(keyer, GPIO.LOW)  #make absolutely certain we are not keyed down

            t1 = datetime.now() - t0  # how long did it take to send?

            print ("-------------------------------------------")
            print ("Completed in " + str(t1) + " seconds at " + str(int(WPM)) + " WPM to send:")
            print (SendasCW)

            # close and delete voice to text cloud file to get ready for the next voice decode
            time.sleep(1)  # file handling delay, probably not needed
            myfile.close()
            os.remove(Get_file)
            time.sleep(1)  # file handling delay, probably not needed
            myfile = open(Get_file, "w")

if __name__ == '__main__':
    main()
-----