#include <SPI.h>
#include <WiFi.h>
#include <time.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <XPT2046_Touchscreen.h>
#include <Fonts/FreeSerif9pt7b.h>
// Obiekt wyświetlacza (Adafruit ILI9341)
// Ustaw piny zgodnie z Twoim projektem / Wokwi
#define TFT_CS 5
#define TFT_DC 2
#define TFT_RST 4 // -1 jeżeli RESET jest podłączony do EN
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
// Obiekt touch (XPT2046) pin CS dla kontrolera dotyku
#define TOUCH_CS 21
XPT2046_Touchscreen ts(TOUCH_CS);
// Dane WiFi
char Router[] = "Wokwi-GUEST";
char Password[] = "";
// Parametry wyświetlacza
#define TFT_WIDTH 240 // szerokość
#define TFT_HEIGHT 320 // wysokość
// Zmienne TFT (wysokość, szerokość, promień)
const int centerX = TFT_WIDTH / 2;
const int centerY = TFT_WIDTH / 2;
const int Radius = centerY - 2;
const int buttonM = 5; // początek przycisków
const int buttonH = 30; // wysokość przycisku
const int buttonW = (TFT_WIDTH - 20) / 3; // szerokość przycisku
const int buttonX = TFT_HEIGHT - buttonH - buttonM; // początek X przycisków
// Mnożniki dla pozycji x-y godzin, minut i sekund
float PosSeconx = 0, PosSecony = 0, PosMinuteX = 0, PosMinuteY = 0, PosHourX = 0, PosHourY = 0;
float DegSecond = 0, DegMinute = 0, DegHour = 0;
// współrzędne x-y do wyświetlania godzin, minut i sekund
int HandSeconx = centerY, HandSecony = centerY;
int HandMinuteX = centerY, HandMinuteY = centerY;
int HandHourX = centerY, HandHourY = centerY;
// Uruchomienie jest wymagane tylko przy pierwszym uruchomieniu w celu skonfigurowania TFT
bool Start = true;
unsigned long TimeMeasure = 0;
// Zmienne czasu
int Hours, Minutes, Seconds;
// Kolory (16-bit)
#define BLACK 0x0000
#define WHITE 0xFFFF
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define BROWN 0x9A60
#define GRAY 0x7BEF
#define LIMEGREEN 0xB7E0
#define DARKCYAN 0x03EF
#define ORANGE 0xFDA0
#define PINK 0xFE19
#define MAROON 0xA000
#define LIGHTBLUE 0x867D
#define VIOLET 0x915C
#define SILVER 0xC618
#define GOLD 0xFEA0
// Kolory wewnętrznego okręgu, kolor obramowania i kolor wskaźnika
// Kolory wskaźników można również ustawić inywidualnie
const int CircleColor = BLACK;
const int HandColor = WHITE;
const int BorderColor = MAROON;
// true -> wyświetl datę; false -> nie wyświetlaj daty
bool ShowDate = true;
// true -> tylko wskazówka sekundowa jako kółko
bool SecondHandCircle = false;
// pokaż/ukryj liczby 12 3 6 9
bool ShowNumbers = true;
// Pin Speaker
int Speaker = 26;
// Współrzędne ekranu dotykowego
uint16_t x, y;
// Budzik
bool AlarmStatus = false;
int DisplayHour = 6;
int DisplayMinute = 0;
int AlarmHour = 6;
int AlarmMinute = 0;
// NTP
#define TimeServer "de.pool.ntp.org"
/*
Lista stref czasowych
https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
Strefa czasowa CET = czas środkowoeuropejski -1 -> 1 godzina wstecz
CEST = czas środkowoeuropejski letni od
M3 = marzec, 5.0 = niedziela 5. tygodnia, 02 = 2:00
do M10 = październik, 5.0 = niedziela 5. tygodnia, 03 = 3:00
*/
#define TimeZone "CET-1CEST,M3.5.0/02,M10.5.0/03"
// time_t zawiera liczbę sekund od 01.01.1970
time_t currentTime;
/*
Struktura tm
tm_hour -> Godzina: od 0 do 23
tm_min -> Minuty: od 0 do 59
tm_sec -> Sekuny: od 0 do 59
tm_mday -> Dzień: od 1 do 31
tm_mon -> Miesiąc: od 0 (styczeń) do 11 (grudzień)
tm_year -> Lata od 1900 roku
tm_yday -> Dni, które upłynęły od 1 stycznia danego roku
tm_isdst -> Wartość > 0 = Czas letni (dst = czas letni)
*/
tm Time;
// Kalibracja dotyku — wartości do dopasowania (przykładowe)
// Jeżeli dotyk jest odwrotnie: zamień min i max
#define TS_MINX 200
#define TS_MAXX 3900
#define TS_MINY 200
#define TS_MAXY 3900
// centrowanie tekstu
void drawCenteredText(int x, int y, int w, int h, String text) {
// dynamiczne obliczenia bez zmiany rozmiaru
int16_t x1, y1;
uint16_t textWidth, textHeight;
tft.getTextBounds(text.c_str(), x, y, &x1, &y1, &textWidth, &textHeight);
// Obliczenia centrowania (dla stałego rozmiaru czcionki)
int textX = x + (w - textWidth) / 2;
int textY = y + (h - textHeight) / 2; // Poprawione centrowanie pionowe
// Rysowanie bez zmiany ustawień
tft.setCursor(textX, textY);
tft.print(text);
}
// rysowanie tarczy
void drawClockBorder() {
int borderWidth = 4;
for (int i = 0; i < borderWidth; i++) {
tft.drawCircle(centerX, centerY, Radius - i, BorderColor);
}
tft.fillCircle(centerX, centerY, Radius - borderWidth, CircleColor);
/*
Serial.println("centerX, centerY, Radius");
char buf[12];
sprintf(buf, "%03d,%03d-%03d", centerX, centerY, Radius);
Serial.println(buf);
*/
}
// rysowanie znaczników czasu
void drawHourMarkers() {
const float outerRatio = 0.96f;
const float innerRatio = 0.91f;
int outerLength = Radius * outerRatio;
int innerLength = Radius * innerRatio;
//Serial.println("PointX1, PointY1 - PointX2, PointY2");
for (int i = 0; i < 360; i += 30) {
float angleRad = (i - 90) * DEG_TO_RAD;
float cosVal = cos(angleRad);
float sinVal = sin(angleRad);
int PointX1 = centerX + (int)roundf(cosVal * outerLength);
int PointY1 = centerY + (int)roundf(sinVal * outerLength);
int PointX2 = centerX + (int)roundf(cosVal * innerLength);
int PointY2 = centerY + (int)roundf(sinVal * innerLength);
bool isNumberPosition = ShowNumbers && (i == 0 || i == 90 || i == 180 || i == 270);
// rysowanie
if (!isNumberPosition) {
tft.drawLine(PointX1, PointY1, PointX2, PointY2, HandColor);
}
/*
char buf[24];
sprintf(buf, "%03d° %03d,%03d - %03d,%03d", i, PointX1, PointY1, PointX2, PointY2);
Serial.println(buf);
*/
}
}
// zaznacz co 6 stopni znaczniki sekundowe
void drawMinutMarkers() {
int markerDistance = Radius * 0.94f;
//Serial.println("PointX, PointY");
for (int i = 0; i < 360; i += 6) {
float angleRad = (i - 90) * DEG_TO_RAD;
float PosX = cos(angleRad);
float PosY = sin(angleRad);
int PointX = centerX + (int)roundf(PosX * markerDistance);
int PointY = centerY + (int)roundf(PosY * markerDistance);
tft.drawPixel(PointX, PointY, HandColor);
/*
char buf[14];
sprintf(buf, "%03d° %03d,%03d", i, PointX, PointY);
Serial.println(buf);
*/
}
}
// cyfry godzin
void drawHourNumbers() {
if (!ShowNumbers) return;
const float outerRatio = 0.96f; // ten sam, co w drawHourMarkers
int outerLength = (int)roundf(Radius * outerRatio);
const uint16_t TEXT_COLOR = HandColor; // kolor cyfr
// Definicje cyfr godzinowych
const struct {
float angle;
const char* text;
} numbers[] = {{0, "12"}, {90, "3"}, {180, "6"}, {270, "9"}};
// ustawienia fontu i koloru
tft.setFont(&FreeSerif9pt7b);
tft.setTextColor(TEXT_COLOR);
for (int i = 0; i < 4; i++) {
// Kąt w radianach z korektą obrotu
float angleRad = (numbers[i].angle - 90) * DEG_TO_RAD;
// końce znacznika godzinowego (PointX2/PointY2 w drawHourMarkers)
int PointX = centerX + (int)roundf(cos(angleRad) * outerLength);
int PointY = centerY + (int)roundf(sin(angleRad) * outerLength);
// Wymiary tekstu
int16_t x1, y1;
uint16_t textWidth, textHeight;
tft.getTextBounds(numbers[i].text, 0, 0, &x1, &y1, &textWidth, &textHeight);
// Centrowanie cyfry dokładnie przy końcu znacznika
int areaX = PointX - textWidth / 2;
int areaY = PointY - textHeight / 2;
// Rysowanie
drawCenteredText(areaX, areaY, textWidth, textHeight, numbers[i].text);
// Debug
char buf[40];
sprintf(buf, "%s -> %03d,%03d", numbers[i].text, areaX, areaY);
Serial.println(buf);
}
}
// lewy przycisk
void drawLeftButton() {
tft.fillRect(buttonM, buttonX, buttonW, buttonH, LIGHTBLUE);
tft.drawRoundRect(buttonM, buttonX, buttonW, buttonH, 5, BLACK);
tft.setTextColor(BLACK);
//tft.setTextSize(2);
drawCenteredText(buttonM, buttonX, buttonW, buttonH, String(DisplayHour));
}
// środkowy przycisk
void drawMiddleButton() {
int middleX = buttonM * 2 + buttonW;
tft.fillRect(middleX, buttonX, buttonW, buttonH, WHITE);
tft.drawRoundRect(middleX, buttonX, buttonW, buttonH, 5, BLACK);
tft.setTextColor(BLACK);
// tft.setTextSize(2);
drawCenteredText(middleX, buttonX, buttonW, buttonH, String(DisplayMinute));
}
// prawy przycisk
void drawRightButton() {
int rightX = buttonM * 3 + buttonW * 2;
tft.fillRect(rightX, buttonX, buttonW, buttonH, YELLOW);
tft.drawRoundRect(rightX, buttonX, buttonW, buttonH, 5, BLACK);
tft.setTextColor(BLACK);
//tft.setTextSize(2);
drawCenteredText(rightX, buttonX, buttonW, buttonH, "WYL");
}
void setup() {
Serial.begin(115200);
//Serial.println(buttonW); Serial.println(buttonX);
SPI.begin(); // inicjalizacja SPI (dla ekranu i touch)
// Inicjalizacja TFT (Adafruit)
tft.begin();
tft.setRotation(0);
// Użyj skalibrowanych danych
//uint16_t calData[5] = {242, 3496, 262, 3622, 4};
//tft.setTouch(calData);
tft.fillScreen(CircleColor);
tft.setFont(&FreeSerif9pt7b);
tft.setTextWrap(false);
// Inicjalizacja touch
ts.begin();
ts.setRotation(0);
// WiFi i NTP
WiFi.mode(WIFI_STA);
WiFi.begin(Router, Password);
Serial.println("------------------------");
while (WiFi.status() != WL_CONNECTED) {
delay(200);
Serial.print(".");
}
Serial.println();
Serial.print("Polaczono z ");
Serial.println(Router);
Serial.print("Adres IP przez DHCP: ");
Serial.println(WiFi.localIP());
configTzTime(TimeZone, TimeServer);
time(¤tTime);
localtime_r(¤tTime, &Time);
String Year = String(Time.tm_year + 1900);
int Counter = 0;
int Search = Year.indexOf("1970");
Serial.println("-------------------------");
Serial.println("Pobieranie daty i czasu (maksymalnie 90 sekund)...");
// dopóki wyszukiwanie się nie powiedzie
while (Search != -1) {
// pobierz aktualny czas
time(¤tTime);
// ustaw czas na lokalną strefę czasową
localtime_r(¤tTime, &Time);
Year = String(Time.tm_year + 1900);
// wyszukaj w ciągu rok wartość „1970”
Search = Year.indexOf("1970");
// czas w godzinach, minutach i sekundach
Hours = int(Time.tm_hour);
Minutes = int(Time.tm_min);
Seconds = int(Time.tm_sec);
delay(1000);
Counter++;
if (Counter >= 90) {
Serial.println();
Serial.println("Nie udało sie pobrac daty i czasu w ciagu " + String(Counter) + " sekund");
Serial.println("Program zostaje zakonczony");
while (1);
}
Serial.print(".");
}
Serial.println();
// Data/czas pomyślnie zsynchronizowany
if (Search == -1) {
Serial.println("-------------------------");
Serial.println("Data/czas pomyślnie zsynchronizowany ...");
if (Time.tm_mday < 10)
Serial.print("0");
Serial.print(Time.tm_mday);
Serial.print(".");
// Miesiąc: dodaj początkowe zera
if (Time.tm_mon < 9)
Serial.print("0");
// Zliczanie zaczyna się od 0 -> +1
Serial.print(Time.tm_mon + 1);
Serial.print(".");
// Liczba lat od 1900 roku
Serial.println(Time.tm_year + 1900);
if (Time.tm_hour < 10)
Serial.print("0");
Serial.print(Time.tm_hour);
Serial.print(":");
if (Time.tm_min < 10)
Serial.print("0");
Serial.println(Time.tm_min);
Serial.println("-------------------------");
drawDate();
}
// Czas w godzinach, minutach i sekundach
Hours = Time.tm_hour;
Minutes = Time.tm_min;
Seconds = Time.tm_sec;
//
drawClockBorder();
drawHourMarkers();
drawMinutMarkers();
drawHourNumbers();
drawLeftButton();
drawMiddleButton();
drawRightButton();
if (ShowDate) {
drawDate();
drawDay();
}
tft.drawRect(0, 0, TFT_WIDTH, TFT_HEIGHT, GREEN);
TimeMeasure = millis() + 1000;
}
void loop() {
//checkTouch();
if (TimeMeasure < millis()) {
// Kontynuuj odliczanie sekund
TimeMeasure += 1000;
Seconds++;
if (Seconds == 60) {
// Synchronizuj czas z serwerem czasu co minutę
// Pobierz aktualny czas
time(¤tTime);
localtime_r(¤tTime, &Time);
// Czas w godzinach, minutach i sekundach
Hours = int(Time.tm_hour);
Minutes = int(Time.tm_min);
Seconds = int(Time.tm_sec);
// Północ -> pokaż zmianę daty
if (Hours == 0 && Minutes == 0) {
Serial.println("Nowa data");
if (ShowDate)
drawDate();
}
// Budzik włączony
if (AlarmStatus) {
if (Hours == AlarmHour && Minutes == AlarmMinute) {
tone(Speaker, 1000, 100);
delay(500);
noTone(Speaker);
Serial.println("Czas budzika: " + String(AlarmHour) + ":" + String(AlarmMinute));
}
}
}
// obliczenie współrzędnych x i y o jedną sekundę co 6°
DegSecond = Seconds * 6;
// jedna minuta do przodu co 6°
DegMinute = Minutes * 6;
// Przesuwa się o godzinę do przodu co 30° > 30 / 3600 = 0,0833333
// Zapewnia, że wskazówka godzinowa „przesuwa się” do przodu zgodnie z liczbą minut
DegHour = Hours * 30 + DegMinute * 0.0833333;
PosHourX = cos((DegHour - 90) * DEG_TO_RAD);
PosHourY = sin((DegHour - 90) * DEG_TO_RAD);
PosMinuteX = cos((DegMinute - 90) * DEG_TO_RAD);
PosMinuteY = sin((DegMinute - 90) * DEG_TO_RAD);
PosSeconx = cos((DegSecond - 90) * DEG_TO_RAD);
PosSecony = sin((DegSecond - 90) * DEG_TO_RAD);
// wyczyść wskazówkę minutową/godzinową co minutę lub raz na początku wyświetlania
if (Seconds == 0 || Start) {
Start = false;
// 62 piksele -> Długość wskazówki godzinowej
// Punkt środkowy + 1 -> Punkt środkowy nie powinien być usuwany
tft.drawLine(HandHourX, HandHourY, centerY, centerY, CircleColor);
HandHourX = PosHourX * 80 + centerY;
HandHourY = PosHourY * 80 + centerY;
// 84 piksele -> Długość wskazówki minutowej
// Punkt środkowy + 1 -> Punkt środkowy nie powinien być usuwany
tft.drawLine(HandMinuteX, HandMinuteY, centerY, centerY, CircleColor);
HandMinuteX = PosMinuteX * 100 + centerY;
HandMinuteY = PosMinuteY * 100 + centerY;
}
// Usuń wskazówkę sekundową
if (!SecondHandCircle)
tft.drawLine(HandSeconx, HandSecony, centerY, centerY, CircleColor);
// Usuń okrąg na wskazówce sekundowej, promień 5
tft.fillCircle(HandSeconx, HandSecony, 5, CircleColor);
// 85 pikseli -> długość wskazówki sekundowej
HandSeconx = PosSeconx * 90 + centerY;
HandSecony = PosSecony * 90 + centerY;
// Przerysuj wskaźnik
// Wyświetlaj drugą linię tylko wtey, gy kółko wskazówki sekundowej jest fałszywe
if (!SecondHandCircle)
tft.drawLine(HandSeconx, HandSecony, centerY, centerY, RED);
// Minuty
tft.drawLine(HandMinuteX, HandMinuteY, centerY, centerY, HandColor);
// Godziny
tft.drawLine(HandHourX, HandHourY, centerY, centerY, HandColor);
// Okrąg na końcu wskazówki sekundowej, promień 5
tft.fillCircle(HandSeconx, HandSecony, 5, RED);
// Narysuj punkt środkowy
tft.fillCircle(centerY, centerY, 3, HandColor);
}
}
void drawDate() {
const int topUsed = centerY * 2 + buttonM * 2;
const int bottomUsed = buttonH + buttonM * 2;
const int freeSpaceHeight = TFT_HEIGHT - topUsed - bottomUsed; // 320 - 241 - 50 = 29
int16_t x1, y1;
uint16_t textWidth, textHeight;
const int dtextY = topUsed + (freeSpaceHeight - textHeight) / 2;
//tft.setFont(&FreeSerif9pt7b);
tft.setTextColor(GREEN);
//tft.setTextWrap(false);
// Przygotowanie ciągu daty
char dateStr[11]; // DD.MM.YYYY + null terminator
snprintf(dateStr, sizeof(dateStr), "%02d.%02d.%04d", Time.tm_mday, Time.tm_mon + 1, Time.tm_year + 1900);
// Obliczenie szerokości i wyświetlenie daty (wyrównanie do prawej)
tft.setCursor(TFT_WIDTH - textWidth - buttonM, dtextY);
tft.getTextBounds(dateStr, 0, 0, &x1, &y1, &textWidth, &textHeight);
/*
Serial.print("Szerokość tekstu: ");
Serial.println(textWidth);
Serial.print("Wysokość tekstu: ");
Serial.println(textHeight);
Serial.print("Pozycja X: ");
Serial.println(TFT_WIDTH - textWidth - buttonM);
Serial.print("Szerokość ekranu: ");
Serial.println(TFT_WIDTH);
*/
tft.print(dateStr);
}
void drawDay() {
/*
int16_t x1, y1;
uint16_t tw, th;
tft.fillRect(rectX, rectY, rectW, rectH, rectColor);
tft.drawRect(rectX, rectY, rectW, rectH, WHITE);
tft.setFont(&FreeSerif9pt7b);
tft.setTextColor(GREEN);
tft.setTextWrap(false);
tft.getTextBounds(text, 0, 0, &x1, &y1, &tw, &th);
int16_t dtextX = rectX + (rectW - tw) / 2 - x1;
int16_t dtextY = rectY + (rectH - th) / 2 - y1;
tft.setCursor(dtextX, dtextY);
tft.print(text);
*/
/*
// Wyczyszczenie tylko potrzebnego obszaru
tft.fillRect(0, topUsed, TFT_WIDTH, freeSpaceHeight, BLACK);
// Ustawienie rozmiaru tekstu i koloru
tft.setFont(&FreeSerif9pt7b);
tft.setTextSize(2);
tft.setTextColor(GREEN);
// Obliczenie pozycji Y dla tekstu (wyśrodkowanie wertykalne)
tft.getTextBounds("Test", 0, 0, &x1, &y1, &textWidth, &textHeight);
// Wyświetlenie dnia tygodnia (wyrównanie do lewej)
tft.setCursor(buttonM, dtextY);
switch (Time.tm_wday) {
case 0: tft.print("Niedziela"); break;
case 1: tft.print("Poniedzialek"); break;
case 2: tft.print("Wtorek"); break;
case 3: tft.print("Sroda"); break;
//case 4: tft.print("Czwartek"); break;
case 4: tft.print("Poniedzialek"); break;
case 5: tft.print("Piatek"); break;
case 6: tft.print("Sobota"); break;
}*/
}
void checkTouch() {
if (ts.touched()) {
TS_Point p = ts.getPoint();
// Po odczycie trzeba odłączyć CS touch, jeżeli używasz wspólnego SPI
digitalWrite(TOUCH_CS, HIGH);
// Mapowanie surowych wartości do współrzędnych ekranu
int x = map(p.x, TS_MINX, TS_MAXX, 0, tft.width());
int y = map(p.y, TS_MINY, TS_MAXY, 0, tft.height());
int z = p.z;
// Sprawdzanie współrzędnych lewego przycisku
if (x >= 10 && x <= 100 && y >= 250 && y <= 290) {
if (DisplayHour == 23)
DisplayHour = 0;
else
DisplayHour++;
tft.fillRect(10, 400, 90, 50, LIGHTBLUE);
tft.drawRoundRect(10, 400, 90, 50, 5, BLACK);
tft.setTextColor(BLACK);
tft.setCursor(45, 265);
//tft.setTextSize(2);
tft.print(DisplayHour);
AlarmHour = DisplayHour;
}
// Sprawdzanie współrzędnych środkowego przycisku
if (x >= 120 && x <= 210 && y >= 250 && y <= 290) {
if (DisplayMinute == 59)
DisplayMinute = 0;
else
DisplayMinute++;
tft.fillRect(120, 400, 90, 50, WHITE);
tft.drawRoundRect(120, 400, 90, 50, 5, BLACK);
tft.setTextColor(BLACK);
tft.setCursor(155, 265);
//tft.setTextSize(2);
tft.print(DisplayMinute);
AlarmMinute = DisplayMinute;
}
// Sprawdzanie współrzędnych prawego przycisku
if (x >= 230 && x <= 320 && y >= 250 && y <= 290) {
AlarmStatus = !AlarmStatus;
}
// Wyświetlanie statusu budzika
if (AlarmStatus) {
tft.fillRect(230, 250, 90, 40, YELLOW);
tft.drawRoundRect(230, 250, 90, 40, 5, BLACK);
tft.setTextColor(BLACK);
tft.setCursor(250, 265);
//tft.setTextSize(2);
tft.print("on");
} else {
tft.fillRect(230, 400, 90, 50, YELLOW);
tft.drawRoundRect(230, 400, 90, 50, 5, BLACK);
tft.setTextColor(BLACK);
tft.setCursor(250, 265);
//tft.setTextSize(2);
tft.print("off");
}
//displayCoordinates(x, y, ts.getPoint().z);
delay(150);
}
}
void displayCoordinates(int x, int y, int z) {
Serial.print("wspolrzedna x = ");
Serial.print(x);
Serial.print(" | wspolrzedna y = ");
Serial.print(y);
Serial.print(" | nacisk = ");
Serial.print(z);
Serial.println();
}Loading
ili9341-cap-touch
ili9341-cap-touch