Showing posts with label API. Show all posts
Showing posts with label API. Show all posts

Sunday, June 8, 2025

FlexRadio gets Rotary Phone Dial Interface

  

-----

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

-----

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

-----

Hook it up like this:

----- 

Run the script and this is what you get this:

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

-----

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

import serial
import time
import socket
import sys
import msvcrt  

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        prev_cts = cts
        time.sleep(0.01)

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

finally:
    ser.close()

-----

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

Tuesday, April 1, 2014

RasPI + RGB LED = Color of Twitter

This project uses a Raspberry PI to scan all posted Tweets in real time for the mention of a color.  When a color is Tweeted  the Red, Green, and/or Blue segments of a RGB LED are turned on to display the Tweeted color.  Video demo below:

-----
The project is pretty cool and simple to duplicate.  You should be able to just copy/past my Python script below into your favorite RasPI editor and go from there.

Note that the code expects the RGB LED to be connected to I/O Pins 11 (Red), 15 (Green) and 13 (Blue).  Also, be sure to add a current limiting resistor to each of the three I/O pins; not the RGB LED ground pin.  My RGB LED was spec'd for 330 Ohm resistors.  This picture should help identify Pins 11, 15, and 13:
-----
You will also need to establish your own Twitter API Token.  Don't worry; it's easy if you already have a Twitter account.  To get them go to https://dev.twitter.com/.  Enter these API Token values where the X's are in the source code below.
-----
#  Program to search Twitter to control a RGB LED
#  by WhiskeyTangoHotel.Com with special thanks to Sparkfun and twython
#  Tracks a tally count after each find
#  APRIL 2014

import time
import datetime # to allow timestamp math
import RPi.GPIO as GPIO
from twython import TwythonStreamer

#  PI I/O 11 = Red
#  PI I/O 13 = Blue
#  PI I/O 15 = Green
#  Red + Blue = Orange
#  Red + Green = Pink
#  None on = Black

# GPIO pin number of LED
Red = 11
Blue = 13
Green = 15

# Setup GPIO as output
GPIO.setmode(GPIO.BOARD)
GPIO.setup(Red, GPIO.OUT)
GPIO.output(Red, GPIO.LOW)

GPIO.setup(Green, GPIO.OUT)
GPIO.output(Green, GPIO.LOW)

GPIO.setup(Blue, GPIO.OUT)
GPIO.output(Blue, GPIO.LOW)

# Twitter application authentication

APP_KEY = 'xxxxxxxxxxxxxxxxxxxx'
APP_SECRET = 'xxxxxxxxxxxxxxxxxxxx'
OAUTH_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
OAUTH_TOKEN_SECRET = 'xxxxxxxxxxxxxxxxxxxx'

# Search terms placed in array TERM[]   # will find any term within the array ['one', 'two', 'three']   CASE SENSITIVE
TERM = []
TERM.append('red') #TERM[0]
TERM.append('blue') #TERM[1]
TERM.append('green') #TERM[2]
TERM.append('orange') #TERM[3]
TERM.append('pink') #TERM[4]

LED_secs_on = 30  # when found; how long to burn the LED

localtime = time.asctime( time.localtime(time.time()) )
print localtime
t0 = datetime.datetime.now()  # for timestamp math
print 'Self testing RGB LED...'
#Test RGB LED
i = 0
for i in range(0,5):
print 'RED...  GREEN...  BLUE...'
GPIO.output(Red, GPIO.HIGH)
time.sleep(0.3)
GPIO.output(Red, GPIO.LOW)

GPIO.output(Green, GPIO.HIGH)
time.sleep(0.3)
GPIO.output(Green, GPIO.LOW)

GPIO.output(Blue, GPIO.HIGH)
time.sleep(0.3)
GPIO.output(Blue, GPIO.LOW)
i = i + 1

print ' '
print "START searching for TERMS: "
print TERM[0]
print TERM[1]
print TERM[2]
print TERM[3]
print TERM[4]
print '................................'
print ' '

Tally_0 = 0
Tally_1 = 0
Tally_2 = 0
Tally_3 = 0
Tally_4 = 0

# Setup callbacks from Twython Streamer
class BlinkyLED(TwythonStreamer):
        def on_success(self, data):
global Tally_0
global Tally_1
global Tally_2
global Tally_3
global Tally_4

if 'text' in data:
check_string = data['text'].encode('utf-8')

if TERM[0] in check_string:
print TERM[0] + ' found on ' + time.asctime( time.localtime(time.time()) )
print ' '
Tally_0 = Tally_0 + 1
print data['text'].encode('utf-8')
GPIO.output(Red, GPIO.HIGH)


if TERM[1] in check_string:
print TERM[1] + ' found on ' + time.asctime( time.localtime(time.time()) )
print ' '
Tally_1 = Tally_1 +1
print data['text'].encode('utf-8')
GPIO.output(Blue, GPIO.HIGH)

if TERM[2] in check_string:
print TERM[2] + ' found on ' + time.asctime( time.localtime(time.time()) )
print ' '
Tally_2 = Tally_2 +1
print data['text'].encode('utf-8')
GPIO.output(Green, GPIO.HIGH)

if TERM[3] in check_string:
print TERM[3] + ' found on ' + time.asctime( time.localtime(time.time()) )
print ' '
Tally_3 = Tally_3 + 1
print data['text'].encode('utf-8')
GPIO.output(Red, GPIO.HIGH)
GPIO.output(Green, GPIO.HIGH)

if TERM[4] in check_string:
print TERM[4] + ' found on ' + time.asctime( time.localtime(time.time()) )
print ' '
Tally_4 = Tally_4 + 1
print data['text'].encode('utf-8')
GPIO.output(Red, GPIO.HIGH)
GPIO.output(Blue, GPIO.HIGH)

if TERM[0] in check_string or TERM[1] in check_string or TERM[2] in check_string or TERM[3] in check_string or TERM[4] in check_string:
print ' '
print 'SCORE:'
print TERM[0] + ' = ' + str(Tally_0)
print TERM[1] + ' = ' + str(Tally_1)
print TERM[2] + ' = ' + str(Tally_2)
print TERM[3] + ' = ' + str(Tally_3)
print TERM[4] + ' = ' + str(Tally_4)
print ' '
print str(Tally_0 + Tally_1 + Tally_2 + Tally_3 + Tally_4) + ' total finds after ' + str(datetime.datetime.now() - t0)
print '--------------------------------'
print ' '
time.sleep(LED_secs_on)    # keep LED on for xx secs
GPIO.output(Red, GPIO.LOW)  # turn off the LED
GPIO.output(Green, GPIO.LOW)
GPIO.output(Blue, GPIO.LOW)

# Create streamer
try:
        stream = BlinkyLED(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET)
        stream.statuses.filter(track=TERM)
except KeyboardInterrupt:
        GPIO.cleanup()

-----
That's it.  Hope you give it a try!