当前位置: 首页 > article >正文

具有多个表盘、心率传感器、指南针和游戏的 DIY 智能手表

在此,我们将使用所学到的知识,结合使用硬件和软件组件从头开始创建自己的智能手表。在项目的这一部分,您将被指导完成组装硬件组件、设置软件以及配置智能手表的设置和功能的过程。到本项目结束时,您将拥有一款功能齐全的智能手表,您可以每天佩戴和使用。因此,我们在这里开始构建您自己的智能手表

这个完整的项目是由 PCBWAY 提供的紧凑型 PCB 板实现的。在这个项目中,我们还将向您展示如何向他们下订单,以将您的 PCB 板送到您家门口。

ESP32 智能手表功能

  • 1.69 英寸 IPS TFT 显示屏,分辨率为 280x240 像素
  • 单按钮控制
  • 深度睡眠省电模式
  • 使用加速度计自动唤醒
  • 使用环境光传感器自动调节亮度
  • 用于导航的数字罗盘
  • 心率监测器
  • 多个看起来很酷的表盘
  • 直观的菜单系统
  • 内置娱乐游戏
  • Micro SD 卡
  • 振动电机
  • 具有深度放电保护的电池充电能力

构建 ESP32 智能手表所需的组件

下面列出了构建智能手表所需的所有部件。每个元件的确切值可以在原理图或 BOM 中找到。

  • TTGO Micro-32 V2.0 x1
  • IPS6404L PSRAM IC x1
  • 1.69 英寸 TFT 显示屏,带 ST7789V 控制器 x1
  • MAX809T 3V 电源监控器复位控制器 x1
  • CP2102 USB UART 控制器 x1
  • MCP7383 1S 电池充电器 x1
  • DW01 电池保护IC x1
  • FS8205A MOSFET x1
  • NCP167AMX330TBG 3.3V LDO x1
  • XC6202P182MR 1.8V LDO x1
  • MPU6050 加速度计 IC x1
  • HMC5883L 磁力计传感器 IC x1
  • LSM303DLHC 加速度计磁力计 IC(可选,替代 MPU6050 和 HMC5883L)x1
  • MAX30102 心率传感器 x1
  • BH1750FVI 环境光传感器 x1
  • S8050 SOT-23 NPN 晶体管 x3
  • Micro SD 插槽 x1
  • 10mm 震动马达 x1
  • 1N5819 贴片二极管 x2
  • LED5D5 TVS ESD 保护二极管(可选) x3
  • 微型 USB 端口 x1
  • 0.5mm 间距 10 针 FPC 连接器 x2
  • SMD 电阻器
  • SMD 电容器
  • 印刷电路板
  • 其他工具和杂项

ESP32 智能手表完整电路图

ESP32 智能手表的完整电路图如下所示。它也可以从最后给出的链接以 PDF 格式下载。

6b79630165d94bfbac295ec218505d71.png

让我们逐节讨论 Schematics 以便更好地理解。micro-USB 端口用于充电和编程目的。micro-USB 端口的电源和数据连接连接到 TVS ESD 保护二极管。这些二极管将保护整个电路免受 USB 输入上的任何 ESD 尖峰的影响。然后将来自 USB 端口的 5V 连接到MCP7383 1S 锂离子电池充电器的输入端。然后,从充电 IC 输出到围绕 DW01 IC 和 FS8205 MOSFET 构建的保护电路。这种保护电路组合将保护电池免受过流放电和深度放电的影响。

 

377901d6b230da365d1b4a287e5a761a.jpeg

然后,电源通过两个 LDO。电路中使用的主要稳压器是 ON Semi 的 NCP167AMX330TBG。它可以提供 700mA 的最大电流。使用这种芯片的主要优点是尺寸。NCP167AMX采用 1mmx1mm 4-XDFN 封装。这节省了大量空间。电路中的第二个低电压稳压器是 XC620P182MR-G 1.8V LDO。该 LDO 用于 MAX30102 心率传感器芯片。

 

ca9880af97d949d969d59a5b5f510286.jpeg

USB UART 控制器的下一部分。本部分围绕 Silicon Labs 的CP2102N设计。它支持最高 12Mbps 的速度。最少数量的外部组件以及小型 QFN-24 封装使其成为同类别其他控制器芯片的更好选择。ESP32 的自动复位电路围绕两个 S8050 NPN 晶体管构建。晶体管连接到 CP2102 的 DTR 和 RTS 引脚以及 ESP32 的 EN 和 RST 引脚。这使我们能够对 ESP32 进行编程,而无需重置按钮。

 

01624ef5250f3893ba4d3fa730b51530.png

MPU6050 加速度计芯片用于检测运动。此功能使我们能够通过简单的手部动作唤醒智能手表。MPU6050 的中断引脚连接到 ESP14 控制器的 GPIO32。当检测到超过设定阈值的运动时,MPU6050 将向 ESP32 发送中断信号,将其从深度睡眠中唤醒。

 

630633138275c17cd30d55585569a68b.jpeg

下一个传感器是 HMC5883 磁力计传感器。此传感器用于实现数字罗盘功能。使用此传感器时,请确保附近没有磁干扰或任何金属,这可能会产生错误的读数。

 

c34708a20403573230b5c42f034625b3.jpeg

在 PCB 中我们还为 LSM303 芯片预留了空间,它结合了加速度计和磁力计传感器。这个传感器包含在内,以防万一我们不想使用 MPU6050 和 HMC5883L。它是一个保留组件。如果您使用的是 MPU6050-HMC5883 组合,则不必填充它。

 

7003d92766545bc12c95a700b1b6d872.jpeg

接下来,我们有 BH1750 环境光传感器。该传感器用于实现自动亮度控制。该传感器位于 TFT 显示屏下方的正面。外壳上设有一个小孔,用于测量环境光。如果开启自动亮度调节,MCU 将从 BH1750 读取环境光数据,并相应地调整显示背光。

 

b320d5125052b0dc88cb7ecbef0d8c56.jpeg

为了测量心率,我们使用了 Maxim Integrated 的 MAX30102。该传感器在 1.8v 电源电压下工作,并且能够使用光传感器检测心率。该代码的调整方式是,当手表放置在手腕或手指以外的表面时,芯片不会误触发。

 

45e2b97da92965f6794e85ce8abec662.jpeg

我们还在 PCB 中包括一个 micro-SD 插槽和一个振动电机,以用于未来的发展。目前,这些未在代码中配置或使用。micro-SD 与 TFT 显示器共享相同的 SPI ba。它可用于存储固件文件、监控日志甚至表盘数据或图像等数据。振动电机使用 S8050 NPN 晶体管进行控制。电机两端还连接了一个续流二极管,以保护电路免受任何电压尖峰的影响。

 

11107634f45b91afc1716b84fe11cbb6.jpeg

对于显示器,我们使用了圆角的 1.69 英寸显示器。这些 IPS 显示屏提供了非常好的显示对比度和色彩饱和度。此显示器使用 ST7789 显示驱动程序。ST7789 可支持高达 100MHz 的 SPI 总线频率。这将使我们能够更快地驱动显示器,提供更好的 FPS。背光使用 N 沟道 MOSFET 进行控制。PWM 用于控制亮度。

 

aa1f4dca594abf6e397ee4f0f8b2d15b.jpeg

该项目的核心是 LILYGO 的 TTGO Micro-32 V2.0 模块。它基于 ESP32-PICO D4 SIP,集成了 ESP32 SoC、晶体振荡器、滤波电容器、射频匹配链路和 4MB 闪存,采用 7mm × 7mm QFN 封装。我们还将 IPS6404L PSRAM 与模块一起使用。MAX809T MPU 管理芯片用于确保 ESP32-PICO-D4 在冷启动期间重启。该芯片将使 ESP32 保持处于复位状态,直到达到阈值电压。一旦达到阈值电压,MAX809T(3V 重置阈值)将重置 ESP32 并将使能引脚钳位到 VCC。

 

89e70c0d7fbd8b310a90626eba164ef5.jpeg

ESP32 智能手表 PCB

对于 PCB,我们选择了两板设计。顶板包含 MCU 以及显示器、UART 控制器、电源电路、光传感器和 MPU6050 芯片。底部凹槽包含 HMC5883LSM303MAX30102、microSD 插槽和振动电机。这两块板使用间距为 10.0mm 的 5 针 FPC 电缆连接。

 

10c2135601a4bfd93626e51eff76663e.jpeg

这是两个板的 3D 视图

 

0f53625fe23bab66ed5a7be721e079f1.jpeg

这是主板上标记的所有组件。

 

d0206e8322d1c1cbbe1fe25e4d973848.jpeg

这是标记了 components 的子板。

 

d037ae6ba421ad8c699c1b0eb4ce1f3d.jpeg

这是完全组装的电路板以及 TFT 显示器。

 

e3ad79e8b3cedcc463cd1c81ec905a5f.jpeg

 

从 PCBWay 订购基于 ESP32 的智能手表 PCB

现在,在完成设计后,您可以继续订购 PCB:

第 1 步:进入 pcbway.com,如果这是您第一次注册。然后,在 PCB 原型选项卡中,输入 PCB 的尺寸、层数和所需的 PCB 数量。

 

a054317c4ee815aa7f7803c7738a9a50.png

第 2 步:单击“立即报价”按钮继续。您将被带到一个页面以设置一些附加参数,例如 板类型、层、PCB 材料、厚度等。默认情况下,它们中的大多数都是选中的,如果您选择任何特定参数,则可以在此处选择它。

 

245cbc3453daf725ba109f7be8565874.png

第 3 步:最后一步是上传 Gerber 文件并继续付款。为确保过程顺利,PCBWAY 会在继续付款之前验证您的 Gerber 文件是否有效。这样,您可以确保您的 PCB 对制造友好,并且会按承诺到达您手中。

 

8fefc1a9cd3b0259d68421ea9c4628f2.png

上传 Gerber 文件并付款后,您的工作就完成了,您将收到一封确认电子邮件,其中包含您的电子邮件地址中的所有详细信息。

3D 打印部件

 

c400f67607374b32a1df64613e9f4858.jpeg

我们为智能手表设计了一个看起来很酷的 3D 打印外壳。所有 3D 打印部件的文件都可以从本文末尾提供的 GitHub 链接以及 Arduino 草图和位图文件下载。建议打印填充度更高的部件,以获得更好的质量和坚固性。点击链接了解有关 3D 打印以及如何开始使用它的更多信息。

ESP32 智能手表 GUI 导航

整个 GUI 的设计方式是,我们可以使用一个按钮浏览每个选项。我们可以使用短按和长按来浏览它们。您可以在下图中对整个 GUI 流程进行 finify。蓝线表示单击/短按 ,而绿线表示长按。在 Time Settings 和 Settings 菜单中,您可以浏览每个选项或使用短时钟进行归档。选择选项并使用长按更改值。

 

9464fb7459096cf0835b224fb15b8fb1.jpeg

ESP32 智能手表的 Arduino 代码

现在让我们看看代码。像往常一样,我们使用 include 函数将所有必要的库包含在代码中,包括 TFT_eSPI、ESP32Time、EEPROM、OneButton、QMC5883L、BH1750 和 MAX30105 库。我们还将位图图像数据与字体文件一起包括在内。之后,我们定义了所有必要的全局变量。稍后,我们为每个单独的组件创建了实例。我们将使用这些实例来访问相应的函数。

