Practical Technomancy
IMG_7592.jpeg

Blog

Thinking out loud

Posts tagged development
Sketchbook page: Witch Lights Harness

Trace paper is where I always start

At World Maker Faire last Sunday, an absolute highlight was being able to see Laura Kampf and Sophy Wong talk about design and making.

I sat with Alex Glow, and was totally thrilled with the presentation. I was especially happy when they showed their notebooks, because I, too, had documenting and sharing my sketchbook drilled into me in design school.

So that's why I'm so happy that I was able to find this photo of the most critical sketch for the Witch Lights, the paper copy of which is in one of about 6 boxes in my closet. This is where I started at one end of a roll of trace paper, and just outlined all the chain of components and elements I'd identified so far. That’s where I realized I could create a unified harness and LED structure with 3D printing.

When I started the sketch, I was considering using polypropylene chinese food containers for the junction boxes. They're recyclable, tough, water-tight, and inexpensive. But my experiment with a hole-saw and the conduit fittings had proved frustrating and laborious.

The conduit I had was 3 times the diameter of what I'd thought I was ordering at 2am the other week. But I liked the way I could make it form shapes in mid-air, and so here I was trying to lay out how using it would change the design.

Note on the left there’s an actual fitting for the harness. I had just found a solidworks model of these online, so I knew I could use my Solidworks assembly context skills to very quickly design hardware to fit it.

At the time I was thinking of printing lids for Chinese take-out containers.

In the upper left, you can see I started a marker sketch of a take-out housing, and then sketched a simpler, streamlined housing that allows the diameter of the conduit to say roughly the same all through the chain.

In the lower right: I figured out I could put sensors in the housings too, reducing another complexity

And you can also see the cable glands and 4-pin waterproof cables for the NeoPixels, which I had already figured out at this point.

It's so cool to find a moment where you pulled it all together and made a design decision that worked out. I'm excited about this!

So yeah... this is basically how I draw. It's not the best. But it gets the idea across I hope.

Next up: building an enchanted notebook so I have shots like this of all my in process work. Because I’ve lost another goddamn notebook.

Faeries go both ways

This is a recap of all the work I did over the weekend. In theory, this stuff goes before the last journal entry. But I'm posting it now. For reasons.

DimTrail() function

Last year, we were seriously concerned about the performance of the Arduino while animating 4-20 sprites at a time, and so we pre-rendered as much as possible as a precaution. As it turns out, we're nowhere near taxing the performance of the processor: our bottleneck for performance is FastLED::Show.

So this year, instead of a pre-rendered pixel trail for the faerie sprites, I added a DimTrail() method.

When I move the pattern CRGB sprite one pixel fowards, it doesn't turn off the LED that, in the previous interval, was the rear-most pixel of the pattern. So I call DimTrail() on that pixel, and it gets faded by ~50% using a FastLED function. The DimTrail() method is recursive, so it then steps back one pixel and fades again, and so on, until it detects that it's reached a black pixel, at which point it exits.

// add int direction and make tailpixel += direction, and when direction is "backwards", offset the dimtrail by 3 pixels to prevent dimming the sprite
    void DimTrail(int tailPixel, int dimFactor, int direction) {
        if (tailPixel < 0) return;
        if (tailPixel > NUM_LEDS) return;
        if (! leds[tailPixel]) return;

        leds[tailPixel].fadeToBlackBy(dimFactor);
        tailPixel += direction;
        DimTrail(tailPixel, dimFactor, direction);
    }

The FastLED fadeToBlackBy function operates in 1/256ths, meaning that if you feed it 128, it fades 50%, if you feed it 64, it fades 25%, and so on. So I also wrote a function that maps updateInterval (the variable framerate) to dimFactor, so that now when faerie sprites move slowly, they start with a short trail, and when they accelerate to move quickly, they stretch out a long trail behind them.

That's great, but when the sprites stop for their idle animation, the trail fade doesn't look right.

So I created a FadeOutTrail() method, which itself calls DimTrail() recursively.

