High-tech zandloper met 128 leds en ESP8266 geprogrammeerd met Arduino en ESPHome

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.

Led 8×8 matrix met Max7219 controller aangesloten op een Arduino

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.

8×8 led matrix displays in het voorpaneel van de zandloper proefpassen en met een drup superlijm vastzetten

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.

Wemos D1mini met ESP8266 als besturingscomputer 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.

Eerste pixels lichten op, maar het zou nog wat langer duren voordat ze zouden bewegen

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.

Eerste 9 frames uitgetekend in PowerPoint, op zoek naar een patroon

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.

Een paar uur verder en een algoritme ontstaat voor de plaatsing van zandkorrels 0-59 (seconden)

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.

Zandloper met animatie

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);
      }

Plaats een reactie