LedBox language animation building blocks

Share on:

In this more practical followup article, we’ll focus on the led language implementation using python. Led box permit to display some led animation using an individually controlled led strip using ws2812 chip.

Hightly configurable animations implementation is shown and presented here, using python generators functions. This article, aims to explain the benefits of such approach in creating the grammar for smart led animations and smart feedback for such devices.

an integration with an interpreter is also explained.

The code referred here in the article is available on mqttiotstuff/mqtt-agent-ledbox

The device setup, for context

In the device setup, a long string, specifying the led strip rgb tuple, is sent to a lua NodeMCU ESP8266 module. This hardware device only displays the MQTT topic received “long” string, and send it throught the wires to the ws2812 chip, activating the leds.

The hardware architecture is illustrated below

Animations and commands are sent to the python animation agent, using mqtt (publish/subscribe).

Why Python ?

Python is still quite simple and accessible to a wide range of developers or hackers. Easy to learn (python is one of the language with the smallest keywords set), and python does not need large tool-chains to start with. Python also benefit from a large community.

Python also offers some interesting features for triggering animations and frames : generators

Generators are often used in data science, for cursors (mainly using in panda and other derivative), but also in other areas. Here the generators offers a great way to specify animations, in a simple, and effective way.

Primitives needed for animations

Before using generators, some functions have been described. We first need some frame construction functions, to provide frame generation primitives. These functions output a string, formatted as : “rgbrgbrgbrgbrgb…” encoding. The length of the rgb tuple depend then on the number of leds existing on the led strip.

As an illustration, these frame’s primitives function lighten a specific pixel in the strip, or creating a frame with only a ring (range of pixels) .. other examples can be met in the implementation referred.

These frame’s constructor functions can then be combined using composed functions. We can find : the add function making a color added operation (and thresholding the result), but also the combinefunction, implemented as below :

1    def combine(self, led_frame1, led_frame2):
2        """
3        mix two led frame
4        :param led_frame1:
5        :param led_frame2:
6        :return:
7        """
8        return self.add(self.fade(led_frame1), self.fade(led_frame2))

Animate the frames using generators

Python generators can be setted up then for frames, using the functions described above. These generators returned a series of Led Frames (strings), throught the yield python function.

This is then possible to create sequences of frames implementation as illustrated below :

 1    # generator for creating a color flash
 2    def flash(self, color, speed=5):
 3
 4        buf = self.feed(self.all_leds, color)
 5        for i in range(0, speed):
 6            yield buf
 7
 8        buf = self.feed(self.all_leds, black)
 9        for i in range(0, speed + 5):
10            yield buf

The feed method used generate a Led Frame using a single color for all the led in the strip, with the given color. the flash generator function, furnish in the implementation 5 frames with the given color, and 5 frames with the black color (meaning, we stop the display).

Ok, why not using for loop, you’ll tell ?

I’d answer then, that generators offers lot more ability for composition. Looking at the ability and readability of generators, this looks obvious seening a simple sequence combination function :

1
2    # generator with a sequence of frames
3    def sequence2(self, frame_generator1, frame_generator2):
4        if not frame_generator1 is None:
5            for i in frame_generator1:
6                yield i
7        if not frame_generator2 is None:
8            for j in frame_generator2:
9                yield j

the sequence2 function takes here two frame_generator functions (as the flash function explained above), and create a new generator, concatenating the two animations. (the second after the first one).

using a parallel combination in not as complex :

 1    # generator for parallel sequences
 2    def parallel2(self, frame_generator1, frame_generator2):
 3
 4        while not (frame_generator1 is None and frame_generator2 is None):
 5            s = None
 6            if frame_generator1 is not None:
 7                try:
 8                    s = next(frame_generator1)
 9                except StopIteration:
10                    frame_generator1 = None
11            s2 = None
12            if frame_generator2 is not None:
13                try:
14                    s2 = next(frame_generator2)
15                except StopIteration:
16                    frame_generator2 = None
17            yield self.add(s, s2)
18

the add function combine the two generators result, playing the animation in parallel.

combinations are also possible, using an additional generators : shift , and clear

1
2    # shift an animation
3    def shift(self, frame_generator1, shift=10):
4        return self.sequence(self.clear(shift), frame_generator1)

shift : delay the animation with a given number of black pre-frames . clear generate a black frame.

Nota: sequence has the same purpose of sequence2, but take variable parameters, to sequence multiple frame generators

Fast / Slow is also possible, stripping some frames or duplicate somes :

 1    def slow(self, frame_generator):
 2        for f in frame_generator:
 3            yield f
 4            yield f
 5
 6    def fast(self, frame_generator):
 7        i = 0
 8        for f in frame_generator:
 9            if i == 0:
10                yield f
11            i = (i + 1) % 2

Combine them all

Some simple combination can then be setted up to make real animations,

a moving up and down ring :

1display(ledring.sequence(movering(1,l2), movering(1,l2)))

a gallery of some animations (a couple) is available here : (sorry for the auto regulation of the web cam used, this stress the eyes in the watching).

https://github.com/mqttiotstuff/mqtt-agent-ledbox/blob/master/doc/gallery.md

Accessing the function throught a simple interpreter

As mentionned in the introduction, the display use MQTT for communication.

In order to trigger custom animation from mqtt message, an interpreter is added to the stack. The interpreter used is expression

1import expression

