kk
Gabriel Morell





Signage

Extensible Signage System

Extensible Signage Architecture
posted Jan. 19, 2018, 9:44 p.m.
under signage · perma

Motivation

After laser cutting and building my information display #link I wanted to have a backing software architecture that met two requirements.

  • First: It should be capable enough to minimize the amount of processing on the receiving software implementation.
  • Secondly: Adding additional datasources should be as easy as possible.

Architecture

Signage Architecture

Signage Architecture

The train sign components are written as a series Crossbar components, each with a specific task. There are the data source components such as providers for time, weather and train data for the area. These publish their cleaned textual data to a data topic, which the signage service spools and publishes periodically to the signs over MQTT.

Configuration

Configuration is handled entirely in a Thingistry namespace that the service reads from. Below is one such example that implements a simple and complex example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
{
  "sources": {
    "us.thingcosm.data.services.rfid2sign": {
      "fields": [
        "__all__"
      ],
      "ttl": 90
    },
    "us.thingcosm.data.services.weather": {
      "fields": [
        "temp_f",
        "temp_c",
        "hum"
      ],
      "config": {
        "incr": {
          "wind_m": [
            2,
            2,
            0,
            1,
            1,
            1
          ],
          "hum": [
            1,
            1,
            0,
            2,
            3,
            3
          ],
          "temp_f": [
            1,
            1,
            1,
            2,
            3,
            3
          ],
          "temp_c": [
            1,
            1,
            1,
            2,
            3,
            3
          ]
        },
        "colormap": {
          "1": [
            40,
            20,
            40
          ],
          "0": [
            0,
            0,
            0
          ],
          "3": [
            20,
            50,
            20
          ],
          "2": [
            40,
            20,
            0
          ],
          "4": [
            20,
            20,
            50
          ]
        }
      },
      "ttl": 600
    }
  }
}

The first example accepts from all available keys within the namespace with a TTL of 90 seconds.

1
2
3
4
5
6
"us.thingcosm.data.services.rfid2sign": {
      "fields": [
        "__all__"
      ],
      "ttl": 90
    },

The second example is slightly more involved. It selects data to read from a specific set of keys.

1
2
3
4
5
6
"us.thingcosm.data.services.weather": {
      "fields": [
        "DATE",
        "TIME24"
      ],
}

The next section is a configuration entry and is involved with incrementing the indices of certain colors under certain indices. In the example below the 0th index is incremented by two, the 1st index by 1 as well, and will end up the same color. The second section is what the color values at the elevated indices end up being.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"us.thingcosm.data.services.weather": {
     "config": {
        "incr": {
          "DATE: [
            2,
            1,
            0
          ],
       },
       "colormap": {
          "0": [
            0,
            0,
            0
          ],
          "1": [
            40,
            20,
            40
          ],
         "2": [
            30,
            20,
            10
          ],

   }
}

Data Sources

Data providers are designed to be standalone publishers that either inject new information into the system or mutate existing information and publish it on a different topic.

In the example given below, the nightshift time provider publishes a dictionary containing a top level text_micro key and then the various topic subkeys and their values.

1
2
3
yield self.publish("us.thingcosm.data.services.nightshift", **{
            "text_micro": {"DATE":str_disp_date, "TIME12": str_disp_time_12, "TIME24": str_disp_time_24}
        })

Spooler

Showing the data as it comes in from the sources would be a bad viewing experience so a buffer must be kept.. As the subscriber receives data from its configured topics they are kept in a dictionary in memory. Each source is configured with a time-to-live and old values are purged periodically. There is an array that gets a value popped off with each tick and the values published to the configured topics. When the array is empty, the array is repopulated from the living dictionary with the new set of values and it the loop continues publishing data.

With our commitment to keeping the receiving implementation simple the publishing step involves transforming the output text into sprites to display. This is what the sprite service is in charge of dealing with. An extracted code block follows

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@inlinecallbacks
def publish(text):
    sprite_res = yield self.call("us.thingcosm.data.services.sprites.letter", string=st_text, map_incr=st_incr)
    sprite = sprite_res.get('sprites')

    # 1) A bit of preprocessing
    rows = {i:[] for i in range(0,7)}
    for letter in sprite:
        row_pos = 0
        for row in letter:
            # let's map this to the values we're going to be putting down while we're in here
            # fail to zero by default
            new_row = [st_color.get(str(j), st_color.get(0)) for j in row]
            rows[row_pos].extend(new_row)
            row_pos += 1

    # 2) append the sides and bottom framing (test patterns for now)
    left_color = st_color.get('sleft', [0,0,0])
    right_color = st_color.get('sright', [0,0,0])
    bottom_color = st_color.get('sbottom', [4,0,0])
    for index,row in rows.items():
        # print(row)
        row.insert(0, left_color)
        row.append(right_color)

    # 8x32 display and the sprites are 7x5
    rows[7] = [bottom_color]*32

    # 3) Build the snake
    snake = []
    dau = down_and_up()
    for i in range(0,32):
        for j in range(0,8):
            row = next(dau)
            snake.append(rows[row][i])


    # 4) ready for the world
    yield self.publish("us.thingcosm.data.signage.pub.%s" % self.topic_name, **{
        "snake":snake
    })