void FadeOutTrail(int tailPixel, int dimFactor, int direction) {
    if (tailPixel < 0) return;
    if (tailPixel > NUM_LEDS) return;
    if (! leds[tailPixel]) return;

    // Recursively run DimTrail() at tailPixel+1, so trail fades from the dim end
    DimTrail(tailPixel, dimFactor, direction);
    FadeOutTrail(tailPixel + direction, dimFactor, direction);
}

The result of this method is that, instead of staying the same length and fading out awkwardly, when the sprites stop and idle, the trails shrink from the far end inwards. It's a subtle improvement, but a nice one.

Algorithmic braking

Now that I've done some pixel animation of how I want the faerie sprites to move, I've worked out that I like to have them slow down and stop over a range of about 4-8 pixels, with the trails fading as they slow. I'm now working on slowing the sprites down by manipulating the framerate (which is how they accelerate).

I just put in some movement logic where the sprites calculate their distance from their destination pixel, and when that is within a certain percentage of the overall travel distance, it starts braking.

However, currently deceleration is handled by a single factor; how many milliseconds do you add or subtract to the updateInterval on each call of updateTravel()?

For acceleration from stop, it's simple enough to start at about 40ms, and subtract 1ms per interval as you travel. The result is a reasonably natural enough acceleration towards "cruising speed".

For making the sprite slow down to a stop, however, that doesn't really result in a pleasing deceleration curve.

After some work, I came up with this acceleration method:

int AccelerateTravel() {
    // Decide whether braking or accelerating
    if (currentDistance < totalTravelDistance * brakePercentage) {
        if (!isBraking) {
            isBraking = true;
            brakeDistance = abs(currentDistance - totalTravelDistance);
            brakePixel = currentPixel;
        }
    } else {
        isBraking = false;
    }
    // Accelerate or brake by returning positive or negative values to subtract from updateInterval
    if (! isBraking) {
        int x = abs(currentDistance - totalTravelDistance);
        return round(sqrt(x) * accelerationFactor);
    } else if (isBraking) {
        int x = abs(currentPixel - brakePixel);
        return -1 * round(sqrt(x) * brakeFactor);
    }
}

Whether braking or accelerating, the change to updateInterval is a function of the distance traveled from the point where you started accelerating. accelerationFactor and brakeFactor are semi-randomized values assigned on sprite creation, whose purpose is to make sure that, even if two sprites start at the same pixel headed in the same direction, they'll have slightly different acceleration and brake speeds, so they'll move a bit differently from each other. I'm still dialing in what those values should be.

Algorithmic idle animation

After the faerie sprite in the above video slows to a stop, it idles across 3 pixels, back and forth. I sketched this in terms of pixel values from the colorPalette array in my new notebook, and my friend Scott Longely translated it into a set of simple algorithms for each pixel.

That isn't the full desired result, though. What I need to do next is to write a colorFade() function to transition smoothly between pixel values.

I've found example code as gists on GitHub. Both use FastLED functions for the fade, which have so far resulted in good animation performance, so I'm game to see how they go.

I have so far not looked at the code at all to see how suitable it may be. So that'll happen soon.

Transition from idle to travel

There's a visual discontinuity at the moment where the animation transitions from isIdling to false; at that moment, it chooses a new destination, and draws the travel sprite. Unfortunately, the last frame of the idle animation and the travel sprite do not match perfectly.

So one of my next action items is to fix that.

Faeries go both ways

Last year's build of the Witch Lights concealed a dirty secret: sprites only had the capacity to move in one direction.

Basically, a sprite started at 0, and when in travel mode, it moved by adding 1 to CurrentPixel. When CurrentPixel was > NUM_LEDS, the sprite was marked done. (Or it started at NUM_LEDS and traveled towards 0.)

What I want, though, is for faeries to make one long travel move in the "forwards" direction (whichever way that happens to be for this particular sprite), and then make a bunch of small, random moves in either direction, basically flitting about while waiting for people to catch up along the path.

So that means that all the movement logic needed to be examined and re-written. Which I will not get into here. But basically, it happened.

So that's working, too.

Reverse the polarity of the neutron flow

