MuxMatrix
Project MuxMatrix | |
---|---|
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)