Generic position sensor class - New sensor easier to support

Hey everyone,

Recently we have added a new feature to the dev branch. Further simplified support for the new position sensors.
Instead of the writing the new sensor class as described in docs, all you need to do now is to implement one function in you Arduino sketch.

The process is very simple, in your arduino sketch you define the functions that read your sensor (and intialise it optionally), for example:

float readMySensor(){
 // read my sensor
 // return the angle value in radians in between 0 and 2PI
 return ...;
}
void initMySensor(){
  // do the init
}

And then just provide them to the GenericSensor class constructor.

// Provide the callbacks in the constructor
GenericSensor sensor = GenericSensor(readMySensor, initMySensor);

or if you prefer:

// empty constructor 
GenericSensor sensor = GenericSensor();

void setup(){
...
  // assign the callbacks directly
  sensor.readCallback = readMySensor;
  sensor.initCallback = initMySensor;
  sensor.init();
....
}

This way of implementing the new sensors will, we hope, enable you guys to more efficiently and easily integrate your own codes into the simplefoc and use other open-source components that inside simplefoc.

You can find an example code in the library examples:

2 Likes

To show off the posibilities of this class, I wanted to show you the example of the functionality that has not been supported in the simplefoc so far but can be realtively easily found online. Hardware counter based encoder support for the ESP32 boards.

I’ve decided to use the awesome library:

You can download it even using the arduino library manager.

Here is the full code of the voltage control using the ESP32 R32 D1 board with AMT103 encoder, simplefoc shield and 11 pole pair gimbal motor.


#include <SimpleFOC.h>
#include <ESP32Encoder.h>


// create the ESP32Encoder class
ESP32Encoder encoder;
// define the sensor cpr (500x4)
int64_t cpr = 2000;
// function intialising the sensor
void initSensor(){
  // use pin 25 and 26 (Arduino pins 2,3) for the encoder
  encoder.attachFullQuad(25, 26);
}
// function reading the encoder 
float readSensor(){
  // return the value in between 0 - 2PI
  float a = ((float)(encoder.getCount()%cpr)*_2PI/((float)cpr));
  return a > 0 ? a : a + _2PI;
}
// create the generic sensor
GenericSensor sensor = GenericSensor(readSensor, initSensor);


// BLDC motor & driver instance
BLDCMotor motor = BLDCMotor(11);
BLDCDriver3PWM driver = BLDCDriver3PWM(16, 27, 5, 12); // (Arduino pins 5,6,10,8)


// commander communication instance
Commander command = Commander(Serial);
void doMotor(char* cmd){ command.motor(&motor, cmd); }

void setup() {

  // initialize sensor hardware
  sensor.init();
  // link the motor to the sensor
  motor.linkSensor(&sensor);

  // driver config
  // power supply voltage [V]
  driver.voltage_power_supply = 12;
  driver.init();
  // link driver
  motor.linkDriver(&driver);

  // set control loop type to be used
  motor.controller = MotionControlType::torque;

  // use monitoring with serial for motor init
  // monitoring port
  Serial.begin(115200);
  // comment out if not needed
  motor.useMonitoring(Serial);
  motor.monitor_downsample = 0; // disable initially

  // initialise motor
  motor.init();
  // align encoder and start FOC
  motor.initFOC();

  // set the inital target value
  motor.target = 2;

  // subscribe motor to the commander
  command.add('M', doMotor, "motor");

  // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com)
  Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V."));

  _delay(1000);
}


void loop() {
  // iterative setting FOC phase voltage
  motor.loopFOC();

  // iterative function setting the outter loop target
  motor.move();

  // motor monitoring
  motor.monitor();

  // user communication
  command.run();
}

I’m looking forward to hear what you guys think! :smiley:

Did you mean motor.linkSensor(&sensor) ?

1 Like

About the interface design:

Having both initSensor()and sensor.init() sounds confusing.

On a more general tone, mixing object-oriented design with functional design usually leads to a confusing interface.

I only had a quick look, but I would stick to OO-design and do the following:

  1. I’d provide an abstract class GenericSensor with a pure virtual method read():
class GenericSensor {
   virtual float read() = 0;
}
  1. I’d ask users to instantiate the abstract class:
#include <SimpleFOC.h>
#include <ESP32Encoder.h>

class MySensor : public ESP32Encoder, public GenericSensor {
   private:
      int64_t m_cpr;
   public:
      void init(int64_t cpr) {
         m_cpr = cpr;
        // use pin 22 and 23 (Arduino pins 2,3) for the encoder
        attachFullQuad(22, 23);
      }
      virtual float read() {
        // return the value in between 0 - 2PI
        float a = ((float)(getCount()%cpr)*_2PI/((float)cpr));
        return a > 0 ? a : a + _2PI;
      } 
};

// create the generic sensor
MySensor sensor;

