Servo millis() code with "ease out" speed control

The code at the bottom, section (C), rotates a servo to a target angle based on a user's serial input. It does so using millis() and the rotation is constant - the servo starts to rotate immediately, rotates fast or slow depending on a chosen value, and then comes to an abrupt halt.

I am not rotating a heavy camera or telescope, just a thin wooden pointer affixed to the servo horn.

I found Robert Penner's old GitHub archive that shows the below C++ implementation:

Quadratic ease out

float Quad::easeOut(float t,float b , float c, float d)
{
  return -c *(t/=d)*(t-2) + b;
}

Cubic ease out

float Cubic::easeOut(float t,float b , float c, float d)
{
  return c*((t=t/d-1)*t*t + 1) + b;
}

Then I found a rather more explanatory site, relating the above to JavaScript, claiming one can just as well forgo factors b and c to arrive at:

Cubic ease out

function cubicEaseOut(t, d)
{
  return 1 - Math.pow(1 - (t / d), 3);
}

CURVE_CUBIC_OUT.PNG

Still, how would I have to change the code below in part (C) to obtain "ease out" speed control, so the servo starts to rotate immediately but eventually comes to a "slow" or "smooth" or "very gentle" halt?

#include <Servo.h>

// Maximum rotation of 140° measured with protractor yielded 610ms = 0°
// 2090ms = 140°. Difference of 1480ms / 140° = 10.57ms per 1° rotation
const int msOffset = 610; // The servo's 0° rotation angle (microseconds)
const byte msAngleOneDegree = 10; // A 1° rotation (microseconds)
int msAnglePrevious;
int msAngleCurrent;
int msAngleTarget;

unsigned long timeNowServo = 0; // Timestamp that updates with each loop iteration (from Metro Mini's clock)
const byte timeServoInterval = 7; // Duration until next servo rotation

char inputBuffer[16]; // Store input from serial port
byte angleInput; // Store converted input (degrees)

Servo ovres;

void setup()
{
  Serial.begin(115200);
  randomSeed(analogRead(0));
  ovres.attach(3);
  ovres.write(msOffset);
  msAnglePrevious = 610; // Seed with initial value to start
  msAngleCurrent = 610; // Seed with initial value to start
}

void loop()
{
  while (Serial.available() > 0)
  {
    Serial.readBytes(inputBuffer, sizeof(inputBuffer)); // Read from serial port into buffer
    angleInput = atoi(inputBuffer); // Convert input string to a number
    memset(inputBuffer, 0, sizeof(inputBuffer)); // Clears the buffer
  }
  rotateServo(angleInput);
}

void rotateServo(byte Angle)
{
  /// (A) /// Basic rotate - but always at servo's max. speed
  /*msAngleTarget = Angle * msAngleOneDegree + msOffset;

    ovres.write(msAngleTarget);
    delay(10);*/


  /// (B) /// Basic rotate with for-loop and delay as speed control - but is blocking
  /*msAngleTarget = Angle * msAngleOneDegree + msOffset;

    if (msAngleTarget > msAnglePrevious)
    {
    for (int ms = msAnglePrevious; ms <= msAngleTarget; ms += msAngleOneDegree * 7)
    {
      ovres.write(ms);
      delay(10);
    }
    }
    if (msAngleTarget < msAnglePrevious)
    {
    for (int ms = msAnglePrevious; ms >= msAngleTarget; ms -= msAngleOneDegree * 3)
    {
      ovres.write(ms);
      delay(10);
    }
    }
    msAnglePrevious = msAngleTarget; // Store target as previous for next rotation*/


  /// (C) /// Basic rotate with millis() - but does not "ease out" towards target angle
  msAngleTarget = Angle * msAngleOneDegree + msOffset;
  if (millis() - timeNowServo >= timeServoInterval)
  {
    timeNowServo = millis();
    if (msAngleCurrent != msAngleTarget) // Don't write anything to the servo if target angle is reached
    {
      if (msAngleCurrent <= msAngleTarget)
      {
        msAngleCurrent += msAngleOneDegree * 4; // < slow or fast, the rotation is constant
        ovres.write(msAngleCurrent);
      }
      else
      {
        if (msAngleCurrent >= msAngleTarget)
        {
          msAngleCurrent -= msAngleOneDegree * 2; // < slow or fast, the rotation is constant
          ovres.write(msAngleCurrent);
        }
      }
    }
  }
}

CURVE_CUBIC_OUT.PNG

One way is presumably to use your "easing" algorithm to continually modify timeServoInterval so that there is more time between moves as the servo approaches the target.

The other way is to modify the size of the moves so they are smaller as you approach the target. That's likely to be smoother but may be a bit more complicated to implement.

I'd try it both ways and see which does what you're looking for.

BTW your code would be easier to make sense of if you used the writeMicroseconds() command instead of just relying on write(angle) defaulting to writeMicroseconds() when the parameter you pass in is outside the valid range for an angle.

Steve

Thanks, appreciated; timeServoInterval is currently 7 milliseconds (the millis() ticker), so if that starts at 3 and ends with 11... I hardcoded these two values and at 11 milliseconds, the rotation looks quite choppy.

Maybe modulating the microseconds (10.57 per 1°) yields "more meat on the bone" to work with? Or maybe both? To me it seems which of the two can be chopped into finer slices. I don't really want to run for a library with this, because then I don't learn anything.

I should change all to "writeMicroseconds()" indeed.

Maybe some kind of high-pass filter, like with an EMA, would approximate easing out...

float EMA_a = 0.3; // EMA alpha
int EMA_S = 0; // EMA S
...
EMA_S = (EMA_a*msAngle) + ((1-EMA_a)*EMA_S);
msAngleFiltered = msAngle - EMA_S;

Well, several ideas to try out now.

I am confused.
Is this a continuous rotation servo, or are you simply trying to make a normal servo smoother?
If your controlling it via millis, why do I see “delay(10)”’s in your code?

Never mind, you put all those details in a different post...

Slumpert:
I am confused.
Is this a continuous rotation servo, or are you simply trying to make a normal servo smoother?
If your controlling it via millis, why do I see “delay(10)”’s in your code?

It's a regular servo, see the link to the product. The delay is only for (A) and (B) when uncommented. I'm using the millis based version (C). I just left all three methods to rotate a servo, from super basic to timed with millis, in one sketch for my convenience. The rotation via millis and microsecond is nicely smooth enough; it's really all about the easing out (deceleration as pointer approaches target), see the image in post #1.