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 "dial 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.

-----



Thursday, January 23, 2025

Easy Graphing of Temperatures with RasPI and NodeRed

-----

After hearing over and over how quick and easy the graphical programming environment for Node-Red was we decided to give it a try with an old Raspberry PI Model B that was 'resting' in a drawer.   Conclusion:  It's easy; it's really really easy.   Plus, there is a large user base that has created examples and libraries for just about any application you can dream up.

-----
Node-Red is free and the install is easy and well documented on their website.  For our Node-Red experiment we decide to graph the ambient air temperature as measured by a DS18B20 and the CPU core temperature of the Raspberry PI Model B.   Connect the DS18B20 sensor like this:

-----

It didn't take us long at all to 'noodle' up this:

-----

Then we went into the "Layout" tab to format what the graphs would look like:

----

All in all the project, including the Node-Red install; was probably under two hours.  That's pretty damn quick for going from zero knowledge to serving up nice graphs.

-----

Importing or Exporting code is easy as well.  It's basically "copy/past".  Here is our code for the project:

[
    {
        "id": "018abb4b7bfb7e86",
        "type": "exec",
        "z": "c0bb5756099d6dbc",
        "command": "vcgencmd measure_temp",
        "addpay": false,
        "append": "",
        "useSpawn": "false",
        "timer": "",
        "oldrc": false,
        "name": "Get CPU Temp",
        "x": 420,
        "y": 40,
        "wires": [
            [
                "05c0f7272e5bc7e0"
            ],
            [],
            []
        ]
    },
    {
        "id": "05c0f7272e5bc7e0",
        "type": "function",
        "z": "c0bb5756099d6dbc",
        "name": "Parse Temp",
        "func": "var tempC = parseFloat(msg.payload.replace('temp=', '').replace(\"'C\", ''));\nvar tempF = (tempC * 9/5) + 32;\nmsg.payload = tempF.toFixed(2);\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 630,
        "y": 40,
        "wires": [
            [
                "7dfbce89f48de845",
                "d97496ad6fb77af6"
            ]
        ]
    },
    {
        "id": "7dfbce89f48de845",
        "type": "ui_chart",
        "z": "c0bb5756099d6dbc",
        "name": "",
        "group": "7",
        "order": 8,
        "width": 11,
        "height": 5,
        "label": "CPU (°F)",
        "chartType": "line",
        "legend": "false",
        "xformat": "HH:mm",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "",
        "ymax": "",
        "removeOlder": "7",
        "removeOlderPoints": "10080",
        "removeOlderUnit": "86400",
        "cutout": 0,
        "useOneColor": false,
        "useUTC": false,
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838",
            "#000000",
            "#000000",
            "#000000",
            "#000000",
            "#000000",
            "#000000"
        ],
        "outputs": 1,
        "useDifferentColor": false,
        "className": "",
        "x": 880,
        "y": 40,
        "wires": [
            []
        ]
    },
    {
        "id": "d97496ad6fb77af6",
        "type": "ui_gauge",
        "z": "c0bb5756099d6dbc",
        "name": "",
        "group": "7",
        "order": 7,
        "width": 5,
        "height": 5,
        "gtype": "donut",
        "title": "CPU (°F)",
        "label": "°F",
        "format": "{{value}}",
        "min": "110",
        "max": "175",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "",
        "seg2": "",
        "diff": false,
        "className": "",
        "x": 880,
        "y": 80,
        "wires": []
    },
    {
        "id": "3b8516632c6153ec",
        "type": "debug",
        "z": "c0bb5756099d6dbc",
        "name": "DC18B20 Temp",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 900,
        "y": 260,
        "wires": []
    },
    {
        "id": "e19a60a2f08ac386",
        "type": "inject",
        "z": "c0bb5756099d6dbc",
        "name": "Every 60 secs",
        "props": [],
        "repeat": "60",
        "crontab": "",
        "once": true,
        "onceDelay": "1",
        "topic": "",
        "x": 160,
        "y": 40,
        "wires": [
            [
                "018abb4b7bfb7e86"
            ]
        ]
    },
    {
        "id": "a3b79e7f7975fa73",
        "type": "ds18b20",
        "z": "c0bb5756099d6dbc",
        "name": "DS18B20",
        "sensorid": "28-089bd445e089",
        "timer": "1",
        "x": 380,
        "y": 220,
        "wires": [
            [
                "3d28949a3fa611d3"
            ]
        ]
    },
    {
        "id": "3d28949a3fa611d3",
        "type": "function",
        "z": "c0bb5756099d6dbc",
        "name": "C_to_F",
        "func": "var tempc = msg.payload;\nvar tempf = tempc * 9/5 + 32;\ntempf = tempf.toFixed(2);\nmsg.payload = tempf;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 600,
        "y": 220,
        "wires": [
            [
                "3b8516632c6153ec",
                "61cf75a9eeeb79be",
                "e146810a6d814e42"
            ]
        ]
    },
    {
        "id": "e146810a6d814e42",
        "type": "ui_chart",
        "z": "c0bb5756099d6dbc",
        "name": "",
        "group": "7",
        "order": 3,
        "width": 11,
        "height": 9,
        "label": "°F",
        "chartType": "line",
        "legend": "false",
        "xformat": "HH:mm",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "",
        "ymax": "",
        "removeOlder": "7",
        "removeOlderPoints": "10080",
        "removeOlderUnit": "86400",
        "cutout": 0,
        "useOneColor": false,
        "useUTC": false,
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838",
            "#000000",
            "#000000",
            "#000000",
            "#000000",
            "#000000",
            "#000000"
        ],
        "outputs": 1,
        "useDifferentColor": false,
        "className": "",
        "x": 870,
        "y": 180,
        "wires": [
            []
        ]
    },
    {
        "id": "61cf75a9eeeb79be",
        "type": "ui_gauge",
        "z": "c0bb5756099d6dbc",
        "name": "",
        "group": "7",
        "order": 2,
        "width": 5,
        "height": 9,
        "gtype": "donut",
        "title": "Ambient (°F)",
        "label": "°F",
        "format": "{{value}}",
        "min": "80",
        "max": "110",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "",
        "seg2": "",
        "diff": false,
        "className": "",
        "x": 890,
        "y": 220,
        "wires": []
    },
    {
        "id": "cc4248afd8356380",
        "type": "debug",
        "z": "c0bb5756099d6dbc",
        "name": "debug 1",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 880,
        "y": 400,
        "wires": []
    },
    {
        "id": "1553124d816901d2",
        "type": "inject",
        "z": "c0bb5756099d6dbc",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "1",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 170,
        "y": 360,
        "wires": [
            [
                "cc4248afd8356380",
                "6f3d2d25b6ad464e"
            ]
        ]
    },
    {
        "id": "6f3d2d25b6ad464e",
        "type": "ui_digital_clock",
        "z": "c0bb5756099d6dbc",
        "name": "",
        "group": "7",
        "order": 5,
        "width": 4,
        "height": 1,
        "x": 890,
        "y": 360,
        "wires": []
    },
    {
        "id": "7",
        "type": "ui_group",
        "name": "RasPI-3B",
        "tab": "6",
        "order": 1,
        "disp": true,
        "width": 16,
        "collapse": false,
        "className": ""
    },
    {
        "id": "6",
        "type": "ui_tab",
        "name": "Home",
        "icon": "dashboard",
        "order": 1
    }
]

