Best way to emulate a rotary encoder wheel?

Got it, thank you both so much!

2 Likes

I thought it would be simple to add mapping to the extended class, but it turns out there are some very odd corner cases on counting ticks on encoder wheels --in particular encoders with either two tick marks or one tick mark on the full circumference. Small mouse movements that linger around the zero rollover are causing the encoder to drop or add ticks, so I need to take another look at the logic or whether those need to be special-cased.

3 Likes

I got inspired by this thread, so thanks for that!

@jeremydouglass, hope you don’t take this “copycat” the wrong way :stuck_out_tongue: I rolled my own class though, even if it was initially based on yours (if only to remind me of syntax and using PVectors). But it’s a bit messy and not as elegant. Could probably (definitively!) be improved.

I figured I’d base it on how a real encoder works, with quadruature encoding. There’s probably better methods, but I wanted to test it.

encoder wheel

One difference is that I count each “quadrature step”, so it will be 4 counts pr. step. For a hardware solution, one step is plenty (and also often detented. Well I guess that’s also read by software in some microcontroller). Anyway in this case it would be a bit slow or error prone if increasing nr. of steps.

Added mouse scroll wheel for fun (bit primitive, and might break if you use the scroll wheel other places in the sketch). There’s probably a better way of doing that, but I just don’t know how atm. Which kind of made the whole quadrature thing moot. It’s an alternative at least.

Depending on the size of the encoder, display resolution, computer speed etc, don’t overdo the nr. of steps (which is multiplied by 4 because of the quadrature encoding), or else it will be easy to skip a few and mess up the sequence, especially with fast mouse movements. Suggest keeping nr. of steps in low-mid single-digit range.

Demo sketch (looks somewhat based on jeremydouglas’s dials):

Click to (un)expand
/*  Rotary Encoder demo

  Based on HW rotary encoder mechanics (quadrature encoding)
  
  Class method getDirection() returns -1, 0, or 1. Erased when read.

  2020.08.21 raron
*/

HwRotaryEncoder enc[];
int encoders = 5;

int [] counter = new int[encoders];

float scrollWheel = 0.0;


void setup()
{
  size(700, 200);
  //smooth(4);
  surface.setTitle( "HW based rotary encoder demo" );
  ellipseMode(RADIUS);
  
  enc = new HwRotaryEncoder[encoders];

  for (int i=0; i<encoders; i++) {
    enc[i] = new HwRotaryEncoder(115*(encoders-i-1)+115, 80, 39, i+1);
    counter[i] = 0;
  }
}


void draw()
{
  background(192);
  fill(0);

  for (int i=0; i<encoders; i++) {
    counter[i] += enc[i].getDirection();
    text(counter[i], 115*(encoders-i-1)+112, 150);
    enc[i].update();
    enc[i].display();
  }
}



void mouseWheel(MouseEvent event)
{
  scrollWheel = event.getCount();
}



class HwRotaryEncoder
{
  PVector center;
  PVector startDragPos;
  PVector currentPos;
  float encSize, slice;
  float rotation, oldRotation;
  int quadsteps, quadIndex, oldQuadIndex;
  int direction;
  boolean encA, encB, oldEncA, oldEncB;
  boolean active, mouseButtonWasPressed;
  color knobColor;

  HwRotaryEncoder(float w, float h, float encSize, int stepsPerRev)
  {
    this.center       = new PVector(w, h);
    this.startDragPos = new PVector(0, w);
    this.currentPos   = new PVector(0, w);
    this.encSize      = encSize;
    this.quadsteps    = stepsPerRev*4; // internal counts per step (quadrature encoder)
    this.slice        = TWO_PI/this.quadsteps;
    this.direction    = 0;
    this.rotation     = 0;
    this.oldRotation  = 0;
    this.oldEncA      = false;
    this.oldEncB      = false;
    this.active       = false;
  }

  boolean mouseInside()
  {
    if(currentPos.mag()<encSize) return true;
    return false;
  }
  
  void update()
  {
    currentPos = new PVector(mouseX, mouseY).sub(center);
    if (mousePressed == false) {
      // a mouse button is not pressed
      if (active) {
        active = false;
        oldRotation = rotation;
      }
      mouseButtonWasPressed = false;
    } else {
      // a mouse button is pressed
      if (mouseButtonWasPressed == false) {
        mouseButtonWasPressed = true;
        if (mouseInside()) {
          active = true;
          startDragPos = new PVector(mouseX, mouseY).sub(center);
          oldEncA = false;
          oldEncB = false;
          direction = 0;
        }
      }
    }

    // Simple scrollwheel rotation
    // scrollWheel MIGHT be more than +/- 1.0, but usually not (not taken into account)
    if (mouseInside()) {
      if (scrollWheel == 0.0 && !active) oldRotation = rotation;
      if (scrollWheel != 0.0) {
        rotation += slice * scrollWheel;
        direction = int(scrollWheel);
        if (rotation<0)      rotation += TWO_PI;
        if (rotation>TWO_PI) rotation -= TWO_PI;
        scrollWheel = 0;
      }
    }

    // Mouse drag rotation
    // If moving mouse fast, will probably skip steps or count backwards
    if (active) {
      rotation = currentPos.heading() - startDragPos.heading() + oldRotation;
      if (rotation<0)      rotation += TWO_PI;
      if (rotation>TWO_PI) rotation -= TWO_PI;

      int quadIndex = (int)((float)quadsteps * (rotation/TWO_PI));

      if (oldQuadIndex != quadIndex) {
        // Make rotary encoder quadrature pattern
        int pa = (quadIndex/2)%2;
        int pb = ((quadIndex+1)/2)%2;
        if (pa == 1) encA = true; else encA = false;
        if (pb == 1) encB = true; else encB = false;
        
        // Count when transitioning pattern edges
        if (encB & (encA ^ oldEncA)) {
          if (encA) direction = 1; else direction = -1;
        }
        if (!encB & (encA ^ oldEncA)) {
          if (encA) direction = -1; else direction = 1;
        }
        if (encA & (encB ^ oldEncB)) {
          if (encB) direction = -1; else direction = 1;
        }
        if (!encA & (encB ^ oldEncB)) {
          if (encB) direction = 1; else direction = -1;
        }
        oldEncA = encA;
        oldEncB = encB;
        oldQuadIndex = quadIndex;
      }
    }
  }
  
  int getDirection()
  {
    int temp = direction;
    direction = 0;
    return temp;
  }

  void display()
  {
    if(mouseInside() || active) knobColor = color(210); else knobColor = color(192);
    pushMatrix();
    pushStyle();
    translate(center.x, center.y);
    stroke(0);
    for (int i=0; i<quadsteps; i++) {
      if (i == 0) strokeWeight(3); else strokeWeight(1.5);
      line( encSize*0.9, 0, encSize, 0 );
      rotate(slice);
    }
    fill(knobColor);
    circle(0, 0, encSize*6/8);
    rotate(rotation);
    line( 0, 0, encSize*6/8, 0 );
    popMatrix();
    popStyle();
  }
}

EDIT: Very minor edit, forgot resetting oldEncB (not really important, just a bit of OCD I guess)

2 Likes