#include <SPI.h>
#include <TFT_eSPI.h>  // Hardware-specific library
#include <ESP32Time.h>
#include "driver/gpio.h"
#include "esp_sleep.h"
#include <EEPROM.h>
#include "OneButton.h"
#include <QMC5883L.h>
#include <BH1750.h>  //BH1750 Library
#include "Free_Fonts.h"
#include "MAX30105.h"   // SparkFun librarry for MAX30102 sensor
#include "heartRate.h"  // Heartrate measurement algorithm
#include "dial240.h"    //Image data
#include "fonts.h"
#include "images.h"
#define PIN_INPUT 0
#define EEPROM_SIZE 25
#define FONT_SMALL NotoSansBold15
#define FONT_LARGE NotoSansBold36
#define TFT_GREY 0x5AEB
#define TFT_SKYBLUE 0x067D
#define color1 TFT_WHITE
#define color2 0x8410  //0x8410
#define color3 0x5ACB
#define color4 0x15B3
#define color5 0x00A3
#define colour6 0x0926
#define colour7 TFT_BLACK
#define Light_Green 0x07E8
#define background 0xB635
#define LCD_BACKLIGHT 4
#define TFTW 240          // screen width
#define TFTH 280          // screen height
#define TFTW2 (TFTW / 2)  // half screen width
#define TFTH2 (TFTH / 2)  // half screen height
#define SPEED 1
#define GRAVITY 9.8
#define JUMP_FORCE 2.15
#define SKIP_TICKS 20.0  // 1000 / 50fps
#define MAX_FRAMESKIP 5
#define BIRDW 16      // bird width
#define BIRDH 16      // bird height
#define BIRDW2 8      // half width
#define BIRDH2 8      // half height
#define PIPEW 24      // pipe width
#define GAPHEIGHT 42  // pipe gap height
#define FLOORH 30     // floor height (from bottom of the screen)
#define GRASSH 4      // grass height (inside floor, starts at floor y)
#define COLOR565(r, g, b) ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
#define BCKGRDCOL COLOR565(138, 235, 244)    // background
#define BIRDCOL COLOR565(255, 254, 174)      // bird
#define PIPECOL COLOR565(99, 255, 78)        // pipe
#define PIPEHIGHCOL COLOR565(250, 255, 250)  // pipe highlight
#define PIPESEAMCOL COLOR565(0, 0, 0)        // pipe seam
#define FLOORCOL COLOR565(246, 240, 163)     // floor
#define GRASSCOL COLOR565(141, 225, 87)      // grass (col2 is the stripe color)
#define GRASSCOL2 COLOR565(156, 239, 88)     // grass (col2 is the stripe color)
#define C0 BCKGRDCOL                         // bird sprite,bird sprite colors (Cx name for values to keep the array readable)
#define C1 COLOR565(195, 165, 75)
#define C2 BIRDCOL
#define C3 TFT_WHITE
#define C4 TFT_RED
#define C5 COLOR565(251, 216, 114)
ESP32Time rtc(0);   // RTC instance with offset in seconds
BH1750 lightMeter;  //BH1750 Instance
QMC5883L compass;
MAX30105 particleSensor;  //MAX30102 instance
OneButton button(PIN_INPUT, true);
TFT_eSPI tft = TFT_eSPI();  // Invoke custom library
TFT_eSprite img = TFT_eSprite(&tft);
static const unsigned int birdcol[] = {
  C0, C0, C1, C1, C1, C1, C1, C0, C0, C0, C1, C1, C1, C1, C1, C0,
  C0, C1, C2, C2, C2, C1, C3, C1, C0, C1, C2, C2, C2, C1, C3, C1,
  C0, C2, C2, C2, C2, C1, C3, C1, C0, C2, C2, C2, C2, C1, C3, C1,
  C1, C1, C1, C2, C2, C3, C1, C1, C1, C1, C1, C2, C2, C3, C1, C1,
  C1, C2, C2, C2, C2, C2, C4, C4, C1, C2, C2, C2, C2, C2, C4, C4,
  C1, C2, C2, C2, C1, C5, C4, C0, C1, C2, C2, C2, C1, C5, C4, C0,
  C0, C1, C2, C1, C5, C5, C5, C0, C0, C1, C2, C1, C5, C5, C5, C0,
  C0, C0, C1, C5, C5, C5, C0, C0, C0, C0, C1, C5, C5, C5, C0, C0
};
// bird structure
static struct BIRD {
  long x, y, old_y;
  long col;
  float vel_y;
} bird;
// pipe structure
static struct PIPES {
  long x, gap_y;
  long col;
} pipes;
// score
int score;
// temporary x and y var
static short tmpx, tmpy;
// ---------------
// draw pixel
// ---------------
// faster drawPixel method by inlining calls and using setAddrWindow and pushColor using macro to force inlining
#define _drawPixel(a, b, c) \
  tft.setAddrWindow(a, b, a, b); \
  tft.pushColor(c)
uint maxScore = 0;
float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;  // Saved H, M, S x & y multipliers
float sdeg = 0, mdeg = 0, hdeg = 0;
uint16_t osx = 120, osy = 140, omx = 120, omy = 140, ohx = 120, ohy = 140;  // Saved H, M, S x & y coords
uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
uint32_t targetTime = 0;                       // for next 1 second timeout
static uint8_t conv2d(const char* p);          // Forward declaration needed for IDE 1.6.x
uint8_t hh = 0, t_mm = 0, t_dd = 0, t_mn = 0;  //
uint32_t t_yr = 0;
uint8_t t_hh = 0, mm = 0, ss = 0;
unsigned long lastfacechange = 0;
unsigned long lastwake = 0;
unsigned long lastpressed = 0;
unsigned long lastvaluechange = 0;
bool initial = 1;
volatile int counter = 0;
float VALUE;
float lastValue = 0;
int lastsec = 0;
int pressstate = 0;
unsigned long lastDisplayUpdate = 0;
const byte RATE_SIZE = 4;  //Increase this for more averaging. 4 is good.
byte rates[RATE_SIZE];     //Array of heart rates
byte rateSpot = 0;
long lastBeat = 0;  //Time at which the last beat occurred
float beatsPerMinute;
int beatAvg;
bool beat = false;
double rad = 0.01745;
float x[360];
float y[360];
bool facechange = false;
bool Screenchange = false;
float px[360];
float py[360];
float lx[360];
float ly[360];
int r = 104;
int ssx = 120;
int ssy = 140;
String cc[12] = { "45", "40", "35", "30", "25", "20", "15", "10", "05", "0", "55", "50" };
String days[] = { "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" };
String days1[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
int start[12];
int startP[60];
const int pwmFreq = 5000;
const int pwmResolution = 8;
const int pwmLedChannelTFT = 0;
int angle = 0;
bool onOff = 0;
bool debounce = 0;
int watchface = 0, Screen = 0, SubScreen = 0, Autoscreen, AutoBright, AutoscreenTime, Brigtnesslevel;
String h, m, s, d1, d2, m1, m2;
unsigned long pressStartTime;

IRAM_ATTR 函数是通过 GPIO0 附加到硬件中断的中断函数。一旦检测到引脚变化,此函数将被调用。然后,此函数将调用 button_tick 函数,该函数负责检测按键操作。

// This function is called from the interrupt when the signal on the PIN_INPUT has changed.
// do not use Serial in here.
void IRAM_ATTR checkTicks() {
  // include all buttons here to be checked
  button.tick();  // just call tick() to check the state.
}

如果 OneButton 库检测到短按键,将调用 Shortclick 函数。调用后,我们将向它传递 next 条件。此函数将检查我们当前所在的任务,并相应地更改变量。

// this function will be called for short click.
void ShortClick() {
  Serial.println("singleClick() detected.");
  lastwake = millis();
  if (Screen == 0) {
    SubScreen++;
    if (SubScreen > 2) {
      SubScreen = 0;
      facechange = true;
    }
    if (SubScreen == 1) {
      particleSensor.wakeUp();
    } else {
      particleSensor.shutDown();
    }
  } else if (Screen == 1) {
    SubScreen++;
    if (SubScreen > 4) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 2) {
    watchface++;
    if (watchface > 5) {
      watchface = 0;
    }
    EEPROM.write(0, watchface);
    EEPROM.commit();
    facechange = true;
    Screenchange = true;
  } else if (Screen == 3) {
    SubScreen++;
    if (SubScreen > 5) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 4) {
    SubScreen++;
    if (SubScreen > 2) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 5) {
    Screen = 6;
    game_init();
    game_loop();
  } else if (Screen == 6) {
    Screen = 7;
  } else if (Screen == 7) {
    Screen = 5;
    Screenchange = true;
  }
  Serial.print("Sub ");
  Serial.println(SubScreen);
  tft.fillScreen(colour7);
  pressstate = 1;
  facechange = true;
  lastDisplayUpdate = millis();
  lastpressed = millis();
}
// ShortClick

当检测到长按按键时,将调用 LongPress 函数,最短持续时间为 1 秒。调用后,它将检测当前正在运行的任务并相应地操作变量。

// long press
void LongPress() {
  Serial.println("pressStart()");
  pressStartTime = millis() - 1000;  // as set in setPressTicks()
  lastwake = millis();
  lastDisplayUpdate = millis();
  particleSensor.shutDown();
  if (Screen == 0) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 1) {
    if (SubScreen == 0) {
      Screen = 2;
      SubScreen = 0;
    } else if (SubScreen == 1) {
      Screen = 3;
      t_hh = rtc.getHour();
      t_mm = rtc.getMinute();
      t_dd = rtc.getDay();
      t_mn = rtc.getMonth();
      t_yr = rtc.getYear();
      Serial.println(rtc.getYear());
      Serial.println(t_yr);
      SubScreen = 0;
    } else if (SubScreen == 2) {
      Screen = 4;
      SubScreen = 0;
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 3) {
      Screen = 5;
      SubScreen = 0;
    } else if (SubScreen == 4) {
      Screen = 0;
      SubScreen = 0;
    }
  } else if (Screen == 2) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 3) {
    if (SubScreen == 0) {
      t_hh++;
      if (t_hh > 23) {
        t_hh = 0;
      }
    } else if (SubScreen == 1) {
      t_mm++;
      if (t_mm > 59) {
        t_mm = 0;
      }
    } else if (SubScreen == 2) {
      t_dd++;
      if (t_dd > 31) {
        t_dd = 0;
      }
    } else if (SubScreen == 3) {
      t_mn++;
      if (t_mn > 12) {
        t_mn = 0;
      }
    } else if (SubScreen == 4) {
      t_yr++;
      if (t_yr > 2041) {
        t_yr = 0;
      }
    } else {
      rtc.setTime(0, t_mm, t_hh, t_dd, t_mn, t_yr);
      Screen = 1;
      SubScreen = 1;
    }
  } else if (Screen == 4) {
    if (SubScreen == 0) {
      AutoBright++;
      if (AutoBright > 5) {
        AutoBright = 0;
      }
      if (AutoBright > 0) {
        analogWrite(LCD_BACKLIGHT, AutoBright * 50);
      }
      EEPROM.write(2, AutoBright);
      EEPROM.commit();
    } else if (SubScreen == 1) {
      Autoscreen++;
      if (Autoscreen > 5) {
        Autoscreen = 0;
      }
      EEPROM.write(1, Autoscreen);
      EEPROM.commit();
    } else if (SubScreen == 2) {
      Screen = 1;
      SubScreen = 2;
    }
  } else if (Screen == 5) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 7) {
    Screen = 1;
    SubScreen = 0;
  }
  facechange = true;
  Screenchange = true;
  pressstate = 1;
  lastpressed = millis();
}

在 setup 函数中,我们已经初始化了所有需要的库和引脚。设置功能还将检查是否使用了 EEPROM 保存区域。如果这些位置具有默认值或空白值,它会将出厂默认值加载到该位置,并将手表初始化为该值。中断附件也在 setup 函数中完成,包括 deep sleep wakeup interrupt 和 button tick interrupt。

void setup(void) {
  Serial.begin(115200);
  Serial.println("ESP32 Watch OS.");
  gpio_hold_dis((gpio_num_t)LCD_BACKLIGHT);
  pinMode(LCD_BACKLIGHT, OUTPUT);
  digitalWrite(LCD_BACKLIGHT, LOW);
  EEPROM.begin(EEPROM_SIZE);
  EEPROM.writeInt(10, 0);
  EEPROM.commit();
  if (EEPROM.read(0) > 3) {
    EEPROM.write(0, 4);
    EEPROM.commit();
  }
  watchface = EEPROM.read(0);
  if (EEPROM.read(1) > 5) {
    EEPROM.write(1, 5);
    EEPROM.commit();
  }
  Autoscreen = EEPROM.read(1);
  if (EEPROM.read(2) > 5) {
    EEPROM.write(2, 5);
    EEPROM.commit();
  }
  AutoBright = EEPROM.read(2);
  //rtc.setTime(ss, mm, hh, 0, 0, 0);  // 26th Jjuly 2022 compile date
  particleSensor.begin(Wire, I2C_SPEED_FAST);
  particleSensor.setup();                     //Configure sensor with default settings
  particleSensor.setPulseAmplitudeRed(0x0A);  //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeIR(0xFF);   //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeGreen(0);   //Turn off Green LED
  particleSensor.shutDown();
  compass.init();
  compass.setSamplingRate(50);
  tft.init();
  tft.setRotation(0);
  //tft.setColorDepth(16);
  tft.setSwapBytes(true);
  tft.fillScreen(colour7);
  int xw = tft.width() / 2;  // xw, yh is middle of screen
  int yh = tft.height() / 2;
  tft.setPivot(xw, yh);  // Set pivot to middle of TFT screen
  img.createSprite(240, 280);
  img.setTextDatum(4);
  img1.createSprite(240, 70);
  img1.setSwapBytes(true);
  img2.createSprite(240, 70);
  img2.setSwapBytes(true);
  targetTime = millis() + 1000;
  facechange = true;
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);  //1 = High, 0 = Low
  // setup interrupt routine
  // when not registering to the interrupt the sketch also works when the tick is called frequently.
  attachInterrupt(digitalPinToInterrupt(PIN_INPUT), checkTicks, CHANGE);
  // link the xxxclick functions to be called on xxxclick event.
  button.attachClick(ShortClick);
  button.setPressTicks(1000);  // that is the time when LongPressStart is called
  button.attachLongPressStart(LongPress);
  lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);  //Init BH1750 library
  if (AutoBright == 0) {
    unsigned int lv = constrain(lightMeter.readLightLevel(), 50, 500);
    analogWrite(LCD_BACKLIGHT, lv / 2);
  } else {
    analogWrite(LCD_BACKLIGHT, AutoBright * 50);
  }
  lastwake = millis();
}
int lastAngle = 0;
float circle = 100;
bool dir = 0;
int rAngle = 359;

loop 函数将定期调用所有 mains 函数,包括 button_tick、watchtask 和 game 函数。button_tick功能负责按键检测,区分射压和长按。watchtask 功能负责所有主要功能,包括表盘显示、心率监测、数字罗盘、菜单处理以及所有设置和导航。同时,游戏功能将处理 watch OS 中包含的 flappy bird 游戏。

