MuxMatrix

From RevSpace
Jump to: navigation, search
Project MuxMatrix
IMG 6806h.jpg
Niet van de film
Status In progress
Contact Mux
Last Update 2017-06-18

De MuxMatrix

Gezien er op Revspace nog lang niet genoeg LEDs, LED-Matrices en ESP8266/Arduino-gebaseerde dingen te vinden zijn, leek het me een goed idee om eens een 80x32 monochroom waterdicht display te maken. De MuxMatrix - gemaakt door onze Heer en Keizer mux bij de gratie van zijn geniale mechanische, elektronische en programmeerkunsten - trekt alle sensordata van MQTT en geeft dit weer op het display.

Constructie

Het hart van dit project is het Baco Army Goods 32x16-pixel rode LED-matrixbord (beschikbaar in 2009-2010), met name bekend van het Tweakers.net EL-forum. Vijf van deze displays zijn ge-daisy-chained tot één groot scherm en aan een Arduino Nano gehangen. Deze microcontroller houdt een buffer van 320 bytes in geheugen die hij zo snel als maar kan het scherm in dauwt. Dit zorgt voor een refresh rates richting de paarhonderd Hz. De buffer kan worden veranderd door simpelweg naar de seriële poort op 115.2kbaud te schrijven; de buffer wordt dan, beginnend bij adres 0, overschreven. Wanneer langer dan 10 msec geen bytes worden gestuurd, wordt de pointer gereset naar 0. De buffer doet automatisch wraparound na 320 bytes.

De ESP8266 draait NodeMCU 5.1.4 en een lua-programma dat automatisch verbindt met het publieke 2.4GHz netwerk, connect naar onze mosquitto-server en subscribet op /revspace/sensors/#. De ontvangen sensoren worden in een hashmap gegooid en iedere 2 seconden wordt een nieuwe sensorwaarde uit deze lijst gekozen om te displayen. De sensorwaarde wordt dan ongezet in een string, de string wordt via een giga-lookup table met een 8x8 pixel font omgezet in letters in een bitmap in geheugen, de bitmap wordt dan omgezet naar het juiste format voor het scherm en vervolgens wordt de buffer door UART naar de AVR gespoten. Simpel.

De ESP8266 en AVR worden gevoed vanuit het scherm, dat op 5V draait. Omdat de ESP8266 op 3.3V draait zit er een level shifter tussen de ESP en AVR, en is er een 1117 3.3V spanningsregelaar onder de ESP te vinden. Een ongebruikte pin op de header wordt gebruikt om vanuit de dotmatrices 5V richting de microcontroller en ESP te sturen. De schermen worden gevoed vanuit een 12 parallel geschakelde TP-link 5V/1A voedinkjes die onze beste vriend en consort in het Keizershuis Sebastius heeft meegenomen, en nog steeds in de snoepautomaat te vinden zijn. Al deze voedingen parallel schakelen heeft objectief geen echte reden, anders dan dat het kan en dat dus iemand het moest doen.

Fysiek zit alles op een frame van 40x40mm t-slotprofiel geklemd, met een polycarbonaat window.

ToDo/toekomstige functionaliteit

Meest gewenste functionaliteit is dat er live getekend kan worden op het scherm. Dit is momenteel echter heel lastig te realiseren, omdat op de space MQTT effectief readonly is vanuit human interfaces. (bijv. browser). Dat gezegd hebbende, is het plan om het scherm naar /revspace/muxmatrix/draw te laten luisteren. Wanneer daar een message wordt gedumpt, interpreteert het scherm dit als een base64-encoded monochrome bitmap, decodeert deze en toont dit op het scherm. Dit is nog onvolledig geïmplementeerd, maar de base64-decoding en conversie is al aanwezig in de firmware.

Verder is wenselijk:

- Een passender font (bijv. 5x7)
- Een diffuser voor het scherm, zodat tekst beter leesbaar wordt (raamfolie is waarschijnlijk voldoende)
- LED-strips om het scherm heen. Het scherm is expres zwevend in het frame geplaatst zodat er meer LEDs langs kunnen schijnen. MEER LEDs!
- impregnering van het laser-gesneden hout en conformal coating op de elektronica
- Meer hotglue

Filmpjes! Foto's!