The Witch Lights have two long-range motion sensors, one at each end. When someone on the "far" end triggers a sensor, we have to spawn a faerie sprite that starts at NUM_LEDS (the far end), and travels towards pixel 0.

There are lots of details involved in making sprites travel in the "other" direction; for example, when transitioning from travel mode to idle, the idle pixel animation has to run in reverse. And the logic to check whether the sprite is "done" (and can be deleted from memory) has to know the sprite's direction, and so on.

Or.

When we run an Update() method, when we take what we've animated and send it to the LEDs, what we actually do is call a function called stripcpy. That function takes the CRGB struct that we've been manipulating, looks up currentPixel, and then uses memcpy to write our pixels to the leds CRGB struct.

OK. So.

What if we told stripcpy to count from the other direction, and write the CRGB data backwards?

In theory, you'd get a perfect mirror image of the animation you're running. The sprite logic would always run from pixel 0 to NUM_LEDS, but it would draw to the pixels as though it was running in the other direction.

That simplifies the requirement for direction-checking logic all through the various methods of the sprite class, and only requires that sprites be created with a drawDirection value, which then gets passed to all stripcpy calls.

That's the theory. I haven't tried it out yet. But I'm eager to try it. If it works, that's a huge simplification, reducing the number of potential logic bugs in Travel mode.

It would very slightly complicate collision detection, but I'm pretty sure that's easily solved.

Up Next

Here's the todo list:

  • Add "flit" behavior, so that faeries make long travel moves towards NUM_LEDS, then a number of short, random moves in either direction, with short idles, and then move on with another long travel move
  • Make stripcpy run backwards
  • Add jumper-pin modes (set some pins to auto-pullup, and then jumper them to ground to activate various modes for setup and documentation)
  • Collision detection
  • Adjust accelerationFactor and brakeFactor randomization, maybe make them values passed to sprites on creation?
  • Create subclasses of the current sprite and change the idle animation
  • Fix the visual glitch when transitioning from idle to travel modes (FadeColor?)
  • Define zones of pixels where faerie sprites should not stop and idle
    • areas where the trail coils around a tree
    • zones at the very beginning and end of pixel strips
  • Use collision detection to make faeries more likely to stop and flit around near other faeries
  • Double-check memory usage and possible memory leaks caused by my messing with things I do not sufficiently understand

And other things. Secret things.

Loopy Debug is my new Frank Zappa cover band

Currently working on looping animations for when the sprites pause.

Here's one in Excel

l_pulsar_a.png

And animated

I would show you what it looks like on the NeoPixels, but the Arduino crashes when it tries to spawn the sprite object, so I've gear shifted from animation design mode into debug mode.

How I understand what the code does

So what's happening is, the Arduino bootloads, runs through all the globals at the top, stuff like:

#define afc_l_pulsar_a_ANIMATION_FRAME_WIDTH    24
#define afc_l_pulsar_a_ANIMATION_FRAMES         22

Which is where I set the parameters for each animation. In this case, you've got 22 frames of animation, each one 24 pixels wide. It also sets all the other parameters to tune the global travel acceleration rate, sprite pixel velocity, and how far each sprite travels before playing the idle animation.

We then go on to use the FastLED library to create a CRGB struct to contain the color values we will write to the NeoPixel strip:

CRGB leds[NUM_LEDS];

CRGB structs are a range of memory that contain 3 bytes for each LED: the red, green, and blue color values. So the amount of SRAM that the struct takes up is basically [NUM_LEDS] * 3 bytes.

To animate the pixels that I've been designing in Excel, what happens is, we define a char and a CRGB struct for each animation, and each is FRAME_WIDTH * FRAMES long. In the case of the pulsar loop above, that's 24 by 22, which is 528.

A char is one byte per entry, and we already know a CRGB is three, so that means we're taking up 528 times 4 bytes, or 2.06K. And we have a total of 96K of SRAM to execute the entire system, including all pre-rendered animations.