-----

Friday, December 20, 2024

WS2812B LED Ring Clock

 -----

There are no shortage of DIY clocks on the internet, but inspiration from this Hack a Day article made us think there is room for at least one more. One major different with ours is we didn't want to wait 30+ years to finish the project so we took the easy way out with a 93 x WS2812B LED setup in a 6 ring configuration. We had a HelTec WiFi 32 Dev Board 'in stock' with a bad display so that became the microcontroller of choice.

-----

The schematic is super simple.  In addition to just showing the boring time we added a "Disco Mode" button that creates a light show (video below).

-----

We needed a box also.  Here are the 3D Printer files: Box and Cover.  

Bolt it all together and this is what is does:  Connects to WiFi, gets current time of day, displays the time.  A quick press of the BRIGHTNESS/RESET button will cycle through 10 LED brightness levels and if you press and hold the rig will RESET.   The DISCO MODE button will put on a LED light show for 10 secs.  The light show also displays for 60 seconds at the top of the hour.

-----

Of course, the software is what makes it work.  The Arduino IDE software is below (yes, ChatGPT helped make things both easier and harder depending on the task):/*
 * "LED Ring Clock" shows time using 6 Ring WS2812B 93-LED Strip
 *           Outer ring=0-31, 5th=32-55, 4th=56-71, 3rd=72-83, 2nd=84-91, Center=92
 *
 * HelTec Automation(TM) ESP32 Series Dev boards OLED (OLED not used.  Selected this uC only because we had it.)
 * Adruino IDE Board Setting: WiFi Kit32, Disabled, 240MHz, 921600, None
 *
 * DEC 2024
 * Project details at: whiskeytangohotel.com
 *
*/

