ajfisher / node-pixel

Library for using addressable LEDs (such as NeoPixels/WS2812) with Firmata and JohnnyFive
MIT License
275 stars 71 forks source link

Refresh performance when updating individual pixels vs the strip #4

Closed timmeador closed 9 years ago

timmeador commented 9 years ago

Have you noticed any performance issues with issuing show() commands for individual pixels? I am getting weird output on a 60led neo-pixel strip when I try to update individual pixels. It takes several seconds for the leds to update. When I update the entire strip then it works perfectly. The only work around is to slow down the refresh timer from 30ms to 100ms.

ajfisher commented 9 years ago

You may be running into some message conflict issues.

If you're updating a specific pixel then calling a show then updating another one and calling show then you have a few congestion points.

The first is you're sending 2 message packages for each pixel and also there is inherent latency in the show that is determined by the number of pixels (more pixels, slower refresh).

Can you post your test code as I'll have access to several hundred NPs again in a couple of days and I can test it (traveling at the moment).

On Fri, 6 Feb 2015 02:53 Tim Meador notifications@github.com wrote:

Have you noticed any performance issues with issuing show() commands for individual pixels? I am getting weird output on a 60led neo-pixel strip when I try to update individual pixels. It takes several seconds for the leds to update. When I update the entire strip then it works perfectly. The only work around is to slow down the refresh timer from 30ms to 100ms.

— Reply to this email directly or view it on GitHub https://github.com/ajfisher/node-pixel/issues/4.

fruityfred commented 9 years ago

Hi, I'm also having performance issues with 121 LEDs (11x11). For each frame, I set all the pixels (with strip.pixel(x+y*11).color(myColor)) and then I call the strip.show() method. Am I wrong? Thank you for your answer!

ajfisher commented 9 years ago

Can you do me a couple of things:

  1. Give me a quick test snippet of code I can run so I can get something up and going when I get home tonight.
  2. Provide some more detail around "performance issues".

Generally speaking if you're setting the whole strip to one colour you shouldn't really run into any update issues. Having said that, with the NeoPixels the length of the strand is inversely proportional to your frame rate - remember that when you push data you have to "ripple" it down the line of pixels so the more you have the longer it takes to propagate the message.

So if you can get me some more details I can take a look for you.

fruityfred commented 9 years ago

Hi, thank you for your quick answer!

Here is an example with 121 LEDs (based on the example provided in the lib). My LEDs are assembled in a matrix (11x11) and the host computer is a Raspberry Pi (model B, revision 2).

var pixel = require("./node_modules/node-pixel/lib/pixel.js");
var five = require("johnny-five");

var board = new five.Board({repl:false});

var strip = null;
var fps = 30;

function colorWheel (pos) {
    pos %= 256;
    pos = 255 - pos;
    if (pos < 85) { return 'rgb('+(255 - pos * 3)+',0,'+(pos * 3)+')'; }
    else if (pos < 170) { pos -= 85; return 'rgb(0,' + (pos * 3) + ',' + (255 - pos * 3) + ')'; }
    else { pos -= 170; return 'rgb(' + (pos * 3) + ',' + (255 - pos * 3) + ',0)'; }
};

board.on("ready", function()
{
    strip = new pixel.Strip({
        data: 6,
        length: 121,
        board: this
    });

    var pos = 0;

    var blinker = setInterval(function()
    {
        console.time('frame');

        strip.color("#000"); // blanks it out

        for (var i=0 ; i<strip.stripLength() ; i++) {
            strip.pixel(i).color(colorWheel(i*2+pos));
        }
        pos++;
        if (pos == 256) pos = 0;

        strip.show();
        console.timeEnd('frame');

    }, 1000/fps);
});

With this in place, each frame takes around 260ms (so 4 frames per second) which is very very slow. I mean, too slow for my project.

I thought it was due to the baud rate of the serial communication. 121 RGB LED → 363 bytes → 2904 bits. With a baud rate of 57600, we should be able to reach 19 frames per second, but I can imagine that it's not so simple ;)

Before this, I was driving the LEDs directly from the Raspberry Pi, with the ws281x lib: it uses a hardware trick to update the WS281x at 800 KHz without being bothered by the OS (Linux/Raspian). But, while using this hardware trick (based on DMA and hardware PWM), it's impossible to play audio (since the DMA buffer is the same, I think). So I searched for another solution, and yours is really interesting!

Thank you by advance for your answer!

ajfisher commented 9 years ago

Okay so there's potentially a lot of factors at play here.

First thing to note is that DMA on an RPI is always going to be faster, the ability of the chip to just bang pulses at near to microsecond timing is great. Likewise an arduino can do this too - but it's much more memory constrained. In this instance though you're not talking directly to the pins that are banging the pulses up and down for the WS2812s.

The pipeline looks like this:

JS code -> NodeJS VM -> Serial Port buffer (C) -> RPI Serial UART -> WIRE -> ATMEGA Serial UART -> Arduino / Wiring Serial Library (C) -> Firmata interpreter -> PIXEL Interpreter -> Digital Pulse on a pin (tight loop in assembly).

