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


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