#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <time.h>

// Configuration for LED strip
#define LED_PIN 5         // Pin where the LED strip is connected
#define NUM_LEDS 93       // Total number of LEDs in the strip
#define BUTTON_PIN 4      // Pin connected to NO switch button to control Brightness and 'long press' RESET
#define DISCO_BUTTON_PIN 15  // Pin connected to button for Disco Mode

// Define up some variables
// Wi-Fi credentials
const char* ssid = "URSSID";           // Replace with your Wi-Fi SSID
const char* password = "URPASSWORD";   // Replace with your Wi-Fi password

int i = 0;
unsigned long ringTime = 1000; // later adjust ringTime to outer ring 1 sec round trip time
unsigned long endTime = 0;
unsigned long lastTimeUpdate = 999999999;   // set in printLocalTime() Function to control how often we check the time server
unsigned long discoStartTime = 0;   // Tracks when Disco Mode started

unsigned long buttonPressStart = 0; // Timestamp when the button was first pressed
bool buttonPressed = false;         // Tracks the current NO Switch button state on PIN 4
int brightnessLevel = 3;            // 0 - 255, but we cycle 1-10 cuz is bright. 100 is really really damn bright!

int currentHour = 0;
int currentMinute = 0;
int currentSecond = 0;
int LEDseconds = 32;

// Timezone settings for Austin, Texas.  Adjust for your timezone
const char* ntpServer = "pool.ntp.org";  // NTP server
const long gmtOffset_sec = -21600;      // Offset for CST (UTC -6)
const int daylightOffset_sec = 3600;   // Daylight Saving Time offset

Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
// Set up some colors to make it easier to customize the clock
uint32_t Red = 0xFF0000;
uint32_t Green = 0x00FF00;
uint32_t Blue = 0x0000FF;
uint32_t Yellow = 0xFFFF00;
uint32_t Cyan= 0x00FFFF;
uint32_t Magenta    = 0xFF00FF;
uint32_t White    = 0xFFFFFF;
uint32_t Black_Off =    0x000000;

