After laser cutting and building my information display I wanted to have a backing software architecture that met two requirements.
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 is handled entirely in a Thingistry namespace that the service reads from. Below are two example configurations, simple and complex.
{
"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.
"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.
"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.
"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 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.
yield self.publish("us.thingcosm.data.services.nightshift", **{ "text_micro": {"DATE":str_disp_date, "TIME12": str_disp_time_12, "TIME24": str_disp_time_24} })
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
@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.
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.
In order to keep the processing on the target hardware low, we preprocess the data coming before it is 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 multiple fonts.
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
The receiver code was written in micropython for the sake of development speed.
# 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.