Das ist schon so, wenn man sich erstmals und mit wenigen oder keinen Vorkenntnissen in die Welt der Mikroprozessoren "hineinwagt". Wenn Du etwas mehr willst, als nur vorhandenen Code zusammenzubasteln, bis das gewünschte Ergebnis mehr oder weniger herauskommt, braucht es einen gewissen Zeit- (und Denk-)aufwand
Wenn man dann stecken bleibt, kann i.d.R. dieses Forum weiterhelfen.
Was sich für den Anfang lohnt, ist es, sich diese Pdf
https://www.arduinoforum.de/referenz.php
herunterzuladen und ganz in Ruhe von vorne nach hinten durchzulesen und Stück für Stück auch auszuprobieren! Lernerfolg stellt sich dann ein, wenn man den Ablauf eines Sketches noch nachvollziehen kann. Wer gleich mit einem komplexen Programm beginnt, hat es wesentlich schwerer Fehler und Missverständnisse aufzudecken, besonders dann, wenn sich mehrere Fehler addieren oder gegenseitig verdecken ...
Hier mal ein einfacher Sketch zum Nachvollziehen der Anwendung von millis():
/*
Ein intensiv dokumentierte Einführungssketch in die Verwendung von millis() zum zeitgesteuerten Schalten
einer LED
von ec2021 für https://forum.arduino.cc/t/esp32-wrover-board/1099241/15
Ziel: Die Schaltung und der Sketch sollen es ermöglichen, eine LED zeitgesteuert blinken zu lassen,
ohne die Abläufe im Hauptprogramm (der loop()) zu blockieren
Ein- und Ausschaltzeit sind identisch
Hardware:
Arduino UNO kann aber auch ein weitgehend beliebiges anderes Board sein
ggf. wäre dann der Digitalausgang für die Led-Ansteuerung anzupassen
Led Farbe beliebig, für das Beispiel wurde grün gewählt
Vorwiderstand Dient der Strombegrenzung am Arduino-Pin, da dort i.d.R. maximal 40 mA
verfügbar sind. Kann entfallen bei Led mit integrierten Vorwiderstand
*/
const byte LEDGRUEN = 6; // Der Anschlusspin der grünen LED wird als const byte definiert,
// damit der Compiler einen Fehler auswirft, wenn versucht wird, diesen Wert
// im Programm zu ändern. Das wäre fatal, da dann alle Aktionen, die den Pin
// LEDGRUEN ansteuern sollten, dann auf irgendeinen anderen Pin wirken würden.
//
// Das muss man nicht so machen, aber es vermeidet im Falle "menschlichen Versagens"
// bei größeren, weniger übersichtlichen Sketchen mühsame Fehlersuche ...
void setup() { // Im sogenannten setup() werden diejenigen Funktionen eingetragen,
// die bei jedem Neustart des Prozessors einmal erfolgen müssen,
// damit eine definierte Ausgangslage für die dann folgenden Abläufe
// vorliegt; i.d.R. werden hier die Modi der Ein-/Ausgabe-Pins gesetzt,
// die serielle Schnittstelle, Wifi- oder Grafik/LCD-Boards initialisiert
// (soweit vorhanden und benötigt)
//
pinMode(LEDGRUEN,OUTPUT); // Hier wird der mit LEDGRUEN bezeichnete Pin als Output gesetzt
}
// Die folgenden Variablen werden in diesem Fall nur in der loop() benötigt.
// Daher kann ihre Deklaration auch nach setup() erfolgen. Werden sie bereits
// im setup() verwendet, müssen diese Zeilen nach oben vor setup() verschoben
// werden.
//
unsigned long currentMillis; // Diese Variable dient der Speicherung des Wertes der Funktion millis()
// Die Funktion millis() zählt die Millisekunden seit dem Start des
// Prozessors; allerdings steckt dahinter keine Echtzeituhr, sondern tatsächlich
// nur ein Zähler, dessen Wert jede Tausendstel Sekunde um 1 erhöht wird.
//
unsigned long zuletztGruen = 0; // Diese Variable dient der Speicherung eines bestimmten Zeitpunkt;
// die Variable zuletztGruen wird definiert mit dem Wert 0 (Null) initialisiert.
// Damit wird sichergestellt, dass bei der allerersten Verwendung
// die Differenz "currentMillis - zuletztGruen" gleich dem Werte von currentMillis
// ist.
//
int WarteZeit = 1000; // Hier wird die Zeit, nach der jeweils geschaltet werden soll, vorgegeben.
// Wäre sie immer fest, würde man hier auch ein const davor schreiben können,
// aber vielleicht möchte man diesen Wert ja auch einmal programmgesteuert
// ändern können ...
void loop() {
currentMillis = millis(); // Hier wird die aktuelle Zeit seit Einschalten des
// zwischengespeichert.
if (currentMillis - zuletztGruen >= WarteZeit){ // Es wird geprüft, ob seit dem letzten Schaltvorgang
// der zum Zeitpunkt zuletztGruen stattfand, mindestens
// die WarteZeit vergangen ist.
//
// Solange das nicht der Fall ist, kann sich der
// Prozessor um andere Dinge kümmern ... sobald wir
// in der loop() Weiteres eingebaut haben
// hier dreht er solange (allerdings ziemlich schnell)
// "Däumchen" ... Aber das kann sich in Zukunft ja noch
// ändern.
//
digitalWrite(LEDGRUEN,!digitalRead(LEDGRUEN)); // Mit digitalRead(LEDGRUEN) wird ermittelt, ob der Pin
// momentan auf LOW oder HIGH liegt. Das Ausrufezeichen
// vor dem digitalRead() invertiert das Ergebnis; aus
// LOW wird alse HIGH, aus HIGH wird LOW.
// digitalWrite(LEDGRUEN,...) schalten damit den Pin
// von LOW auf HIGH oder umgekehrt, und damit
// die LED im Wechsel ein und aus.
//
zuletztGruen = currentMillis; // Hier halten wir den Zeitpunkt dieses Umschaltens fest
// indem wir den in currentMillis gespeicherten Wert
// der Variablen zuletztGruen zuweisen.
}
// hier endet die "Runde" durch loop() und es
// geht oben mit "currentMillis = millis();" weiter
}

siehe hier https://wokwi.com/projects/358810974590930945
Gerne mal ausprobieren, nachvollziehen und z.B. um eine oder zwei LED erweitern ...
Gruß
ec2021