Actually, we have 32K. See, the Arduino Due has a 32K bank of SRAM, and a 64K bank of SRAM, and I'm not sure how they interact, but I can say for certain that if you define too many CRGB structs and chars, the Arduino Due crashes on boot, and our best guess for memory usage is that we're around the 30-33K mark. I'll get back to that in a bit.

So we define all the animations that we're going to run in this build, and then we define our function prototypes and our object classes. I won't get into the structure of the classes, except to say that there's a SpriteVector and SpriteManager class, which combine to handle the creation, lifecycle, and deconstruction of each animation sprite.

There is also the LoopTestSprite or FragmentTestSprite classes, which are subclasses of the Sprite class. Each different sprite class currently has code to move from point to point, and to play a pre-rendered animation for a certain number of repetitions when it reaches its destination pixel. It then moves to the next destination pixel, loops, and repeat until the whole thing goes off the pixel grid, at which point the object is marked is_done. At which point, SpriteManager deletes the object from memory on its next run-through.

So that's where the classes are defined. If the Arduino passes safely through those, we reach the point where we create our permanent objects:

InfraredSensor *sensor1;
InfraredSensor *sensor2;

SpriteManager *spriteManager;

bool isBooted;
bool testSpritesCreated;

int starttime = millis();

So we've got 2 infrared sensors per strip of the Witch Lights, and they get created and assigned the names sensor1 and sensor2.

We boot up SpriteManager, prime some bools, and set the starttime value, and then it's time to run setup().

void setup() {
    createColorsets();
    createAnimationFrames();

    isBooted = false;
    testSpritesCreated = false;


    spriteManager = new SpriteManager();

    sensor1 = new InfraredSensor(PIR_SENSOR_1_PIN);
    sensor2 = new InfraredSensor(PIR_SENSOR_2_PIN);

    resetStrip();
}

setup() reads our arrays of predefined color sets into RAM, and then runs the createAnimationFrames() function. Which reads all of the animations defined within the function into the chars we created earlier. So now, in memory, we have a full set of pixel animations, in the form of char structs with the animation's name.

Setup also sets isBooted and testSpritesCreated to false, which are bits that trigger a test pattern at the beginning of loop() later. And it links the infrared sensors to their appropriate input pins, resets the NeoPixel strip, and we're ready for the main loop().

loop() is the main engine of an Arduino project. It cycles forever, and each time, you have a chance to do some logic. In this case, we run a rainbow flag test pattern down the length of the NeoPixel strips, which gives us a nice way to test the strips as the installation is assembled.

After that (which only runs once), we check sensor1, check sensor2, create the appropriate sprite object if either is triggered. So when someone walks by the Witch Lights, an animation sprite is called into memory. But the sprite object only knows how to respond to Update() calls from SpriteManager: it won't do anything on its own.

So loop() calls spriteManager->Update(); last of all, and then the loop repeats.

Lots of stuff happens behind the scenes when you call Update() of course. But we'll get into that as we need it. Right now, I'm finishing the talk-through of the whole boot cycle of the witchlights-fastled.ino sketch, because right now, the Arduino is crashing before it draws the new animation sprite I just defined.

When your shit crashes

A few days ago, when I was just learning to create custom animation sprites, I tried to define all the animations I had converted at once. I knew animations took up SRAM, but thinking that I had 96K to play with, I wasn't concerned about SRAM usage yet.

The Arduino crashed so hard I had to manually erase its flash RAM before I could reprogram it again. For reference: that's pretty bad.

In that case, it crashed while creating the char and CRGB structs, before it reached setup(). So the Arduino would turn on, but nothing else would happen.

Today's problem is different.

Today, when I turn on the Arduino, it plays the test pattern, which means it's made it all the way through everything we just defined, to the loop(), and at least started the main loop.

But when I activate sensor1 with a pushbutton, nothing happens.

So something ain't right.

For comparison, here is what I have defined in memory for animation:

char afc_w8v1r[ANIMATION_FRAME_WIDTH * ANIMATION_FRAMES];
CRGB af_w8v1r[ANIMATION_FRAME_WIDTH * ANIMATION_FRAMES];

