Dołącz do naszej grupy Telegram — tam osobiście odpowiadam na pytania, dzielę się poradami, schematami, kodem i trikami dotyczącymi montażu i konfiguracji.
Перейти в Telegram

NEWS

ESP32s2 mini radio z adresowalnymi diodami LED

Instrukcja konfiguracji i podłączenia

 Aby zapewnić stabilną pracę Twojego internetowego radia opartego na ESP32s2, należy postępować zgodnie z kilkoma ważnymi krokami. Jednym z kluczowych aspektów jest zamontowanie radiatora na mikrokontrolerze, aby uniknąć przegrzania i niestabilnej pracy. Podczas testów wykazano, że bez radiatora urządzenie może działać niestabilnie.

Przygotowanie i zapis interfejsu webowego

 Przed użyciem internetowego radia należy zapisać plik index.html w pamięci mikrokontrolera. Ten plik jest interfejsem webowym, za pomocą którego można skonfigurować nazwę sieci i hasło, a także edytować listę stacji radiowych. Postępując zgodnie z instrukcjami z poprzedniego artykułu, łatwo poradzisz sobie z tym zadaniem i od razu będziesz mógł korzystać z interfejsu webowego do konfiguracji urządzenia.

Podstawowe funkcje urządzenia

 Po zainstalowaniu interfejsu webowego i wgraniu kodu Twoje urządzenie zyskuje następujące funkcje:

  • Regulacja głośności i wyciszenie przyciskami: Wygodne sterowanie głośnością i możliwość wyciszenia dźwięku poprzez naciśnięcie przycisku.
  • Przełączanie stacji radiowych: Długie naciśnięcie (1 sekunda) przycisku zwiększania lub zmniejszania głośności pozwala przełączać się między stacjami radiowymi.
  • Wielofunkcyjny przycisk na pinie 12: Ten przycisk wyłącza diody LED lub, przy dłuższym naciśnięciu, wyłącza internetowe radio i przełącza mikrokontroler w tryb punktu dostępowego w celu edycji listy stacji radiowych lub konfiguracji WiFi.
  • Stany włączenia: Białe świecenie / odczyt konfiguracji WiFi z pamięci: Pomarańczowe świecenie / połączenie z WiFi: Zielone świecenie / Odtwarzanie pierwszej stacji z listy: Efekt ognia.
  • Włączenie konfiguracji: Niebieskie świecenie / Uruchomienie punktu dostępowego: Efekt tęczy.


Komponenty do podłączenia

Do złożenia i działania urządzenia będą potrzebne następujące komponenty:

  • ESP32s2 mini: Mikrokontroler zarządzający całym systemem.
  • Moduł wzmacniacza na MAX98357: Zapewnia wysoką jakość dźwięku.
  • Pierścień LED na 12 adresowalnych diod WS2812: Do efektów wizualnych i wskazywania stanu.
  • Głośnik 3 W (średnica 57 mm): Do odtwarzania dźwięku.
  • Cztery przyciski taktowe 6x6x5 mm.
  • Bateria 18350: Zapewnia zasilanie urządzenia i kompaktowe wymiary.
  • Moduł ładowania baterii TP4056 Type-C z funkcją ochrony: Do bezpiecznego ładowania baterii.
  • Moduł podwyższający napięcie DC-DC na 5 woltów: Zapewnia stabilne napięcie dla urządzenia.
  • Przełącznik dwupozycyjny: Do włączania i wyłączania urządzenia.
  • Złącza do baterii 18350.
  • Wydrukować obudowę (pliki dostępne na stronie) lub zamówić już wydrukowaną na drukarce 3D obudowę.

Podłączenie komponentów

 Podłączenie wszystkich komponentów nie powinno sprawiać trudności, ponieważ teoretycznie rozgryzienie przeznaczenia pinów na podstawie kodu nie będzie skomplikowane. Możesz również zmienić przypisanie pinów według własnych preferencji.

 Po złożeniu wszystkich komponentów i wykonaniu wszystkich kroków konfiguracyjnych otrzymasz miniaturowe i funkcjonalne internetowe radio sterowane mikrokontrolerem ESP32s2mini. Ciesz się swoim nowym urządzeniem i dziel się wynikami!


