Close

Full Functionality - Rotational Encoder and Remote Control

A project log for Muffsy Stereo Relay Input Selector

Open Source, versatile audio relay input selector controlled by an ESP32 or a rotary switch.

skrodahlskrodahl 12/06/2018 at 23:360 Comments

Here's my test setup:

More about this below. But first, getting there was not all that easy:

IRremote.h and the ESP32

IRremote.h does not want to play nice with ANY library saving anything to the NVS memory. That's sort of a letdown if you'd like to use an infrared remote AND save preferences or states to the ESP32's internal storage.

Here's what happens (continuously, like every 0.2 of a second), showing only one instance of the error:

ets Jun  8 2016 00:22:57

rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0018,len:4
load:0x3fff001c,len:808
load:0x40078000,len:6084
load:0x40080000,len:6696
entry 0x400802e4
Guru Meditation Error: Core  1 panic'ed (Cache disabled but cached memory region accessed)
Core 1 register dump:
PC      : 0x400d0d78  PS      : 0x00060034  A0      : 0x40081664  A1      : 0x3ffc0be0  
A2      : 0x00000001  A3      : 0x00000002  A4      : 0x000000ff  A5      : 0x40086d14  
A6      : 0xf0000040  A7      : 0x00290000  A8      : 0x80081188  A9      : 0x3ff5f024  
A10     : 0x3ffc1044  A11     : 0x20000000  A12     : 0x00000400  A13     : 0x3ffb1e60  
A14     : 0x00000020  A15     : 0xffffffff  SAR     : 0x00000015  EXCCAUSE: 0x00000007  
EXCVADDR: 0x00000000  LBEG    : 0x400012e5  LEND    : 0x40001309  LCOUNT  : 0x800d3245  
Core 1 was running in ISR context:
EPC1    : 0x4008a377  EPC2    : 0x00000000  EPC3    : 0x00000000  EPC4    : 0x400d0d78

Backtrace: 0x400d0d78:0x3ffc0be0 0x40081661:0x3ffc0c00 0x4008a374:0x00000000

Rebooting...

I tried using EEPROM.h and Preferences.h. It still behaves the same, so I guess the problem must lie with IRremote.h.

The Solution

Sure enough, here's somebody who's encountered the same problem. Not only that, he's also solved it:

https://github.com/espressif/arduino-esp32/issues/928

His solution lets you turn off the IR Receiver when you do an EEPROM.commit, and turn it on again afterwards.

A small problem though, the instructions were a little off. Here's how to make IRremote.h work on the ESP32:

Change the File IRremote.h

On Windows, you'll find the file in Documents\Arduino\Libraries\IRremote\IRremote.h. Search for the following line:

void  enableIRIn ( ) ;

Replace it with:

void  enableIRIn (bool enable) ;

Don't forget to do the changes described here as well:

At the end of the file IRremote.h, find this piece of code:

#else
const int sendPin = SEND_PIN;
#endif
} ;