char afc_f_slow_stop[afc_f_slow_stop_ANIMATION_FRAME_WIDTH * afc_f_slow_stop_ANIMATION_FRAMES];
CRGB af_f_slow_stop[afc_f_slow_stop_ANIMATION_FRAME_WIDTH * afc_f_slow_stop_ANIMATION_FRAMES];

char afc_f_slow_stop_c[afc_f_slow_stop_c_ANIMATION_FRAME_WIDTH * afc_f_slow_stop_c_ANIMATION_FRAMES];
CRGB af_f_slow_stop_c[afc_f_slow_stop_c_ANIMATION_FRAME_WIDTH * afc_f_slow_stop_c_ANIMATION_FRAMES];

char afc_l_pulsar_a[afc_l_pulsar_a_ANIMATION_FRAME_WIDTH * afc_l_pulsar_a_ANIMATION_FRAMES];
CRGB af_l_pulsar_a[afc_l_pulsar_a_ANIMATION_FRAME_WIDTH * afc_l_pulsar_a_ANIMATION_FRAMES];

Looking at this, we have:

  • afc_w8v1r is the original animation sprite from last year, defined backwards for sprites heading in the "reverse" direction from sensor2.

  • afc_f_slow_stop is the Better slow stop go animation that I was testing a couple days ago.

  • afc_f_slow_stop_c is an experimental variation of that animation, using manual anti-aliasing for slow pixel-to-pixel movements.

  • afc_l_pulsar_a is the first loop test, featured at the top of this post.

When I first uploaded this sketch, in the sensor1 check in loop(), we had:

if (sensor1->IsActuated()) {
    Sprite *s1 = new FragmentTestSprite();
    // Sprite *s1 = new LoopTestSprite();

    if (! spriteManager->Add(s1)) {
        delete s1;
    }
}

And pressing the sensor button would spawn a "slow stop go" sprite. Great.

So I changed it to:

if (sensor1->IsActuated()) {
    // Sprite *s1 = new FragmentTestSprite();
    Sprite *s1 = new LoopTestSprite();

    if (! spriteManager->Add(s1)) {
        delete s1;
    }
}

And now, when you hit the sensor button, nothing happens.

Huh.

OK, so.

Change it back? Sprite spawns. Change it again? Nothing happens.

So it's consistent, whatever it is. That's a bonus. Consistent means repeatable.

My first thought was, what did I do wrong in the LoopTestSprite() class definition that I didn't do in FragmentTestSprite()? So I did a diff between the two.

diff.png

And for the life of me, I can't find anything in the LoopTestSprite() that's different and is Arduino-crashingly bad. That's the differences above. On the left, the sprite that runs, and on the right, the one that don't.

Is it SRAM usage?

afc_f_slow_stop, which does run, is 4.32K in SRAM.

afc_l_pulsar_a, which does not run, is 2.06K in SRAM.

We can't rule that out, but it seems unlikely.

So what now, smart guy?

Welp.

Fortunately, there's a debug() function, which lights up a specific LED when you call it.

In the past, this was used to show the size of the SRAM heap while the Arduino ran. That helped greatly to identify memory leaks.

What I can do with it now is, I can place debug(20) at the start of loop(), debug(21) at the next step, 22 at the next, and so on, and put debug statements into the sprite construction logic of the sprite that works, and do a control experiment to make sure I can identify all the working pieces of that sprite doing their thing. As the sprite is constructed and runs through the logic to process the animation char into CRGB frames, the most easily-seen LEDs under my desk will turn on, one by one.

(And I'll be checking that into git for sanity's sake.)

Once that's done, I will change the animation logic values in that sprite to point at the animation for LoopTestSprite().

Why? Because I just tested the debug logic, and if I used copy and paste or hand-typed all the debug into LoopTestSprite(), I'm running the risk of a typo or mistake causing it to light up the wrong LEDs or malfunction in some other unknown way.

So I will modify the sprite to run the new animation, compile, and run it on the Arduino. At which point... I will probably learn something unexpected. That's how it usually happens.

But for now, rain clouds are moving in, and I'm going to sit on my screened-in back porch and watch the rain. Animation can wait.