#include <WiFi.h>
#include <LittleFS.h>
#include <Preferences.h>
#include <ArduinoJson.h>
#include <WebServer.h>
#include "AudioFileSourceHTTPStream.h"
#include "AudioFileSourceBuffer.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
#include <FastLED.h>
#include <vector>

const char* defaultStreamUrl = "http://zt01.cdn.eurozet.pl/zet-net.mp3";
std::vector<String> radioStations;
int currentStationIndex = 0;
// Obiekty do pracy z Wi-Fi i pamięcią
Preferences preferences;
WebServer server(80);

AudioGeneratorMP3 *mp3;
AudioFileSourceHTTPStream *file;
AudioFileSourceBuffer *buff;
AudioOutputI2S *out;

#define MUTE_PIN 35
#define VOL_UP 37
#define VOL_DOWN 33
#define BLU_PIN 15
#define LED_PIN 11
#define RES_PIN 12
#define NUM_LEDS 12
#define BRIGHTNESS 30

CRGB leds[NUM_LEDS];

unsigned long lastReconnectAttempt = 0;
unsigned long lastLedUpdate = 0;
unsigned long lastBlinkTime = 0;
bool ledOn = true;

bool wifiStatus = false;
bool isMuted = false;
float currentVolume = 0.09;
unsigned long lastButtonPressTime = 0;
bool enableLedEffect = true;
String ledMode = "Blue";

void reconnectStream() {
    if (radioStations.empty()) {
        Serial.println("No radio stations available.");
        return;
    }

    String stationUrl = radioStations[currentStationIndex];
    Serial.println("Starting playback of station: " + stationUrl);

    if (mp3) {
        mp3->stop();
        delete mp3;
        mp3 = nullptr;
    }
    if (buff) {
        delete buff;
        buff = nullptr;
    }
    if (file) {
        delete file;
        file = nullptr;
    }

    file = new AudioFileSourceHTTPStream(stationUrl.c_str());
    size_t bufferSize = ESP.getFreeHeap() / 4;
    if (bufferSize < 1024) bufferSize = 1024;
    buff = new AudioFileSourceBuffer(file, bufferSize);
    mp3 = new AudioGeneratorMP3();
    mp3->begin(buff, out);
    ledMode = "Fire";
}

void connectToWiFi(String ssid, String password) {
    WiFi.begin(ssid.c_str(), password.c_str());
    unsigned long startAttemptTime = millis();

    while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 10000) {
        delay(500);
    }

    wifiStatus = (WiFi.status() == WL_CONNECTED);
    if (wifiStatus) {
        Serial.println("WiFi connected!");
        fill_solid(leds, NUM_LEDS, CRGB::Green);
        FastLED.show();
        delay (1000);
    } else {
        Serial.println("WiFi connection failed.");
        fill_solid(leds, NUM_LEDS, CRGB::Red);
        FastLED.show();
        delay (1000);
        startAccessPoint();
    }
}

void loadWiFiCredentials() {
    preferences.begin("WiFi", true);
    String savedSSID = preferences.getString("wifiSSID", "");
    String savedPassword = preferences.getString("wifiPassword", "");
    preferences.end();

    if (savedSSID.length() > 0 && savedPassword.length() > 0) {
        fill_solid(leds, NUM_LEDS, CRGB::Orange);
        FastLED.show();
        delay(1000);
        connectToWiFi(savedSSID, savedPassword);
    } else {
        fill_solid(leds, NUM_LEDS, CRGB::Blue);
        FastLED.show();
        delay (1000);
        startAccessPoint();
    }
}

