Led-matrix displays zijn handig in het gebruik van apparaten waar je op prominente wijze wat informatie op wilt tonen. Ik gebruik bijvoorbeeld vier 8×8 led matrix displays met een Max7219 die tot 32×8 zijn gecombineerd voor een klein display voor tijdweergave en stroomverbruik gedurende de dag. De displays verbinden onderling en aan de microcontroller via drie draden en vereisen een enkelvoudige 5V voedingsspanning. Bij het researchen naar een handig lettertype voor het 32×8 display kwam ik een fascinerende led-zandloper van David Projectos op Instagram tegen. De animatie van de ‘zandkorrels’ deden me haast magisch aan en mijn vingers jeukten om dat zelf te programmeren.
David liet in zijn Instagram zien, dat hij de zandlopers had gemaakt met 8×8 led matrix displays en ‘een Arduino’. Nog wat zoektijd later kwam ik de website van AdaFruit tegen, bekend van hun open source elektronicaprojecten, die met dezelfde Max7219-gebaseerde 8×8 displays een eigen variant van de led matrix zandloper hadden gemaakt. AdaFruit had hun hele project als open source hardware en software gepubliceerd, maar de Arduino code, met vele bibliotheken en onnavolgbare afslagen, maakten me niet direct enthousiast. Het viel alleen te compileren als je alle Adafruit bibliotheken gebruikte en de code zelf was grotendeels weggeborgen. En laten we wel wezen: ik was naar dit project gekomen om zélf software te schrijven, niet om die van Adafruit gebruiken.
Om aan het programmeren te raken heb ik eerst een kunststof zandloperbehuizing gemaakt, afgedrukt met mijn Dremel 3D45 en ‘goudkleurige’ PLA aan de hand van de .STL bestanden van AdaFruit op Thingiverse. Het voorpaneel had nog geen uur nodig om te printen zodat ik gelijk daarna mijn eigen matrixdisplays kon uitproberen: ze pasten! De zandloper heeft veel verschillende onderdelen, die allemaal op een ingenieuze wijze in elkaar klikken, daarom op een lik superlijm voor de basisbehuizing op de voet en het vastzetten van beide 8×8 displays na geen extra gereedschap of montage nodig.
Ik besloot gebruik te maken van een Wemos D1mini met ESP8266, te programmeren met een nieuw tooltje genaamd ESPHome dat Arduino/Processing ondersteunt. ESPHome heeft als bijzondere eigenschap het via WiFi programmeren van de ESP8266, hetgeen een uitkomst is als je met een prototype bezig bent. De ESP8266 maakt via drie draden verbinding met het matrixdisplay en de kleine print van de D1mini die ik hiervoor gebruikte was gemakkelijk weg te bergen in de behuizing van de zandloper.
Het aansluiten van de led matrix displays ging eenvoudig, en ik had er net veel ervaring mee opgedaan bij het maken van het bovengenoemde 32×8 tijdweergavedisplay, dus na een paar minuten solderen en 24 uur 3D-printen later kon ik aan de slag met de animatie van de zandloper. Na het bepalen van een coordinatenstelsel ging de eerste pixels aan- en uitzetten eenvoudig genoeg, maar ik realiseerde me al snel dat het animeren een ding zou gaan worden. Ik wendde me tot mijn beste animatiegereedschap om een ontwerp te maken van realistisch ‘bewegende’ zandkorrels: Microsoft PowerPoint.
Ik had besloten dat ik iedere zandkorrel zou uitprogrammeren. Er passen 64 pixels op ieder display en ik had bedacht, dat het bovenste display 59 pixels aan had staan die één voor één via een overtuigende valbeweging naar het onderste display zouden vallen, iedere seconde één. Het kostte me wat tijd, voor ik een algoritme had ontdekt die een redelijk natuurlijke animatie van de vallende zandkorrels zou realiseren, zonder dat ik hiervoor formules van zwaartekracht of golfbewegingen zou moeten implementeren. Ik heb de eerste frames uitgetekend in PowerPoint en de x en y posities van de meest recente zandkorrel in een tabel gezet om te zien, of ik hier een patroon in kon ontdekken.
Met een patroon in de hand heb ik vervolgens gekeken of ik een vallende zandkorrel (de blauwe pixel) op de al aanwezige zandkorrels (de rode) zou kunnen laten vallen en de vallende zandkorrel (oranje) dan naar de plek zou kunnen animeren. Hier leek een elegant algoritme voor mogelijk.
In het kort zijn er drie situaties te onderkennen: de vallende zandkorrel, de bewegende zandkorrel en de positie waar de zandkorrel op moet landen. De positie van een zandkorrel wordt gerepresenteerd door zijn coordinaten (x,y)
. Het coordinatenstelsel valt in het bereik (0,0)-(15,7). Afgeleiden van de coordinaten zijn de val_pos (y-coordinaat), de laag en de korrels_op_laag.
De functionaliteit is de volgende geworden:
- Iedere seconde ‘valt’, in de seconde voorafgaande aan de seconde, een zandkorrel (led) van de voorraad op het bovenste display op de hoop zandkorrels in het onderste display. Een zandkorrel verdwijnt van de voorraad en animeert:
- Eerst als een vallende zandkorrel totdat de stapel is bereikt
- Dan links of rechts van de stapel afhankelijk van een even of oneven seconde
- De zandkorrel blijft oplichten op de plek waar deze hoort
- De laatste seconde (59) worden de verstreken seconden weer omhoog gezogen
- Het is ook mogelijk om een tijd in decimalen te tonen
Ik moet zeggen dat ik blij verrast was met de relatief natuurlijke manier waarop de zandkorrels lijken te vallen en over de stapel lijken te rollen naar hun plek. Ik kan de lusjes en condities dromen en herken hoe de zandkorrels links of rechts vallen. Maar toch, mooi eindresultaat.
Het volledige programma in ESPHome configuratie met Arduino programmcode erin verwerkt ziet er als volgt uit:
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | # Goudkleurige full-scale zandloper met twee 8x8 matrix displays esphome: name: esphome-web-9fc5fd esp8266: board: d1_mini logger: level: info api: ota: wifi: ssid: !secret wifi_ssid password: !secret wifi_password ap: ssid: "Esphome-Web-9Fc5Fd" password: !secret wifi_ap_password captive_portal: spi: clk_pin: D0 mosi_pin: D1 font: # Nette 3x6 font voor kleine teksten en getallen - file: "fonts/uncle_lee3x6.ttf" id: uncle_lee_font size: 11 glyphs: '0123456789' time: - platform: sntp id: rtctime switch: - platform: template name: "Zandloper Toon Tijd" id: toon_tijd optimistic: true display: - platform: max7219digit id: zandloperdisplay cs_pin: D2 num_chips: 2 intensity: 1 update_interval: 30ms lambda: > int MAX_X = 15; int MAX_Y = 7; static int vorige_seconde = 0; static int val_pos = 0; static int vul_aantal = 0; int korrel, korrels, laag, korrels_op_laag, volgende_korrels; int resterende_korrels_op_laag, resterend_volume; int huidige_seconde = id(rtctime).now().second; if (!id(toon_tijd).state) { korrels = vorige_seconde; laag = sqrt(korrels); korrels_op_laag = korrels - (laag * laag); volgende_korrels = huidige_seconde; resterend_volume = sqrt(volgende_korrels + 4); resterende_korrels_op_laag = volgende_korrels + 4 - (resterend_volume * resterend_volume); it.filled_rectangle(0, 0, 8, 8, COLOR_ON); it.filled_rectangle(0, 0, resterend_volume, resterend_volume, COLOR_OFF); it.line(resterend_volume, 0, resterend_volume, resterende_korrels_op_laag / 2, COLOR_OFF); if (resterende_korrels_op_laag % 2 != 0) { it.line(0, resterend_volume, resterende_korrels_op_laag / 2, resterend_volume, COLOR_OFF); } else { if (resterende_korrels_op_laag / 2 > 0) { it.line(0, resterend_volume, resterende_korrels_op_laag / 2 - 1, resterend_volume, COLOR_OFF); } } if (huidige_seconde != vorige_seconde) { if (huidige_seconde == 0) { if (vul_aantal < 60) { vul_aantal += 6; int aantal_korrels = 60 - vul_aantal; resterend_volume = sqrt(aantal_korrels + 4); resterende_korrels_op_laag = aantal_korrels + 4 - (resterend_volume * resterend_volume); it.filled_rectangle(0, 0, 8, 8, COLOR_ON); it.filled_rectangle(0, 0, resterend_volume, resterend_volume, COLOR_OFF); it.line(resterend_volume, 0, resterend_volume, resterende_korrels_op_laag / 2, COLOR_OFF); if (resterende_korrels_op_laag % 2 != 0) { it.line(0, resterend_volume, resterende_korrels_op_laag / 2, resterend_volume, COLOR_OFF); } else { if (resterende_korrels_op_laag / 2 > 0) { it.line(0, resterend_volume, resterende_korrels_op_laag / 2 - 1, resterend_volume, COLOR_OFF); } } resterend_volume = sqrt(aantal_korrels); it.filled_rectangle(MAX_X - resterend_volume + 1,MAX_Y - resterend_volume + 1, resterend_volume, resterend_volume); } else { vul_aantal = 0; vorige_seconde = huidige_seconde; } } else { if (val_pos < (8 - laag)) { it.draw_pixel_at(8 + val_pos, val_pos); } else { if (korrels_op_laag % 2 == 0) { it.draw_pixel_at(8 + val_pos, MAX_Y - laag); } else { it.draw_pixel_at(MAX_X - laag, val_pos); } } val_pos += 1; if (val_pos >= (7 - (korrels_op_laag / 2))) { vorige_seconde = huidige_seconde; val_pos = 0; } } } if (huidige_seconde != 0) { it.filled_rectangle(MAX_X - laag + 1,MAX_Y - laag + 1, laag, laag); for (korrel = 0; korrel < korrels_op_laag; korrel++) { if (korrel % 2 == 0) { it.draw_pixel_at(MAX_X - (korrel / 2), MAX_Y - laag); } else { it.draw_pixel_at(MAX_X - laag, MAX_Y - (korrel / 2)); } } } } else { it.printf(0, 4, id(uncle_lee_font), COLOR_ON, TextAlign::CENTER_LEFT, "%02u", id(rtctime).now().hour); it.printf(8, 4, id(uncle_lee_font), COLOR_ON, TextAlign::CENTER_LEFT, "%02u", id(rtctime).now().minute); } |