Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@ Adding a rotary encoder instance is easy:
{
Serial.begin( 115200 );

// This tells the library that the encoder has its own pull-up resistors
rotaryEncoder.setEncoderType( EncoderType::HAS_PULLUP );
// Uncomment if your encoder does not have its own pull-up resistors
//rotaryEncoder.enableEncoderPinPullup();
//rotaryEncoder.enableButtonPinPullup();

// Range of values to be returned by the encoder: minimum is 1, maximum is 10
// The third argument specifies whether turning past the minimum/maximum will
Expand Down
13 changes: 7 additions & 6 deletions examples/BasicRotaryEncoder/BasicRotaryEncoder.ino
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/**
* ESP32RotaryEncoder: BasicRotaryEncoder.ino
*
*
* This is a basic example of how to instantiate a single Rotary Encoder.
*
*
* Turning the knob will increment/decrement a value between 1 and 10 and
* print it to the serial console.
*
*
* Pressing the button will output "boop!" to the serial console.
*
*
* Created 3 October 2023
* Updated 1 November 2023
* By Matthew Clark
Expand Down Expand Up @@ -41,8 +41,9 @@ void setup()
{
Serial.begin( 115200 );

// This tells the library that the encoder has its own pull-up resistors
rotaryEncoder.setEncoderType( EncoderType::HAS_PULLUP );
// Uncomment if your encoder does not have its own pull-up resistors
//rotaryEncoder.enableEncoderPinPullup();
//rotaryEncoder.enableButtonPinPullup();

// Range of values to be returned by the encoder: minimum is 1, maximum is 10
// The third argument specifies whether turning past the minimum/maximum will
Expand Down
13 changes: 7 additions & 6 deletions examples/ButtonPressDuration/ButtonPressDuration.ino
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* ESP32RotaryEncoder: ButtonPressDuration.ino
*
*
* This example shows how to handle long button-presses differently
* from long button-presses
*
*
* Turning the knob will increment/decrement a value between 1 and 10 and
* print it to the serial console.
*
*
* Pressing the button will output "boop!" to the serial console.
*
*
* Created 1 November 2023
* By Matthew Clark
*/
Expand Down Expand Up @@ -62,8 +62,9 @@ void setup()
{
Serial.begin( 115200 );

// This tells the library that the encoder has its own pull-up resistors
rotaryEncoder.setEncoderType( EncoderType::HAS_PULLUP );
// Uncomment if your encoder does not have its own pull-up resistors
//rotaryEncoder.enableEncoderPinPullup();
//rotaryEncoder.enableButtonPinPullup();

// Range of values to be returned by the encoder: minimum is 1, maximum is 10
// The third argument specifies whether turning past the minimum/maximum will
Expand Down
9 changes: 5 additions & 4 deletions examples/LeftOrRight/LeftOrRight.ino
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* ESP32RotaryEncoder: LeftOrRight.ino
*
*
* This is a simple example of how to track whether the knob was
* turned left or right instead of tracking a numeric value
*
*
* Created 1 November 2023
* By Matthew Clark
*/
Expand Down Expand Up @@ -82,8 +82,9 @@ void setup()
{
Serial.begin( 115200 );

// This tells the library that the encoder has its own pull-up resistors
rotaryEncoder.setEncoderType( EncoderType::HAS_PULLUP );
// Uncomment if your encoder does not have its own pull-up resistors
//rotaryEncoder.enableEncoderPinPullup();
//rotaryEncoder.enableButtonPinPullup();

// The encoder will only return -1, 0, or 1, and will not wrap around.
rotaryEncoder.setBoundaries( -1, 1, false );
Expand Down
25 changes: 13 additions & 12 deletions examples/TwoRotaryEncoders/TwoRotaryEncoders.ino
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
/**
* ESP32RotaryEncoder: TwoRotaryEncoders.ino
*
*
* This is a basic example of how to instantiate two distinct Rotary Encoders.
*
*
* Rotary Encoder #1:
* - Turning the knob will increment/decrement a value between 1 and 10,
* and print it to the serial console.
*
*
* - Pressing the button will enable/disable Rotary Encoder #2.
*
*
* Rotary Encoder #2:
* - Turning the knob will increment/decrement a value between -100 and 100,
* and print it to the serial console.
*
*
* - Pressing the button will enable/disable Rotary Encoder #1.
*
*
* While a rotary encoder is disabled, turning the knob or pressing the button
* will have no effect.
*
*
* Created 3 October 2023
* Updated 1 November 2023
* By Matthew Clark
Expand Down Expand Up @@ -81,8 +81,9 @@ void button2ToggleRE1( unsigned long duration )

void setup_RE1()
{
// This tells the library that the encoder has its own pull-up resistors
rotaryEncoder1.setEncoderType( EncoderType::HAS_PULLUP );
// Uncomment if your encoder does not have its own pull-up resistors
//rotaryEncoder.enableEncoderPinPullup();
//rotaryEncoder.enableButtonPinPullup();

// Range of values to be returned by the encoder: minimum is 1, maximum is 10
// The third argument specifies whether turning past the minimum/maximum will
Expand All @@ -104,9 +105,9 @@ void setup_RE1()

void setup_RE2()
{
// This tells the library that the encoder does not have its own pull-up
// resistors, so the internal pull-up resistors will be enabled
rotaryEncoder2.setEncoderType( EncoderType::FLOATING );
// Uncomment if your encoder does not have its own pull-up resistors
//rotaryEncoder.enableEncoderPinPullup();
//rotaryEncoder.enableButtonPinPullup();

// Range of values to be returned by the encoder: minimum is -100, maximum is 100
// The third argument specifies whether turning past the minimum/maximum will wrap
Expand Down
39 changes: 6 additions & 33 deletions src/ESP32RotaryEncoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,33 +39,6 @@ RotaryEncoder::~RotaryEncoder()
esp_timer_delete( loopTimer );
}

void RotaryEncoder::setEncoderType( EncoderType type )
{
switch( type )
{
case FLOATING:
encoderPinMode = INPUT_PULLUP;
buttonPinMode = INPUT_PULLUP;
break;

case HAS_PULLUP:
encoderPinMode = INPUT;
buttonPinMode = INPUT;
break;

case SW_FLOAT:
encoderPinMode = INPUT;
buttonPinMode = INPUT_PULLUP;
break;

default:
ESP_LOGE( LOG_TAG, "Invalid encoder type %i", type );
return;
}

ESP_LOGD( LOG_TAG, "Encoder type set to %i", type );
}

void RotaryEncoder::setBoundaries( long minValue, long maxValue, bool circleValues )
{
ESP_LOGD( LOG_TAG, "boundary minValue = %ld, maxValue = %ld, circular = %s", minValue, maxValue, ( circleValues ? "true" : "false" ) );
Expand Down Expand Up @@ -127,12 +100,6 @@ void RotaryEncoder::onPressed( ButtonCallback f )
callbackButtonPressed = f;
}

static void timerCallback( void *arg )
{
RotaryEncoder *instance = (RotaryEncoder *)arg;
instance->loop();
}

void RotaryEncoder::beginLoopTimer()
{
/**
Expand All @@ -159,6 +126,12 @@ void RotaryEncoder::beginLoopTimer()
esp_timer_start_periodic( loopTimer, RE_LOOP_INTERVAL );
}

void RotaryEncoder::timerCallback( void *self )
{
RotaryEncoder *instance = (RotaryEncoder *)self;
instance->loop();
}

void RotaryEncoder::attachInterrupts()
{
#if defined( BOARD_HAS_PIN_REMAP ) && ( ESP_ARDUINO_VERSION < ESP_ARDUINO_VERSION_VAL(3,0,0) )
Expand Down
77 changes: 37 additions & 40 deletions src/ESP32RotaryEncoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,17 @@

#include <atomic>

static constexpr int8_t RE_DEFAULT_PIN = -1;
static constexpr uint8_t RE_DEFAULT_STEPS = 4;
static constexpr uint64_t RE_LOOP_INTERVAL = 100000U; // 0.1 seconds

typedef enum {
FLOATING,
HAS_PULLUP,
SW_FLOAT
} EncoderType;

class RotaryEncoder {
public:
static constexpr int8_t RE_DEFAULT_PIN = -1;
static constexpr uint8_t RE_DEFAULT_STEPS = 4;
static constexpr uint64_t RE_LOOP_INTERVAL = 100000U; // 0.1 seconds

protected:
mutable portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
typedef enum {
FLOATING,
HAS_PULLUP,
SW_FLOAT
} Type;
Copy link
Contributor Author

@cleishm cleishm Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enum feels awkward. Given it's a breaking change, would you consider changing the API to specify floating/pullup separately for the encoder and the switch? Then you wouldn't need the enum either - a bool would suffice (unless you wanted to add support for a reversed logic level?).

E.g.

rotaryEncoder.setEncoderInternalPullup(false);
rotaryEncoder.setButtonInternalPullup(false);

or perhaps:

rotaryEncoder.disableEncoderInternalPullup(); // rotaryEncoder.enableEncoderInternalPullup();
rotaryEncoder.disableButtonInternalPullup();  // rotaryEncoder.enableButtonInternalPullup();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you could avoid methods entirely and provide direction on pull-ups as constructor args. This would avoid surprising errors where someone attempts to call these after calling begin().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - I'm not sure I actually like passing them in constructor arguments. It gets unwieldy. Here's the commit for that: 07538dd

One could go further and replace the constructor arguments with a parameters struct. For c++17 and above, this would also work with named initialization in some cases, which is somewhat elegant - except that the user must still declare the arguments in-order for the initializer to work. Commit for that is here: 3ec2214

Not sure I like either of those especially, so perhaps methods-that-must-be-called-before-begin is the way to go.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it's been a while since I wrote this library, chunks of it are a bit fuzzy in my memory, and I haven't taken the time to deeply refresh myself since you started pouring all your effort into it.

But I can say this much: I chose to use an Enum rather than multiple .set...() or .enable()/.disable() methods purely because it looked "prettier" to me (and I thought it would make it easy to use). You're obviously more skilled in C++ than me and see a opportunities to optimize, so I certainly appreciate that.

...perhaps methods-that-must-be-called-before-begin is the way to go

After thinking through this a bit, I would agree with this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the issue is that there are mixing concerns - the encoder and the button are two separate devices (albeit in one physical package). Ditto with the VCC pin: I personally would've made that a concern for the user to take care of externally to the library.

But it's all personal preference and there is no standard for any of this (that I'm aware of). And it's your library, so you get to choose :)

I've pushed a commit to this PR that provides individual methods.


#if defined( ESP32 )
typedef std::function<void(long)> EncoderCallback;
Expand All @@ -38,9 +35,6 @@ class RotaryEncoder {
typedef void (*ButtonCallback)(unsigned long);
#endif


public:

/**
* @brief Construct a new Rotary Encoder instance
*
Expand All @@ -60,20 +54,34 @@ class RotaryEncoder {

/**
* @brief Responsible for detaching interrupts and clearing the loop timer
*
*/
~RotaryEncoder();

/**
* @brief Specifies whether the encoder pins need to use the internal pull-up resistors.
* @brief Enable the internal pull-up resistor for the encoder pin.
*
* @note Call this in `setup()`.
* By default, the encoder pin on the ESP32 is floating - which requires that
* the encoder module has its own pull-up resistors or external pull-ups are used.
*
* @param type FLOATING if you're using a raw encoder not mounted to a PCB (internal pull-ups will be used);
* HAS_PULLUP if your encoder is a module that has pull-up resistors, (internal pull-ups will not be used);
* SW_FLOAT your encoder is a module that has pull-up resistors, but the resistor for the switch is missing (internal pull-up will be used for switch input only)
* If the encoder module does not have its own pull-ups, calling this method
* will enable the internal pull-up in the ESP32.
*
* @note Call this before `begin()`. It has no effect after.
*/
void setEncoderType( EncoderType type );
void enableEncoderPinPullup() { encoderPinMode = INPUT_PULLUP; }

/**
* @brief Enable the internal pull-up resistor for the button pin.
*
* By default, the button pin on the ESP32 is floating - which requires that
* the button has its own pull-up resistors or external pull-ups are used.
*
* If the button does not have its own pull-ups, calling this method
* will enable the internal pull-up in the ESP32.
*
* @note Call this before `begin()`. It has no effect after.
*/
void enableButtonPinPullup() { buttonPinMode = INPUT_PULLUP; }

/**
* @brief Set the minimum and maximum values that the encoder will return.
Expand Down Expand Up @@ -147,21 +155,18 @@ class RotaryEncoder {
* @brief Sets up the GPIO pins specified in the constructor and attaches the ISR callback for the encoder.
*
* @note Call this in `setup()` after other "set" methods.
*
*/
void begin( bool useTimer = true );

/**
* @brief Enables the encoder knob and pushbutton if `disable()` was previously used.
*
*/
void enable();

/**
* @brief Disables the encoder knob and pushbutton.
*
* Knob rotation and button presses will have no effect until after `enable()` is called
*
*/
void disable();

Expand Down Expand Up @@ -214,27 +219,12 @@ class RotaryEncoder {
* @note This will try to set the value to 0, but if the minimum and maximum configured
* by `setBoundaries()` does not include 0, then the minimum or maximum will be
* used instead
*
*/
void resetEncoderValue() { setEncoderValue( 0 ); }

/**
* @brief Synchronizes the encoder value and button state from ISRs.
*
* Runs on a timer and calls `encoderChanged()` and `buttonPressed()` to determine
* if user-specified callbacks should be run.
*
* This would normally be called in userspace `loop()`, but we're using the `loopTimer` instead.
*
*/
void loop();

private:
const char *LOG_TAG = "ESP32RotaryEncoder";

EncoderCallback callbackEncoderChanged = NULL;
ButtonCallback callbackButtonPressed = NULL;

typedef enum {
LEFT = -1,
STILL = 0,
Expand All @@ -248,6 +238,11 @@ class RotaryEncoder {
STILL, RIGHT, LEFT, STILL
};

mutable portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

EncoderCallback callbackEncoderChanged = NULL;
ButtonCallback callbackButtonPressed = NULL;

int encoderPinMode = INPUT;
int buttonPinMode = INPUT;

Expand Down Expand Up @@ -279,6 +274,8 @@ class RotaryEncoder {

esp_timer_handle_t loopTimer;
void beginLoopTimer();
static void timerCallback( void *self );
void loop();

void attachInterrupts();
void detachInterrupts();
Expand Down