void loadStations() {
    if (LittleFS.exists("/stations.json")) {
        File file = LittleFS.open("/stations.json", "r");
        if (file) {
            StaticJsonDocument<1024> doc;
            DeserializationError error = deserializeJson(doc, file);
            if (!error) {
                JsonArray stationArray = doc.as<JsonArray>();
                for (JsonVariant v : stationArray) {
                    if (v.containsKey("url")) {
                        String url = v["url"].as<String>();
                        Serial.println("Found station: " + url);
                        radioStations.push_back(url);
                    }
                }
                if (radioStations.empty()) {
                    Serial.println("Station list is empty, using default.");
                    radioStations.push_back(defaultStreamUrl); // Dodajemy domyślny URL, jeśli lista jest pusta
                }
                Serial.println("Loaded radio stations.");
            } else {
                Serial.print("Failed to parse stations.json: ");
                Serial.println(error.c_str());
                radioStations.push_back(defaultStreamUrl); // Dodajemy domyślny URL
            }
            file.close();
        } else {
            Serial.println("Failed to open stations.json, using default.");
            radioStations.push_back(defaultStreamUrl); // Dodajemy domyślny URL
        }
    } else {
        Serial.println("stations.json not found, using default.");
        radioStations.push_back(defaultStreamUrl); // Dodajemy domyślny URL
    }

    // Wyświetlamy wszystkie załadowane stacje do weryfikacji
    Serial.println("Radio Stations:");
    for (size_t i = 0; i < radioStations.size(); i++) {
        Serial.printf("Station %d: %s\n", i + 1, radioStations[i].c_str());
    }
}

// Funkcja do uruchamiania punktu dostępowego
void startAccessPoint() {
  setSolidColor(CRGB::Blue);
  delay (500);
  WiFi.softAP("esp32radio", "123456");
  Serial.println("Access Point mode enabled.");
  server.on("/", handleRoot);
  server.on("/saveWiFi", handleSaveWiFi);
  server.on("/addStation", handleAddStation);
  server.on("/getStations", handleGetStations);
  server.on("/deleteStation", handleDeleteStation);
  server.on("/restart", handleRestart);
  server.begin();
  Serial.println("HTTP server started.");
  Serial.print("ESP32 IP Address: ");
  Serial.println(WiFi.softAPIP());
  delay (500);
}

void handleRoot() {
  Serial.println("Handling root request...");
  File file = LittleFS.open("/index.html", "r");
  if (!file) {
    server.send(500, "text/plain", "Error opening index file");
    return;
  }
  server.streamFile(file, "text/html");
  file.close();
  Serial.println("Index file sent to client.");
}

void handleGetStations() {
  File file = LittleFS.open("/stations.json", "r");
  if (!file) {
    server.send(500, "text/plain", "Error opening stations file");
    return;
  }

  StaticJsonDocument<1024> doc;
  DeserializationError error = deserializeJson(doc, file);
  file.close();

  if (error) {
    server.send(500, "text/plain", "Error parsing stations file");
    return;
  }

  String response;
  serializeJson(doc, response);
  server.send(200, "application/json", response);
}

void handleSaveWiFi() {
  if (server.hasArg("plain") == false) {
    server.send(400, "text/plain", "Body not received");
    return;
  }

  String body = server.arg("plain");
  Serial.println("Received body: " + body);
  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, body);

  if (error) {
    server.send(400, "text/plain", "Invalid JSON");
    return;
  }

  const char* ssid = doc["ssid"];
  const char* password = doc["password"];
  Serial.println("Received WiFi settings: SSID=" + String(ssid) + ", Password=" + String(password));
  preferences.begin("WiFi", false);
  preferences.putString("wifiSSID", ssid);
  preferences.putString("wifiPassword", password);
  server.send(200, "text/plain", "Wi-Fi settings saved");
}

void handleAddStation() {
  if (server.hasArg("plain") == false) {
    server.send(400, "text/plain", "Body not received");
    return;
  }

  String body = server.arg("plain");
  Serial.println("Received body: " + body);

  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, body);

  if (error) {
    server.send(400, "text/plain", "Invalid JSON");
    return;
  }

  if (!doc.containsKey("url")) {
    server.send(400, "text/plain", "Missing URL");
    return;
  }

  const char* url = doc["url"];
  Serial.println("Received station URL: " + String(url));

  File file = LittleFS.open("/stations.json", "r");
  StaticJsonDocument<1024> stationsDoc;

  JsonArray stations;

  // Jeśli plik istnieje i jest poprawny
  if (file) {
    DeserializationError readError = deserializeJson(stationsDoc, file);
    file.close();
    if (readError) {
      Serial.println("Error parsing stations file, creating new one.");
      stations = stationsDoc.to<JsonArray>();
    } else {
      stations = stationsDoc.as<JsonArray>();
    }
  } else {
    Serial.println("No stations file found, creating new one.");
    stations = stationsDoc.to<JsonArray>();
  }

  // Sprawdzenie, czy liczba stacji <= 8
  if (stations.size() >= 8) {
    server.send(400, "text/plain", "Station list is full (maximum 8 stations)");
    return;
  }

  // Dodanie nowej stacji
  JsonObject newStation = stations.createNestedObject();
  newStation["url"] = url;

  // Zapisanie zaktualizowanej listy do pliku
  file = LittleFS.open("/stations.json", "w");
  if (!file) {
    server.send(500, "text/plain", "Error opening stations file for writing");
    return;
  }

  serializeJson(stationsDoc, file);
  file.close();

  server.send(200, "text/plain", "Station added");
}