// Now, Customize by choosing the color for the selected LED function
int Middle_blink = Red;      //color for the 2 inner rings
int Fast_seconds = Middle_blink;      // color for the fast paced outer ring
int Normal_seconds = Middle_blink;    // adjacent to the outer ring
int Minute_hand = White;         // 4th from center
int Hour_hand = Minute_hand;           // 3rd from center
int How_bright = brightnessLevel;      // Cycles from 1 to 10    

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Set button pin with internal pull-up resistor
  pinMode(DISCO_BUTTON_PIN, INPUT_PULLUP); // Button for Disco Mode
  strip.setBrightness(brightnessLevel); // Initialize brightness
 
  // Initialize LED strip
  strip.begin();
  strip.show();
  strip.clear();
  strip.setBrightness(How_bright);

  // Connect to Wi-Fi
  Serial.print("Connecting to Wi-Fi [flash RED]...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    // Toggle LEDs 84 to 92 to show trying to connect
    for (int j = 84; j <= 92; j++) {
      uint32_t currentColor = strip.getPixelColor(j); // Get current color of the LED
      if (currentColor == 0) { // If the LED is off
        strip.setPixelColor(j, strip.Color(255, 0, 0));                 
      }else { // If the LED is on
        strip.setPixelColor(j, 0); // Turn it off
      }        
    }  // End j loop for center LED toggle
    strip.show(); // Update the strip
    delay(500);
  }
  Serial.println("\nWi-Fi connected [flash GREEN]!!!");
  for (int i = 0; i <11; i++) {  // fast flash the inner LEDs to show 'wifi connected'
      for (int j = 84; j <= 92; j++) {
      uint32_t currentColor = strip.getPixelColor(j); // Get current color of the LED
      if (currentColor == 0) { // If the LED is off
        strip.setPixelColor(j, strip.Color(0, 255, 0)); // Define RGB color
      }else { // If the LED is on
        strip.setPixelColor(j, 0); // Turn it off
      }  
    }  // End j loop for center LED toggle
    strip.show(); // Update the strip
    delay(200);
  }

  // Center LED Red to show we are trying to get the time
  strip.setPixelColor(92, strip.Color(255, 0, 0)); // Define RGB color
  strip.show(); // Update the strip
  delay(100);
  // Initialize time
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  printLocalTime();  // Print the current local time to the Serial Monitor
}

void loop() {  
  // Scanner effect for the first 32 LEDs on the outter ring
  unsigned long startTime = millis();  // We time the loop and aim for 1 sec lap time
 
  for (i = 0; i < 32; i++) {   
    handleButtonPress();   // is the Button pressed for Brightness or RESET?
    runDiscoMode();  // Disco Mode pressed?
    strip.setPixelColor(i, Fast_seconds); // Define RGB color
    strip.show();  // Update the strip    
    unsigned long elapsedTime = millis() - startTime; // Calculate elapsed time
    unsigned long delayPerLED = (1000 - elapsedTime) / (32 - i); // Dynamically adjust delay
    delay(delayPerLED); // Apply calculated delay
    strip.setPixelColor(i, strip.Color(0, 0, 0)); // Turn off the current LED

    if (i == 0) {   // Update the clock 'hands'
      // Toggle LEDs 84 to 92 (the center LEDs at 1Hz)
      for (int j = 84; j <= 92; j++) {
        uint32_t currentColor = strip.getPixelColor(j); // Get current color of the LED
        if (currentColor == 0) { // If the LED is off     
          strip.setPixelColor(j, Middle_blink); // ON and Define RGB color  
        }else { // If the LED is on
          strip.setPixelColor(j, 0); // Turn it off
        }      
      }  // End j loop for center LED 1 Hz toggle
      
      uint32_t currentColor = strip.getPixelColor(91); // Flash center LED at opposite ON/OFF of surrounding ring   
      if (currentColor == 0) { // If the near LED is off   
        strip.clear();  // This helps avoid 'artifacts' just in case
        strip.setPixelColor(92, Middle_blink);  
      }

      // Position minute hand (4th ring with 16 LEDs for limited resolution)
      // 56(top) and 71 is last, so...  
      int minuteLED = 56 + (currentMinute / 3.75);
      strip.setPixelColor(minuteLED, Minute_hand);

      // Position hour hand (3rd ring with 12 LEDs.  LED72=12 Noon.  LED83=11]
      if (int(currentHour == 12)) {   // We want LED 72, not LED 12 + 72
        currentHour = 0;  
      }
      int LEDhour = int(currentHour) + 72;
      strip.setPixelColor(LEDhour, Hour_hand); // Define RGB color   
      // End position Hour Hand
      
      // Position the second hand (4th ring: LED 32 = TOP, LED 55 = LAST, 24 LEDs in total)
      strip.setPixelColor(LEDseconds, 0); // Turn off the previous second's LED      
      // Update LEDseconds and ensure it stays within the range [32, 55]
      LEDseconds = (LEDseconds < 55) ? LEDseconds + 1 : 32;  // less than 55 then add +1 else reset LEDseconds to 32
      strip.setPixelColor(LEDseconds, Normal_seconds); // Turn on the current second's LED
      // End Position Second Hand

    }  // End if i == 0 to Update the clock 'hands'
  }  // End For i loop for outer 'scanner' ring

  unsigned long endTime = millis();
  ringTime = endTime - startTime;  // How long did a full outer scanner ring take
  Serial.println();
  //Serial.println("Outer ring time: " + String(ringTime) + " ms");
  printLocalTime();  // Print Local time  
}

