Monday, October 28, 2024
Wednesday, September 18, 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, 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.
Sunday, May 12, 2024
"Remote Control Finger" for FlexRadio (or any button)
-----
Ever need to have physical access to a button or switch at your house when you are nowhere near your house? This doesn't happen often with the FlexRadio, but sometimes you really really really need to cycle the power or even more importantly "Press and Hold" the power button for a "WHITE LED REBOOT". A key feature of the FlexRadio is it's built in remote capability that allows a ham radio operator the easy use the rig from anywhere in the world. That, of course, is unless if you are on the opposite side of the planet and need access to the rigs front panel power button.
-----
One way to solve this is with a "Remote Control Finger". We had an Arduino IDE compatible D1 Mini and a hobby servo motor already in the parts box so these acted as the main ingredients for the build. The "Remote Control Finger" works stand alone when you are on your Local Area Network (LAN) and you just have the rig in the basement, or attic, or antenna shack. If you are anyway from your LAN it assumes you are running a VPN which if you are a serious FlexRadio remote user you already are doing or should really consider doing. The D1 Mini boots up as a webserver and presents these options:
-----
Here's a short video of the "Remote Control Finger" in action:
Notice that the "finger" is 3D Printed. The .STL file is here. I used a paperclip to hinge the "finger" to the servo.
-----
My quick, sloppy, but perfectly working IDE code is:// Remote Control Finger
//
// Ardiuno IDE for
// LOLIN(WEMOS) D1 R2 & D1 MINI (should work fine with others)
//
// Controls a low cost hobby servo motor to be moved into two positions
// via a web interface from anywhere in the world.
//
// This is handy when you need to physically push a button, PTT, or move a switch.
// Inspired by the infrequent need to hold down the FlexRadio power button when
// physical access to the rig is not possible.
//
// Works stand alone when you are on your Local Area Network (LAN).
// Assumes you are running a VPN if not on the LAN, which if you are a serious
// FlexRadio remote user you already are or should really consider doing.
//
// WhiskeyTangoHotel.Com for details
// May 2024
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <Servo.h>
// Adjust the PRESSED and RELEASED servo positions to match your mounting position
const int released_servo_position = 50; // Value limits are 0 to 180
const int pressed_servo_postion = 65; // Higher number presses "harder" Value limits are 0 to 180
const int tap_delay = 750; // mSeconds to stay in PRESSED position when we just want to tap the power button
const char* ssid = "ur_SSID_name"; // Your SSID
const char* password = "ur_SSID_password"; //Your SSID Password
String FingerStatus = "STATUS:<br/>Remote Finger is RELEASED and LED OFF"; // This is set as the the 'wake up' state below
ESP8266WebServer server(80);
Servo servo; // Servo object to control the servo motor
void handleRoot() { // Do this for URL of: http://localIP/ For example http://192.168.1.53/
String message = "<br/>";
message = FingerStatus + message;
message += "<br/>Valid http://" + WiFi.localIP().toString() + " options are: <a href=\"http://" + WiFi.localIP().toString() + "/press\">/press</a> or ";
message += "<a href=\"http://" + WiFi.localIP().toString() + "/release\">/release</a> or <a href=\"http://" + WiFi.localIP().toString() + "/tap\">/tap</a><br/>";
for (uint8_t i=0; i<server.args(); i++){
message += " " + server.argName(i) + ": " + server.arg(i) + "<br/>";
}
server.send(404, "text/html", "<b>" + message + "<b/>");
}
void handleNotFound(){ // Do this for URLs that are invalid. For example http://192.168.1.53/junkjunkpage
String message = "The address " + WiFi.localIP().toString();
message += server.uri();
message += " is NOT FOUND!!! ";
message += "<br/>";
message = FingerStatus + "<br/><br/>" + message;
message += "<br/>Valid http://" + WiFi.localIP().toString() + " options are: <a href=\"http://" + WiFi.localIP().toString() + "/press\">/press</a> or ";
message += "<a href=\"http://" + WiFi.localIP().toString() + "/release\">/release</a> or <a href=\"http://" + WiFi.localIP().toString() + "/tap\">/tap</a><br/>";
for (uint8_t i=0; i<server.args(); i++){
message += " " + server.argName(i) + ": " + server.arg(i) + "<br/>";
}
server.send(200, "text/html", "<b>" + message + "<b/>");
}
void setup(void){
// ASAP we want to wake the unit up in RELEASED and not PRESSED condition
servo.attach(D1); // Attach the servo to pin
servo.write(released_servo_position); // Move servo to RELEASE position
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(115200);
WiFi.begin(ssid, password);
Serial.println(" ");
// Wait for WiFi connection
while (WiFi.status() != WL_CONNECTED) {
Serial.print("Trying to connect to ");
Serial.print(ssid);
Serial.print(" on ");
Serial.print(WiFi.localIP());
Serial.println(".");
// Blink the LED while trying to connect
digitalWrite(LED_BUILTIN, LOW); // turn the On Board LED ON [inverse logic]
delay(500);
digitalWrite(LED_BUILTIN, HIGH); // turn the On Board LED OFF [inverse logic]
delay(500);
}
// Update the Serial Monitor (for debug)
Serial.println("");
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.println(WiFi.localIP());
for (int i = 0; i <= 25; i++) { // Fast flashing of the LED when WiFi connected.
digitalWrite(LED_BUILTIN, LOW); // turn the On Board LED ON [inverse logic]
delay(50);
digitalWrite(LED_BUILTIN, HIGH); // turn the On Board LED OFF [inverse logic]
delay(50);
}
server.on("/", handleRoot);
server.on("/press", [](){ // Do this for URL of: http://localIP/press For example http://192.168.1.53/press
// This is called when http://WiFi.localIP/press is called in a browser
FingerStatus = "STATUS:<br/>Remote Finger is PRESSED and LED ON";
FingerStatus += "<br/>";
FingerStatus += "<br/>Valid http://" + WiFi.localIP().toString() + " options are: <a href=\"http://" + WiFi.localIP().toString() + "/press\">/press</a> or ";
FingerStatus += "<a href=\"http://" + WiFi.localIP().toString() + "/release\">/release</a> or <a href=\"http://" + WiFi.localIP().toString() + "/tap\">/tap</a><br/>";
server.send(200, "text/html", "<b>" + FingerStatus + "<b/>");
FingerStatus = "STATUS:<br/>Remote Finger is PRESSED and LED ON";
digitalWrite(LED_BUILTIN, LOW); // turn the On Board LED ON [inverse logic]
// Update the Serial Monitor (for debug)
Serial.println("STATUS: Remote Finger is PRESSED and LED ON");
servo.attach(D1); // Attach the servo to pin
servo.write(pressed_servo_postion); // Move servo full anti clockwise
});
server.on("/release", [](){ // Do this for URL of: http://localIP/release For example http://192.168.1.53/release
// This is called when http://WiFi.localIP/release is called in a browser
FingerStatus = "STATUS:<br/>Remote Finger is RELEASED and LED OFF";
FingerStatus += "<br/>";
FingerStatus += "<br/>Valid http://" + WiFi.localIP().toString() + " options are: <a href=\"http://" + WiFi.localIP().toString() + "/press\">/press</a> or ";
FingerStatus += "<a href=\"http://" + WiFi.localIP().toString() + "/release\">/release</a> or <a href=\"http://" + WiFi.localIP().toString() + "/tap\">/tap</a><br/>";
server.send(200, "text/html", "<b>" + FingerStatus + "<b/>");
FingerStatus = "STATUS:<br/>Remote Finger is RELEASED and LED OFF";
digitalWrite(LED_BUILTIN, HIGH); // turn the On Board LED OFF [inverse logic]
// Update the Serial Monitor (for debug)
Serial.println("STATUS: Remote Finger is RELEASED and LED OFF");
servo.attach(D1); // Attach the servo to pin
servo.write(released_servo_position); // Move servo to released_servo_postion
});
server.on("/tap", [](){ // Do this for URL of: http://localIP/press For example http://192.168.1.53/press
// This is called when http://WiFi.localIP/press is called in a browser
FingerStatus = "STATUS:<br/>Remote Finger is PRESSED and LED ON";
FingerStatus += "<br/>";
FingerStatus += "<br/>Valid http://" + WiFi.localIP().toString() + " options are: <a href=\"http://" + WiFi.localIP().toString() + "/press\">/press</a> or ";
FingerStatus += "<a href=\"http://" + WiFi.localIP().toString() + "/release\">/release</a> or <a href=\"http://" + WiFi.localIP().toString() + "/tap\">/tap</a><br/>";
server.send(200, "text/html", "<b>" + FingerStatus + "<b/>");
FingerStatus = "STATUS:<br/>Remote Finger is PRESSED and LED ON";
digitalWrite(LED_BUILTIN, LOW); // turn the On Board LED ON [inverse logic]
// Update the Serial Monitor (for debug)
Serial.println("STATUS: Remote Finger is PRESSED and LED ON");
servo.attach(D1); // Attach the servo to pin
servo.write(pressed_servo_postion); // Move servo to pressed_servo_postion
delay(tap_delay); // How long is the button pressed
digitalWrite(LED_BUILTIN, HIGH); // turn the On Board LED OFF [inverse logic]
// Update the Serial Monitor (for debug)
Serial.println("STATUS: Remote Finger is RELEASED and LED OFF");
servo.attach(D1); // Attach the servo to pin
servo.write(released_servo_position); // Move servo to released_servo_postion
});
server.onNotFound(handleNotFound); // Handle an invalid URL and show correct options
server.begin(); //Wooo Hooo !!!
// Update the Serial Monitor (for debug)
Serial.println("Remote Finger HTTP server started at: " + WiFi.localIP().toString());
}
void loop() { // Do this loop until the Dallas Cowboys win the Super Bowl
server.handleClient(); // Any URL requests?
delay(250); // wait some milliseconds, mainly for debounce
}
-----
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!
-----
Thursday, March 21, 2024
HTML Help for Guitar Chord Changes
-----
We committed to learning the guitar (again). However, this time we are taking a more methodical approach vs. just getting frustrated that we don't sound like Monte Montgomery.
The internet is helpful and one of our favorite resources has been Lauren Bateman who encourages "Embace the Suck". Lauren encourages other things too... like practicing chord changes with a metronome and keeping a progress log. These two suggestions are huge. We created some simple HTML markup to help us practice random chord changes and, gotta say, you show more results with practice than with excuses.
Just copy and save the HTML markup between the "-----"s below on your hard drive as "random_guitar_chords.html" and it should work in any web browser. NOTE: There is no spyware, tracking, cookies, disk writes, etc. Feel free to edit the HTML to meet your needs.
Good luck and see you on the stage!
-----
<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Random Guitar Chords</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
}
#chordDisplay {
font-size: 36px;
margin-top: 50px;
}
.checkbox-container {
margin-bottom: 5px;
}
</style>
</head>
<body>
<h1>Select Guitar Chords</h1>
<form id="chordForm">
<div class="checkbox-container">
<input type="checkbox" id="A" value="A" checked>
<label for="A">A</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="C" value="C" checked>
<label for="C">C</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="D" value="D" checked>
<label for="D">D</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="E" value="E" checked>
<label for="E">E</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="Em" value="Em" checked>
<label for="Em">Em</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="F" value="F" checked>
<label for="F">F</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="F#m" value="F#m" checked>
<label for="F#m">F#m</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="G" value="G" checked>
<label for="G">G</label>
</div>
</form>
<p><strong>!!! ALWAYS USE A METRONOME AND LOG YOUR PROGRESS!!!</strong></p>
<label for="initialChordsSelect">How many chords to cycle:</label><br>
<select id="initialChordsSelect">
<option value="2" selected>2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
</select><br>
<br>
<label for="timeSelect">Seconds until next random chords:</label><br>
<select id="timeSelect">
<option value="10">10</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="25">25</option>
<option value="30">30</option>
<option value="35">35</option>
<option value="40">40</option>
<option value="45">45</option>
<option value="50">50</option>
<option value="55">55</option>
<option value="60" selected>60</option>
<option value="65">65</option>
<option value="70">70</option>
<option value="75">75</option>
<option value="80">80</option>
<option value="85">85</option>
<option value="90">90</option>
<option value="95">95</option>
<option value="100">100</option>
<option value="105">105</option>
<option value="110">110</option>
<option value="115">115</option>
<option value="120">120</option>
</select><br>
<br>
<button onclick="startDisplay()">Let's Jam!</button>
<div id="chordDisplay"></div>
<br>
<div id="timer"></div>
<br>
<div id="totalMinutes"></div>
<script>
var startTime;
var timerInterval;
function startDisplay() {
clearInterval(timerInterval); // Clear previous interval
document.getElementById("chordDisplay").textContent = ""; // Clear previous chords
document.getElementById("timer").textContent = ""; // Clear previous timer
startTime = new Date().getTime();
displayChords();
displayTimer();
}
function displayChords() {
var chords = [];
var checkboxes = document.querySelectorAll('input[type="checkbox"]:checked');
for (var i = 0; i < checkboxes.length; i++) {
chords.push(checkboxes[i].value);
}
var numInitialChords = parseInt(document.getElementById("initialChordsSelect").value);
var initialChords = [];
for (var i = 0; i < numInitialChords; i++) {
var randomChord;
do {
randomChord = chords[Math.floor(Math.random() * chords.length)];
} while (initialChords.includes(randomChord));
initialChords.push(randomChord);
}
document.getElementById("chordDisplay").textContent = initialChords.join(" ");
}
function displayTimer() {
var timeInSeconds = parseInt(document.getElementById("timeSelect").value);
var count = timeInSeconds;
timerInterval = setInterval(function() {
count--;
if (count <= 0) {
clearInterval(timerInterval);
document.getElementById("timer").textContent = "";
displayChords();
setTimeout(displayTimer, 1000); // Restart the timer after displaying new chords
} else {
document.getElementById("timer").textContent = "SPACEBAR for immediately ELSE selecting new chords in: " + count + " seconds";
updateTimeElapsed();
}
}, 1000);
function updateTimeElapsed() {
var currentTime = new Date().getTime();
var elapsedTime = (currentTime - startTime) / (1000 * 60);
document.getElementById("totalMinutes").textContent = "Total practice time in minutes: " + elapsedTime.toFixed(1);
}
}
document.body.addEventListener('keydown', function(e) {
if (e.keyCode === 32) { // Spacebar key code
clearInterval(timerInterval);
document.getElementById("chordDisplay").textContent = ""; // Clear previous chords
document.getElementById("timer").textContent = ""; // Clear previous timer
displayChords();
displayTimer();
e.preventDefault(); // Prevent default spacebar behavior (scrolling)
}
});
</script>
</body>
</html>
-----
Wednesday, February 28, 2024
USB Jump Battery "Hack" for Meshtastic WisBlock Base Board
Meshtasic is cool. The quickie description from their website is: "An open source, off-grid, decentralized, mesh network built to run on affordable, low-power devices". It's a way to send SMS type messages without the use of the internet, cell phone infrastructure, etc. The hardware is cheap and there are Android and iPhone apps to that support it.
We wanted to experiment with Meshtasic and purchased the RAKwireless WisBlock Base Board from Rokland. One nice thing about the WisBlock Base Board are the many supported 'plug and play' modules that allow for easy expansion.
-----
Enough about that... the problem we had was running the WisBlock base board with battery power. There is a right way to do this and there is the way we did it. The right way is to use a rechargeable LiPo battery with the connector already installed.
But... this is what we did mainly because we already had a drawer full of USB portable charging battery bricks. Like many other USB powered devices, the current draw from the WisBlock Base Board is so low that when powering it from a USB portable charging battery the portable charger will just shut down thinking there is nothing connected to it. Our fix was to add a 100 ohm load. The WisBlock makes this super easy from the on board BATTERY socket (see "right way" above).
----
Here's the current draw (~15mA) without the 100 ohm load added. All of our tested USB portable charging battery bricks would shut down:
-----We plug in the 100 ohm resistor and now all of our tested USB portable charging battery bricks stay activated with ~53mA current draw:
-----
Does it use more juice? Of course it does, but even the small lipstick battery shown will last for days and that's good enough for our use.
-----
FWIW, here is what our Meshtasic portable rig looks like:
-----
Tuesday, January 23, 2024
WS2812B LED Fireworks Simulator
-----
WS2812B LED strips are pretty cool. They are string of individually addressable RGB LEDs. This allows control of the color and brightness of each LED.
Fireworks are also pretty cool. On the downside they can be dangerous, scare wildlife, start fires, be expensive, illegal, etc. So, until we can afford our own fleet of drones we settled on this alternative. Like many projects, we stand on the shoulders of giants mentioned in the Ardunio source code below. Our main issue with their code was the effect was never changing so we improved mainly on that aspect; a few other things as well.
----
Result:
The hardware is an ESP8266 and a WS2812B LED-strip with 300 LEDs (16.5 feet). We wanted to use a Ardunio Nano (because we had one), but due to the amount of memory needed to define the arrays for the 300 LEDs we went with an ESP8266 (because we had one).
-----You are also going to need a big pole.
-----
/*
* LED Fireworks Simulator
* WhiskeyTangoHotel.Com
* JAN 2024
*
* To vary the effect experiments randomizing variables
* within acceptable limits was done. Otherwise the effect
* just looks to same 'shot after shot'. Other mods as well
*
* Leverage from https://www.Daniel-Westhof.de and
* https://www.anirama.com/1000leds/1d-fireworks/
*
* Hardware:
* HiLetgo 1PC ESP8266 NodeMCU CP2102 ESP-12E Development Board and a
* WS2812B LED-strip with 300 LEDs (16.5 feet).
*/
#include <FastLED.h>
#define NUM_LEDS 300
#define DATA_PIN 5 // Labeled a D1 on the board.
#define LED_PIN 2 // This is the BLUE LED on board
#define NUM_SPARKS NUM_LEDS/2 // OG: NUM_LEDS/2
CRGB leds[NUM_LEDS]; // sets up block of memory
float sparkPos[NUM_SPARKS];
float sparkVel[NUM_SPARKS];
float sparkCol[NUM_SPARKS];
float flarePos;
float gravity = -.008; // m/s/s
int launch_delay; // we later randomize seconds between launches
void setup() {
Serial.begin(115200);
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
// Delay untill next launch. Blink BLUE on board LED
Serial.println(" ");
launch_delay = int(random(5,30)); // Min>=5. Randomize secs between BOOMs.
//launch_delay = 5;
for (int i = (launch_delay - 5); i > 0; i--) {
Serial.println(String(i + 5) + " seconds to launch...");
digitalWrite(LED_PIN, LOW); // ON
delay(500);
digitalWrite(LED_PIN, HIGH); // OFF
delay(500);
}
// Slower timer done . Fast blink for ~5 seconds to warn of BOOM
Serial.print("5 seconds to launch!!!");
for (int i = 50; i > 0; i--) {
Serial.print(".");
digitalWrite(LED_PIN, LOW); // ON
delay(50);
digitalWrite(LED_PIN, HIGH); // OFF
delay(50);
}
Serial.println(".");
Serial.print("BOOM...");
digitalWrite(LED_PIN, LOW); // LED ON
// send up flare
flare();
digitalWrite(LED_PIN, HIGH); // LED OFF
// explode
explodeLoop();
}
void flare() {
flarePos = 0; // 0
// flareVel is how hight the BOOM is. 2.2 is max height
float flareVel = float(random(180, 215)) / 100; // Start: (180, 195)) / 100; trial and error to get reasonable range
Serial.println(" with flare height of " + String((flareVel*100)/2.2) + "%"); // How high is the BOOM?
float brightness = 5; // OG: 1
// initialize launch sparks
int blast_base = random(5,20); // number of sparks at blast base.
for (int i = 0; i < blast_base; i++) { // OG: int i = 0; i < 5; i++
sparkPos[i] = 0; sparkVel[i] = (float(random8()) / 255) * (flareVel / 2); // OG: (float(random8()) / 255) * (flareVel / 5); the / xx); is control value for BURST
sparkCol[i] = sparkVel[i] * 1000; sparkCol[i] = constrain(sparkCol[i], 0, 255);
//Serial.println(String(i) + " " + String(sparkVel[i]) + " " + String(sparkCol[i]));
}
// launch
while (flareVel >= -.2) { // OG: flareVel >= -.2 when to explode after peak BOOM. Bigger neg val means more fall before sparks
// sparks
for (int i = 0; i < blast_base; i++) { // OG: int i = 0; i < 5; i++
sparkPos[i] += sparkVel[i];
sparkPos[i] = constrain(sparkPos[i], 0, 120);
sparkVel[i] += gravity;
sparkCol[i] += -.8;
sparkCol[i] = constrain(sparkCol[i], 0, 255);
leds[int(sparkPos[i])] = HeatColor(sparkCol[i]);
leds[int(sparkPos[i])] %= 50; // reduce brightness to 50/255
}
// flare
leds[int(flarePos)] = CHSV(0, 0, int(brightness * 255));
FastLED.show();
delay(5);
FastLED.clear();
flarePos += flareVel;
flarePos = constrain(flarePos, 0, NUM_LEDS-1);
flareVel += gravity;
brightness *= .99; // OG = .98
} // while (flareVel >= -.2)
} // end void flare
void explodeLoop() {
int nSpark_var = random(2, 10); // Bigger number is less BOOM sparks
int nSparks = flarePos / nSpark_var; // OG: nSparks = flarePos / 2
//Serial.println(String(nSparks));
// initialize sparks
for (int i = 0; i < nSparks; i++) {
sparkPos[i] = flarePos; sparkVel[i] = (float(random(0, 20000)) / 10000.0) - 1.0; // from -1 to 1
sparkCol[i] = abs(sparkVel[i]) * 500; // set colors before scaling velocity to keep them bright
sparkCol[i] = constrain(sparkCol[i], 0, 255);
sparkVel[i] *= flarePos / NUM_LEDS; // proportional to height
}
sparkCol[0] = 255; // OG: 255 This will be our known spark
float dying_gravity = gravity;
float c1 = random(80,130); // OG: 120
float c2 = random(1,30); // OG: 50
//Serial.println("c1 is: " + String(c1));
//Serial.println("c2 is: " + String(c2));
while(sparkCol[0] > c2/128) { // OG: (sparkCol[0] > c2/128) As long as our known spark is lit, work with all the sparks
int decay_rate = (random(0,50)); // Slow to decay the blast sparks. (0,50) seems right. Bigger is slower. OG: delay(0);
delay(decay_rate);
//Serial.println(String(decay_rate));
FastLED.clear();
for (int i = 0; i < nSparks; i++) {
sparkPos[i] += sparkVel[i];
sparkPos[i] = constrain(sparkPos[i], 0, NUM_LEDS-1);
sparkVel[i] += dying_gravity;
sparkCol[i] *= .975;
sparkCol[i] = constrain(sparkCol[i], 0, 255); // RED cross dissolve. OG: constrain(sparkCol[i], 0, 255);
if(sparkCol[i] > c1) { // fade white to yellow
leds[int(sparkPos[i])] = CRGB(random(0,255), random(200,255), (255 * (sparkCol[i] - c1)) / (255 - c1)); // OG: CRGB(255, 255, (255 * (sparkCol[i] - c1)) / (255 - c1));
}
else if (sparkCol[i] < c2) { // fade from red to black
leds[int(sparkPos[i])] = CRGB((random(200,255) * sparkCol[i]) / c2, 0, 0); // OG: CRGB((255 * sparkCol[i]) / c2, 0, 0);
}
else { // fade from yellow to red
leds[int(sparkPos[i])] = CRGB(random(0,255), (random(200,255) * (sparkCol[i] - c2)) / (c1 - c2), 0); // OG: CRGB(255, (255 * (sparkCol[i] - c2)) / (c1 - c2), 0);
}
}
dying_gravity *= .99; // OG: dying_gravity *= .99; As sparks burn out they fall slower
FastLED.show();
} // end while(sparkCol)
delay(5);
FastLED.clear();
delay(5);
FastLED.show();
Serial.println("Effect Complete!!!");
} // end void explodeLoop()
-----
Saturday, December 23, 2023
Make Your Own Tombstone and SAVE!
-----
We saw this Hackaday article where [Proper DIY] used his woodworking skills to produce some excellent quality concrete signs and decorations. We liked the idea but the effort required fashioning the wood for the letters seemed like a lot of work to us.
-----
So.... we have a 3D printer and set out to find an easier way. The 'recipe' is pretty easy:
1)Print your design on a 3D printer. 2)Glue the design to the bottom of a concrete mold. 3)Pour concrete, shake out the bubbles, and wait for curing. 4)Unscrew the mold and release the final product.
The results are good enough with only a fraction of the time (and skill).
-----
Maybe not needed, but here is a little more detail:
- Build mold (use long wood screws to allow reuse):
- Print your 3D design and glue it the the bottom of the mold:
- Fill the mold, vibrate out the voids, then wait. We used this material mainly because we had some on hand:
- Results:
-----
We are happy with outcome. [Proper DIY] did have better results, but with a whole, whole, whole lot more effort.
-----
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:
-----