void handleDeleteStation() {
  if (!server.hasArg("index")) {
    server.send(400, "text/plain", "Index not provided");
    return;
  }

  int index = server.arg("index").toInt();

  File file = LittleFS.open("/stations.json", "r");
  if (!file) {
    server.send(500, "text/plain", "Error opening stations file");
    return;
  }

  StaticJsonDocument<1024> stationsDoc;
  deserializeJson(stationsDoc, file);
  file.close();

  JsonArray stations = stationsDoc.as<JsonArray>();
  if (index < 0 || index >= stations.size()) {
    server.send(400, "text/plain", "Invalid index");
    return;
  }

  stations.remove(index);

  file = LittleFS.open("/stations.json", "w");
  if (!file) {
    server.send(500, "text/plain", "Error writing stations file");
    return;
  }

  serializeJson(stationsDoc, file);
  file.close();
  server.send(200, "text/plain", "Station deleted");
}

void handleRestart() {
  Serial.println("Handling restart request...");
  server.send(200, "text/plain", "Restarting...");
  delay(1000); // Dajemy czas na wysłanie odpowiedzi do klienta
  ESP.restart();
}

void setup() {
    Serial.begin(9600);
    pinMode(BLU_PIN, OUTPUT);
    pinMode(MUTE_PIN, INPUT_PULLUP);
    pinMode(VOL_UP, INPUT_PULLUP);
    pinMode(VOL_DOWN, INPUT_PULLUP);
    pinMode(RES_PIN, INPUT_PULLUP);
    digitalWrite(BLU_PIN, HIGH);
    delay (1000);

    FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);
    FastLED.setBrightness(BRIGHTNESS);
    fill_solid(leds, NUM_LEDS, CRGB::White);
    FastLED.show();

    delay (1000);

    // Inicjalizacja LittleFS
    if (!LittleFS.begin()) {
        Serial.println("Failed to mount LittleFS!");
        return;
    }

    loadWiFiCredentials();

    if (wifiStatus) {
        loadStations();
        delay(500);
        out = new AudioOutputI2S();
        out->SetPinout(5, 3, 7);
        out->SetGain(currentVolume);
        delay(500);
        reconnectStream();
    }
    digitalWrite(BLU_PIN, LOW);
}

void setSolidColor(const CRGB& color) {
    fill_solid(leds, NUM_LEDS, color);
    FastLED.show();
}

void updateLeds() {
    // Aktualizacja co 100 ms
    if (millis() - lastLedUpdate < 100) return;

    lastLedUpdate = millis();

    // Jeśli efekty są wyłączone, gasimy diody i wychodzimy
    if (!enableLedEffect) {
        fill_solid(leds, NUM_LEDS, CRGB::Black);
        FastLED.show();
        return;
    }

    if (isMuted) {
        setSolidColor(CRGB::Green); // Logika diod przy wyciszeniu dźwięku
        return;
    }

    if (ledMode == "White") {
        setSolidColor(CRGB::White);
    } else if (ledMode == "Blue") {
        setSolidColor(CRGB::Blue);
    } else if (ledMode == "Green") {
        setSolidColor(CRGB::Green);
    } else if (ledMode == "Red") {
        setSolidColor(CRGB::Red);
    } else if (ledMode == "Fire") {
        static uint8_t heat[NUM_LEDS];
        for (int i = 0; i < NUM_LEDS; i++) {
            heat[i] = qsub8(heat[i], random(0, 50));
        }
        for (int i = NUM_LEDS - 1; i >= 2; i--) {
            heat[i] = (heat[i - 1] + heat[i - 2] + heat[i - 2]) / 3;
        }
        if (random(0, 10) > 7) {
            int sparkIndex = random(0, 3);
            heat[sparkIndex] = qadd8(heat[sparkIndex], random(160, 255));
        }
        for (int i = 0; i < NUM_LEDS; i++) {
            leds[i] = HeatColor(heat[i]);
        }
    } else if (ledMode == "Rainbow") {
        static uint8_t hue = 0;
        fill_rainbow(leds, NUM_LEDS, hue, 7); // 7 - krok zmiany koloru
        hue += 1; // Przesunięcie dla animacji
    } else if (ledMode == "Strobe") {
        static uint32_t lastBlinkTime = 0;
        static bool ledOn = false;

        if (millis() - lastBlinkTime >= 100) { // Interwał migania 100 ms
            lastBlinkTime = millis();
            ledOn = !ledOn;
            setSolidColor(ledOn ? CRGB::White : CRGB::Black);
        }
        return; // Strobe już zaktualizował FastLED
    }

    // Aktualizujemy diody na końcu (jeśli nie wróciliśmy wcześniej)
    FastLED.show();
}