Ik heb een JijBuis-filmpje gemaakt over dit project. Om internationale diplomatieke redenen introduceer ik mijzelf hier niet als Heer en Keizer, hoewel dit feit uiteraard niet ter discussie staat.

https://www.youtube.com/watch?v=RQCOlsAp9yA

Code

Omdat wikipagina's op deze website niet lang genoeg zijn, hier verbatim alle code

main.c

/*
 * wifi-leddisplay.c
 *
 * Created: 30-May-17 14:49:30
 * Author : Emile
 */ 

/*

Short explanation of this program:
This AVR receives serial data from an ESP8266 and puts this into a big array. Then it displays
this array on the dotmatrix display every 8ms (~125fps). 

The display routine runs every 1ms, drawing one line every time. 8 lines make a full display.

The USART is configured to simply put all received bytes into the buffer. It is configured at
76k8 and expects 320 bytes. Theoretically this could sustain 24fps, but in practice the
interrupts do tend to crowd each other out a bit, so be safe and keep it to ~10fps

The end of a transmission is denoted by at least 10ms of no data. 
*/

#define F_CPU 16000000UL

#include <util/delay.h>
#include <avr/io.h>
#include <avr/interrupt.h>

//PORTD
#define LDAT_PIN		(1 << 3) //arduino nano D2
#define	UDAT_PIN		(1 << 2) //D3
#define CLK_PIN			(1 << 7) //D7

//PORTC
#define A0_PIN			(1 << 0) //A0
#define A1_PIN			(1 << 1) //A1
#define A2_PIN			(1 << 2) //A2

//PORTB
#define STROBE_PIN		(1 << 0) //D8
#define OE_PIN			(1 << 1) //D9

//macros
#define CLK_STROBE()	PORTD |= CLK_PIN; _delay_us(10); PORTD &= ~CLK_PIN
#define STROBE_STROBE()	PORTB &= ~STROBE_PIN; _delay_us(10); PORTB |= STROBE_PIN

//constants
#define BITMAP_SIZE		340

//globals
volatile	uint8_t		i			= 0;
		uint16_t	j			= 0;
		uint8_t		bitmap[BITMAP_SIZE];
		uint16_t	bmp_i		= 0;


//prototypes
void init(void);
void drawbitmap(void);

int main(void)
{
	init();
	drawbitmap();
    /* Replace with your application code */
    while (1) 
    {
		PORTB &= ~STROBE_PIN;
		for(j = 0; j < 160; j++){
			PORTD &= ~(CLK_PIN | LDAT_PIN | UDAT_PIN);
			if(bitmap[j + 160] & (1 << i)) PORTD |= LDAT_PIN;
			if(bitmap[j] & (1 << i)) PORTD |= UDAT_PIN;
			PORTD |= CLK_PIN;
		}
		PORTB |= STROBE_PIN;
		PORTD |= OE_PIN;
		PORTC = i;
		PORTD &= ~OE_PIN;
		if(++i >= 8) i = 0;
    }
}


void init(void){
	DDRD	= LDAT_PIN | UDAT_PIN | CLK_PIN;
	DDRC	= A0_PIN | A1_PIN | A2_PIN;
	DDRB	= STROBE_PIN | OE_PIN;
	
	PORTB  |= STROBE_PIN;
	PORTD  |= OE_PIN;
	
	//setup data timeout timer: 10ms (16MHz/256/625=100Hz)
	OCR1A	= 625;
	TCCR1B	= (1 << WGM12) | (1 << CS12);		//16MHz/64, CTC
	TIMSK1	= (1 << OCIE1A);					//enable compare interrupt
	
	
	//setup USART at 115.2kHz (16MHz/(16*(8+1)), 8N0
	UCSR0A |= (1 << U2X0);						//double baud rate
	UCSR0B	= (1 << RXCIE0) | (1 << RXEN0);		//enable receive and rx interrupt
	UBRR0	= 16;								//baud rate, see calc above
	
	sei();
}

//draw reset bitmap: just a block of 8 red pixels at the top-left
void drawbitmap(void){
	for(j = 0; j < 340; j++){
		bitmap[j] = 0;
	}
	bitmap[160] = 0xff;
}