Replace the first line (#else) with:

#elif defined(SEND_PIN)

Replace the File esp32.cpp

On Windows, you'll find the file in Documents\Arduino\Libraries\IRremote\esp32.cpp. Replace everything in the file with this code:

#ifdef ESP32

// This file contains functions specific to the ESP32.

#include "IRremote.h"
#include "IRremoteInt.h"

// "Idiot check"
#ifdef USE_DEFAULT_ENABLE_IR_IN
#error Must undef USE_DEFAULT_ENABLE_IR_IN
#endif

hw_timer_t *timer;
void IRTimer(); // defined in IRremote.cpp, masqueraded as ISR(TIMER_INTR_NAME)

//+=============================================================================
// initialization
//
void  IRrecv::enableIRIn (bool enable)
{
    // Interrupt Service Routine - Fires every 50uS
    // ESP32 has a proper API to setup timers, no weird chip macros needed
    // simply call the readable API versions :)
    // 3 timers, choose #1, 80 divider nanosecond precision, 1 to count up

    if (enable) {

        timer = timerBegin(1, 80, 1);
        timerAttachInterrupt(timer, &IRTimer, 1);
        // every 50ns, autoreload = true
        timerAlarmWrite(timer, 50, true);
        timerAlarmEnable(timer);
}
    else {

        timerEnd(timer);
        timerDetachInterrupt(timer);
    }

    // Initialize state machine variables
    irparams.rcvstate = STATE_IDLE;
    irparams.rawlen = 0;

    // Set pin modes
    pinMode(irparams.recvpin, INPUT);
}

#endif // ESP32

The Program 

Before we begin, here's how I've hooked up everything:

IR Receiver: TSOP4838

Rotary Encoder: KY-040

I'm using the KY-040 rotary encoder, and connecting the pins to the ESP32 this way:

Power ON LED

Code

I'll readily admit that I'm relatively bad at programming. This code has a good basic structure though, and everything is done as functions. It should be reasonably understandable, and quite easy to modify.

It all revolves around the function relayOn():

The code will most likely change (there's probably stuff that's left over from testing), but here's the current functionality:

Enough talk, here's the code:

/* Muffsy Relay Input Selector
 *      
 *  Control relays using IR and rotary encoder
 *  Control external power to amp using IR and push button
 *  
 */

 /*
  * powerState:
  * 
  *   0: Boot
  *     powerOn()
  *       startup procedure
  *       read NVRAM (relayCount)
  *       set relays to off (previousRelay = relayCount)
  *       set power amp to off, SSR = LOW
  *       
  *   1: Powered ON
  *       turn on power button LED
  *       set power amp to on, SSR = HIGH
  *       trigger relayOn(): previousRelay = relayCount + 1
  *     rotaryEncoder()
  *       increases or decreases relayCount depending on rotational direction
  *       pushbutton: Power ON/OFF
  *       does only Power ON if powerState == 2
  *     irRemote()
  *       input up/down
  *       input direct (buttons 1-5)
  *       power on/off
  *       does only Power ON if powerState == 2
  *     relayOn()
  *       activates relays based on the relayCount
  *       handles relayCount too high or low
  *     powerControl()
  *       read power button, set powerState accordingly
  *       
  *    2: Powered OFF
  *     turn off all relays
  *     set power amp to off (SSR = LOW)
  *     powerControl()
  *       read power button, set powerState == 1 if pushed
  *     irRemote()
  *       read power button, set powerState == 1 if pushed
  */

 // Libraries
 #include <IRremote.h> // IR Remote Library
 #include <EEPROM.h> // EEPROM Library

 //  Size: 1 (relayCount)
 #define EEPROM_SIZE 1

 // Variables, pin definitions

 // Onboard LED/Power LED
 #define LED 2

 // IR Receiver pin and setup
 #define IR_Recv 13
 IRrecv irrecv(IR_Recv);
 decode_results results;
  
 // Power button
 #define poweronButton 14

 // Pins for the rotary encoder:
 #define rotaryA 35
 #define rotaryB 34

 // Relays
 #define R1 23
 #define R2 22
 #define R3 21
 #define R4 19
 #define R5 18

 //Solid State Relay
 #define SSR 17

 // Rotary Encoder variables
 int counter = 0; 
 int previous = 0;
 int aState;
 int aPreviousState;  

 // Relay Array
 int relays[] = {23, 22, 21, 19, 18};

 // Relay variables
 int relayCount;
 int previousRelay;
 int relayNumber;

 // Power/Mute variables
 int powerState;
 int buttonState = 1;       // the current reading from the input pin
 int lastButtonState = 1;   // the previous reading from the input pin
 unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
 unsigned long debounceDelay = 50;    // the debounce time; increase if the output flickers
 int mute = 0; // Mute on/off (1/0)

 // Setup
 void setup() { 

   // Power button
   pinMode (poweronButton,INPUT_PULLUP);
   
   // Onboard LED
   pinMode (LED,OUTPUT);
   
   // Rotary Encoder
   pinMode (rotaryA,INPUT);
   pinMode (rotaryB,INPUT);

   // Reads the initial state of the rotaryA
   aPreviousState = digitalRead(rotaryA); 

   // Relays
   pinMode (R1,OUTPUT);
   pinMode (R2,OUTPUT);
   pinMode (R3,OUTPUT);
   pinMode (R4,OUTPUT);
   pinMode (R5,OUTPUT);
   pinMode (SSR,OUTPUT);

   // Relay variables
   EEPROM.begin(EEPROM_SIZE);
   relayCount = EEPROM.read(0);
   previousRelay = relayCount + 1; // Start out not matching relayCount???

   // Start the IR Receiver
   //pinMode(IR_Recv, INPUT_PULLDOWN);
   irrecv.enableIRIn(true); // Starts the receiver
   
   /*
    * powerStates:
      * 0: Powering on
      * 1: Powered on
      * 2: Powered off
    */
   powerState = 0;
   mute = 0; // Mute on/off (1/0)

   // Serial monitor
   Serial.begin (115200);
 } 

 /*
 * Main program
 */
 void loop() {
  if (powerState == 0) {
    powerOn();
  } else if (powerState == 1){
    relayOn();
    rotaryEncoder(); // Include Push = MUTE
    powerControl(); // Read power button
    irRemote(); // Up, Down, Direct, Volume, MUTE, Power
  } else {
    rotaryEncoder(); // Rotary push button is temporarily power button???
    powerControl(); // Read power button
    irRemote(); // Power on/off only
  }
 }

 /*
  * Turn on current relay
  */
void relayOn() {
  
  // If relayCount has changed: Turn on the selected relay (next, previous, direct)
  // If previousRelay has changed: Turn on the last selected relay
  if (relayCount != previousRelay) {

    // Rollover 4 or 0
    if (relayCount > 4) {
      relayCount = 0;
    } else if (relayCount < 0) {
      relayCount = 4;
    }

    // Turn off all relays, then turn on relayCount
    relayOff();
    digitalWrite(relays[relayCount], HIGH);

    // Stop IR, write relayCount to memory, start IR
    irrecv.enableIRIn(false);
    EEPROM.write(0,relayCount);
    EEPROM.commit();
    irrecv.enableIRIn(true);
    Serial.print("[http://muffsy.com]: Written \"relayCount = ");
    Serial.print(relayCount);
    Serial.println("\" to save slot 0");
      
   // Reset counters, output relayNumber
   previousRelay = relayCount;
   relayNumber = relayCount + 1;
   Serial.print("[http://muffsy.com]: Activated relay #");
   Serial.println(relayNumber);
   Serial.println();
  }
}

/*
 * Power on amplifier
 */
void powerOn() { // Only called if powerState is 0 (Powering on)
    Serial.println("\n       --- http://muffsy.com ---\n");
    Serial.println("The Muffsy Relay Input Selector has woken up!\n");
    Serial.print(" ** Reading saved relay state from NVRAM: ");
    Serial.println(relayCount);
    Serial.println("\n ** All input relays are turned OFF");
    relayOff();
    Serial.println(" ** Power amplifier is turned OFF\n");
    digitalWrite (SSR,LOW);
    Serial.println(" ** Startup completed - waiting for Power ON\n");
    Serial.println("       -------------------------\n");
    
    // Set powerState to 2 (Powered off):
    powerState = 2;
}

/*
 * Read powerbutton, turn on or off
 */
void powerControl() {
    int reading = digitalRead(poweronButton);

    if (reading != lastButtonState) {
      lastDebounceTime = millis();
    }

    if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state:

    // if the button state has changed:
    if (reading != buttonState) {
      buttonState = reading;

      // only toggle the LED if the new button state is HIGH
      if (buttonState == 1) {
        Serial.println("[http://muffsy.com]: Power button pushed");
        
        if (powerState == 1) { // Turning power OFF: All relays OFF, power amp OFF
          powerState = 2;
          digitalWrite (SSR,LOW);
          digitalWrite (LED,LOW);
          relayOff();
          Serial.println("[http://muffsy.com]: Power amplifier OFF");
          Serial.println("[http://muffsy.com]: Power OFF\n");
          
        } else if (powerState == 2) { // Turning power ON: Last selected relay ON, power amp ON
          powerState = 1;
          digitalWrite (SSR,HIGH);
          digitalWrite (LED,HIGH);
          previousRelay = relayCount + 1; // Trigger relayOn()
          Serial.println("[http://muffsy.com]: Power ON");
          Serial.println("[http://muffsy.com]: Power amplifier ON\n");
        } 
      } 
    }
  }
  lastButtonState = reading;
}

 /*
  * IR Remote
  */