void blinkRedLeds() {
  while (true) {
    // Aktualizacja co 100 ms
    if (millis() - lastLedUpdate >= 100) {
      lastLedUpdate = millis();

      // Interwał migania 500 ms
      if (millis() - lastBlinkTime >= 500) {
        lastBlinkTime = millis();
        ledOn = !ledOn;
        fill_solid(leds, NUM_LEDS, ledOn ? CRGB::Red : CRGB::Black);
        FastLED.show();
      }
    }
  }
}

void handleStationChange(int direction) {
    if (direction > 0) {
        currentStationIndex = (currentStationIndex + 1) % radioStations.size();
    } else {
        currentStationIndex = (currentStationIndex - 1 + radioStations.size()) % radioStations.size();
    }
    Serial.println("Switched to station: " + radioStations[currentStationIndex]);
    reconnectStream();
}

void handleButtons() {
    static unsigned long volUpPressTime = 0;
    static unsigned long volDownPressTime = 0;
    static unsigned long resPressTime = 0;

    int muteState = digitalRead(MUTE_PIN);
    int nextState = digitalRead(VOL_UP);
    int prevState = digitalRead(VOL_DOWN);
    int resState = digitalRead(RES_PIN);  // Odczyt stanu pinu RES_PIN
    unsigned long currentTime = millis();

    // Obsługa przycisku MUTE
    if (muteState == LOW && currentTime - lastButtonPressTime >= 300) {
        isMuted = !isMuted;
        if (isMuted) {
            enableLedEffect = true;
            ledMode = "Green";
            out->SetGain(0);
            reconnectStream();
        } else {
            enableLedEffect = true;
            ledMode = "RGB";
            out->SetGain(currentVolume);
            reconnectStream();
        }
        lastButtonPressTime = currentTime;
    }

    // Przytrzymanie przycisku VOL_UP
    if (nextState == LOW) {
        if (volUpPressTime == 0) volUpPressTime = currentTime;  // Zapamiętujemy czas rozpoczęcia przytrzymania

        if (currentTime - volUpPressTime >= 1000) {  // Jeśli przytrzymano dłużej niż 1 sekundę
            handleStationChange(1);  // Przełączamy na następną stację radiową
            volUpPressTime = 0;  // Zerujemy czas przytrzymania
            lastButtonPressTime = currentTime;  // Zerujemy opóźnienie dla debounce
        }
    } else if (volUpPressTime > 0) {  // Jeśli przycisk został zwolniony
        if (currentTime - volUpPressTime < 1000) {  // Jeśli przytrzymano krócej niż 1 sekundę
            if (isMuted) {
                isMuted = false;
                ledMode = "RGB";
                out->SetGain(currentVolume);
                reconnectStream();
            }
            if (currentVolume < 1.0) {
                currentVolume += 0.03;
                if (currentVolume > 1.0) currentVolume = 1.0;
                out->SetGain(currentVolume);
            }
        }
        volUpPressTime = 0;
    }

    // Przytrzymanie przycisku VOL_DOWN
    if (prevState == LOW) {
        if (volDownPressTime == 0) volDownPressTime = currentTime;  // Zapamiętujemy czas rozpoczęcia przytrzymania

        if (currentTime - volDownPressTime >= 1000) {  // Jeśli przytrzymano dłużej niż 1 sekundę
            handleStationChange(-1);  // Przełączamy na poprzednią stację radiową
            volDownPressTime = 0;  // Zerujemy czas przytrzymania
            lastButtonPressTime = currentTime;  // Zerujemy opóźnienie dla debounce
        }
    } else if (volDownPressTime > 0) {  // Jeśli przycisk został zwolniony
        if (currentTime - volDownPressTime < 1000) {  // Jeśli przytrzymano krócej niż 1 sekundę
            if (isMuted) {
                isMuted = false;
                ledMode = "RGB";
                out->SetGain(currentVolume);
                reconnectStream();
            }
            if (currentVolume > 0.0) {
                currentVolume -= 0.03;
                if (currentVolume < 0.0) currentVolume = 0.0;
                out->SetGain(currentVolume);
            }
        }
        volDownPressTime = 0;  // Zerujemy czas przytrzymania
    }

    // Obsługa RES_PIN
    if (resState == LOW) {
        if (resPressTime == 0) resPressTime = currentTime;  // Zapamiętujemy czas rozpoczęcia przytrzymania

        if (currentTime - resPressTime >= 1000) {  // Jeśli przytrzymano dłużej niż 1 sekundę
            stopAudioStream();         // Zatrzymanie odtwarzania strumienia audio
            WiFi.disconnect();         // Rozłączenie z Wi-Fi
            WiFi.mode(WIFI_OFF);
            wifiStatus = false;
            startAccessPoint();    // Uruchomienie punktu dostępowego
            Serial.println("Access Point mode activated.");
            resPressTime = 0;          // Zerujemy czas przytrzymania
        }
    } else if (resPressTime > 0) {  // Jeśli przycisk został zwolniony
        if (currentTime - resPressTime < 1000) {  // Krótkie naciśnięcie
            if (!enableLedEffect) {
                enableLedEffect = true;
                Serial.println("LED effects enabled.");
            } else {
                enableLedEffect = false;
                Serial.println("LED effects disabled.");
            }
        }
        resPressTime = 0; // Zerujemy czas przytrzymania
    }
}