//fires on each byte reception
ISR(USART_RX_vect){
	bitmap[bmp_i++] = UDR0;		//store byte
	TCNT1	= 0;				//reset timeout timer	
	TIMSK1	= (1 << OCIE1A);	//enable compare interrupt
}

ISR(TIMER1_COMPA_vect){
	bmp_i = 0;
	TIMSK1 = 0;					//disable interrupt
}

init.lua

uart.setup(0, 115200, 8, uart.PARITY_NONE, uart.STOPBITS_1)


-- load fonts
dofile('font.lc')
-- load base64 decoding
dofile('base64.lc')
-- load main program
dofile('main.lc')

base64.lua

-- Lua 5.1+ base64 v3.0 (c) 2009 by Alex Kloss <alexthkloss@web.de>
-- licensed under the terms of the LGPL2

--local moduleName = ... 
--local M = {}
--_G[moduleName] = M

-- character table string
local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
 
-- encoding
function base64enc(data)
    return ((data:gsub('.', function(x) 
        local r,b='',x:byte()
        for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
        return r;
    end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
        if (#x < 6) then return '' end
        local c=0
        for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
        return b:sub(c+1,c+1)
    end)..({ '', '==', '=' })[#data%3+1])
end
 
-- decoding
function base64dec(data)
    data = string.gsub(data, '[^'..b..'=]', '')
    return (data:gsub('.', function(x)
        if (x == '=') then return '' end
        local r,f='',(b:find(x)-1)
        for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
        return r;
    end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
        if (#x ~= 8) then return '' end
        local c=0
        for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
        return string.char(c)
    end))
end

font.lua

--this function copies the relevant characters of an 8x8 font
--into the display buffer
local font = {
0, 0, 0, 0, 0, 0, 0, 0, 
24, 60, 60, 24, 24, 0, 24, 0, 
108, 108, 0, 0, 0, 0, 0, 0, 
108, 108, 254, 108, 254, 108, 108, 0, 
48, 124, 192, 120, 12, 248, 48, 0, 
0, 198, 204, 24, 48, 102, 198, 0, 
56, 108, 56, 118, 220, 204, 118, 0, 
96, 96, 192, 0, 0, 0, 0, 0, 
24, 48, 96, 96, 96, 48, 24, 0, 
96, 48, 24, 24, 24, 48, 96, 0, 
0, 102, 60, 255, 60, 102, 0, 0, 
0, 48, 48, 252, 48, 48, 0, 0, 
0, 0, 0, 0, 0, 48, 48, 96, 
0, 0, 0, 252, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 48, 48, 0, 
6, 12, 24, 48, 96, 192, 128, 0, 
124, 198, 206, 222, 246, 230, 124, 0, 
48, 112, 48, 48, 48, 48, 252, 0, 
120, 204, 12, 56, 96, 204, 252, 0, 
120, 204, 12, 56, 12, 204, 120, 0, 
28, 60, 108, 204, 254, 12, 30, 0, 
252, 192, 248, 12, 12, 204, 120, 0, 
56, 96, 192, 248, 204, 204, 120, 0, 
252, 204, 12, 24, 48, 48, 48, 0, 
120, 204, 204, 120, 204, 204, 120, 0, 
120, 204, 204, 124, 12, 24, 112, 0, 
0, 48, 48, 0, 0, 48, 48, 0, 
0, 48, 48, 0, 0, 48, 48, 96, 
24, 48, 96, 192, 96, 48, 24, 0, 
0, 0, 252, 0, 0, 252, 0, 0, 
96, 48, 24, 12, 24, 48, 96, 0, 
120, 204, 12, 24, 48, 0, 48, 0, 
124, 198, 222, 222, 222, 192, 120, 0, 
48, 120, 204, 204, 252, 204, 204, 0, 
252, 102, 102, 124, 102, 102, 252, 0, 
60, 102, 192, 192, 192, 102, 60, 0, 
248, 108, 102, 102, 102, 108, 248, 0, 
254, 98, 104, 120, 104, 98, 254, 0, 
254, 98, 104, 120, 104, 96, 240, 0, 
60, 102, 192, 192, 206, 102, 62, 0, 
204, 204, 204, 252, 204, 204, 204, 0, 
120, 48, 48, 48, 48, 48, 120, 0, 
30, 12, 12, 12, 204, 204, 120, 0, 
230, 102, 108, 120, 108, 102, 230, 0, 
240, 96, 96, 96, 98, 102, 254, 0, 
198, 238, 254, 254, 214, 198, 198, 0, 
198, 230, 246, 222, 206, 198, 198, 0, 
56, 108, 198, 198, 198, 108, 56, 0, 
252, 102, 102, 124, 96, 96, 240, 0, 
120, 204, 204, 204, 220, 120, 28, 0, 
252, 102, 102, 124, 108, 102, 230, 0, 
120, 204, 224, 112, 28, 204, 120, 0, 
252, 180, 48, 48, 48, 48, 120, 0, 
204, 204, 204, 204, 204, 204, 252, 0, 
204, 204, 204, 204, 204, 120, 48, 0, 
198, 198, 198, 214, 254, 238, 198, 0, 
198, 198, 108, 56, 56, 108, 198, 0, 
204, 204, 204, 120, 48, 48, 120, 0, 
254, 198, 140, 24, 50, 102, 254, 0, 
120, 96, 96, 96, 96, 96, 120, 0, 
192, 96, 48, 24, 12, 6, 2, 0, 
120, 24, 24, 24, 24, 24, 120, 0, 
16, 56, 108, 198, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 0, 0, 255, 
48, 48, 24, 0, 0, 0, 0, 0, 
0, 0, 120, 12, 124, 204, 118, 0, 
224, 96, 96, 124, 102, 102, 220, 0, 
0, 0, 120, 204, 192, 204, 120, 0, 
28, 12, 12, 124, 204, 204, 118, 0, 
0, 0, 120, 204, 252, 192, 120, 0, 
56, 108, 96, 240, 96, 96, 240, 0, 
0, 0, 118, 204, 204, 124, 12, 248, 
224, 96, 108, 118, 102, 102, 230, 0, 
48, 0, 112, 48, 48, 48, 120, 0, 
12, 0, 12, 12, 12, 204, 204, 120, 
224, 96, 102, 108, 120, 108, 230, 0, 
112, 48, 48, 48, 48, 48, 120, 0, 
0, 0, 204, 254, 254, 214, 198, 0, 
0, 0, 248, 204, 204, 204, 204, 0, 
0, 0, 120, 204, 204, 204, 120, 0, 
0, 0, 220, 102, 102, 124, 96, 240, 
0, 0, 118, 204, 204, 124, 12, 30, 
0, 0, 220, 118, 102, 96, 240, 0, 
0, 0, 124, 192, 120, 12, 248, 0, 
16, 48, 124, 48, 48, 52, 24, 0, 
0, 0, 204, 204, 204, 204, 118, 0, 
0, 0, 204, 204, 204, 120, 48, 0, 
0, 0, 198, 214, 254, 254, 108, 0, 
0, 0, 198, 108, 56, 108, 198, 0, 
0, 0, 204, 204, 204, 124, 12, 248, 
0, 0, 252, 152, 48, 100, 252, 0, 
28, 48, 48, 224, 48, 48, 28, 0, 
24, 24, 24, 0, 24, 24, 24, 0, 
224, 48, 48, 28, 48, 48, 224, 0, 
118, 220, 0, 0, 0, 0, 0,  0
}


function displaytext(str, xa, ya)
    local s = 0
    local jx = 0

    strlen = string.len(str)
    if strlen > 40 then
        strlen = 40
    end
    for i = 1,strlen do
        for j = 1, 8 do
            y = ya + math.floor((xa + i - 1) / 10)
            x = (xa + i - 1) % 10 + 1
            s = string.byte(str,i)
            if s < 32 then s = 32 end
            if s > 126 then s = 32 end
            s = s - 32            
            bytebuf[y * 80 + x + j * 10] = font[s * 8 + j]
        end
    end
    bmp2display()
end

main.lua

-- This screen subscribes to /revspace/sensors
-- and displays all sensor values in sequence,
-- about 2 seconds each

-------------------
--    GLOBALS    --
-------------------
local displaybuf = {} --this goes to the display
bytebuf    = {} --this contains the decoded base64
--local mqttbuffer = "" --this is received from the mqtt server in base64
local connected = 0
sensors = {}
table_idx = 0
tablelen = 0

--empty buffer
function clear()
    for i=1,320 do
        displaybuf[i] = 0
        bytebuf[i] = 0
    end
end

clear()
----------------------
-- HELPER FUNCTIONS --
----------------------

-- converts bitmaps to display format
function bmp2display()
    local a = 1
    for i = 2, 10, 2 do
        for j = 1, 32, 1 do
            displaybuf[a] = bytebuf[i + (j - 1) * 10]
            a = a + 1
        end
    end
    for i = 1, 9, 2 do
        for j = 1, 32, 1 do
            displaybuf[a] = bytebuf[i + (j - 1) * 10]
            a = a + 1
        end
    end
end

-- converts base64 to bytes
function msg2buffer(strin)
    local strout = base64dec(strin)
    for i=1,320 do
        bytebuf[i] = string.byte(strout,i)
    end
end



--------------------
--   MAIN SETUP   --
--------------------

-- periodic screen refresh timer
timer1 = tmr.create()
timer1:register(2000,tmr.ALARM_AUTO, function(t)
    
    -- increment index and wraparound
    table_idx = table_idx + 1
    if table_idx > tablelen then table_idx = 1 end

    local it = 1
    for k,v in pairs(sensors) do
        if(it == table_idx) then
            clear() 
            displaytext(string.sub(k,18,-1),0,0)
            displaytext(string.sub(v,1,10),0,3)
        end
        it = it + 1
    end        
    
    drawdisplay()
end)
timer1:start()

function drawdisplay()
    uart.write(0,0) --shift display down by 1
    for i=1,320 do
        local str = tmr.now()
        uart.write(0, displaybuf[i])
    end
end

-- wifi setup
wifi.setmode(wifi.STATION)
wifi.sta.config({ssid="revspace-pub-2.4ghz"})
wifi.sta.connect()

-- display 'not connected'
displaytext('NO CONNECTION',0,1)


-- display 'connected' message when it gets an IP
wifi.eventmon.register(wifi.eventmon.STA_CONNECTED, function(T)
    displaytext('CONNECTED    ',0,1)
    m = mqtt.Client("muxmatrix", 10)

    m:on("message", function(client, topic, message)
        sensors[topic] = message
        -- get true size of table
        tablelen = 0
        for _ in pairs(sensors) do tablelen = tablelen + 1 end 
        --displaytext('MESSAGE   ')
    end)
    
    m:connect("mosquitto.space.revspace.nl", 1883, 0, function(client)
        --displaytext("MQTT CONNECT")
        client:subscribe({["/revspace/sensors/#"]=0,["/revspace/muxmatrix/draw"]=0}, 0, function(client)
            --displaytext("SUBSCRIBED")
            connected = 1
        end)
    end, function(client, reason)
        if    (reason == mqtt.CONN_FAIL_SERVER_NOT_FOUND) then
            displaytext("SERVER OFF",0,0)
        elseif(reason == mqtt.CONN_FAIL_NOT_A_CONNACK_MSG) then
            displaytext("NO CONNACK",0,0)
        elseif(reason == mqtt.CONN_FAIL_DNS) then
            displaytext("DNS FAIL  ",0,0)
        elseif(reason == mqtt.CONN_FAIL_TIMEOUT_RECEIVING) then
            displaytext("TIMEOUT RX",0,0)
        elseif(reason == mqtt.CONN_FAIL_TIMEOUT_SENDING) then
            displaytext("TIMEOUT TX",0,0)
        elseif(reason == mqtt.CONNACK_REFUSED_PROTOCOL_VER) then
            displaytext("PROTOCOL F",0,0)
        elseif(reason == mqtt.CONNACK_REFUSED_ID_REJECTED) then
            displaytext("ID REFUSED",0,0)
        elseif(reason == mqtt.CONNACK_REFUSED_SERVER_UNAVAILABLE) then
            displaytext("UNAVAILABL",0,0)
        elseif(reason == mqtt.CONNACK_REFUSED_BAD_USER_OR_PASS) then
            displaytext("BAD PWDUSR",0,0)
        elseif(reason == mqtt.CONNACK_REFUSED_NOT_AUTHORIZED) then
            displaytext("NOAUTH    ",0,0)
        else
            displaytext("UNKNOWNERR",0,0)
        end 
    end)    
end)