void loop() {
  button.tick();
  if (Screen < 5) {
    watchtask();
  } else {
    game();
  }
}

如前所述,watchtask 将处理与智能手表相关的大部分任务。此功能将检查亮度是否设置并相应地调整背光 PWM。它还将检查屏幕超时设置,并相应地使 ESP32 进入深度睡眠。所有 sub 函数都将根据当前活动任务进行相应调用。我们使用了 gpio_deep_sleep_hold_en 函数和 gpio_hold_en 函数,以在深度睡眠期间保持背光引脚有效。如果没有这些功能,GPIO 将从任何设置的状态中释放出来,并且会影响背光控制。

void watchtask() {
  if (pressstate == 1 && digitalRead(0) == 1) {
    pressstate = 0;
  }
  if (AutoBright == 0) {
    unsigned int lv = constrain(lightMeter.readLightLevel(), 50, 500);
    analogWrite(LCD_BACKLIGHT, lv / 2);
    Serial.print("Light");
    Serial.println(lv / 2);
  }
  if (Autoscreen != 0 && millis() - lastwake > Autoscreen * 60000) {
    analogWrite(LCD_BACKLIGHT, 0);
    delay(1000);
    tft.fillScreen(colour7);
    gpio_deep_sleep_hold_en();
    gpio_hold_en((gpio_num_t)LCD_BACKLIGHT);
    esp_deep_sleep_start();
  }
  if (Screen == 0) {
    if (SubScreen == 0) {
      watchfacedsp();
    } else if (SubScreen == 1) {
      HRApp();
    } else {
      CompassApp();
    }
  } else if (Screen == 1 && Screenchange == true) {
    if (SubScreen == 0) {
      tft.pushImage(0, 0, 240, 280, facechangeicon);
    } else if (SubScreen == 1) {
      tft.pushImage(0, 0, 240, 280, timeseticon);
    } else if (SubScreen == 2) {
      tft.pushImage(0, 0, 240, 280, settingsicon);
    } else if (SubScreen == 3) {
      tft.pushImage(0, 0, 240, 280, gamesicon);
    } else if (SubScreen == 4) {
      tft.pushImage(0, 0, 240, 280, exiticon);
    }
  } else if (Screen == 3 && Screenchange == true) {
    timesetiings();
    Screenchange = false;
  } else if (Screen == 2 && Screenchange == true) {
    watchfacedsp();
    Screenchange = false;
  } else if (Screen == 4 && Screenchange == true) {
    settings();
    Screenchange = false;
  } else if (Screen == 3 && millis() - lastpressed > 2000 && millis() - lastvaluechange > 500 && pressstate == 1) {
    if (SubScreen == 0) {
      t_hh++;
      if (t_hh > 23) {
        t_hh = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 1) {
      t_mm++;
      if (t_mm > 59) {
        t_mm = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 2) {
      facechange = true;
      Screenchange = true;
      t_dd++;
      if (t_dd > 31) {
        t_dd = 0;
      }
    } else if (SubScreen == 3) {
      t_mn++;
      if (t_mn > 12) {
        t_mn = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 4) {
      t_yr++;
      if (t_yr > 2041) {
        t_yr = 0;
      }
      facechange = true;
      Screenchange = true;
    }
    lastvaluechange = millis();
  }
}

timesettings 函数处理时间设置菜单。使用此功能,我们可以设置正确的日期和时间。短按更改字段,长按更改值。

void timesetiings() {
  tft.pushImage(0, 0, 240, 280, TimeSettings);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  tft.setFreeFont(FF18);
  int tt_hh = t_hh;
  if (tt_hh > 12) {
    tt_hh = tt_hh - 12;
  }
  if (tt_hh < 10) {
    tft.drawString("0" + String(tt_hh), 25, 96);
  } else {
    tft.drawString(String(tt_hh), 25, 96);
  }
  if (t_mm < 10) {
    tft.drawString("0" + String(t_mm), 93, 96);
  } else {
    tft.drawString(String(t_mm), 93, 96);
  }
  if (t_hh < 13) {
    tft.drawString("AM", 164, 96);
  } else {
    tft.drawString("PM", 164, 96);
  }
  if (t_dd < 10) {
    tft.drawString("0" + String(t_dd), 25, 183);
  } else {
    tft.drawString(String(t_dd), 25, 183);
  }
  if (t_mn < 10) {
    tft.drawString("0" + String(t_mn), 93, 183);
  } else {
    tft.drawString(String(t_mn), 93, 183);
  }
  if (t_yr < 2022) {
    t_yr = 2022;
  }
  tft.drawString(String(t_yr), 161, 183);
  if (SubScreen == 0) {
    tft.drawRoundRect(9, 81, 58, 48, 6, Light_Green);
  } else if (SubScreen == 1) {
    tft.drawRoundRect(77, 81, 58, 48, 6, Light_Green);
  } else if (SubScreen == 2) {
    tft.drawRoundRect(9, 168, 58, 48, 6, Light_Green);
  } else if (SubScreen == 3) {
    tft.drawRoundRect(77, 168, 58, 48, 6, Light_Green);
  } else if (SubScreen == 4) {
    tft.drawRoundRect(144, 168, 88, 48, 6, Light_Green);
  } else {
    tft.drawRoundRect(79, 226, 88, 48, 6, Light_Green);
  }
}

Using the settings menu, we can manage the brightness and screen time out settings. We can either set the watch to adjust the screen brightness according to the BH1750 ambient light sensor reading or we can set it manually from 20-100% in 20% steps. For screen time out we can choose either to keep the screen on all the time or we can set the screen time out from 1 minute to 5 minutes in 1-minute steps.

void settings() {
  tft.pushImage(0, 0, 240, 280, Settingspage);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  tft.setFreeFont(FF18);
  switch (AutoBright) {
    case 0: tft.drawString("  Auto  ", 80, 100); break;
    case 1: tft.drawString("  20%   ", 85, 100); break;
    case 2: tft.drawString("  40%   ", 85, 100); break;
    case 3: tft.drawString("  60%   ", 85, 100); break;
    case 4: tft.drawString("  80%   ", 85, 100); break;
    case 5: tft.drawString(" 100%   ", 80, 100); break;
  }
  switch (Autoscreen) {
    case 0: tft.drawString("Always On", 65, 185); break;
    case 1: tft.drawString("1 Minute ", 75, 185); break;
    case 2: tft.drawString("2 Minute ", 75, 185); break;
    case 3: tft.drawString("3 Minute ", 75, 185); break;
    case 4: tft.drawString("4 Minute ", 75, 185); break;
    case 5: tft.drawString("5 Minute ", 75, 185); break;
  }
  if (SubScreen == 0) {
    tft.drawRoundRect(16, 85, 208, 48, 6, Light_Green);
  } else if (SubScreen == 1) {
    tft.drawRoundRect(16, 169, 208, 48, 6, Light_Green);
  } else {
    tft.drawRoundRect(66, 225, 108, 48, 6, Light_Green);
  }
}

HRApp 函数处理心率传感器。一旦调用此函数,我们将激活 MAX30102 传感器并开始读取。如果未检测到手腕或手指,手表将显示错误消息。检测到后,手表将检测跳动,并以 bps 为单位计算心率。一旦我们退出此功能,手表会将 MAX30102 关机到低功耗模式以节省电量。

void HRApp() {
  long irValue = particleSensor.getIR();  //Reading the IR value it will permit us to know if there's a finger on the sensor or not
  //Also detecting a heartbeat
  if (checkForBeat(irValue) == true)  //If a heart beat is detected
  {
    long delta = millis() - lastBeat;  //Measure duration between two beats
    lastBeat = millis();
    beatsPerMinute = 60 / (delta / 1000.0);  //Calculating the BPM
    if (beatsPerMinute < 255 && beatsPerMinute > 20)  //To calculate the average we strore some values (4) then do some math to calculate the average {
      rates[rateSpot++] = (byte)beatsPerMinute;  //Store this reading in the array
      rateSpot %= RATE_SIZE;                     //Wrap variable
      //Take average of readings
      beatAvg = 0;
      for (byte x = 0; x < RATE_SIZE; x++)
        beatAvg += rates[x];
      beatAvg /= RATE_SIZE;
    }
  }
  if (millis() - lastDisplayUpdate > 500) {
    if (irValue < 60000) {  //If no finger is detected it inform the user and put the average BPM to 0 or it will be stored for the next measure
      beatAvg = 0;
      img.loadFont(FONT_SMALL);
      img.setCursor(80, 120);
      img.setTextColor(TFT_CYAN, colour7);
      img.fillSprite(colour7);
      img.println("Please Place ");
      img.setCursor(80, 140);
      img.println("your finger ");
      img.pushSprite(0, 0);
    } else {
      img.fillSprite(colour7);  //Clear the display
      if (beat == true) {
        img.pushImage(0, 0, 240, 280, hr1);
      } else {
        img.pushImage(0, 0, 240, 280, hr2);
      }
      beat = !beat;
      img.setTextColor(TFT_CYAN, colour7);
      img.loadFont(FONT_LARGE);
      img.setCursor(100, 130);
      img.print(beatAvg);
      img.setCursor(100, 175);
      img.loadFont(FONT_SMALL);
      img.print("BPM ");
      img.pushRotated(0);
    }
    lastDisplayUpdate = millis();
  }
  Serial.print("IR=");
  Serial.print(irValue);
  Serial.print(", BPM=");
  Serial.print(beatsPerMinute);
  Serial.print(", Avg BPM=");
  Serial.print(beatAvg);
  if (irValue < 60000)
    Serial.print(" No finger?");
  if (millis() - lastBeat > 5000) {
    beatsPerMinute = 0;
    beatAvg = 0;
  }
  Serial.println();
}

同样,CompassApp 函数将与 HMC5883L 传感器通信,并相应地计算航向。计算出角度后,该功能将以适当的方向显示罗盘刻度盘,指示方向。

void CompassApp() {
  for (int i = 0; i < 10; i++) {
    angle = angle + compass.readHeading();
  }
  angle = angle / 10;
  img.fillSprite(colour7);
  img.pushImage(0, 20, 240, 240, dial240);
  img.pushRotated(angle);  // create rotated image as per the angle from the compass sensor
  angle = 0;
}

For displaying the selected watch face we will use the watchfacedp function. This function will check for the current set watch face, and it will display the current time using that specific watch face. Current time is read from the internal RTC registries. The internal RTC will keep running even if the ESP32 goes to deep sleep, keeping the exact time.

void watchfacedsp() {
  if (facechange) {
    tft.fillScreen(colour7);
    if (watchface == 1) {
      tft.setTextSize(0);
      tft.pushImage(0, 0, 240, 280, Casio2);
      tft.setTextColor(0x0081, background);
      tft.fillRoundRect(48, 127, 128, 48, 5, background);
    } else if (watchface == 2) {
      tft.setTextSize(0);
      tft.pushImage(0, 0, 240, 280, Casio1);
      tft.setTextColor(0x0081, background);
      tft.fillRoundRect(48, 127, 128, 48, 5, background);
    } else if (watchface == 4) {
      tft.pushImage(0, 0, 240, 280, cdface1);
      img2.pushImage(0, 0, 240, 100, cdface11);
      tft.setTextColor(TFT_WHITE);
      tft.setFreeFont(FF18);
      tft.setTextSize(2);
    } else if (watchface == 5) {
      tft.pushImage(0, 0, 240, 280, cdface2);
      img2.pushImage(0, 0, 240, 100, cdface12);
      tft.setTextColor(TFT_WHITE);
      tft.setFreeFont(FF18);
      tft.setTextSize(2);
    }
    facechange = false;
    lastfacechange = millis();
  }
  if (watchface == 0) {
    int b = 0;
    int b2 = 0;
    for (int i = 0; i < 360; i++) {
      x[i] = (r * cos(rad * i)) + ssx;
      y[i] = (r * sin(rad * i)) + ssy;
      px[i] = ((r - 16) * cos(rad * i)) + ssx;
      py[i] = ((r - 16) * sin(rad * i)) + ssy;
      lx[i] = ((r - 26) * cos(rad * i)) + ssx;
      ly[i] = ((r - 26) * sin(rad * i)) + ssy;
      if (i % 30 == 0) {
        start[b] = i;
        b++;
      }
      if (i % 6 == 0) {
        startP[b2] = i;
        b2++;
      }
    }
    rAngle = rAngle - 2;
    angle = rtc.getSecond() * 6;
    s = String(rtc.getSecond());
    m = String(rtc.getMinute());
    h = String(rtc.getHour());
    if (m.toInt() < 10)
      m = "0" + m;
    if (h.toInt() < 10)
      h = "0" + h;
    if (s.toInt() < 10)
      s = "0" + s;
    if (rtc.getDay() > 10) {
      d1 = rtc.getDay() / 10;
      d2 = rtc.getDay() % 10;
    } else {
      d1 = "0";
      d2 = String(rtc.getDay());
    }
    if (rtc.getMonth() > 10) {
      m1 = rtc.getMonth() / 10;
      m2 = rtc.getMonth() % 10;
    } else {
      m1 = "0";
      m2 = String(rtc.getMonth());
    }
    if (angle >= 360)
      angle = 0;
    if (rAngle <= 0)
      rAngle = 359;
    if (dir == 0)
      circle = circle + 0.5;
    else
      circle = circle - 0.5;
    if (circle > 140)
      dir = !dir;
    if (circle < 100)
      dir = !dir;
    if (angle > -1) {
      lastAngle = angle;
      VALUE = ((angle - 270) / 3.60) * -1;
      if (VALUE < 0)
        VALUE = VALUE + 100;
      img.fillSprite(colour7);
      img.fillCircle(ssx, ssy, 124, colour7);
      img.setTextColor(TFT_WHITE, colour7);
      img.drawString(days[rtc.getDayofWeek()], circle, 140, 2);
      for (int i = 0; i < 12; i++)
        if (start[i] + angle < 360) {
          img.drawString(cc[i], x[start[i] + angle], y[start[i] + angle], 2);
          img.drawLine(px[start[i] + angle], py[start[i] + angle], lx[start[i] + angle], ly[start[i] + angle], color1);
        } else {
          img.drawString(cc[i], x[(start[i] + angle) - 360], y[(start[i] + angle) - 360], 2);
          img.drawLine(px[(start[i] + angle) - 360], py[(start[i] + angle) - 360], lx[(start[i] + angle) - 360], ly[(start[i] + angle) - 360], color1);
        }
      img.setFreeFont(&DSEG7_Modern_Bold_20);
      img.drawString(s, ssx, ssy - 36);
      img.setFreeFont(&DSEG7_Classic_Regular_28);
      img.drawString(h + ":" + m, ssx, ssy + 28);
      img.setTextFont(0);
      img.fillRect(70, 86, 12, 20, color3);
      img.fillRect(84, 86, 12, 20, color3);
      img.fillRect(150, 86, 12, 20, color3);
      img.fillRect(164, 86, 12, 20, color3);
      img.setTextColor(0x35D7, colour7);
      img.drawString("MONTH", 84, 78);
      img.drawString("DAY", 162, 78);
      img.setTextColor(TFT_SKYBLUE, colour7);
      img.drawString("Circuit Digest", 120, 194);
      img.drawString("***", 120, 124);
      img.setTextColor(TFT_WHITE, color3);
      img.drawString(m1, 77, 96, 2);
      img.drawString(m2, 91, 96, 2);
      img.drawString(d1, 157, 96, 2);
      img.drawString(d2, 171, 96, 2);
      for (int i = 0; i < 60; i++)
        if (startP[i] + angle < 360)
          img.fillCircle(px[startP[i] + angle], py[startP[i] + angle], 1, color1);
        else
          img.fillCircle(px[(startP[i] + angle) - 360], py[(startP[i] + angle) - 360], 1, color1);
      img.fillTriangle(ssx - 1, ssy - 70, ssx - 5, ssy - 56, ssx + 4, ssy - 56, TFT_ORANGE);
      img.fillCircle(px[rAngle], py[rAngle], 6, TFT_RED);
      img.pushSprite(0, 0);
    }
  } else if (rtc.getSecond() != lastsec || Screen == 3) {
    if (watchface == 1 || watchface == 2) {
      /*
      String med;
      if (rtc.getSecond() % 2) {
        med = ":";
      } else {
        med = " ";
      }
      */
      tft.setFreeFont(&DSEG7_Classic_Bold_30);
      if (rtc.getHour() > 9 && rtc.getMinute() > 9) {
        tft.drawString(String(rtc.getHour()) + ":" + String(rtc.getMinute()), 46, 135);
      } else if (rtc.getHour() < 10 && rtc.getMinute() > 9) {
        tft.drawString("0" + String(rtc.getHour()) + ":" + String(rtc.getMinute()), 46, 135);
      } else if (rtc.getHour() > 9 && rtc.getMinute() < 10) {
        tft.drawString(String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 46, 135);
      } else {
        tft.drawString("0" + String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 46, 135);
      }
      tft.setFreeFont(&DSEG7_Classic_Bold_20);
      if (rtc.getSecond() < 10) {
        tft.drawString("0" + String(rtc.getSecond()), 154, 145);
      } else {
        tft.drawString(String(rtc.getSecond()), 154, 145);
      }
      tft.setFreeFont(&DSEG14_Classic_Bold_18);
      tft.drawString(days1[rtc.getDayofWeek()], 94, 106);
      tft.drawString(String(rtc.getDay()), 156, 106);
    } else if (watchface == 3) {
      img.setTextColor(TFT_WHITE, colour7);  // Adding a background colour erases previous text automatically
      // Draw clock face
      img.fillCircle(120, 140, 118, TFT_GREEN);
      img.fillCircle(120, 140, 110, colour7);
      // Draw 12 lines
      for (int i = 0; i < 360; i += 30) {
        sx = cos((i - 90) * 0.0174532925);
        sy = sin((i - 90) * 0.0174532925);
        x0 = sx * 114 + 120;
        yy0 = sy * 114 + 140;
        x1 = sx * 100 + 120;
        yy1 = sy * 100 + 140;
        img.drawLine(x0, yy0, x1, yy1, TFT_GREEN);
      }
      // Draw 60 dots
      for (int i = 0; i < 360; i += 6) {
        sx = cos((i - 90) * 0.0174532925);
        sy = sin((i - 90) * 0.0174532925);
        x0 = sx * 102 + 120;
        yy0 = sy * 102 + 140;
        // Draw minute markers
        img.drawPixel(x0, yy0, TFT_WHITE);
        // Draw main quadrant dots
        if (i == 0 || i == 180) img.fillCircle(x0, yy0, 2, TFT_WHITE);
        if (i == 90 || i == 270) img.fillCircle(x0, yy0, 2, TFT_WHITE);
      }
      img.fillCircle(120, 141, 3, TFT_WHITE);
      // Pre-compute hand degrees, x & y coords for a fast screen update
      sdeg = rtc.getSecond() * 6;                      // 0-59 -> 0-354
      mdeg = rtc.getMinute() * 6 + sdeg * 0.01666667;  // 0-59 -> 0-360 - includes seconds
      hdeg = rtc.getHour() * 30 + mdeg * 0.0833333;    // 0-11 -> 0-360 - includes minutes and seconds
      hx = cos((hdeg - 90) * 0.0174532925);
      hy = sin((hdeg - 90) * 0.0174532925);
      mx = cos((mdeg - 90) * 0.0174532925);
      my = sin((mdeg - 90) * 0.0174532925);
      sx = cos((sdeg - 90) * 0.0174532925);
      sy = sin((sdeg - 90) * 0.0174532925);
      if (rtc.getSecond() == 0 || initial) {
        initial = 0;
        // Erase hour and minute hand positions every minute
        img.drawLine(ohx, ohy, 120, 141, colour7);
        ohx = hx * 62 + 121;
        ohy = hy * 62 + 141;
        img.drawLine(omx, omy, 120, 141, colour7);
        omx = mx * 84 + 120;
        omy = my * 84 + 141;
      }
      // Redraw new hand positions, hour and minute hands not erased here to avoid flicker
      img.drawLine(osx, osy, 120, 141, colour7);
      osx = sx * 90 + 121;
      osy = sy * 90 + 141;
      img.drawLine(osx, osy, 120, 141, TFT_RED);
      img.drawLine(ohx, ohy, 120, 141, TFT_WHITE);
      img.drawLine(omx, omy, 120, 141, TFT_WHITE);
      img.drawLine(osx, osy, 120, 141, TFT_RED);
      img.fillCircle(120, 141, 3, TFT_RED);
      img.pushSprite(0, 0);
    } else if (watchface == 4 || watchface == 5) {
      img1.setTextColor(TFT_WHITE, TFT_BLACK);
      img1.setFreeFont(FF24);
      img1.fillSprite(TFT_BLACK);
      //img1.setTextSize(2);
      if (rtc.getHour() > 9 && rtc.getMinute() > 9) {
        img1.drawString(String(rtc.getHour()) + ":" + String(rtc.getMinute()), 66, 30);
      } else if (rtc.getHour() < 10 && rtc.getMinute() > 9) {
        img1.drawString("0" + String(rtc.getHour()) + ":" + String(rtc.getMinute()), 66, 30);
      } else if (rtc.getHour() > 9 && rtc.getMinute() < 10) {
        img1.drawString(String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 66, 30);
      } else {
        img1.drawString("0" + String(rtc.getHour()) + +":0" + String(rtc.getMinute()), 66, 30);
      }
      img1.setFreeFont(FF22);
      if (rtc.getSecond() < 10) {
        img1.drawString("0" + String(rtc.getSecond()), 190, 40);
      } else {
        img1.drawString(String(rtc.getSecond()), 190, 40);
      }
      img1.drawString(days1[rtc.getDayofWeek()] + " " + String(rtc.getDay()) + " " + String(rtc.getYear()), 54, 0);
      //img1.drawString(String(rtc.getDay()), 156, 0);
      img2.pushSprite(0, 180);
      img1.pushSprite(0, 180, TFT_BLACK);
    }
    lastsec = rtc.getSecond();
  }
}
static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9')
    v = *p - '0';
  return 10 * v + *++p - '0';
}
void game() {
  if (Screen == 5 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    tft.fillRect(10, TFTH2 - 20, TFTW - 20, 1, TFT_WHITE);
    tft.fillRect(10, TFTH2 + 32, TFTW - 20, 1, TFT_WHITE);
    tft.setTextColor(TFT_WHITE);
    tft.setFreeFont(0);
    tft.setTextSize(2);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 - 16);
    tft.println("FLAPPY");
    tft.setTextSize(2);
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 + 8);
    tft.println("-BIRD-");
  } else if (Screen == 7 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    maxScore = EEPROM.readInt(10);
    if (score > maxScore) {
      EEPROM.writeInt(10, score);
      EEPROM.commit();
      maxScore = score;
      tft.setTextColor(TFT_RED);
      tft.setTextSize(2);
      tft.setCursor(TFTW2 - (13 * 6), TFTH2 - 26);
      tft.println("NEW HIGHSCORE");
    }
    tft.setTextColor(TFT_WHITE);
    tft.setTextSize(3);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (9 * 9), TFTH2 - 6);
    tft.println("GAME OVER");
    tft.setTextSize(2);
    tft.setCursor(10, 10);
    tft.print("score: ");
    tft.print(score);
    tft.setCursor(TFTW2 - (12 * 6), TFTH2 + 18);
    tft.println("press button");
    tft.setCursor(10, 28);
    tft.print("Max Score:");
    tft.print(maxScore);
    tft.setTextSize(0);
    facechange = 1;
  }
}
static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9')
    v = *p - '0';
  return 10 * v + *++p - '0';
}