void stopAudioStream() {
    // Zatrzymujemy generator MP3
    if (mp3) {
        if (mp3->isRunning()) {
            mp3->stop();   // Zatrzymujemy odtwarzanie
        }
        delete mp3;         // Usuwamy obiekt
        mp3 = nullptr;      // Zerujemy wskaźnik
    }

    // Czyszczenie bufora
    if (buff) {
        delete buff;        // Usuwamy obiekt bufora
        buff = nullptr;     // Zerujemy wskaźnik
    }

    // Czyszczenie źródła danych
    if (file) {
        delete file;        // Usuwamy obiekt źródła danych
        file = nullptr;     // Zerujemy wskaźnik
    }

    // Zerowanie głośności na wyjściu
    if (out) {
        out->SetGain(0);    // Ustawiamy głośność na 0
        out->stop();        // Zatrzymujemy strumień wyjściowy
    }

    // Logowanie zakończenia
    Serial.println("Audio stream stopped successfully.");
}

void loop() {

  if (wifiStatus) {
    if (mp3 && mp3->isRunning() && !isMuted) {
        if (!mp3->loop()) {
            setSolidColor(CRGB::Red);
            delay(250);
            reconnectStream();
        }
    } else {
      if (ledMode != "Fire") {
        ledMode = "Fire";
      }
    }
    handleButtons();
  } else {
    if (ledMode != "Blue") {
    ledMode = "Blue";
    }
    server.handleClient(); // Obsługa przychodzących żądań HTTP
    delay(10);
  }
    updateLeds();
}
  • W kodzie do sterowania diodami LED zaprogramowano kilka efektów, na przykład FIRE, który można łatwo zastąpić innym trybem, zmieniając 2 linie w pętli loop. Efekty i tryby pracy LED mają zapas na przyszłe warianty wizualizacji stanu kontrolera.
  • Mam nadzieję, że kod jest wystarczająco logicznie zorganizowany, a funkcje mają zrozumiałe nazwy, co powinno ułatwić czytanie i zrozumienie.
  • Używane są standardowe biblioteki i komponenty, więc kod powinien być uniwersalny i zrozumiały dla innych programistów.
  • Jeśli masz dodatkowe pytania lub potrzebujesz pomocy, daj znać! Powodzenia z Twoim projektem!