Black Widow, Part 8: Control Software

Ghost in the Shell

In this third part of the Widow’s Bite build, I’ll explain how the software that I wrote works. I’m an experienced software engineer – it’s what I do for a living. With that in mind, it’s possible that even though I will try to keep this article simple, some if it may be difficult to understand unless you already know programming. Fortunately, once the bracers are built and since the software for them already exists, all you need to do is figure out how to install and configure the Arduino development kit on a computer, configure the plug-ins for the Trinket and then load up the code on the Trinket. Arduino is really popular, so the IDE will work with MacOS, Window and Linux.

Without software, this is all the bracer would do...

Without software, this is all the bracer would do…

Here’s a link to the source code. If anyone is interested in collaborating & developing this further, I could put this on Github. Let me know!

Stunning Bracelets and How to Use Them

Once the Widow’s Bite has booted up, it does a short startup animation and then goes into a background animation mode. The bracer just shows that animation endlessly until you tell it to do something else or you turn it off. It’s always running either a background loop or a triggered action sequence. Starting an action will suspend the current background loop, then run through the action until it ends or until you trigger another action. When an action reaches the end of its animation sequence, the bracer will resume running the background animation where it left off.

Since we only have one button, we control the bracer using short and long presses and pauses, almost like using Morse code. The main difference between Morse code and the bracer is that the bracer will not wait for a pause to execute your action, so if you tap twice, the first tap will activate the action for one tap and then the second tap will replace that with the action for two taps. It’s pretty easy to add new commands and sequences by modifying the C program code, but here are the built-in ones as of this writing:

  • short: all LEDs flash briefly (great for throwing a punch)
  • short-short: all LEDs flash briefly followed by a spinning animation (double punch)
  • long: background animation 1 (mostly steady low brighness with periodic spinning animation)
  • long-short: background animation 2 (constant low brightness spinning animation that flashes and reverses direction periodically)
  • long-short-short: go dark (no animation, LEDs are all off. Don’t mistake this for turning off the bracer, because the Trinket is still on and uses power)
  • long-long: background animation 3 (LEDs on at low brightness, but with a constantly rotating light that flashes bright and then turns off briefly)
  • long-long-short: activate normal power/dim mode (one LED group is bright, 3 others off for 2 seconds to indicate low power)
  • long-long-long: activate high power/bright mode (one LED is off, 3 others are full brightness for 2 seconds to indicate high brightness)

After the brighness changes, the default background mode is activated automatically. The maximum brightness isn’t affected by the mode change at all, but anything dimmer than that is affect. Essentially, the LEDs switch between gamma curve 1.6 (normal) and 1.2 (bright), if you are familiar with display gamma curves.

The Code

The software consists of four functional modules. I wanted an easy and compact way to animate the LEDs. I also wanted to be able to control the brightness of each LED group. Due to the limitations of the Adafruit Trinket (5 GPIO pins), each output pin controls three LEDs. Essentially, we just have four pixels (picture elements) that repeat three times around the bracer. The Trinket has two “analog” outputs, but I wanted to have four, so instead of relying on the PWM functions on the Trinket, I wrote software that emulates that behavior on any of the GPIO pins.

Arduino is based on a simple idea where there’s an initialization call and then after that another function is called from a never-ending loop. Now imagine a card game and some people sitting at the table, playing cards. The card table is the device, the players are the functional modules and the loop function represents one round at the table where each player has their turn to play.

Pace It

In order to make the code easier to write, the first thing I wrote was a pace function. This module sets pace at which the loop function runs. Using the card game analogy, this player watches a clock and holds up the game until a certain time has run. If this was a real card game, each round might be set to last no less than 1 minute. After some experimentation, I set the loop to run once every 75 microseconds. That’s roughly 13000 times per second. The pace function is really simple, but it makes the LED brightness control function easier to write.

Tap recognizer

The next “player” at the table is responsible for checking the state of the glove switch that you use to control the bracer and translating your actions into commands that typically tell the bytecode module to start running a specific background or action animation. It recognizes sequences of short and long button presses and of course pauses between sequences. If you are adding new commands, find the switch(fullSequence) statement in the program and look at the code below that. The values for the case statements are octal numbers again, so they always start with a zero. Ones represent short presses and twos represent long presses. For example, case 0211: means that the code below it is run after you have performed a long press and two quick taps.

LED Control

The LEDs can only ever be on or off. In order for an LED to appear only half lit, the software needs to turn it on and off really quickly. Let’s say the heartbeat limits us to 1000 loops per second (easy round number for the sake of demonstration – it’s actually faster than that). The human eye starts to see flicker when a light is on or off for longer than about 1/25th of a second. Some people are more sensitive, so faster flickering is better… If the LED is on every even heartbeat and off every odd heartbeat, then the human eye will receive about 50% of the amount of light that a constantly powered up LED would emit. Using the card game analogy, the LED control player lights up and turns off LEDs and then instantly lets the next player do something.

The brightness control system uses a kind of bucket or credit system for determining which LED should be on and when. Let’s say we only have two LEDs (or the others are simply off all the time) and we want them both to be at 50% duty. Each heartbeat, they bring 0.5 points to a shared pot and 0.5 is added to their “credit account”. Then, the controller looks to see if it has more than zero in the shared pot. If it has, it finds the first LED that has more than zero credit, turns it on for that heartbeat and then subtracts 1.0 from the pot and 1.0 from the credit of the LED that will be lit for one heartbeat. The lit LED now stands at -0.5 credits. The pot has zero credits left, so the controller has spent all it’s budget and just turns off any remaining LEDs. The next round, the pot goes to 1.0 again and the credit of the first LED is zero, but the second one is at 1.0, so the second one gets its turn and now we’re back to where we started.