parseExpression convert then a string into python code (with limited functions provided).

 1
 2def parseExpression(ledexpression):
 3
 4    functions= {
 5                "sequence": ledring.sequence,
 6                "slow": ledring.slow,
 7                "fast": ledring.fast,
 8                "parallel": ledring.parallel,
 9                "parallel2": ledring.parallel2,
10                "shift": ledring.shift,
11
12                "cglinear": ledring.linear_color,
13                "cgcolor": ledring.fixed_color,
14                "cgrainbow": ledring.rainbow_color,
15                # switch colors at each step
16                "cgswitch": ledring.switch_color,
17
18
19                #### Frame constructor functions
20                "fg" : ledring.fg,
21                # 
22                "fpattern": ledring.fill_patterns,
23                "fadd": ledring.add,
24                "fpixel": ledring.pixel,
25
26                "fring": ledring.ring,
27                "fdots": ledring.dots,
28
29
30
31
32                ### Frame generator functions
33
34                # clear
35                "clear": ledring.clear,
36
37                # take color, and speed (default to 5)
38                "flash": ledring.flash,
39
40                # take color
41                "rain": ledring.rain,
42
43                # create square patterns, take color generator, nbpatterns, square pattern size, and shift
44                "square": ledring.colored_square,
45
46                # random dots moves
47                "random": ledring.randomDotColor,
48
49                # dots animation
50                "dotanim": ledring.dotAnim,
51
52                # dot animation with color generator
53                "dotanimcg": ledring.dotAnimCg,
54
55                # take direction, and associated color
56                "movering": ledring.movering,
57                
58                # take colors
59                "wave": ledring.wave
60            }
61    
62    variables = { 
63        "black":black,
64
65        "green":green,
66        "red":red,
67        "blue":blue,
68        "white":white,
69
70        # colors from https://materialuicolors.co/
71        "uipink":uipink,
72        "uired":uired,
73        "uiblue":uiblue,
74        "uilightblue":uilightblue,
75        "uipurple":uipurple,
76        "uideeppurple":uideeppurple,
77        "uiindigo":uiindigo,
78        "uicyan":uicyan,
79        "uiteal":uiteal,
80        "uigreen":uigreen,
81        "uilightgreen":uilightgreen,
82        "uilime":uilime,
83        "uiyellow":uiyellow,
84        "uiamber":uiamber,
85        "uiorange":uiorange,
86        "uideeporange":uideeporange,
87        "uibrown":uibrown,
88        "uigrey":uigrey,
89        "uibluegrey":uibluegrey}
90
91
92    parser = expression.Expression_Parser(variables=variables, functions=functions,
93                                          assignment=False)
94    e = parser.parse(ledexpression)
95    return e
96

Some pragmatic usage examples :

When my professionnal website, does not respond, a specific animation is sent to the ledbox :

string sent on the agent /run topic, connected to the interpreter :

1sequence(sequence(fg(fpixel(12,red)), fg(fpixel(11,green)), fg(fpixel(10,blue))), sequence(dotanim(red,2,5), slow(sequence(flash(red), clear(), flash(red)))))

When running out of space on the system, an other animation is sent, providing a separate feeling in the feedback.

1parallel(parallel(dotanim(red, 1, 11),rain(blue)), dotanim(green,1,9))

The display main loop, for curious

Running the frames, needs a main loop, with a care attention to mqtt clients, and threading, to preserve animations :

 1
 2class MainThread(threading.Thread):
 3    def run(self):
 4        global client2
 5        global username
 6        global password
 7        global mqttbroker
 8        count = 5
 9        client2 = mqtt.Client(clean_session=True, userdata=None, protocol=mqtt.MQTTv311)
10        client2.username_pw_set(username,password)
11        client2.connect(mqttbroker, 1883, 5)
12        client2.will_set("home/agents/ledbox/lwt", payload="disconnected", qos=0, retain=False)
13
14        client2.loop_start()
15        print("init done")
16        currentGenerator = None
17        cptwatch = 0
18        while True:
19            try:
20                s = None
21                if currentGenerator != None:
22                    try:
23                        s = next(currentGenerator)
24                        assert len(s) == ledring.all_leds * 3
25                    except StopIteration:
26                        currentGenerator = None
27
28                if s is not None:
29                    count = 5
30
31                if s is None:
32                    count = count - 1
33                    if count > 0:
34                        s = ledring.feed(ledring.all_leds, (0,0,0))
35
36                if s is not None:
37                    ledring.display(client2,s)
38
39                try:
40                    # pump generator and combine with previous if exists
41                    incomingGenerator = generatorQueue.get(timeout=0.005)
42                    assert incomingGenerator != None
43                    currentGenerator = ledring.parallel(currentGenerator, incomingGenerator);
44                except queue.Empty:
45                    pass
46            except:
47                traceback.print_exc()
48            time.sleep(0.1)
49            cptwatch = (cptwatch + 1) % 100
50            if cptwatch == 0:
51                client2.publish(WATCHDOG_TOPIC, "1")
52
53
54mainThread = MainThread()
55mainThread.start()
56

The ending words

Using this ledbox, is really comfortable, and adding new animations change the way the ledbox is used. Hoping this approach and share can invite to hack you own ledbox.

All current code is available on the github repository, mqttiotstuff/mqtt-agent-ledbox

feel free to add your contributions, or feedbacks.