As you can see there are a LOT of points between your code and actually making the pixels turn on and off.

Yes the baud rate in theory is quite high but let me break this down a bit more to show you what's going on.

Because you're doing the rainbow you're not actually sending a single message for the whole strip you're sending 121 Firmata messages for each frame followed by a "show" which is the instruction to latch the strip and start sending data down to the pixels.

Specifically in firmata you're sending a SYSEX message and this one in particular:

https://github.com/ajfisher/node-pixel/blob/master/firmware/libs/protocol.md#pixel_set_pixel

here it is broken out - each line is ONE byte.

0   START_SYSEX         0xF0
1   PIXEL_COMMAND       0x51
2   PIXEL_SET_STRIP     0x03
3   max 14 bit index of pixel in strip LSB
4   max 14 bit index of pixel in strip MSB
5   24 bit packed RGB color value LSB
6   24 bit packed RGB color value lower middle bits
7   24 bit packed RGB color value upper middle bits
8   24 bit packed RGB color value MSB
9   END_SYSEX           0xF7

One thing you have to note with firmata is because it's based on MIDI it uses 7 bit bytes, this means you have to send a 24 bit value using 4 bytes instead of three.

So basically to send a single colour value you're sending 10 bytes PER pixel.

121 x 10 bytes = 1210 bytes = 9680 bps (baud is BITs remember).

Assuming you could actually max out the serial connection (you can't - there's other messages going back and forth on firmata - things like acks on messages, buffer purges etc - though the reality is it's quite low volume so you can get close) then you're basically looking at:

57600 / 9680 = 5.9 frames per second (theoretical maximum).

And that's really before we get into the efficiency of firmata processing all of those messages, not hitting buffer limits (not much memory) and what not. Let's say that at best I estimate you can hit about 4.5 - 5 fps with this many messages being processed per frame. How does that stack up compared to what you're actually seeing?

So what can you do to improve this??

If you're trying to do high framerate (high is anything more than 10fps) with a big strip (big is anything beyond about 40) then these are the wrong tools for the job. Have a look at a teensy using a library called fadecandy. The stuff peeps are doing with this - especially scanlime (Micah Scott) is mind blowing.

The other option you have is to build out a light "engine". You script light events on the arduino and then potentially use firmata or some other protocol "trigger" them. I have some work on that here: https://github.com/ajfisher/led-control (though it's going in for a big refactor at the moment once I get some time).

The next option you have is just change the pixels you need to change. The colour wheel you're using with 121 pixels will have a crazy amount of gradation between the colours that you simply won't be able to see (consider tones of orange and tones of cyan for example). In this sort of arrangement and with bright light, your eyes just can't pick out the differences. An old trick is to make the the gradations more abrupt so you're actually partitioning your 121 pixels into ranges of similarly coloured ones.

Once you do that then all you need to do is keep track of the ones that need to "change" and then send messages for those - that might only be 8-10 pixels on any given frame depending on your partitioning so then you're back inside the territory of things moving pretty quick.

With good partitioning I can get things like 10 pixels moving around at about 60fps so this is definitely doable.

What's your project seeking to do and I can probably point you in an appropriate direction? I've done quite a bit of work with these little pixels over the last few years.

fruityfred commented 9 years ago

Thank you very much for your quick and very helpful answer!

For the moment, I will discard sound management in my project and keep on using the rpi-ws281x library to drive my LEDs. I really need a quick refresh time and my "LED screen" can display many different things in my project.

Maybe I'll search for RPi "sound cards", but there is a risk that the DMA buffer is the same, no matter which sound card is connected... Or maybe should I have another RPi (or an Arduino) dedicated to playing sounds, and my master RPi can just send a short message containing which sound to play?

I'll keep you tuned as soon as I find a solution ;-)

Again, thank you very much for your help!

ajfisher commented 9 years ago

Have a look at dedicated driver boards for your LEDs. At the end of the day if you have a lot of pixels which are each 24 bits, you very quickly run into frame rate issues. This is exacerbated when you think about the chips you're using to drive them (eg an Arduino) which don't have a lot of memory, isn't real time or have slow processors (compared to modern computers or GPUs).

If I was doing something like this I'd look at separating concerns in a way that allows you to "trigger" actions on the LEDs through an LED driver rather than sending the data for every single pixel over the wire every time. Things like a fadecandy / teensy board is great at driving hundreds and hundreds of pixels but the way to do it is to have trigger messages being passed across that are starting sequences which then run autonomously.

An example of this is a game I built that allowed people to interact with a light display interactively. Many people could change a light at the same time and it's natural interaction was to fade away back to black over a defined time. If you touched it, your colour would be added to whatever the current state was and then it would start fading out again.

In order to maintain the frame rate under high interactivity levels only additive messages are sent to the arduino which then managed the actual calculations of the additions, fading etc. This made it extremely responsive. The pic above was a prototype I built (couldn't find the big version of this) but the big version had a couple of hundred LEDs in it and sustained 30 people playing with it all at the same time with a high framerate (system was managed to be between 50-60 fps) and about 30-40 messages per second.

So it's definitely doable depending on where you push processing tasks to (and you manage the messaging between them)