Search
Write a publication
Pull to refresh

Делаем настольные часы с Wi-Fi из тетриса

Level of difficultyMedium
Reading time5 min
Views7.3K

Если вы посмотрели видео, то уже хорошо понимаете, что представляет из себя этот гаджет, но на всякий случай ещё раз перечислю, что он умеет: онлайн‑синхронизация времени, фоторамка, демонстрация логотипа, имитация волшебного шара из фильма «Трасса 60». В часах используется модуль WeAct ESP32‑C6 Mini с процессором ESP32‑C6 QFN32 и дисплей WeAct ST7735 (9 $ за всё вместе с доставкой с AliExpress).

Вид изнутри
Вид изнутри

Написание кода не обошлось без проблем: многие библиотеки для обновления времени через сеть не работали, а также библиотеку Adafruit ST7735 пришлось немного подправить, чтобы убрать белые полоски с краёв экрана — об этом будет написано ниже. Заодно сразу отмечу: подтягивающий резистор для кнопок не нужен, код задействует встроенный в плату резистор, предназначенный для этих целей.

Небоходимые библиотеки для Arduino IDE

  1. NTPClient from Fabrice Weinberg

  2. Adafruit-GFX-Library

  3. Adafruit-ST7735-Library

Код (из кода вырезаны массивы с изображениями, полный код на GitHub):

Скрытый текст
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <Arduino.h>
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>



// пины управления дисплеем
#define TFT_CS    20    // Chip select
#define TFT_DC    9    // Data/Command
#define TFT_RST   8   // Reset
#define TFT_BL   19    // Backlight

// мягкая замена стандартных SPI-пинов:
#define TFT_SCK   6 
#define TFT_MOSI  2 
// инициализация дисплея
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

char ssid[] = "Your Wi-fi SSID";
char password[] = "Your Wi-fi password";
int status = WL_IDLE_STATUS;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
int page = 0;
int frames = 0;

#define LOGO_HEIGHT 64
#define LOGO_WIDTH  128


void setup() {
  pinMode(0, INPUT_PULLUP);
  pinMode(1, INPUT_PULLUP);
  pinMode(3, INPUT_PULLUP);
  pinMode(14, INPUT_PULLUP);
  // запускаем SPI с указанием своих ножек
  // порядок: SCK, MISO (не нужен, ставим -1), MOSI, SS (CS)
  SPI.begin(TFT_SCK, -1, TFT_MOSI, TFT_CS);

  // инициализация дисплея
  tft.initR(INITR_BLACKTAB);
  tft.setRotation(0);
  tft.fillScreen(ST77XX_BLACK);
  ledcAttach(TFT_BL, 5000, 8);
  ledcWrite(TFT_BL, 60);

  tft.setTextColor(ST77XX_GREEN);
  tft.setTextSize(1);
  uint16_t x = (tft.width()  - 6 * 2 * 5) / 2;
  uint16_t y = (tft.height() - 8 * 2)     / 2;
  tft.setCursor(0, 0);
  tft.println("Display initialized");
  delay (1000);
  status = WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED) {
    tft.println("Wi-Fi connection error");
    return;
  } else {
    tft.println("Wi-Fi connected");
  }
  IPAddress ip = WiFi.localIP();
  tft.println("Local IP:");
  tft.println(ip);
  timeClient.begin();
  tft.println("NTP Time update");
  timeClient.update();
  WiFi.disconnect();
  tft.println(timeClient.getFormattedTime());
  tft.print("\n");
  tft.print("\n");
  matrix("Wake up, Neo...", 15);
  matrix("The Matrix has you...", 21);
  
}

void loop() {

  switch(page) {
    case 0:
       home();
       break;
    case 1:
       home1();
       break;
     case 2:
       home2();
       break;
     case 3:
       home3();
       break;
    case 4:
       home4();
       break;
  }

}

void matrix(char text[], int size) {
  for (int i = 0; i < size; i++) {
    tft.print(text[i]);
    delay(200);
  }
  tft.print("\n");
}

void matrixstr(String text) {
  for (int i = 0; i < text.length(); i++) {
    tft.print(text[i]);
    delay(200);
  }
  tft.print("\n");
}

bool onoff() {
  if (!digitalRead(3)) {
    delay(200);
    return true;
  } else {
    return false;
  }
}

bool startpause() {
  if (!digitalRead(14)) {
    delay(200);
    return true;
  } else {
    return false;
  }
}

bool sound() {
  if (!digitalRead(1)) {
    delay(200);
    return true;
  } else {
    return false;
  }
}

bool reset() {
  if (!digitalRead(0)) {
    delay(200);
    return true;
  } else {
    return false;
  }
}

void home() {
  while(page==0) {
    if (frames==90) {
              static int y = 0;
              if (y==120) {y = 0;} else {y+=40;}
              tft.fillScreen(ST77XX_BLACK);
              tft.setTextSize(2);
              tft.setCursor(0, y);
              day();
              //tft.setTextSize(4);
              //time();
              tft.println(timeClient.getFormattedTime());
              frames = 0;
    } else {frames++;}
      if (onoff()) {page=1;}
      if (startpause()) {page=2;}
      if (sound()) {page=3;}
      if (reset()) {page=4;}
    delay(30);
  }
}