void irRemote() { // Start irRemote function

    // Decode the infrared input
      // Decodes the infrared input
  if (irrecv.decode(&results)) {
    long int decCode = results.value;
    Serial.print("[http://muffsy.com]: Received IR code: ");
    Serial.print(decCode);
    Serial.println();
    // Switch case to use the selected remote control button
    switch (results.value) { // Start switch/case
      
      case 7770223: // Relay 1
        {
          Serial.println("[http://muffsy.com]: Button \"1\"");
          if (powerState == 1) {
            relayCount = 0;
          } else {
            Serial.println("[http://muffsy.com]: Powered off, doing nothing...\n");
          }
          break;
        }

      case 7774303: // Relay 2
        {
          Serial.println("[http://muffsy.com]: Button \"2\"");
          if (powerState == 1) {
            relayCount = 1;
          } else {
            Serial.println("[http://muffsy.com]: Powered off, doing nothing...\n");
          }
          break;
        }

      case 7766143: // Relay 3
        {
          Serial.println("[http://muffsy.com]: Button \"3\"");
          if (powerState == 1) {
            relayCount = 2;
          } else {
            Serial.println("[http://muffsy.com]: Powered off, doing nothing...\n");
          }
          break;
        }

      case 7787053: // Relay 4
        {
          Serial.println("[http://muffsy.com]: Button \"4\"");
          if (powerState == 1) {
            relayCount = 3;
          } else {
            Serial.println("[http://muffsy.com]: Powered off, doing nothing...\n");
          }
          break;
        }

      case 7791133: // Relay 5
        {
          Serial.println("[http://muffsy.com]: Button \"5\"");
          if (powerState == 1) {
            relayCount = 4;
          } else {
            Serial.println("[http://muffsy.com]: Powered off, doing nothing...\n");
          }
          break;
        }

      case 7742173: // Channel UP
        {
          Serial.println("[http://muffsy.com]: Button \"UP\"");
          if (powerState == 1) {
            relayCount++;
          } else {
            Serial.println("[http://muffsy.com]: Powered off, doing nothing...\n");
          }
          break;
        }

      case 7738093: // Channel DOWN
        {
          Serial.println("[http://muffsy.com]: Button \"DOWN\"");
          if (powerState == 1) {
            relayCount--;
          } else {
            Serial.println("[http://muffsy.com]: Powered off, doing nothing...\n");
          };
          break;
        }

      case 7745743: // Power button
        {
          Serial.println("[http://muffsy.com]: Button \"POWER\"");
          if (powerState == 1) {
            powerState = 2;
            digitalWrite (SSR,LOW);
            digitalWrite (LED,LOW);
            relayOff();
            Serial.println("[http://muffsy.com]: Power amplifier OFF");
            Serial.println("[http://muffsy.com]: Power OFF\n");
            
          } else {
            powerState = 1;
            digitalWrite (SSR,HIGH);
            digitalWrite (LED,HIGH);
            previousRelay = relayCount + 1; // Trigger relayOn()
            Serial.println("[http://muffsy.com]: Power ON");
            Serial.println("[http://muffsy.com]: Power amplifier ON\n");
          }
          break;
        }

      default:
      {
        Serial.println("[http://muffsy.com]: Going back to waiting for IR remote keypress\n");
      }
    } // End switch/case
    irrecv.resume(); // Receives the next value from the button you press
  }
    
  } // End irRemote function

  
 /*
  * Mute (turn off all relays)
  */
   void relayOff() {
      for (int off = 0; off <= 4; off++) {
      digitalWrite(relays[off], LOW);
      }
   }


 /*
 * Rotary Encoder Control of Relays
 */
 void rotaryEncoder() { 
   aState = digitalRead(rotaryA); // Reads the "current" state of the rotaryA
   
   // If the previous and the current state of the rotaryA are different, that means a Pulse has occured
   if (aState != aPreviousState){  

     // If the rotaryB state is different to the rotaryA state, that means the encoder is rotating clockwise
     if (digitalRead(rotaryB) != aState) { 
       counter ++;
     } else {
       counter --;
     }
    }

   // What to do if rotating Right of Left 
   if (previous != counter) {
     if (counter > 1) { // Since the encoder gives two signals when turning
       Serial.print("[http://muffsy.com]: Rotational encoder turned ");
       Serial.println("clockwise");

       if (powerState == 1) {
        // Increase relayCount
        relayCount++;
       } else {
        Serial.println("[http://muffsy.com]: Powered off, doing nothing...\n"); // Powered off???
       }
       
     } else if (counter < -1) { // Since the encoder gives two signals when turning
       Serial.print("[http://muffsy.com]: Rotational encoder turned ");
       Serial.println("counter-clockwise");
       
       if (powerState == 1) {
        // Increase relayCount
        relayCount--;
       } else {
        Serial.println("[http://muffsy.com]: Powered off, doing nothing...\n"); // Powered off???
       }
     }
   }

   // Reset counters
   previous = counter; 
   if (counter < -1) {
    counter = 0;
    previous = 0;
   } else if (counter > 1){
    counter = 0;
    previous = 0;
   }
    
   // Updates the previous state of the rotaryA with the current state
   aPreviousState = aState; 
 }

You can get an idea of what's going on by looking at the serial console:

-skrodahl

Discussions