void printLocalTime() {
  // Update from the time server every 60 secs (1 min = 60000 milliseconds)
  if (millis() - lastTimeUpdate < 60000) {
    // Print the current local time from global variables
    Serial.print("Current local time: ");
    Serial.print(currentHour);
    Serial.print(":");
    if (currentMinute < 10) Serial.print("0"); // Add leading zero to minutes if needed
    Serial.print(currentMinute);
    Serial.print(":");
    if (currentSecond < 10) Serial.print("0"); // Add leading zero to seconds if needed
    Serial.println(currentSecond);
    Serial.println( "Next time update in: " + String(60 - (millis() - lastTimeUpdate)/1000) + " seconds." );
    Serial.println("Brightness set to: " + String(brightnessLevel));
    return;  // Exit the function without updating the time
  }

  // Fetch time from the NTP server
  struct tm timeinfo;
  Serial.println("Fetching time from the NTP server...");
  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
    strip.setPixelColor(92, Red); // Error code on center LED
    strip.show();
    delay(500);
    return;
  }

  int hour = timeinfo.tm_hour;
  String meridian = "AM";
 
  // Convert 24-hour to 12-hour format
  if (hour >= 12) {
    meridian = "PM";
    if (hour > 12) hour -= 12;  // Convert to 12-hour format
  } else if (hour == 0) {
    hour = 12; // Midnight in 12-hour format
  }

  // Update global variables
  currentHour = hour;
  currentMinute = timeinfo.tm_min;
  currentSecond = timeinfo.tm_sec;

  // Print time in 12-hour format
  Serial.print("Current local time: ");
  Serial.print(hour);
  Serial.print(":");
  if (timeinfo.tm_min < 10) Serial.print("0"); // Add leading zero to minutes if needed
  Serial.print(timeinfo.tm_min);
  Serial.print(":");
  if (timeinfo.tm_sec < 10) Serial.print("0"); // Add leading zero to seconds if needed
  Serial.print(timeinfo.tm_sec);
  Serial.print(" ");
  Serial.println(meridian);
  // Update the last time update to the current time
  lastTimeUpdate = millis();
}

void handleButtonPress() {   
  static bool showingBrightness = false;  // Tracks if brightness feedback is being displayed
  static unsigned long brightnessDisplayStart = 0; // Tracks when feedback started

  bool buttonState = digitalRead(BUTTON_PIN); // Read button state

  if (buttonState == LOW) { // Button pressed (LOW because of pull-up)
    if (!buttonPressed) {
      buttonPressed = true;
      buttonPressStart = millis(); // Record the press start time
    }

    // Check if the button is held for 5 seconds
    if (millis() - buttonPressStart >= 5000) {
      Serial.println("Resetting ESP32...");
      ESP.restart(); // Reset the ESP32
    }
  } else { // Button released
    if (buttonPressed) { // Only act if it was pressed before
      buttonPressed = false;

      if (millis() - buttonPressStart < 5000) { // Short press
        // Cycle brightness level (1 to 9)
        brightnessLevel = (brightnessLevel % 9) + 1;
        strip.setBrightness(brightnessLevel);
        strip.show(); // Apply the new brightness
        Serial.println("Brightness adjusted to: " + String(brightnessLevel));

        // Start brightness feedback display
        showingBrightness = true;
        brightnessDisplayStart = millis();
        strip.clear(); // Clear previous LED states
        for (int i = 84; i < 84 + brightnessLevel; i++) {
          strip.setPixelColor(i, Magenta); // Brightness level feedback via middle LEDs
        }
        strip.show();
      }
    }
  }

  // Handle non-blocking brightness feedback display
  if (showingBrightness && (millis() - brightnessDisplayStart >= 1000)) {
    // Turn off LEDs after 1 second
    showingBrightness = false;
    for (int i = 84; i < 92; i++) {
      strip.setPixelColor(i, 0); // Turn off the LEDs
    }
    strip.show();
  }
}