游戏函数处理内置的 flappy 游戏。此功能负责开始和结束屏幕。它还将调用游戏初始化函数,并在触发后调用 game_loop 函数。

void game() {
  if (Screen == 5 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    tft.fillRect(10, TFTH2 - 20, TFTW - 20, 1, TFT_WHITE);
    tft.fillRect(10, TFTH2 + 32, TFTW - 20, 1, TFT_WHITE);
    tft.setTextColor(TFT_WHITE);
    tft.setFreeFont(0);
    tft.setTextSize(2);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 - 16);
    tft.println("FLAPPY");
    tft.setTextSize(2);
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 + 8);
    tft.println("-BIRD-");
  } else if (Screen == 7 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    maxScore = EEPROM.readInt(10);
    if (score > maxScore) {
      EEPROM.writeInt(10, score);
      EEPROM.commit();
      maxScore = score;
      tft.setTextColor(TFT_RED);
      tft.setTextSize(2);
      tft.setCursor(TFTW2 - (13 * 6), TFTH2 - 26);
      tft.println("NEW HIGHSCORE");
    }
    tft.setTextColor(TFT_WHITE);
    tft.setTextSize(3);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (9 * 9), TFTH2 - 6);
    tft.println("GAME OVER");
    tft.setTextSize(2);
    tft.setCursor(10, 10);
    tft.print("score: ");
    tft.print(score);
    tft.setCursor(TFTW2 - (12 * 6), TFTH2 + 18);
    tft.println("press button");
    tft.setCursor(10, 28);
    tft.print("Max Score:");
    tft.print(maxScore);
    tft.setTextSize(0);
    facechange = 1;
  }
}

game_init 函数负责在启动游戏之前清除显示并设置初始游戏变量值。

void game_init() {
  // clear screen
  tft.fillScreen(BCKGRDCOL);
  // reset score
  score = 0;
  // init bird
  bird.x = 144;
  bird.y = bird.old_y = TFTH2 - BIRDH;
  bird.vel_y = -JUMP_FORCE;
  tmpx = tmpy = 0;
  // generate new random seed for the pipe gape
  randomSeed(analogRead(12));
  // init pipe
  pipes.x = 0;
  pipes.gap_y = random(20, TFTH - 60);
}

游戏函数处理内置的 flappy 游戏。此功能负责开始和结束屏幕。它还将在触发后调用游戏初始化函数和 game_loop 函数。

