Alan De Windt
Published © GPL3+

SDRSharp Controller

Control the SDRSharp software with physical controls for a more natural experience using a software defined radio.

IntermediateFull instructions provided1,865
SDRSharp Controller

Things used in this project

Hardware components

Raspberry Pi Pico W
Raspberry Pi Pico W
×1
12MM round head momentary push button
×5
Black all aluminum solid audio amplifier volume potentiometer knob
×1
5-24V 360 pulses per rotation optical rotary encoder with 6mm shaft
×1
DIY prototype PCB circuit board
×1
Blue 3mm diameter LED
×5
USB Cable Assembly, USB Type A Plug to Micro USB Type B Plug
USB Cable Assembly, USB Type A Plug to Micro USB Type B Plug
×1

Story

Read more

Schematics

SDRSharp Controller Electric Schematic

Note: I used 3.3K Ohm resistors for the LEDs as I wanted them to be quite dim. You could use lower Ohm resistors and PWM in the sketch to adjust their brightness.

Code

SDRSharp Controller

Arduino
This is the sketch for the Raspberry Pi Pico.

I wrote this sketch using the Arduino IDE and uploaded it to the Pico using this IDE.

IMPORTANT NOTE: You should use the Raspberry Pi Pico/RP2040 board manager from Earle F. Philhower. At first I tried using another Pico board manager but had issues and could not get this code to work when compiled with that board manager (the sketch would load to the Pico but then the Pico constantly froze up flashing its onboard LED).
// SDRSharp Controller
// Written by Alan De Windt
// October 2023

// DISCLAIMER:  Use at your own risk!  This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

long int shaft_position = 0;
long int current_shaft_position = 0;
long int previous_shaft_position = 0;
int turn_direction = 1;
long int adjustment = 0;
long int pulses = 0;
int new_value = 0;

unsigned long last_run = 0;
unsigned long last_update = 0;

int rotary_A_pin = 5;
int rotary_B_pin = 6;

int push_button[5] {20, 19, 18, 17, 16};
// 20 = Volume
// 19 = Tuning
// 18 = Mode
// 17 = Zoom
// 16 = Memory

int led[5] {0, 1, 2, 3, 4};
// 0 = Volume
// 1 = Tuning
// 2 = Mode
// 3 = Zoom
// 4 = Memory

boolean last_button[5] {HIGH, HIGH, HIGH, HIGH, HIGH};
boolean current_button[5] {HIGH, HIGH, HIGH, HIGH, HIGH};
boolean is_short_press[5] {false, false, false, false, false};
boolean is_long_press[5] {false, false, false, false, false};
unsigned long time_pressed[5] {0, 0, 0, 0, 0};

int current_mode = 0;  // Set to volume mode by default

int mode_status[5] {0, 0, 0, 0, 0};
// 0 = Not active
// 1 = Main mode active
// 2 = Sub-mode active

int main_mode_sp[5] {0, 0, 0, 0, 0};  // Shaft position
int main_mode_ppa[5] {10, 15, 30, 15, 0};  // Pulses per unit of adjustment
int main_mode_value[5] {0, 0, 0, 0, 0};

int sub_mode_sp[5] {0, 0, 0, 0, 0};  // Shaft position
int sub_mode_ppa[5] {10, 30, 0, 0, 0};  // Pulses per unit of adjustment
int sub_mode_value[5] {0, 0, 0, 0, 0};

boolean blink_led = false;
boolean led_on = false;
unsigned long time_led_changed;

boolean blink_memory_led = false;
boolean memory_led_on = false;
unsigned long time_memory_led_changed = 0;
int memory_led_blink_count = 0;
int memory_led_blink_max = 0;

String data_received = "";

void  setup() {

  Serial.begin(115200);
  Serial.setTimeout(100);
  delay(100);

  // Initialize pushbuttons and LEDs
  for (int i=0; i < 5; i++) {
    pinMode(push_button[i], INPUT_PULLUP);
    pinMode(led[i], OUTPUT);
    digitalWrite(led[i],LOW);
  }

  // Initialize rotary encoder
  pinMode(rotary_A_pin, INPUT_PULLUP);
  pinMode(rotary_B_pin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(rotary_A_pin), shaft_moved, FALLING);

  // Initialize built-in LED and turn on for 1 second
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);

  // Turn on LED for volume as this is the default mode
  digitalWrite(led[current_mode], HIGH);
  
}