Instead of values from zero to one, I’m using 10 bit fixed point, so a fully lit LED is at 1024 and a fully turned off LED is at 0.

Another thing that the LED control function does is to map from “virtual” LED numbers (in our case 0-3) to actual I/O pins. Because I wired the left and right bracers using different GPIO pins, the mapping is slightly different. From the animation software point of view though, it just controls LED groups 0-3 and doesn’t care what GPIO pin is used.

As a side effect of how the LED control function was written, it also controls the LEDs so that the maximum amount of current used is as low as possible. In the example above, only one LED at a time is on at a given time, so the two LEDs are using the same amount of energy as if we had one LED on all the time. If we just used a dumb PWM, it would likely have both LEDs on at the same time and then both off. The coin batteries we use aren’t particular great for high current  draw, so it makes sense to try to use an even trickle of current rather than a spiky mess that constantly alternates between zero and max.

Bytecode

The bytecode interpreter is much like a simple microprocessor even though it’s not a general purpose CPU. It has a program counter and some registers and stacks of sorts. The animations are stored as arrays of bytes in program memory on the Trinket, so I just declare a static “PROGMEM” array in the C code and use macros and octal numbers for the program. It looks a lot like assembly language and it’s a very efficient way to store animations. The interpreter is always either running a background animation or an action. If a background animation is running when an action starts running, the state of the background task is stored so that the animation can resume at the same point later on. I’ll give a brief overview of the instructions, as that’s most likely something that people will like to tinker with.

Using the card game analogy, this function reads instructions from a manual and does whatever the manual says. In this case, the bytecode program is the manual and the actions are opcodes.

LED Operations

The interpreter also simplifies brightness to just eight different levels (0-7) and uses a gamma table to translate to the low level 0-1024 fixed point brightness scale. When the level is zero, the LED isn’t lit at all and when it’s 7, it’s lit all the time (full brightness).

  • Single LED brightness can controlled with a three digit octal number where the first digit is always zero, the second digit is the number of the LED (0-3) and the third one is the brightness (0-7).
  • The opAll(n) macro allows you to quickly set the brightness of all your LEDs to n in one operation.
  • opClearAll is the same as opAll(0).

The opcodes just change the requested brightness for the LED. The actual GPIO port isn’t directly affected at all. Once the LED Control function runs again, it will turn LEDs and and off based on the requested brightness. This means it’s OK to combine an opAll macro with a code that changes just one or two LED values. Here’s an example:

opAll(1),
007,

Pausing

In order to actually see the animation, we can’t just change the lights infinitely fast and expect the changes to be visible. When a pause is encountered, the interpreter stops reading instructions until the required time has passed. Using the card player analogy, the pause opcode tells the player to pass all game actions until told otherwise or until a specific time has passed.

  • The opPauseTime(n) macro takes a number of milliseconds (n) from 0 to 1000 (1 second) and generates an instruction to pause for that long. I often use a #define macro to define a constant time delay, so that I can adjust the running speed of an animation simply by changing the number in the macro.
  • The opPause1Sec opcode just pauses for one second.

Here’s an example that also uses the previously shown LED commands:

opAll(1),
opPause1Sec,
002,
opPauseTime(200),
012,
opPauseTime(200),
022,
opPauseTime(200),
032,
opPauseTime(200),

Looping

Looping just means that the interpreter will repeat whatever is between the start and the end of a the loop a certain number of times, possibly infinitely. You can nest loops up to a few levels, but the stack only has 8 slots, which are shared by background and action animations, so you probably shouldn’t have more than 4 nested loops in any animation to be safe. Increase the stack size in the C code if you need more.

  • opStartLoop will start an infinite loop. You probably want one of these if you are writing a background animation as they should never end.
  • opRepeat(n) will start a counted loop where the loop is run ‘n’ times. For example, you might run an animation clockwise 4 times and then use another loop to run it counterclockwise 4 times before starting over again.
  • opEndLoop will decrement and check the loop counter and either pass through or go back to the start of  the loop depending on the counter. Note that this opcode will also cause the interpreter to yield control (effectively pause for the shortest possible time). The yield prevents your animation programming errors from making the bracer unresponsive.

Here’s a typical example (back_forth_speed is assumed to be defined a bit earlier):

 opRepeat(2),
 opPauseTime(back_forth_speed), opAll(0), 022,
 opPauseTime(back_forth_speed), opAll(0), 012,
 opPauseTime(back_forth_speed), opAll(0), 002,
 opPauseTime(back_forth_speed), opAll(0), 032,
 opEndLoop,

You can of course find many more examples in the tables within the C code.

Other Opcodes

  • You should have an opEndProgram opcode at the very end of each action. Background programs shouldn’t end, so if you use this opcode in a background animation, it will simply stop the program there. Action animations on the other hand will cause the previously running background animation to resume where it left off.
  • opNormalGamma switches to the default gamma table, which has better power conservation but slightly lower mid-range LED brightness.
  • opBrightGamma switches to the brighter gamma table, using a bit more power.
  • opToggleGamma switches between the two gamma tables (I ended up not using this, as it’s hard to keep track of what mode you are in).
  • opYield isn’t really useful. 🙂

Leave a Reply