void game() {
  if (Screen == 5 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    tft.fillRect(10, TFTH2 - 20, TFTW - 20, 1, TFT_WHITE);
    tft.fillRect(10, TFTH2 + 32, TFTW - 20, 1, TFT_WHITE);
    tft.setTextColor(TFT_WHITE);
    tft.setFreeFont(0);
    tft.setTextSize(2);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 - 16);
    tft.println("FLAPPY");
    tft.setTextSize(2);
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 + 8);
    tft.println("-BIRD-");
  } else if (Screen == 7 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    maxScore = EEPROM.readInt(10);
    if (score > maxScore) {
      EEPROM.writeInt(10, score);
      EEPROM.commit();
      maxScore = score;
      tft.setTextColor(TFT_RED);
      tft.setTextSize(2);
      tft.setCursor(TFTW2 - (13 * 6), TFTH2 - 26);
      tft.println("NEW HIGHSCORE");
    }
    tft.setTextColor(TFT_WHITE);
    tft.setTextSize(3);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (9 * 9), TFTH2 - 6);
    tft.println("GAME OVER");
    tft.setTextSize(2);
    tft.setCursor(10, 10);
    tft.print("score: ");
    tft.print(score);
    tft.setCursor(TFTW2 - (12 * 6), TFTH2 + 18);
    tft.println("press button");
    tft.setCursor(10, 28);
    tft.print("Max Score:");
    tft.print(maxScore);
    tft.setTextSize(0);
    facechange = 1;
  }
}

The game_init function is responsible for clearing the display prior to starting the games along with setting the initial game variable values.

void game_init() {
  // clear screen
  tft.fillScreen(BCKGRDCOL);
  // reset score
  score = 0;
  // init bird
  bird.x = 144;
  bird.y = bird.old_y = TFTH2 - BIRDH;
  bird.vel_y = -JUMP_FORCE;
  tmpx = tmpy = 0;
  // generate new random seed for the pipe gape
  randomSeed(analogRead(12));
  // init pipe
  pipes.x = 0;
  pipes.gap_y = random(20, TFTH - 60);
}

处理整个游戏的主要函数是 game_loop 函数。它负责所有游戏图形以及游戏动态。它还将监控按键操作。游戏的按键检测是直接在此功能中完成的,无需 OneButton 库。Sprite 和其他快速渲染技术用于实现流畅的游戏性能。

void game_loop() {
  // ===============
  // prepare game variables
  // draw floor
  // ===============
  // instead of calculating the distance of the floor from the screen height each time store it in a variable
  const unsigned char GAMEH = TFTH - FLOORH;
  // draw the floor once, we will not overwrite on this area in-game
  // black line
  tft.drawFastHLine(0, GAMEH, TFTW, TFT_BLACK);
  // grass and stripe
  tft.fillRect(0, GAMEH + 1, TFTW2, GRASSH, GRASSCOL);
  tft.fillRect(TFTW2, GAMEH + 1, TFTW2, GRASSH, GRASSCOL2);
  // black line
  tft.drawFastHLine(0, GAMEH + GRASSH, TFTW, TFT_BLACK);
  // mud
  tft.fillRect(0, GAMEH + GRASSH + 1, TFTW, FLOORH - GRASSH, FLOORCOL);
  // grass x position (for stripe animation)
  long grassx = TFTW;
  // game loop time variables
  double delta, old_time, next_game_tick, current_time;
  next_game_tick = current_time = millis();
  // passed pipe flag to count score
  bool passed_pipe = false;
  // temp var for setAddrWindow
  unsigned char px;
  while (true) {
    yield();
    int loops = 0;
    while (millis() > next_game_tick && loops < MAX_FRAMESKIP) {
      // ===============
      // input
      // ===============
      if (digitalRead(0) == LOW) {
        // if the bird is not too close to the top of the screen apply jump force
        if (bird.y > BIRDH2 * 0.5)
          bird.vel_y = -JUMP_FORCE;
        // else zero velocity
        else
          bird.vel_y = 0;
      }
      // ===============
      // update
      // ===============
      // calculate delta time
      // ---------------
      old_time = current_time;
      current_time = millis();
      delta = (current_time - old_time) / 1000;
      // bird
      // ---------------
      bird.vel_y += GRAVITY * delta;
      bird.y += bird.vel_y;
      // pipe
      // ---------------
      pipes.x -= SPEED;
      // if pipe reached edge of the screen reset its position and gap
      if (pipes.x < -PIPEW) {
        pipes.x = TFTW;
        pipes.gap_y = random(10, GAMEH - (10 + GAPHEIGHT));
      }
      // ---------------
      next_game_tick += SKIP_TICKS;
      loops++;
    }
    // ===============
    // draw
    // ===============
    // pipe
    // ---------------
    // we save cycles if we avoid drawing the pipe when outside the screen
    if (pipes.x >= 0 && pipes.x < TFTW) {
      // pipe color
      tft.drawFastVLine(pipes.x + 3, 0, pipes.gap_y, PIPECOL);
      tft.drawFastVLine(pipes.x + 3, pipes.gap_y + GAPHEIGHT + 1, GAMEH - (pipes.gap_y + GAPHEIGHT + 1), PIPECOL);
      // highlight
      tft.drawFastVLine(pipes.x, 0, pipes.gap_y, PIPEHIGHCOL);
      tft.drawFastVLine(pipes.x, pipes.gap_y + GAPHEIGHT + 1, GAMEH - (pipes.gap_y + GAPHEIGHT + 1), PIPEHIGHCOL);
      // bottom and top border of pipe
      _drawPixel(pipes.x, pipes.gap_y, PIPESEAMCOL);
      _drawPixel(pipes.x, pipes.gap_y + GAPHEIGHT, PIPESEAMCOL);
      // pipe seam
      _drawPixel(pipes.x, pipes.gap_y - 6, PIPESEAMCOL);
      _drawPixel(pipes.x, pipes.gap_y + GAPHEIGHT + 6, PIPESEAMCOL);
      _drawPixel(pipes.x + 3, pipes.gap_y - 6, PIPESEAMCOL);
      _drawPixel(pipes.x + 3, pipes.gap_y + GAPHEIGHT + 6, PIPESEAMCOL);
    }
    // erase behind pipe
    if (pipes.x <= TFTW)
      tft.drawFastVLine(pipes.x + PIPEW, 0, GAMEH, BCKGRDCOL);
    // bird
    // ---------------
    tmpx = BIRDW - 1;
    do {
      px = bird.x + tmpx + BIRDW;
      // clear bird at previous position stored in old_y
      // we can't just erase the pixels before and after current position
      // because of the non-linear bird movement (it would leave 'dirty' pixels)
      tmpy = BIRDH - 1;
      do {
        _drawPixel(px, bird.old_y + tmpy, BCKGRDCOL);
      } while (tmpy--);
      // draw bird sprite at new position
      tmpy = BIRDH - 1;
      do {
        _drawPixel(px, bird.y + tmpy, birdcol[tmpx + (tmpy * BIRDW)]);
      } while (tmpy--);
    } while (tmpx--);
    // save position to erase bird on next draw
    bird.old_y = bird.y;
    // grass stripes
    // ---------------
    grassx -= SPEED;
    if (grassx < 0)
      grassx = TFTW;
    tft.drawFastVLine(grassx % TFTW, GAMEH + 1, GRASSH - 1, GRASSCOL);
    tft.drawFastVLine((grassx + 64) % TFTW, GAMEH + 1, GRASSH - 1, GRASSCOL2);
    // ===============
    // collision
    // ===============
    // if the bird hit the ground game over
    if (bird.y > GAMEH - BIRDH)
      break;
    // checking for bird collision with pipe
    if (bird.x + BIRDW >= pipes.x - BIRDW2 && bird.x <= pipes.x + PIPEW - BIRDW) {
      // bird entered a pipe, check for collision
      if (bird.y < pipes.gap_y || bird.y + BIRDH > pipes.gap_y + GAPHEIGHT)
        break;
      else
        passed_pipe = true;
    }
    // if bird has passed the pipe increase score
    else if (bird.x > pipes.x + PIPEW - BIRDW && passed_pipe) {
      passed_pipe = false;
      // erase score with background color
      tft.setTextColor(BCKGRDCOL);
      tft.setCursor(TFTW2, 4);
      tft.print(score);
      // set text color back to white for new score
      tft.setTextColor(TFT_WHITE);
      // increase score since we successfully passed a pipe
      score++;
    }
    // update score
    // ---------------
    tft.setCursor(TFTW2, 4);
    tft.print(score);
  }
  // add a small delay to show how the player lost
  Screen = 7;
  Screenchange = 1;
  delay(1200);
}

代码
#include <SPI.h>
#include <TFT_eSPI.h>  // Hardware-specific library
#include <ESP32Time.h>
#include "driver/gpio.h"
#include "esp_sleep.h"
#include <EEPROM.h>
#include "OneButton.h"
#include <QMC5883L.h>
#include <BH1750.h>  //BH1750 Library
#include "Free_Fonts.h"
#include "MAX30105.h"   // SparkFun librarry for MAX30102 sensor
#include "heartRate.h"  // Heartrate measurement algorithm
#include "dial240.h"    //Image data
#include "fonts.h"
#include "images.h"
#define PIN_INPUT 0
#define EEPROM_SIZE 25
#define FONT_SMALL NotoSansBold15
#define FONT_LARGE NotoSansBold36
#define TFT_GREY 0x5AEB
#define TFT_SKYBLUE 0x067D
#define color1 TFT_WHITE
#define color2 0x8410  //0x8410
#define color3 0x5ACB
#define color4 0x15B3
#define color5 0x00A3
#define colour6 0x0926
#define colour7 TFT_BLACK
#define Light_Green 0x07E8
#define background 0xB635
#define LCD_BACKLIGHT 4
#define TFTW 240          // screen width
#define TFTH 280          // screen height
#define TFTW2 (TFTW / 2)  // half screen width
#define TFTH2 (TFTH / 2)  // half screen height
#define SPEED 1
#define GRAVITY 9.8
#define JUMP_FORCE 2.15
#define SKIP_TICKS 20.0  // 1000 / 50fps
#define MAX_FRAMESKIP 5
#define BIRDW 16      // bird width
#define BIRDH 16      // bird height
#define BIRDW2 8      // half width
#define BIRDH2 8      // half height
#define PIPEW 24      // pipe width
#define GAPHEIGHT 42  // pipe gap height
#define FLOORH 30     // floor height (from bottom of the screen)
#define GRASSH 4      // grass height (inside floor, starts at floor y)
#define COLOR565(r, g, b) ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
#define BCKGRDCOL COLOR565(138, 235, 244)    // background
#define BIRDCOL COLOR565(255, 254, 174)      // bird
#define PIPECOL COLOR565(99, 255, 78)        // pipe
#define PIPEHIGHCOL COLOR565(250, 255, 250)  // pipe highlight
#define PIPESEAMCOL COLOR565(0, 0, 0)        // pipe seam
#define FLOORCOL COLOR565(246, 240, 163)     // floor
#define GRASSCOL COLOR565(141, 225, 87)      // grass (col2 is the stripe color)
#define GRASSCOL2 COLOR565(156, 239, 88)     // grass (col2 is the stripe color)
#define C0 BCKGRDCOL                         // bird sprite,bird sprite colors (Cx name for values to keep the array readable)
#define C1 COLOR565(195, 165, 75)
#define C2 BIRDCOL
#define C3 TFT_WHITE
#define C4 TFT_RED
#define C5 COLOR565(251, 216, 114)

ESP32Time rtc(0);   // RTC instance with offset in seconds
BH1750 lightMeter;  //BH1750 Instance
QMC5883L compass;
MAX30105 particleSensor;  //MAX30102 instance
OneButton button(PIN_INPUT, true);
TFT_eSPI tft = TFT_eSPI();  // Invoke custom library
TFT_eSprite img = TFT_eSprite(&tft);
TFT_eSprite img1 = TFT_eSprite(&tft);
TFT_eSprite img2 = TFT_eSprite(&tft);



static const unsigned int birdcol[] = {
  C0, C0, C1, C1, C1, C1, C1, C0, C0, C0, C1, C1, C1, C1, C1, C0,
  C0, C1, C2, C2, C2, C1, C3, C1, C0, C1, C2, C2, C2, C1, C3, C1,
  C0, C2, C2, C2, C2, C1, C3, C1, C0, C2, C2, C2, C2, C1, C3, C1,
  C1, C1, C1, C2, C2, C3, C1, C1, C1, C1, C1, C2, C2, C3, C1, C1,
  C1, C2, C2, C2, C2, C2, C4, C4, C1, C2, C2, C2, C2, C2, C4, C4,
  C1, C2, C2, C2, C1, C5, C4, C0, C1, C2, C2, C2, C1, C5, C4, C0,
  C0, C1, C2, C1, C5, C5, C5, C0, C0, C1, C2, C1, C5, C5, C5, C0,
  C0, C0, C1, C5, C5, C5, C0, C0, C0, C0, C1, C5, C5, C5, C0, C0
};
// bird structure
static struct BIRD {
  long x, y, old_y;
  long col;
  float vel_y;
} bird;
// pipe structure
static struct PIPES {
  long x, gap_y;
  long col;
} pipes;
// score
int score;
// temporary x and y var
static short tmpx, tmpy;
// ---------------
// draw pixel
// ---------------
// faster drawPixel method by inlining calls and using setAddrWindow and pushColor using macro to force inlining
#define _drawPixel(a, b, c) \
  tft.setAddrWindow(a, b, a, b); \
  tft.pushColor(c)