void loop()  {

  // Check all push buttons
  for (int i=0; i < 5; i++) {

    current_button[i] = debounce(last_button[i], push_button[i]);
    
    // If push button has been pressed down, record time to determine short push vs. long push vs. ignore (super long push)
    if (last_button[i] == HIGH && current_button[i] == LOW) {
      time_pressed[i] = millis();
    }
    
    // If push button has been released
    if (last_button[i] == LOW && current_button[i] == HIGH) {

      // Determine if it was a short or long press
      if (millis() < (time_pressed[i] + 1000)) {  // Short press is less than 1 second
        is_short_press[i] = true;
        is_long_press[i] = false;
      } else if (millis() < (time_pressed[i] + 4000)) {  // Long press is between 1 and 4 seconds (longer presses are ignored)
        is_short_press[i] = false;
        is_long_press[i] = true;
      } else {
        is_short_press[i] = false;
        is_long_press[i] = false;
      }

      switch (i) {

      case 0: // Volume & squelch threshold
      
        if (is_short_press[i] == true) {  // Process short press only
          if (mode_status[i] == 0 || mode_status[i] == 2) {

            Serial.println("show_mode:audio gain");

            mode_status[current_mode] = 0; // Reset mode of previously active mode
            digitalWrite(led[current_mode],LOW);  // Turn off LED for previously active mode

            current_mode = i;  // Set current mode to this mode
            mode_status[current_mode] = 1; // Set mode for this now active mode to "main mode"

            digitalWrite(led[current_mode], HIGH);  // Turn on LED for this mode
            blink_led = false;  // Turn off blinking of LED

            // Restore shaft position to previous shaft position for this mode
            shaft_position = main_mode_sp[current_mode];
            previous_shaft_position = shaft_position;

          } else if (mode_status[i] == 1) {

            Serial.println("show_mode:squelch threshold");

            mode_status[current_mode] = 2; // Set mode to "sub-mode"
            digitalWrite(led[current_mode], LOW);  // Turn off LED for this mode
            led_on = false;
            blink_led = true;  // Turn on blinking of LED
            time_led_changed = millis();  // Store current millis to then be able to turn LED on 200 millis from now

            // Restore shaft position to previous shaft position for this sub-mode
            shaft_position = sub_mode_sp[current_mode];
            previous_shaft_position = shaft_position;

          }
        }
        break;

      case 1: // Tuning and tuning step
      
        if (is_short_press[i] == true) {  // Process short press only
          if (mode_status[i] == 0 || mode_status[i] == 2) {

            Serial.println("show_mode:tuning");

            mode_status[current_mode] = 0; // Reset mode of previously active mode
            digitalWrite(led[current_mode],LOW);  // Turn off LED for previously active mode

            current_mode = i;  // Set current mode to this mode
            mode_status[current_mode] = 1; // Set mode for this now active mode to "main mode"

            digitalWrite(led[current_mode], HIGH);  // Turn on LED for this mode
            blink_led = false;  // Turn off blinking of LED

            // Restore shaft position to previous shaft position for this mode
            shaft_position = main_mode_sp[current_mode];
            previous_shaft_position = shaft_position;

          } else if (mode_status[i] == 1) {

            Serial.println("show_mode:tuning step");

            mode_status[current_mode] = 2; // Set mode to "sub-mode"
            digitalWrite(led[current_mode], LOW);  // Turn off LED for this mode
            led_on = false;
            blink_led = true;  // Turn on blinking of LED
            time_led_changed = millis();  // Store current millis to then be able to turn LED on 200 millis from now

            // Restore shaft position to previous shaft position for this sub-mode
            shaft_position = sub_mode_sp[current_mode];
            previous_shaft_position = shaft_position;

          }
        }
        break;

      case 2: // Mode & filter bandwidth
      
        if (is_short_press[i] == true) {  // Process short press only
          if (mode_status[i] == 0 || mode_status[i] == 2) {

            Serial.println("show_mode:mode");

            mode_status[current_mode] = 0; // Reset mode of previously active mode
            digitalWrite(led[current_mode],LOW);  // Turn off LED for previously active mode

            current_mode = i;  // Set current mode to this mode
            mode_status[current_mode] = 1; // Set mode for this now active mode to "main mode"

            digitalWrite(led[current_mode], HIGH);  // Turn on LED for this mode
            blink_led = false;  // Turn off blinking of LED

            // Restore shaft position to previous shaft position for this mode
            shaft_position = main_mode_sp[current_mode];
            previous_shaft_position = shaft_position;

          } else if (mode_status[i] == 1) {

            Serial.println("show_mode:filter bandwidth");

            mode_status[current_mode] = 2; // Set mode to "sub-mode"
            digitalWrite(led[current_mode], LOW);  // Turn off LED for this mode
            led_on = false;
            blink_led = true;  // Turn on blinking of LED
            time_led_changed = millis();  // Store current millis to then be able to turn LED on 200 millis from now

            // Restore shaft position to previous shaft position for this sub-mode
            shaft_position = sub_mode_sp[current_mode];
            previous_shaft_position = shaft_position;

          }
        }
        break;

      case 3: // Zoom
      
        if (is_short_press[i] == true) {  // Process short press only
          if (mode_status[i] == 0) {

            Serial.println("show_mode:zoom");

            mode_status[current_mode] = 0; // Reset mode of previously active mode
            digitalWrite(led[current_mode],LOW);  // Turn off LED for previously active mode

            current_mode = i;  // Set current mode to this mode
            mode_status[current_mode] = 1; // Set mode for this now active mode to "main mode"

            digitalWrite(led[current_mode], HIGH);  // Turn on LED for this mode
            blink_led = false;  // Turn off blinking of LED

            // Restore shaft position to previous shaft position for this mode
            shaft_position = main_mode_sp[current_mode];
            previous_shaft_position = shaft_position;

          }
        }
        break;
        
      case 4: // Memory recall & memory set
      
        if (is_short_press[i] == true) {  // If short press...
            Serial.println("memory:recall");
            blink_memory_led = true;
            memory_led_blink_max = 2;
            memory_led_blink_count = 1;
            digitalWrite(led[4], HIGH);  // Turn on memory pushbutton LED
            memory_led_on = true;
            time_memory_led_changed = millis();
        } else if (is_long_press[i] == true) {  // If long press...
            Serial.println("memory:store");  // Recall memory
            blink_memory_led = true;
            memory_led_blink_max = 5;
            memory_led_blink_count = 1;
            digitalWrite(led[4], HIGH);  // Turn on memory pushbutton LED
            memory_led_on = true;
            time_memory_led_changed = millis();
        }
        break;
        
      }
    }

    last_button[i] = current_button[i];
  
  }

  // Blink LED of pushbutton belonging to currently active mode if necessary
  if ((blink_led == true) && (millis() - time_led_changed > 200)) {  
      if (led_on == true) {
        led_on = false;
        digitalWrite(led[current_mode], LOW);  // Turn off LED
      } else {
        led_on = true;
        digitalWrite(led[current_mode], HIGH);  // Turn on LED
      }
      time_led_changed = millis();
  }

  // Blink LED of memory pushbutton if necessary
  if ((blink_memory_led == true) && (millis() - time_memory_led_changed > 100)) {  
      if (memory_led_on == true) {
        memory_led_on = false;
        digitalWrite(led[4], LOW);  // Turn off LED
        if (memory_led_blink_count == memory_led_blink_max) {
          blink_memory_led = false;
        }
      } else {
        memory_led_on = true;
        digitalWrite(led[4], HIGH);  // Turn on LED
        memory_led_blink_count += 1;
      }
      time_memory_led_changed = millis();
  }

  if (Serial.available() > 0) {
    data_received = Serial.readString();
    if (data_received == "connection_request") {
      Serial.println("connection_status:established");
    }
  }

  if (millis() - last_update > 100) {  // Check if rotary encoder moved every 100 ms

    current_shaft_position = shaft_position;
    pulses = current_shaft_position - previous_shaft_position;  // Calculate number of pulses/steps that the rotary encoder was turned since last check

    if (pulses > 0) {
      turn_direction = 1;
    } else {
      turn_direction = -1;
    }
    pulses = abs(pulses);

    if (pulses > 0) { 

      // The rotary encoder has 360 steps/pulses per rotation.
      // For every mode we keep track of the shaft position which is always 0 for every mode when the Pico starts.
      // When the shaft is turned CW one step/degree while in a given mode, then the shaft position is 1, when it is turned 
      // one more step/degree CW then the shaft position is 2, etc.  When it is turned CCW by one step/degree then
      // the shaft position is the shaft position - 1, etc.
      // In a given mode, when the shaft is turned we calculate the virtual/local value for the mode by
      // taking the shaft position and dividing it by the pulses per unit of adjustment for the mode

      // Example:  
      // Mode = volume
      // Volume pulses per unit of adjustment = 10 (we want 10 pulses/degrees of rotation to change the volume by 1 level)
      // Shaft position = 100
      // Volume is at 10 (shaft position of 100 / pulses per unit of adjustmetn of 10)
      // This is a virtual/local volume setting since we never get the actual volume from SDR#.

      switch (current_mode) {
      case 0: // Volume & squelch threshold

        if (mode_status[current_mode] == 1) {
          main_mode_sp[current_mode] = main_mode_sp[current_mode] + (pulses * turn_direction);
          new_value = main_mode_sp[current_mode] / main_mode_ppa[current_mode];
          if (new_value != main_mode_value[current_mode]){
            Serial.print("adjust_audio_gain:");
            Serial.println(new_value - main_mode_value[current_mode]);
            main_mode_value[current_mode] = new_value;
          }
        } else if (mode_status[current_mode] == 2) {
          sub_mode_sp[current_mode] = sub_mode_sp[current_mode] + (pulses * turn_direction);
          new_value = sub_mode_sp[current_mode] / sub_mode_ppa[current_mode];
          if (new_value != sub_mode_value[current_mode]){
            Serial.print("adjust_squelch_threshold:");
            Serial.println(new_value - sub_mode_value[current_mode]);
            sub_mode_value[current_mode] = new_value;
          }
        }
        break;

      case 1: // Tuning & tuning step

        if (mode_status[current_mode] == 1) {
          main_mode_sp[current_mode] = main_mode_sp[current_mode] + (pulses * turn_direction);
          new_value = main_mode_sp[current_mode] / main_mode_ppa[current_mode];
          if (new_value != main_mode_value[current_mode]){
            Serial.print("adjust_tuning:");
            Serial.println(new_value - main_mode_value[current_mode]);
            main_mode_value[current_mode] = new_value;
          }
        } else if (mode_status[current_mode] == 2) {
          sub_mode_sp[current_mode] = sub_mode_sp[current_mode] + (pulses * turn_direction);
          new_value = sub_mode_sp[current_mode] / sub_mode_ppa[current_mode];
          if (new_value != sub_mode_value[current_mode]){
            Serial.print("adjust_tuning_step:");
            Serial.println(new_value - sub_mode_value[current_mode]);
            sub_mode_value[current_mode] = new_value;
          }
        }
        break;
 
      case 2: // Mode & filter bandwidth
  
        if (mode_status[current_mode] == 1) {
          main_mode_sp[current_mode] = main_mode_sp[current_mode] + (pulses * turn_direction);
          new_value = main_mode_sp[current_mode] / main_mode_ppa[current_mode];
          if (new_value != main_mode_value[current_mode]){
            Serial.print("adjust_mode:");
            Serial.println(new_value - main_mode_value[current_mode]);
            main_mode_value[current_mode] = new_value;
          }
        } else if (mode_status[current_mode] == 2) {
          if (pulses < 11) {
            adjustment = 1;
          } else {
            adjustment = (pulses - 9) * (pulses - 10);
          }
          Serial.print("adjust_filter_bandwidth:");
          Serial.println(adjustment * turn_direction);
        }
        break;

      case 3: // Zoom
  
        if (mode_status[current_mode] == 1) {
          main_mode_sp[current_mode] = main_mode_sp[current_mode] + (pulses * turn_direction);
          new_value = main_mode_sp[current_mode] / main_mode_ppa[current_mode];
          if (new_value != main_mode_value[current_mode]){
            Serial.print("adjust_zoom:");
            Serial.println(new_value - main_mode_value[current_mode]);
            main_mode_value[current_mode] = new_value;
          }
        }
        break;
        
      }
    }

    previous_shaft_position = current_shaft_position;
    last_update = millis();
    
  }
 
}

void shaft_moved(){
  if (millis() - last_run > 1){
    if (digitalRead(rotary_A_pin) == 0 && digitalRead(rotary_B_pin) == 1){
      shaft_position--;
    }
    if (digitalRead(rotary_A_pin) == 0 && digitalRead(rotary_B_pin) == 0){
      shaft_position++;  
    }
    last_run = millis();
  }
}

// Button debouncer
boolean debounce(boolean last, int pin) {
  boolean current = digitalRead(pin);
  if (last != current) {
    delay(5);
    current = digitalRead(pin);
  }
  return current;
}

Credits

Alan De Windt

Alan De Windt

4 projects • 23 followers
Currently a Business Analyst and UI/UX Designer. Started career as a developer, still coding but as a hobby only.
Thanks to Al Brown and Jean-Jacques Risch.

Comments