void runDiscoMode() { //Flash all the LEDs in random colors
  static unsigned long lastUpdate = 0;      // Tracks last LED update time
  static unsigned long discoStartTime = 0; // Tracks when Disco Mode started
  static bool discoActive = false;         // Tracks if Disco Mode is active
  static bool buttonStateLast = LOW;       // Tracks previous button state

  // Determine the button state of the black NC button
  bool buttonState;
  int TopofHour = 0;  // We Disco Mode flash for the whole minute
  if (currentMinute == TopofHour) {
    buttonState = HIGH; // Force Disco Mode at the top of the hour
  } else {
    buttonState = digitalRead(DISCO_BUTTON_PIN); // Read the actual button state
  }

  // Toggle Disco Mode on button press (HIGH) with debounce
  if (buttonState == HIGH && buttonStateLast == LOW) { // Button was just pressed
    discoActive = !discoActive; // Toggle discoActive
    if (discoActive) {
      Serial.println("Entering Disco Mode...");
      discoStartTime = millis(); // Record the start time
    } else {
      Serial.println("Exiting Disco Mode via button press...");
      strip.clear();
      strip.show(); // Clear LEDs
    }
  }

  if (currentMinute == TopofHour) {  // remain in Disco Mode
    buttonState = HIGH; // Force Disco Mode at the top of the hour
  } else {
    buttonStateLast = buttonState; // Update last button state
  }  

  // If Disco Mode is active
  if (discoActive) {    
    if (millis() - discoStartTime >= 10000) {   // Exit Disco Mode after set delay in milliseconds
      Serial.println("Exiting Disco Mode via timeout...");
      discoActive = false;
      strip.clear();
      strip.show(); // Clear LEDs
      return;
    }

    // Update LEDs non-blocking
    if (millis() - lastUpdate > 10) {  // in mSecs delay on the flashes.  i think faster is better
      lastUpdate = millis();
      for (int i = 0; i < NUM_LEDS; i++) {
        strip.setPixelColor(i, strip.Color(random(256), random(256), random(256)));
      }
      strip.show();
      //Serial.println("Update disco...");
    }
  }
}

-----

Monday, October 28, 2024

Wednesday, September 18, 2024

DIY: Banana plug to Alligator Clip Adapter

 

Always a handy thing to have a few of on the bench.

-----

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, July 1, 2024

Vinage Fender Tube Amp gets New Life

 


----

In 1973 a good beer buddy of ours purchased this used Fender Champ Amp.  The amp is rumored to be used in one of Leon Russell's studios.  According to the S/N it was manufactured in 1964.  The problem was the amp no longer worked and we asked if we could try a shot at fixing it.

-----

My assumption (that was correct) was the capacitors had lost all their capacitor magic.  However, I quickly discovered the amp was also missing a tube.   Oddly, the T-12AX7 tube was easy to get. 

-----

Surprisingly the odd value 450V capacitors that Fender used were hard to get and pretty expensive.  We found a 'kit' of 450V capacitors and put some values in parallel to get the desired values.   Not "pure Fender", but the amp is rockin' again and that was the main goal.

-----

Original:



Now:


-----


General Radio VARIAC Gets New Life

 

-----

A good ham radio pal gave us a VARIAC that was collecting dust in his shack.   We often wanted a VARIAC, but really didn't need one so this presented a prefect opportunity to make our VARIAC dreams come true.

----

Without an enclosure the danger of a 120VAC unprotected appliance rolling around on the test bench can be pretty hazardous.  We fixed that by repurposing the cremation remains box of a loved one.   We also mounted a switch and voltmeter to the box.

-----
The simple schematic is show here:
-----