uint maxScore = 0;
float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;  // Saved H, M, S x & y multipliers
float sdeg = 0, mdeg = 0, hdeg = 0;
uint16_t osx = 120, osy = 140, omx = 120, omy = 140, ohx = 120, ohy = 140;  // Saved H, M, S x & y coords
uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
uint32_t targetTime = 0;                       // for next 1 second timeout
static uint8_t conv2d(const char* p);          // Forward declaration needed for IDE 1.6.x
uint8_t hh = 0, t_mm = 0, t_dd = 0, t_mn = 0;  //
uint32_t t_yr = 0;
uint8_t t_hh = 0, mm = 0, ss = 0;
unsigned long lastfacechange = 0;
unsigned long lastwake = 0;
unsigned long lastpressed = 0;
unsigned long lastvaluechange = 0;
bool initial = 1;
volatile int counter = 0;
float VALUE;
float lastValue = 0;
int lastsec = 0;
int pressstate = 0;
unsigned long lastDisplayUpdate = 0;
const byte RATE_SIZE = 4;  //Increase this for more averaging. 4 is good.
byte rates[RATE_SIZE];     //Array of heart rates
byte rateSpot = 0;
long lastBeat = 0;  //Time at which the last beat occurred
float beatsPerMinute;
int beatAvg;
bool beat = false;
double rad = 0.01745;
float x[360];
float y[360];
bool facechange = false;
bool Screenchange = false;
float px[360];
float py[360];
float lx[360];
float ly[360];
int r = 104;
int ssx = 120;
int ssy = 140;
String cc[12] = { "45", "40", "35", "30", "25", "20", "15", "10", "05", "0", "55", "50" };
String days[] = { "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" };
String days1[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
int start[12];
int startP[60];
const int pwmFreq = 5000;
const int pwmResolution = 8;
const int pwmLedChannelTFT = 0;
int angle = 0;
bool onOff = 0;
bool debounce = 0;
int watchface = 0, Screen = 0, SubScreen = 0, Autoscreen, AutoBright, AutoscreenTime, Brigtnesslevel;
String h, m, s, d1, d2, m1, m2;
unsigned long pressStartTime;


// This function is called from the interrupt when the signal on the PIN_INPUT has changed.
// do not use Serial in here.
void IRAM_ATTR checkTicks() {
  // include all buttons here to be checked
  button.tick();  // just call tick() to check the state.
}


// this function will be called for short click.
void ShortClick() {
  Serial.println("singleClick() detected.");
  lastwake = millis();
  if (Screen == 0) {
    SubScreen++;
    if (SubScreen > 2) {
      SubScreen = 0;
      facechange = true;
    }
    if (SubScreen == 1) {
      particleSensor.wakeUp();
    } else {
      particleSensor.shutDown();
    }
  } else if (Screen == 1) {
    SubScreen++;
    if (SubScreen > 4) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 2) {
    watchface++;
    if (watchface > 5) {
      watchface = 0;
    }

    EEPROM.write(0, watchface);
    EEPROM.commit();
    facechange = true;
    Screenchange = true;
  } else if (Screen == 3) {
    SubScreen++;
    if (SubScreen > 5) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 4) {
    SubScreen++;
    if (SubScreen > 2) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 5) {
    Screen = 6;
    game_init();
    game_loop();
  } else if (Screen == 6) {
    Screen = 7;
  } else if (Screen == 7) {
    Screen = 5;
    Screenchange = true;
  }

  Serial.print("Sub ");
  Serial.println(SubScreen);
  tft.fillScreen(colour7);
  pressstate = 1;
  facechange = true;
  lastDisplayUpdate = millis();
  lastpressed = millis();
}  // ShortClick


// long press
void LongPress() {
  Serial.println("pressStart()");
  pressStartTime = millis() - 1000;  // as set in setPressTicks()
  lastwake = millis();
  lastDisplayUpdate = millis();
  particleSensor.shutDown();
  if (Screen == 0) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 1) {
    if (SubScreen == 0) {
      Screen = 2;
      SubScreen = 0;
    } else if (SubScreen == 1) {
      Screen = 3;
      t_hh = rtc.getHour();
      t_mm = rtc.getMinute();
      t_dd = rtc.getDay();
      t_mn = rtc.getMonth();
      t_yr = rtc.getYear();
      Serial.println(rtc.getYear());
      Serial.println(t_yr);
      SubScreen = 0;
    } else if (SubScreen == 2) {
      Screen = 4;
      SubScreen = 0;
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 3) {
      Screen = 5;
      SubScreen = 0;
    } else if (SubScreen == 4) {
      Screen = 0;
      SubScreen = 0;
    }
  } else if (Screen == 2) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 3) {
    if (SubScreen == 0) {
      t_hh++;
      if (t_hh > 23) {
        t_hh = 0;
      }
    } else if (SubScreen == 1) {
      t_mm++;
      if (t_mm > 59) {
        t_mm = 0;
      }
    } else if (SubScreen == 2) {
      t_dd++;
      if (t_dd > 31) {
        t_dd = 0;
      }
    } else if (SubScreen == 3) {
      t_mn++;
      if (t_mn > 12) {
        t_mn = 0;
      }
    } else if (SubScreen == 4) {
      t_yr++;
      if (t_yr > 2041) {
        t_yr = 0;
      }
    } else {
      rtc.setTime(0, t_mm, t_hh, t_dd, t_mn, t_yr);
      Screen = 1;
      SubScreen = 1;
    }
  } else if (Screen == 4) {
    if (SubScreen == 0) {
      AutoBright++;
      if (AutoBright > 5) {
        AutoBright = 0;
      }
      if (AutoBright > 0) {
        analogWrite(LCD_BACKLIGHT, AutoBright * 50);
      }
      EEPROM.write(2, AutoBright);
      EEPROM.commit();
    } else if (SubScreen == 1) {
      Autoscreen++;
      if (Autoscreen > 5) {
        Autoscreen = 0;
      }
      EEPROM.write(1, Autoscreen);
      EEPROM.commit();
    } else if (SubScreen == 2) {
      Screen = 1;
      SubScreen = 2;
    }
  } else if (Screen == 5) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 7) {
    Screen = 1;
    SubScreen = 0;
  }

  facechange = true;
  Screenchange = true;
  pressstate = 1;
  lastpressed = millis();
}


void setup(void) {
  Serial.begin(115200);
  Serial.println("ESP32 Watch OS.");
  gpio_hold_dis((gpio_num_t)LCD_BACKLIGHT);
  pinMode(LCD_BACKLIGHT, OUTPUT);
  digitalWrite(LCD_BACKLIGHT, LOW);
  EEPROM.begin(EEPROM_SIZE);
  EEPROM.writeInt(10, 0);
  EEPROM.commit();
  if (EEPROM.read(0) > 3) {
    EEPROM.write(0, 4);
    EEPROM.commit();
  }
  watchface = EEPROM.read(0);

  if (EEPROM.read(1) > 5) {
    EEPROM.write(1, 5);
    EEPROM.commit();
  }
  Autoscreen = EEPROM.read(1);

  if (EEPROM.read(2) > 5) {
    EEPROM.write(2, 5);
    EEPROM.commit();
  }
  AutoBright = EEPROM.read(2);
  //rtc.setTime(ss, mm, hh, 0, 0, 0);  // 26th Jjuly 2022 compile date

  particleSensor.begin(Wire, I2C_SPEED_FAST);

  particleSensor.setup();                     //Configure sensor with default settings
  particleSensor.setPulseAmplitudeRed(0x0A);  //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeIR(0xFF);   //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeGreen(0);   //Turn off Green LED
  particleSensor.shutDown();
  compass.init();
  compass.setSamplingRate(50);

  tft.init();
  tft.setRotation(0);
  //tft.setColorDepth(16);
  tft.setSwapBytes(true);
  tft.fillScreen(colour7);
  int xw = tft.width() / 2;  // xw, yh is middle of screen
  int yh = tft.height() / 2;
  tft.setPivot(xw, yh);  // Set pivot to middle of TFT screen
  img.createSprite(240, 280);
  img.setTextDatum(4);
  img1.createSprite(240, 70);
  img1.setSwapBytes(true);
  img2.createSprite(240, 70);
  img2.setSwapBytes(true);
  targetTime = millis() + 1000;
  facechange = true;
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);  //1 = High, 0 = Low
  // setup interrupt routine
  // when not registering to the interrupt the sketch also works when the tick is called frequently.
  attachInterrupt(digitalPinToInterrupt(PIN_INPUT), checkTicks, CHANGE);

  // link the xxxclick functions to be called on xxxclick event.
  button.attachClick(ShortClick);

  button.setPressTicks(1000);  // that is the time when LongPressStart is called
  button.attachLongPressStart(LongPress);
  lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);  //Init BH1750 library
  if (AutoBright == 0) {
    unsigned int lv = constrain(lightMeter.readLightLevel(), 50, 500);
    analogWrite(LCD_BACKLIGHT, lv / 2);
  } else {
    analogWrite(LCD_BACKLIGHT, AutoBright * 50);
  }

  lastwake = millis();
}
int lastAngle = 0;
float circle = 100;
bool dir = 0;
int rAngle = 359;


void loop() {
  button.tick();
  if (Screen < 5) {
    watchtask();
  } else {
    game();
  }
}