void home1() {
	   static int disp = 0;
		 if (disp==0) {
       		tft.fillScreen(ST77XX_BLACK);
		tft.setTextSize(1);
		tft.setCursor(0, 40);
		tft.println("Press on/off when");
		tft.println("you're ready.");
		disp = 1;
		 }

     if (onoff()) {
		          tft.fillScreen(ST77XX_BLACK);
							tft.setTextColor(ST77XX_WHITE);
							tft.fillTriangle(64, 33, 118, 126, 10, 126, 0x296d);
              tft.setTextSize(2);
              tft.setCursor(50, 80);
							if ((timeClient.getEpochTime() % 2) > 0) {
								tft.println("YES");
							} else {
								tft.println("NO");
							}
							delay(3000);
							tft.setTextColor(ST77XX_GREEN);
							page=0;
							disp=0;
	}
}

void home2() {
  while(page==2) {
    if (frames==90) {
              static int y = 0;
              if (y==120) {y = 0;} else {y+=40;}
              tft.fillScreen(ST77XX_BLACK);
              tft.setTextSize(2);
              tft.setCursor(0, y);
              daym();
              //tft.setTextSize(4);
              //time();
              matrixstr(timeClient.getFormattedTime());
              frames = 0;
    } else {frames++;}
      if (onoff()) {page=1;}
      if (startpause()) {page=2;}
      if (sound()) {page=3;}
      if (reset()) {page=4;}
      delay(30);
  }
}

void home3() {
  while(page==3) {
     posters();
  }
}

void home4() {
  while(page==4) {
      logoanimation();
      if (onoff()) {page=1;}
      if (startpause()) {page=2;}
      if (sound()) {page=3;}
      if (reset()) {page=4;}
  }
}

void nextpage() {
  if (page == 2) {
    page = 0;
  } else {
    page++;
  }
}

void logoanimation() {
	for (int i = 0; i < 31; i++){
		tft.fillScreen(ST77XX_BLACK);
    tft.drawBitmap(0, 48, logoallArray[i], LOGO_WIDTH, LOGO_HEIGHT, 0x07E0);
		delay(21);
	}
}

void posters() {
	for (int i = 0; i < 6; i++){
		tft.drawRGBBitmap(0, 0, postersallArray[i], 128, 160);
		  if (onoff()) {page=1;}
      if (startpause()) {page=2;}
      if (sound()) {page=3;}
      if (reset()) {page=4;}
		delay(5000);
	}
}

void day() {
 int x = timeClient.getDay();
 switch(x) {
  case 0:
    tft.println("Sunday");
    break;
  case 1:
    tft.println("Monday");
    break;
  case 2:
    tft.println("Tuesday");
    break;
  case 3:
    tft.println("Wednesday");
    break;
  case 4:
    tft.println("Thursday");
    break;
  case 5:
    tft.println("Friday");
    break;
  case 6:
    tft.println("Saturday");
    break;
 }
}

void daym() {
 int x = timeClient.getDay();
 switch(x) {
  case 0:
    matrixstr("Sunday");
    break;
  case 1:
    matrixstr("Monday");
    break;
  case 2:
    matrixstr("Tuesday");
    break;
  case 3:
    matrixstr("Wednesday");
    break;
  case 4:
    matrixstr("Thursday");
    break;
  case 5:
    matrixstr("Friday");
    break;
  case 6:
    matrixstr("Saturday");
    break;
 }
}

void time() {
  tft.print(timeClient.getHours());
  tft.print(":");
  if (timeClient.getMinutes() < 10) {tft.print(0);}
  tft.print(timeClient.getMinutes());
  tft.print("\n");
}

В Arduino IDE вам необходимо в первую очередь в менеджере плат установить пакет ESP32 от автора Espressif Systems. Для прошивки выбираем в инструментах плату ESP32C6 Dev Module. Но сначала нужно отредактировать файл Adafruit_ST7735.cpp библиотеки Adafruit_ST7735_and_ST7789_Library. У меня он лежит здесь:
C:\Users\Paperclip\Documents\Arduino\libraries\Adafruit_ST7735_and_ST7789_Library

Нам необходимо добавить строчки в if options функции Adafruit_ST7735::initR :

_colstart = 2;
_rowstart = 1;
Вот таким образом
Вот таким образом

Теперь дисплей должен после компиляции работать корректно.

Кнопки и пины

Вы можете подключить кнопки и дисплей к другим пинам, а не так, как у меня, но нужно учитывать, что есть пины, специально отведённые под ту или иную функцию, например SPI, а некоторые пины лучше вообще не трогать. Предлагаю руководствоваться таблицами из даташита производителя.

Итого

Изначально я планировал использовать кнопки на устройстве в качестве Bluetooth‑клавиатуры, но все библиотеки, которые я перепробовал, не заработали, поэтому от этой идеи пришлось отказаться. Генератор случайных ответов YES/NO в имитации «волшебного шара» из фильма Трасса 60 определяет результат, исходя из того, чётное или нечётное количество секунд на момент выполнения функции. Более правильно было бы использовать сбор энтропии по фоновому напряжению на пинах или специальную функцию генерации случайных чисел от ESP, которую поддерживает этот процессор, вероятнее всего тоже собирая энтропию каким‑либо образом.

Исходя из вышенаписанного можно сделать вывод о том, что пора с Arduino переходить на ESP-IDF для более тонкой работы с возможностями платы и использования FreeRTOS.

Tags:
Hubs:
+9
Comments13

Articles