The sprite service is called and the text is converted to a sprite array.

  • The array gets modified in the first step to fit 8x32
  • In the second step the values are set in the non sprite ranges
  • In the third step we use the down_and_up generator to rearrange the two dimensional array into a single array that matches the ordering of the LEDs on the display board.
  • The fourth step is publishing the output to the correct topic.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def down_and_up():
    first = False
    status = 0
    going_up = True
    while True:
        yield status
        if status == 7:
            going_up = False
            # this will make sense one day
            if first:
                first=False
            else:
                status-=1
                first=True
        elif status == 0:
            going_up = True
            # just think 01234567 and 76543210 emitted forever from the machine
            if first:
                first=False
            else:
                status+=1
                first=True
        elif going_up:
            status +=1
        elif going_up == False:
            status -=1

The generator that returns the snake returns the correct index to be converted into the snake. With each iteration it snakes back and forth across the array retrieving values and putting them in the correct order.

Sprites

In order to keep the processing on the target hardware low we preprocess the data coming before sent. This means converting the text into RGB values on the signage service before sending it down the pipeline. The sprite services exposes an interface that converts strings to arrays, supporting mutiple fonts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
font = {"x": [
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0],
        [1, 0, 0, 0, 1],
        [0, 1, 0, 1, 0],
        [0, 0, 1, 0, 1],
        [0, 1, 0, 1, 0],
        [1, 0, 0, 0, 1],
    ],
    "y": [
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0],
        [1, 0, 0, 0, 1],
        [1, 0, 0, 0, 1],
        [0, 1, 1, 1, 1],
        [0, 0, 0, 0, 1],
        [0, 1, 1, 1, 0],
    ],
    "z": [
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0],
        [1, 1, 1, 1, 1],
        [0, 0, 0, 1, 0],
        [0, 0, 1, 0, 0],
        [0, 1, 0, 0, 0],
        [1, 1, 1, 1, 1],
    ]
}

The code block above shows an available font and sprites for the letters X, Y, and Z

Receiver

The receiver code was written in micropython for the sake of development speed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# setup
import network
sta_if = network.WLAN(network.STA_IF)
ap_if = network.WLAN(network.AP_IF)
if not sta_if.isconnected():
    sta_if.active(True)
    sta_if.connect('lol', 'no')
    ap_if.active(False)

    while not sta_if.isconnected():
        pass


import neopixel, machine

np = neopixel.NeoPixel(machine.Pin(13), 8*32)
import time, ujson
from umqtt.simple import MQTTClient

# callbacks
def sub_cb(topic, msg):
    #print((topic, msg))
    decoded = msg.decode('utf-8')
    loaded = ujson.loads(decoded)
    kw = loaded.get('kwargs')
    snake = kw.get('snake')
    for i in range(0, 8*32):
        np[i] = snake[i]
    np.write()

# main function
def main(server="192.168.13.71"):
    c = MQTTClient("umqtt_client",server)
    c.set_callback(sub_cb)
    c.connect()
    c.subscribe(b"us.thingcosm.data.signage.pub.door.panel")

    while True:
        if True:
            c.wait_msg()
        else:
            c.check_msg()
            time.sleep(1)

    c.disconnect()

# wrap it all in a try:except    
if __name__ == "__main__":
    try:
        main()
    except Exception:
        import machine
        machine.reset()

The board connects to the wifi, connects to the MQTT transport running on the crossbar.io router and then subscribes to the MQTT topic and has it setup to trigger a callback. The callback simply takes the array values that were published and shoves them down the wire to the display components.

Train Sign
posted Nov. 27, 2017, 6 p.m.
under signage · perma

Train Sign

I had a bit of a problem with ordering random LEDs on Ali so when I saw 8x32 WS2812 LED arrays for a good price I jumped on them. A while after the displays arrived I was looking at the train signs on the MBTA platform and was inspired. I did some measurements and designed an enclosure in Inkscape. I wanted it to be visible from across the room. It features an acrylic diffusal layer infront of a grid of holes on the layer below. This allows the light to be contained to the segment below and then has it diffuse on the layer above. After some initial trouble with scaling to fit the array, I finished cutting all the parts at the local space.

Cut and Assembled

Cut and Assembled

Cut and Assembled 2

Cut and Assembled 2

Assembly

The box was assembled and the electronics put inside. A hole was cut on the other side to run a USB cable in to power the ESP8266 and Array.

Electronics Assembly

Electronics Assembly

A quick of the back napkin calculation notes that the display at full white will draw over fifteen amps, so I had to be careful to limit the style of the display.

Grid Testing

Grid Testing

Software

The first test involved figuring out the electrical order of the array. This was done by iterating over values and noting how the array was filled.

Abstract Train

Abstract Train

From there the implementation was incredibly abstract and was a series of lines representing trains and how far away from the stop they were. This was done by publishing to an MQTT topic and doing some transformation on the time and spreading it across the display. In the image above, there are four trains within twentry minutes of the station, the green ones ten+ minutes away, the yellow one less than ten and more than 3, and the red one imminent.

TrainSign Result Time

TrainSign Result Time

TrainSign Result Train

TrainSign Result Train

TrainSign Result Weather

TrainSign Result Weather

This implementation however, was not super friendly to people reading it so I created the Extensible Signage Architecture and gave it the data sources for time, weather and train distance.

In the next post I detail the architectural considerations in building the Extensible Signage Architecture.