void watchtask() {

  if (pressstate == 1 && digitalRead(0) == 1) {
    pressstate = 0;
  }
  if (AutoBright == 0) {
    unsigned int lv = constrain(lightMeter.readLightLevel(), 50, 500);
    analogWrite(LCD_BACKLIGHT, lv / 2);
    Serial.print("Light");
    Serial.println(lv / 2);
  }
  if (Autoscreen != 0 && millis() - lastwake > Autoscreen * 60000) {
    analogWrite(LCD_BACKLIGHT, 0);
    delay(1000);

    tft.fillScreen(colour7);
    gpio_deep_sleep_hold_en();
    gpio_hold_en((gpio_num_t)LCD_BACKLIGHT);
    esp_deep_sleep_start();
  }
  if (Screen == 0) {
    if (SubScreen == 0) {
      watchfacedsp();
    } else if (SubScreen == 1) {
      HRApp();
    } else {
      CompassApp();
    }
  } else if (Screen == 1 && Screenchange == true) {
    if (SubScreen == 0) {
      tft.pushImage(0, 0, 240, 280, facechangeicon);
    } else if (SubScreen == 1) {
      tft.pushImage(0, 0, 240, 280, timeseticon);
    } else if (SubScreen == 2) {
      tft.pushImage(0, 0, 240, 280, settingsicon);
    } else if (SubScreen == 3) {
      tft.pushImage(0, 0, 240, 280, gamesicon);
    } else if (SubScreen == 4) {
      tft.pushImage(0, 0, 240, 280, exiticon);
    }
  } else if (Screen == 3 && Screenchange == true) {

    timesetiings();
    Screenchange = false;
  } else if (Screen == 2 && Screenchange == true) {
    watchfacedsp();
    Screenchange = false;
  } else if (Screen == 4 && Screenchange == true) {
    settings();
    Screenchange = false;
  } else if (Screen == 3 && millis() - lastpressed > 2000 && millis() - lastvaluechange > 500 && pressstate == 1) {
    if (SubScreen == 0) {
      t_hh++;
      if (t_hh > 23) {
        t_hh = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 1) {
      t_mm++;
      if (t_mm > 59) {
        t_mm = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 2) {
      facechange = true;
      Screenchange = true;
      t_dd++;
      if (t_dd > 31) {
        t_dd = 0;
      }
    } else if (SubScreen == 3) {
      t_mn++;
      if (t_mn > 12) {
        t_mn = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 4) {
      t_yr++;
      if (t_yr > 2041) {
        t_yr = 0;
      }
      facechange = true;
      Screenchange = true;
    }
    lastvaluechange = millis();
  }
}



void timesetiings() {
  tft.pushImage(0, 0, 240, 280, TimeSettings);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  tft.setFreeFont(FF18);
  tft.setTextSize(0);
  int tt_hh = t_hh;
  if (tt_hh > 12) {
    tt_hh = tt_hh - 12;
  }
  if (tt_hh < 10) {
    tft.drawString("0" + String(tt_hh), 25, 96);
  } else {
    tft.drawString(String(tt_hh), 25, 96);
  }

  if (t_mm < 10) {
    tft.drawString("0" + String(t_mm), 93, 96);
  } else {
    tft.drawString(String(t_mm), 93, 96);
  }
  if (t_hh < 13) {
    tft.drawString("AM", 164, 96);
  } else {
    tft.drawString("PM", 164, 96);
  }
  if (t_dd < 10) {
    tft.drawString("0" + String(t_dd), 25, 183);
  } else {
    tft.drawString(String(t_dd), 25, 183);
  }
  if (t_mn < 10) {
    tft.drawString("0" + String(t_mn), 93, 183);
  } else {
    tft.drawString(String(t_mn), 93, 183);
  }
  if (t_yr < 2022) {
    t_yr = 2022;
  }
  tft.drawString(String(t_yr), 161, 183);
  if (SubScreen == 0) {
    tft.drawRoundRect(9, 81, 58, 48, 6, Light_Green);
  } else if (SubScreen == 1) {
    tft.drawRoundRect(77, 81, 58, 48, 6, Light_Green);
  } else if (SubScreen == 2) {
    tft.drawRoundRect(9, 168, 58, 48, 6, Light_Green);
  } else if (SubScreen == 3) {
    tft.drawRoundRect(77, 168, 58, 48, 6, Light_Green);
  } else if (SubScreen == 4) {
    tft.drawRoundRect(144, 168, 88, 48, 6, Light_Green);
  } else {
    tft.drawRoundRect(79, 226, 88, 48, 6, Light_Green);
  }
}



void settings() {
  tft.pushImage(0, 0, 240, 280, Settingspage);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  tft.setFreeFont(FF18);
  tft.setTextSize(0);
  switch (AutoBright) {
    case 0: tft.drawString("  Auto  ", 80, 100); break;
    case 1: tft.drawString("  20%   ", 85, 100); break;
    case 2: tft.drawString("  40%   ", 85, 100); break;
    case 3: tft.drawString("  60%   ", 85, 100); break;
    case 4: tft.drawString("  80%   ", 85, 100); break;
    case 5: tft.drawString(" 100%   ", 80, 100); break;
  }
  switch (Autoscreen) {
    case 0: tft.drawString("Always On", 65, 185); break;
    case 1: tft.drawString("1 Minute ", 75, 185); break;
    case 2: tft.drawString("2 Minute ", 75, 185); break;
    case 3: tft.drawString("3 Minute ", 75, 185); break;
    case 4: tft.drawString("4 Minute ", 75, 185); break;
    case 5: tft.drawString("5 Minute ", 75, 185); break;
  }

  if (SubScreen == 0) {
    tft.drawRoundRect(16, 85, 208, 48, 6, Light_Green);
  } else if (SubScreen == 1) {
    tft.drawRoundRect(16, 169, 208, 48, 6, Light_Green);
  } else {
    tft.drawRoundRect(66, 225, 108, 48, 6, Light_Green);
  }
}


void HRApp() {
  long irValue = particleSensor.getIR();  //Reading the IR value it will permit us to know if there's a finger on the sensor or not
  //Also detecting a heartbeat
  if (checkForBeat(irValue) == true)  //If a heart beat is detected
  {

    long delta = millis() - lastBeat;  //Measure duration between two beats
    lastBeat = millis();

    beatsPerMinute = 60 / (delta / 1000.0);  //Calculating the BPM

    if (beatsPerMinute < 255 && beatsPerMinute > 20)  //To calculate the average we strore some values (4) then do some math to calculate the average
    {
      rates[rateSpot++] = (byte)beatsPerMinute;  //Store this reading in the array
      rateSpot %= RATE_SIZE;                     //Wrap variable

      //Take average of readings
      beatAvg = 0;
      for (byte x = 0; x < RATE_SIZE; x++)
        beatAvg += rates[x];
      beatAvg /= RATE_SIZE;
    }
  }
  if (millis() - lastDisplayUpdate > 500) {
    if (irValue < 60000) {  //If no finger is detected it inform the user and put the average BPM to 0 or it will be stored for the next measure
      beatAvg = 0;
      img.loadFont(FONT_SMALL);
      img.setCursor(80, 120);
      img.setTextColor(TFT_CYAN, colour7);
      img.fillSprite(colour7);
      img.println("Please Place ");
      img.setCursor(80, 140);
      img.println("your finger ");
      img.pushSprite(0, 0);
    } else {
      img.fillSprite(colour7);  //Clear the display
      if (beat == true) {
        img.pushImage(0, 0, 240, 280, hr1);
      } else {
        img.pushImage(0, 0, 240, 280, hr2);
      }
      beat = !beat;

      img.setTextColor(TFT_CYAN, colour7);
      img.loadFont(FONT_LARGE);
      img.setCursor(100, 130);
      img.print(beatAvg);
      img.setCursor(100, 175);
      img.loadFont(FONT_SMALL);
      img.print("BPM ");
      img.pushRotated(0);
    }
    lastDisplayUpdate = millis();
  }
  Serial.print("IR=");
  Serial.print(irValue);
  Serial.print(", BPM=");
  Serial.print(beatsPerMinute);
  Serial.print(", Avg BPM=");
  Serial.print(beatAvg);

  if (irValue < 60000)
    Serial.print(" No finger?");
  if (millis() - lastBeat > 5000) {
    beatsPerMinute = 0;
    beatAvg = 0;
  }
  Serial.println();
}


void CompassApp() {
  for (int i = 0; i < 10; i++) {
    angle = angle + compass.readHeading();
  }
  angle = random(355, 360);
  angle = angle / 10;
  img.fillSprite(colour7);
  img.pushImage(0, 20, 240, 240, dial240);
  img.pushRotated(angle);  // create rotated image as per the angle from the compass sensor
  angle = 0;
}
void watchfacedsp() {
  if (facechange) {
    tft.fillScreen(colour7);
    if (watchface == 1) {
      tft.setTextSize(0);
      tft.pushImage(0, 0, 240, 280, Casio2);
      tft.setTextColor(0x0081, background);
      tft.fillRoundRect(48, 127, 128, 48, 5, background);
    } else if (watchface == 2) {
      tft.setTextSize(0);
      tft.pushImage(0, 0, 240, 280, Casio1);
      tft.setTextColor(0x0081, background);
      tft.fillRoundRect(48, 127, 128, 48, 5, background);
    } else if (watchface == 4) {
      tft.pushImage(0, 0, 240, 280, cdface1);
      img2.pushImage(0, 0, 240, 100, cdface11);
      tft.setTextColor(TFT_WHITE);
      tft.setFreeFont(FF18);
      tft.setTextSize(2);
    } else if (watchface == 5) {
      tft.pushImage(0, 0, 240, 280, cdface2);
      img2.pushImage(0, 0, 240, 100, cdface12);
      tft.setTextColor(TFT_WHITE);
      tft.setFreeFont(FF18);
      tft.setTextSize(2);
    }
    facechange = false;
    lastfacechange = millis();
  }
  if (watchface == 0) {
    int b = 0;
    int b2 = 0;
    for (int i = 0; i < 360; i++) {
      x[i] = (r * cos(rad * i)) + ssx;
      y[i] = (r * sin(rad * i)) + ssy;
      px[i] = ((r - 16) * cos(rad * i)) + ssx;
      py[i] = ((r - 16) * sin(rad * i)) + ssy;

      lx[i] = ((r - 26) * cos(rad * i)) + ssx;
      ly[i] = ((r - 26) * sin(rad * i)) + ssy;

      if (i % 30 == 0) {
        start[b] = i;
        b++;
      }

      if (i % 6 == 0) {
        startP[b2] = i;
        b2++;
      }
    }

    rAngle = rAngle - 2;

    angle = rtc.getSecond() * 6;

    s = String(rtc.getSecond());
    m = String(rtc.getMinute());
    h = String(rtc.getHour());

    if (m.toInt() < 10)
      m = "0" + m;

    if (h.toInt() < 10)
      h = "0" + h;

    if (s.toInt() < 10)
      s = "0" + s;


    if (rtc.getDay() > 10) {
      d1 = rtc.getDay() / 10;
      d2 = rtc.getDay() % 10;
    } else {
      d1 = "0";
      d2 = String(rtc.getDay());
    }

    if (rtc.getMonth() > 10) {
      m1 = rtc.getMonth() / 10;
      m2 = rtc.getMonth() % 10;
    } else {
      m1 = "0";
      m2 = String(rtc.getMonth());
    }


    if (angle >= 360)
      angle = 0;

    if (rAngle <= 0)
      rAngle = 359;



    if (dir == 0)
      circle = circle + 0.5;
    else
      circle = circle - 0.5;

    if (circle > 140)
      dir = !dir;

    if (circle < 100)
      dir = !dir;



    if (angle > -1) {
      lastAngle = angle;

      VALUE = ((angle - 270) / 3.60) * -1;
      if (VALUE < 0)
        VALUE = VALUE + 100;



      img.fillSprite(colour7);
      img.fillCircle(ssx, ssy, 124, colour7);

      img.setTextColor(TFT_WHITE, colour7);

      img.drawString(days[rtc.getDayofWeek()], circle, 140, 2);


      for (int i = 0; i < 12; i++)
        if (start[i] + angle < 360) {
          img.drawString(cc[i], x[start[i] + angle], y[start[i] + angle], 2);
          img.drawLine(px[start[i] + angle], py[start[i] + angle], lx[start[i] + angle], ly[start[i] + angle], color1);
        } else {
          img.drawString(cc[i], x[(start[i] + angle) - 360], y[(start[i] + angle) - 360], 2);
          img.drawLine(px[(start[i] + angle) - 360], py[(start[i] + angle) - 360], lx[(start[i] + angle) - 360], ly[(start[i] + angle) - 360], color1);
        }
      img.setFreeFont(&DSEG7_Modern_Bold_20);
      img.drawString(s, ssx, ssy - 36);
      img.setFreeFont(&DSEG7_Classic_Regular_28);
      img.drawString(h + ":" + m, ssx, ssy + 28);
      img.setTextFont(0);

      img.fillRect(70, 86, 12, 20, color3);
      img.fillRect(84, 86, 12, 20, color3);
      img.fillRect(150, 86, 12, 20, color3);
      img.fillRect(164, 86, 12, 20, color3);

      img.setTextColor(0x35D7, colour7);
      img.drawString("MONTH", 84, 78);
      img.drawString("DAY", 162, 78);
      img.setTextColor(TFT_SKYBLUE, colour7);
      img.drawString("Circuit Digest", 120, 194);
      img.drawString("***", 120, 124);
      img.setTextColor(TFT_WHITE, color3);
      img.drawString(m1, 77, 96, 2);
      img.drawString(m2, 91, 96, 2);
      img.drawString(d1, 157, 96, 2);
      img.drawString(d2, 171, 96, 2);
      for (int i = 0; i < 60; i++)
        if (startP[i] + angle < 360)
          img.fillCircle(px[startP[i] + angle], py[startP[i] + angle], 1, color1);
        else
          img.fillCircle(px[(startP[i] + angle) - 360], py[(startP[i] + angle) - 360], 1, color1);
      img.fillTriangle(ssx - 1, ssy - 70, ssx - 5, ssy - 56, ssx + 4, ssy - 56, TFT_ORANGE);
      img.fillCircle(px[rAngle], py[rAngle], 6, TFT_RED);
      img.pushSprite(0, 0);
    }
  } else if (rtc.getSecond() != lastsec || Screen == 3) {
    if (watchface == 1 || watchface == 2) {
      /*
      String med;
      if (rtc.getSecond() % 2) {
        med = ":";
      } else {
        med = " ";
      }
      */
      tft.setFreeFont(&DSEG7_Classic_Bold_30);
      if (rtc.getHour() > 9 && rtc.getMinute() > 9) {
        tft.drawString(String(rtc.getHour()) + ":" + String(rtc.getMinute()), 46, 135);
      } else if (rtc.getHour() < 10 && rtc.getMinute() > 9) {

        tft.drawString("0" + String(rtc.getHour()) + ":" + String(rtc.getMinute()), 46, 135);
      } else if (rtc.getHour() > 9 && rtc.getMinute() < 10) {

        tft.drawString(String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 46, 135);
      } else {

        tft.drawString("0" + String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 46, 135);
      }
      tft.setFreeFont(&DSEG7_Classic_Bold_20);
      if (rtc.getSecond() < 10) {
        tft.drawString("0" + String(rtc.getSecond()), 154, 145);
      } else {
        tft.drawString(String(rtc.getSecond()), 154, 145);
      }
      tft.setFreeFont(&DSEG14_Classic_Bold_18);
      tft.drawString(days1[rtc.getDayofWeek()], 94, 106);
      tft.drawString(String(rtc.getDay()), 156, 106);
    } else if (watchface == 3) {
      img.setTextColor(TFT_WHITE, colour7);  // Adding a background colour erases previous text automatically
      // Draw clock face
      img.fillCircle(120, 140, 118, TFT_GREEN);
      img.fillCircle(120, 140, 110, colour7);
      // Draw 12 lines
      for (int i = 0; i < 360; i += 30) {
        sx = cos((i - 90) * 0.0174532925);
        sy = sin((i - 90) * 0.0174532925);
        x0 = sx * 114 + 120;
        yy0 = sy * 114 + 140;
        x1 = sx * 100 + 120;
        yy1 = sy * 100 + 140;
        img.drawLine(x0, yy0, x1, yy1, TFT_GREEN);
      }
      // Draw 60 dots
      for (int i = 0; i < 360; i += 6) {
        sx = cos((i - 90) * 0.0174532925);
        sy = sin((i - 90) * 0.0174532925);
        x0 = sx * 102 + 120;
        yy0 = sy * 102 + 140;
        // Draw minute markers
        img.drawPixel(x0, yy0, TFT_WHITE);
        // Draw main quadrant dots
        if (i == 0 || i == 180) img.fillCircle(x0, yy0, 2, TFT_WHITE);
        if (i == 90 || i == 270) img.fillCircle(x0, yy0, 2, TFT_WHITE);
      }
      img.fillCircle(120, 141, 3, TFT_WHITE);
      // Pre-compute hand degrees, x & y coords for a fast screen update
      sdeg = rtc.getSecond() * 6;                      // 0-59 -> 0-354
      mdeg = rtc.getMinute() * 6 + sdeg * 0.01666667;  // 0-59 -> 0-360 - includes seconds
      hdeg = rtc.getHour() * 30 + mdeg * 0.0833333;    // 0-11 -> 0-360 - includes minutes and seconds
      hx = cos((hdeg - 90) * 0.0174532925);
      hy = sin((hdeg - 90) * 0.0174532925);
      mx = cos((mdeg - 90) * 0.0174532925);
      my = sin((mdeg - 90) * 0.0174532925);
      sx = cos((sdeg - 90) * 0.0174532925);
      sy = sin((sdeg - 90) * 0.0174532925);
      if (rtc.getSecond() == 0 || initial) {
        initial = 0;
        // Erase hour and minute hand positions every minute
        img.drawLine(ohx, ohy, 120, 141, colour7);
        ohx = hx * 62 + 121;
        ohy = hy * 62 + 141;
        img.drawLine(omx, omy, 120, 141, colour7);
        omx = mx * 84 + 120;
        omy = my * 84 + 141;
      }
      // Redraw new hand positions, hour and minute hands not erased here to avoid flicker
      img.drawLine(osx, osy, 120, 141, colour7);
      osx = sx * 90 + 121;
      osy = sy * 90 + 141;
      img.drawLine(osx, osy, 120, 141, TFT_RED);
      img.drawLine(ohx, ohy, 120, 141, TFT_WHITE);
      img.drawLine(omx, omy, 120, 141, TFT_WHITE);
      img.drawLine(osx, osy, 120, 141, TFT_RED);
      img.fillCircle(120, 141, 3, TFT_RED);
      img.pushSprite(0, 0);
    } else if (watchface == 4 || watchface == 5) {
      img1.setTextColor(TFT_WHITE, TFT_BLACK);
      img1.setFreeFont(FF24);
      img1.fillSprite(TFT_BLACK);
      //img1.setTextSize(2);

      if (rtc.getHour() > 9 && rtc.getMinute() > 9) {
        img1.drawString(String(rtc.getHour()) + ":" + String(rtc.getMinute()), 66, 30);
      } else if (rtc.getHour() < 10 && rtc.getMinute() > 9) {

        img1.drawString("0" + String(rtc.getHour()) + ":" + String(rtc.getMinute()), 66, 30);
      } else if (rtc.getHour() > 9 && rtc.getMinute() < 10) {

        img1.drawString(String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 66, 30);
      } else {

        img1.drawString("0" + String(rtc.getHour()) + +":0" + String(rtc.getMinute()), 66, 30);
      }
      img1.setFreeFont(FF22);
      if (rtc.getSecond() < 10) {
        img1.drawString("0" + String(rtc.getSecond()), 190, 40);
      } else {
        img1.drawString(String(rtc.getSecond()), 190, 40);
      }
      img1.drawString(days1[rtc.getDayofWeek()] + " " + String(rtc.getDay()) + " " + String(rtc.getYear()), 54, 0);
      //img1.drawString(String(rtc.getDay()), 156, 0);
      img2.pushSprite(0, 180);
      img1.pushSprite(0, 180, TFT_BLACK);
    }
    lastsec = rtc.getSecond();
  }
}

static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9')
    v = *p - '0';
  return 10 * v + *++p - '0';
}

void game() {
  if (Screen == 5 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    tft.fillRect(10, TFTH2 - 20, TFTW - 20, 1, TFT_WHITE);
    tft.fillRect(10, TFTH2 + 32, TFTW - 20, 1, TFT_WHITE);
    tft.setTextColor(TFT_WHITE);
    tft.setFreeFont(0);
    tft.setTextSize(2);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 - 16);
    tft.println("FLAPPY");
    tft.setTextSize(2);
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 + 8);
    tft.println("-BIRD-");
  } else if (Screen == 7 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    maxScore = EEPROM.readInt(10);

    if (score > maxScore) {
      EEPROM.writeInt(10, score);
      EEPROM.commit();
      maxScore = score;
      tft.setTextColor(TFT_RED);
      tft.setTextSize(2);
      tft.setCursor(TFTW2 - (13 * 6), TFTH2 - 26);
      tft.println("NEW HIGHSCORE");
    }
    tft.setTextColor(TFT_WHITE);
    tft.setTextSize(3);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (9 * 9), TFTH2 - 6);
    tft.println("GAME OVER");
    tft.setTextSize(2);
    tft.setCursor(10, 10);
    tft.print("score: ");
    tft.print(score);
    tft.setCursor(TFTW2 - (12 * 6), TFTH2 + 18);
    tft.println("press button");
    tft.setCursor(10, 28);
    tft.print("Max Score:");
    tft.print(maxScore);
    tft.setTextSize(0);
    facechange = 1;
  }
}