void setup() {

  // link the motor to the sensor
  sensor.init(2000);
  motor.linkSensor(&sensor);

   ...
}

Notice that, as an interface provider, you don’t want to bother with sensor init. It’s the responsibility of the user to provide you with a fully functional sensor object. In the above example, the user does sensor init using a MySensor::init() method, but it’s his choice. For example, maybe he doesn’t want to init anything:

class MyTestSensor : public GenericSensor {
   public:
      virtual float read() {
         return 0.666f;
      } 
};
1 Like

Hey @quentin , thanks for these insightful and very valid comments.

Nonetheless I’d like to justify this choice a little bit:

I actually agree with you on the OO interface, and the refactoring of the sensor classes I did this summer actually provides this, you subclass Sensor and implement the getSensorAngle() method, and done! Maybe I should have called it read(), but in essence its the same.

The drivers I’ve written in the drivers repo all follow this pattern. I generally use PlatformIO and I’m writing C/C++.
But for a beginner user, or someone coming from the mech eng world with little connection to programming, they see a different view of the world. They use ArduinoIDE, and are working in an .ino file. ArduinoIDE makes it very easy for them to pull in a library, but not to work with multiple .cpp and .h files, it doesn’t really have that philosophy.

So for such a user, it is actually much easier to write a callback that connects a library, all in the same .ino file as main() and setup(), than it is to create a new class. The required knowledge on the syntax of C++ is also much less.

That’s because the Sensor class does have its internal fields to initialise, and so in this construct you have to call both Sensor.init() and provide the initSensor() method. A bit confusing due to the naming maybe.

Lets see how this goes. Since the refactoring of the Sensor API a few people have asked questions about new sensors, and I’ve even seen one attempt with the OO approach. Lets see if this approach leads to more people being able to use new sensors independently (and hence less hardware specific code to maintain in the library).

I don’t think multiple .cpp and .h files are required with my suggestion. My code above is just a replacement for the code that was initially proposed (in the same file).

You might be right, I have no knowledge in this area. Sounds like a slippery slope, though.

Hey @quentin,

Thanks for sharing your opinions, I really appreciate it.

Regarding the object oriented (OO) structure. SimpleFOC has never been a true cpp OO library and the APIs that we provide are not always 100% OO. The reason whay is because it was built for simplicity, not for the structure. It is written to be understandable for the beginner user, and it assumes that the expert user will be able to customize it an extend it to their needs, because they have the knowledge.

The API you want to argue that we can use is already implemented in the library. We already have the abstract class (Sensor class) that only needs one function to be implemented, and we have it for a long time as Richard already mentioned. As you already showed in your code, the same can be done using the Sensor class and in terms of cpp is very easy to do.
However the cpp interface is not the most intuitive for most people. The experts in the motion control domain are not experts in the cpp domain. That is the reason why we have so much docs and so much examples :smiley:

This addition to the library is not directly OO compliant but, I’d argue that is very intuitive to the end user, because it only has to think about one function to implement. Similar approaches are very common in the Arduino world and make sense when you think about classes not just as cpp objects but also the physical hardware objects.

I’d also like to draw a parallel here, similar issue can be raised form the point of view of the motion control community. If our library’s goal is to provide the motion control functionalities, why are we using the OO paradigm at all?
It is only taking space on the disk, it is longer to execute and optimize, and at the end you also need to implement a lot of stuff in your classes that you don’t really need in every application.
So the response in our case, is as for your question, is that it is easier for the non-expert user to use and to understand. At least that is what we are hoping for. :smiley:

Anyway, it is a trial in the dev branch and if it doesn’t work it doesn’t work. But at least for me, it is easier to use than to create a new cpp class, I am also not a cpp expert in any way. :grin:

Sorry for triggering an OO debate, it was not my intention. In web development, where I come from, OO is somewhat despised (the hype is on purely functional programming), so it’s quite funny you took me for a OO purist! :slight_smile:

So please forget everything I said and keep only this: initSensor() and sensor.init() sound confusing.

Please, don’t be sorry! It is an interesting debate, and all of us here at SimpleFOC really like to hear feedback from the software’s users, and see real-world use cases.

In the end I think this kind of question doesn’t have one perfect answer, so it is really important to hear back what people think.

Hey @quentin,

It is an important discussion and please do not be discouraged to raise these topics again. It makes us rethink our choices and this is always a good thing. :smiley:

The function names initSensor and readSensor are not part of the API. The user is free to name them as he likes. It only has to provide the function pointer to the class constructor. Also, the user is not obliged to implement the custom init sequence.

But you are right that the naming ambiguities, maybe it would be a good idea to call this class differently. Not the GenericSensor because we already have a generic Sensor class, maybe call it a QuickSensor or SimpleSensor or something like that to avoid confusion. :smiley:

Or maybe CallbackSensor ?

1 Like