rwaldron / johnny-five

JavaScript Robotics and IoT programming framework, developed at Bocoup.
http://johnny-five.io
Other
13.26k stars 1.76k forks source link

2+ servos can't handle .to method frequently #1340

Closed under24 closed 6 years ago

under24 commented 7 years ago

I have a nodejs server and 3 servos.

I init the servos like this:

var servo1 = new five.Servo({
  address: 0x40,
  controller: "PCA9685",
  pin: 8,
});
...

I push values via socketio from front-end to the back-end where I set the values like this:

.on('change-angle', function(data) {
    servo1.to(data.value);
    servo2.to(data.value);
    servo3.to(data.value);
})

My problem is that the servos get laggy and unresponsive (in other words they begin to pile up the queue). The issue can be observed only with 2+ servos. If I use it with 1 servo there's no delay and it's perfectly responsive no matter how frequently I push new data from the front-end.

P.S. I also tried like this and it doesn't help:

servo1.stop();
servo1.to(data.value);
servo2.stop();
servo2.to(data.value);
servo3.stop();
servo3.to(data.value);
dtex commented 7 years ago

Hi @under24

Can you tell us more about your project? What environment is your host server on? Can you share the complete code? A video of the behavior could also be really helpful.

I'm sure you should be able to control that many servos. I have a project that uses 18 servos and each updates about 60 times per second (with a whole ton of other stuff going on). I will admit that I haven't done it with a PCA9685 so maybe this is an issue with I2C communications.

under24 commented 7 years ago

Hello @dtex.

Here's a video: https://youtu.be/TJq6XeNXwZY Listen to the servos making noise. With 1 servo the update rate is perfect and as soon as I add one more servo it starts lagging horribly.

dtex commented 7 years ago

I've got the code running, but need to run to the lab an pick up a couple of servos so I can test. Assuming I get the same behavior the first thing I'm going to try is bypassing the PCA9685 and see if the behavior still occurs (just hooking the servos up to pins on the Arduino).

under24 commented 7 years ago

@dtex do you have any news on it?

dtex commented 7 years ago

Sorry, no I haven't been able to get to the servos but I will be at the lab tomorrow.

under24 commented 7 years ago

@dtex actually, I've already found a solution for it. I2C seems to be unable to transfer data faster than 10 ms which creates this bottleneck. Anything < 10ms will lag horribly. The solution is very simple, you just throttle it down like this:

    let prevDate = 0;

    const setValue = (value) => {
      a.to(value);
      b.to(value);
      c.to(value);
    }

    client.on('angle-changed', function(data) {
      if (+new Date() - prevDate >= 15) { // i use 15 ms just to let it breathe in between 
        setValue(data.value);
        prevDate = +new Date();
      }
    });
dtex commented 7 years ago

Oh wow, that's really interesting. I had a project where I was seeing the same kind of servo jankiness on a Tessel 2 with a PCA9685. I had assumed that the T2 just couldn't handle the IK calculations and that was my limiting factor, but I was updating 18 servos and each one was updated with a separate I2C message...

This changes my assumption about what the problem was.

I propose that we rename and reopen this issue.

under24 commented 7 years ago

@dtex What do i rename it to? The problem seems to be in the low computing power of the controllers or in the transfer speed of the I2C protocol. I am not really sure in any of this

Here's my solution for this kind of bottleneck with servos:

servo.js.zip I've added a new property for the class Servo: throttle: [Boolean] [default: false]

So you just add it to every servo instance and push the data as frequently as you want and it will automatically be throttled down to 20ms per data batch allowing your expander boards to work perfectly responsive.

servo init example:

  let servo1 = new five.Servo({
    address: 0x40,
    controller: "PCA9685",
    pin: 8,
    invert: true,
    throttle: true // <-- this one
  });

  let servo2 = new five.Servo({
    address: 0x40,
    controller: "PCA9685",
    range: [0, 180],
    pin: 15,
    throttle: true // <-- this one
  });
under24 commented 7 years ago

Hello @dtex.

Could you please review/include my solution to the framework? I think this is pretty neat if you work with sockets and multiple servos. I have the file attached in the prev post.

Or do I make a pull request and do it myself?

dtex commented 7 years ago

I'm not sure this is something that should be fixed in Johnny-Five. It might belong in firmtata.js or firmata itself. To know where requires truly knowing where the bottleneck actually lies.

Are you certain it is a limitation of I2C, the PCA9685, or maybe it's something higher up the chain?

My assumption would be that socket-io is much more of a bottleneck than I2C. Do you have the same problem when the commands are not sent over socket-io?

Can we solve the problem in a better way. For example, should we update the Servos (note the plural) class and add the ability to pipe down multiple values in a single I2C data push instead of a separate push for each servo spaced out over time?

under24 commented 7 years ago

@dtex It's not socket for sure. Because I only changed the back-end and it started working the way it should. Plus I tested the performance and it's blazing fast.

How do we implement it with multiple values? How do we know when to expect multiple values? Do we send a batch and then collect a new batch in a span of some ms? As I understood it's the same idea that I had with my throttler. Can you sketch some pseudo code? Also, can you direct me where I can play with multiple values in one I2C data push?

dtex commented 7 years ago

To see if writing multiple values in one push even works I would build a test case that requires only firmata.js and sends an I2C message that writes 64 consecutive register values (16 servos * 4 per servo) at once vs just the 4 we need for a single servo.

This is the part of Johnny-Five that handles communication with the PCA-9685 and you can see here where we are only writing to four register values. You can crib code from here and modify it to support writing for N servos at once (this.io is an instance of firmata.js in your case).

If all that works, we would need to modify Johnny-Five Servos so that it has an awareness of situations where writing multiple servo values at once makes sense. I could even see adding a command to the firmata protocol to support this for non-I2C connected servos. We could modify Servos.to so it would handle this new message style and not just delegate to Servo.to.

This is not a trivial investigation so I totally understand if you don't want to take it on. I'm interested in getting to the bottom of it for my own projects, but I can't imagine that I will get around to it anytime soon.

Practical Alternatives: A) Are you only controlling two servos? Ditch the PCA-9685 and just wire directly to the Arduino. Like I said earlier, I've done this with 18 servos at a time (on a mega) with great results. B) Monkey-patch J5 with your fix C) Create a wrapper for Servo that throttles write speeds

dtex commented 6 years ago

Hi @under24 ,

One of the things that came out of this discussion was the realization that it would be helpful to send multiple servo writes in a single i2c message. It's one of those features that will require a fair amount of work and we just haven't gotten to it yet. Rather than leave this languishing as an open issue we have created a Requested Features page and added the request for bulk servo writes there.