void game_init() {
  // clear screen
  tft.fillScreen(BCKGRDCOL);
  // reset score
  score = 0;
  // init bird
  bird.x = 144;
  bird.y = bird.old_y = TFTH2 - BIRDH;
  bird.vel_y = -JUMP_FORCE;
  tmpx = tmpy = 0;
  // generate new random seed for the pipe gape
  randomSeed(analogRead(12));
  // init pipe
  pipes.x = 0;
  pipes.gap_y = random(20, TFTH - 60);
}

void game_loop() {

  // ===============
  // prepare game variables
  // draw floor
  // ===============
  // instead of calculating the distance of the floor from the screen height each time store it in a variable
  const unsigned char GAMEH = TFTH - FLOORH;
  // draw the floor once, we will not overwrite on this area in-game
  // black line
  tft.drawFastHLine(0, GAMEH, TFTW, TFT_BLACK);
  // grass and stripe
  tft.fillRect(0, GAMEH + 1, TFTW2, GRASSH, GRASSCOL);
  tft.fillRect(TFTW2, GAMEH + 1, TFTW2, GRASSH, GRASSCOL2);
  // black line
  tft.drawFastHLine(0, GAMEH + GRASSH, TFTW, TFT_BLACK);
  // mud
  tft.fillRect(0, GAMEH + GRASSH + 1, TFTW, FLOORH - GRASSH, FLOORCOL);
  // grass x position (for stripe animation)
  long grassx = TFTW;
  // game loop time variables
  double delta, old_time, next_game_tick, current_time;
  next_game_tick = current_time = millis();
  // passed pipe flag to count score
  bool passed_pipe = false;
  // temp var for setAddrWindow
  unsigned char px;
  while (true) {
    yield();

    int loops = 0;
    while (millis() > next_game_tick && loops < MAX_FRAMESKIP) {
      // ===============
      // input
      // ===============
      if (digitalRead(0) == LOW) {
        // if the bird is not too close to the top of the screen apply jump force
        if (bird.y > BIRDH2 * 0.5)
          bird.vel_y = -JUMP_FORCE;
        // else zero velocity
        else
          bird.vel_y = 0;
      }
      // ===============
      // update
      // ===============
      // calculate delta time
      // ---------------
      old_time = current_time;
      current_time = millis();
      delta = (current_time - old_time) / 1000;
      // bird
      // ---------------
      bird.vel_y += GRAVITY * delta;
      bird.y += bird.vel_y;
      // pipe
      // ---------------
      pipes.x -= SPEED;
      // if pipe reached edge of the screen reset its position and gap
      if (pipes.x < -PIPEW) {
        pipes.x = TFTW;
        pipes.gap_y = random(10, GAMEH - (10 + GAPHEIGHT));
      }
      // ---------------
      next_game_tick += SKIP_TICKS;
      loops++;
    }

    // ===============
    // draw
    // ===============
    // pipe
    // ---------------
    // we save cycles if we avoid drawing the pipe when outside the screen
    if (pipes.x >= 0 && pipes.x < TFTW) {
      // pipe color
      tft.drawFastVLine(pipes.x + 3, 0, pipes.gap_y, PIPECOL);
      tft.drawFastVLine(pipes.x + 3, pipes.gap_y + GAPHEIGHT + 1, GAMEH - (pipes.gap_y + GAPHEIGHT + 1), PIPECOL);
      // highlight
      tft.drawFastVLine(pipes.x, 0, pipes.gap_y, PIPEHIGHCOL);
      tft.drawFastVLine(pipes.x, pipes.gap_y + GAPHEIGHT + 1, GAMEH - (pipes.gap_y + GAPHEIGHT + 1), PIPEHIGHCOL);
      // bottom and top border of pipe
      _drawPixel(pipes.x, pipes.gap_y, PIPESEAMCOL);
      _drawPixel(pipes.x, pipes.gap_y + GAPHEIGHT, PIPESEAMCOL);
      // pipe seam
      _drawPixel(pipes.x, pipes.gap_y - 6, PIPESEAMCOL);
      _drawPixel(pipes.x, pipes.gap_y + GAPHEIGHT + 6, PIPESEAMCOL);
      _drawPixel(pipes.x + 3, pipes.gap_y - 6, PIPESEAMCOL);
      _drawPixel(pipes.x + 3, pipes.gap_y + GAPHEIGHT + 6, PIPESEAMCOL);
    }
    // erase behind pipe
    if (pipes.x <= TFTW)
      tft.drawFastVLine(pipes.x + PIPEW, 0, GAMEH, BCKGRDCOL);
    // bird
    // ---------------
    tmpx = BIRDW - 1;
    do {
      px = bird.x + tmpx + BIRDW;
      // clear bird at previous position stored in old_y
      // we can't just erase the pixels before and after current position
      // because of the non-linear bird movement (it would leave 'dirty' pixels)
      tmpy = BIRDH - 1;
      do {
        _drawPixel(px, bird.old_y + tmpy, BCKGRDCOL);
      } while (tmpy--);
      // draw bird sprite at new position
      tmpy = BIRDH - 1;
      do {
        _drawPixel(px, bird.y + tmpy, birdcol[tmpx + (tmpy * BIRDW)]);
      } while (tmpy--);
    } while (tmpx--);
    // save position to erase bird on next draw
    bird.old_y = bird.y;
    // grass stripes
    // ---------------
    grassx -= SPEED;
    if (grassx < 0)
      grassx = TFTW;

    tft.drawFastVLine(grassx % TFTW, GAMEH + 1, GRASSH - 1, GRASSCOL);
    tft.drawFastVLine((grassx + 64) % TFTW, GAMEH + 1, GRASSH - 1, GRASSCOL2);
    // ===============
    // collision
    // ===============
    // if the bird hit the ground game over
    if (bird.y > GAMEH - BIRDH)
      break;
    // checking for bird collision with pipe
    if (bird.x + BIRDW >= pipes.x - BIRDW2 && bird.x <= pipes.x + PIPEW - BIRDW) {
      // bird entered a pipe, check for collision
      if (bird.y < pipes.gap_y || bird.y + BIRDH > pipes.gap_y + GAPHEIGHT)
        break;
      else
        passed_pipe = true;
    }
    // if bird has passed the pipe increase score
    else if (bird.x > pipes.x + PIPEW - BIRDW && passed_pipe) {
      passed_pipe = false;
      // erase score with background color
      tft.setTextColor(BCKGRDCOL);
      tft.setCursor(TFTW2, 4);
      tft.print(score);
      // set text color back to white for new score
      tft.setTextColor(TFT_WHITE);
      // increase score since we successfully passed a pipe
      score++;
    }
    // update score
    // ---------------
    tft.setCursor(TFTW2, 4);
    tft.print(score);
  }
  // add a small delay to show how the player lost
  Screen = 7;
  Screenchange = 1;
  delay(1200);
}

 

 


http://www.kler.cn/a/409949.html

相关文章:

  • ArcGIS应用指南:ArcGIS制作局部放大地图
  • Springboot 读取 resource 目录下的Excel文件并下载
  • ollama教程——在Linux上运行大型语言模型的完整指南
  • ISAAC Gym 7. 使用箭头进行数据可视化
  • 【强化学习的数学原理】第04课-值迭代与策略迭代-笔记
  • 养老院管理系统+小程序项目需求分析文档
  • 2024年跨行业跨领域工业互联网平台
  • 17.5k Star,ThingsBoard 一款开源、免费、功能全面的物联网 IoT 平台 -慧知开源充电桩平台
  • Linux中的共享内存
  • 【CSP CCF记录】201803-1第13次认证 跳一跳
  • matlab -炉温串级控制PID
  • 24.11.21深度学习
  • .NET Core发布网站报错 HTTP Error 500.31
  • 视频分析设备平台EasyCVR视频设备轨迹回放平台与应急布控球的视频监控方案
  • 嵌入式硬件杂谈(六)充电器原理 线性电源 开关电源 反激电源原理
  • 论文阅读:A fast, scalable and versatile tool for analysis of single-cell omics data
  • node.js nvm 安装和使用
  • 前端面试笔试(五)
  • 网络安全等级保护测评机构管理办法(全文)
  • 【前端学习笔记】Web API——BOM与DOM
  • Python 版本的 2024详细代码
  • AI安全:从现实关切到未来展望
  • Jmeter中的监听器
  • 信息收集(1)
  • WPF中的Button按钮中的PreviewMouseLeftButtonDown事件和MouseLeftButtonDown的区别
  • ThingsBoard规则链节点:AWS SNS 节点详解