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

news2024/12/27 22:02:07

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

这个完整的项目是由 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.coloradmin.cn/o/2247695.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Django+Nginx+uwsgi网站Channels+redis+daphne多人在线聊天实现粘贴上传图片

在DjangoNginxuwsgi网站Channelsredisdaphne多人在线的基础上&#xff08;详见DjangoNginxuwsgi网站使用Channelsredisdaphne实现简单的多人在线聊天及消息存储功能-CSDN博客&#xff09;&#xff0c;实现在输入框粘贴或打开本地图片&#xff0c;上传到网站后返回图片路径&…

[ubuntu]编译共享内存读取出现read.c:(.text+0x1a): undefined reference to `shm_open‘问题解决方案

问题log /tmp/ccByifPx.o: In function main: read.c:(.text0x1a): undefined reference to shm_open read.c:(.text0xd9): undefined reference to shm_unlink collect2: error: ld returned 1 exit status 程序代码 #include <stdio.h> #include <stdlib.h> #…

Otter 安装流程

优质博文&#xff1a;IT-BLOG-CN 一、背景 随着公司的发展&#xff0c;订单库的数据目前已达到千万级别&#xff0c;需要进行分表分库&#xff0c;就需要对数据进行迁移&#xff0c;我们使用了otter&#xff0c;这里简单整理下&#xff0c;otter 的安装过程&#xff0c;希望对…

wsl2的Ubuntu18.04安装ros和anaconda

参考&#xff1a;超详细 WSL2 安装 ros 和 anaconda_wsl2安装anaconda-CSDN博客 一.安装ros 1. 更换系统源 输入 wget http://fishros.com/install -O fishros && . fishros 和上面的链接一样&#xff0c;依次输入5-2-1 2. 安装ros 输入 wget http://fishros.c…

鸿蒙NEXT开发案例:字数统计

【引言】 本文将通过一个具体的案例——“字数统计”组件&#xff0c;来探讨如何在鸿蒙NEXT框架下实现这一功能。此组件不仅能够统计用户输入文本中的汉字、中文标点、数字、以及英文字符的数量&#xff0c;还具有良好的用户界面设计&#xff0c;使用户能够直观地了解输入文本…

【经典】抽奖系统(HTML,CSS、JS)

目录 1、添加参与者 2、多次添加 3、点击抽奖 功能介绍&#xff1a; 使用方法&#xff1a; 完整代码&#xff1a; 一个简单但功能强大的抽奖系统的示例&#xff0c;用于在网页上实现抽奖。 1、添加参与者 2、多次添加 3、点击抽奖 功能介绍&#xff1a; 参与者添加&…

用树莓派Pico控制8×8 LED点阵屏:深入解析C++核心知识与动态显示实现

88 LED点阵屏是一种直观的硬件显示工具,广泛应用于嵌入式开发中。本项目结合树莓派Pico和HT16K33驱动芯片,通过C++编程实现动态图案和文字的显示功能。本文将全面解析项目中的C++核心知识点,帮助读者深入理解C++在硬件编程中的实际应用。 一、项目背景与硬件简介 1. 项目目…

什么是 WPF 中的依赖属性?有什么作用?

依赖属性&#xff08;Dependency Property&#xff09;是 WPF 的一个核心概念&#xff0c;它为传统的 .NET 属性提供了增强功能&#xff0c;支持绑定、样式、动画和默认值等功能。通过依赖属性&#xff0c;WPF 提供了一种灵活的数据驱动的方式来处理 UI 属性。 1. 什么是依赖属…

视频分析设备平台EasyCVR视频设备轨迹回放平台与应急布控球的视频监控方案

在现代社会&#xff0c;随着城市化进程的加快和信息技术的不断进步&#xff0c;对于公共安全、交通管理、城市管理以及环境保护等领域的监控需求日益增长。应急布控球与EasyCVR视频监控方案的结合&#xff0c;正是为了满足这些领域对实时监控和快速响应的需求。这一组合利用最新…

MySQL原理简介—12.MySQL主从同步

大纲 1.异步复制为MySQL搭建一套主从复制架构 2.半同步复制为MySQL搭建一套主从复制架构 3.GTID为MySQL搭建一套主从复制架构 4.并行复制降低主从同步延迟或强制读主库 1.异步复制为MySQL搭建一套主从复制架构 (1)MySQL主从复制的原理 (2)搭建主从复制架构的配置 (1)MySQ…

Node报错:npm error code ETIMEDOUT

1、报错详细信息 npm error code ETIMEDOUT npm error syscall connect npm error errno ETIMEDOUT npm error network request to https://registry.npmjs.org/express failed, reason: connect ETIMEDOUT 104.16.1.35:443 npm error network This is a problem related to ne…

一篇文章了解Linux

目录 一&#xff1a;命令 1 ls命令作用 2 目录切换命令&#xff08;cd/pwd&#xff09; &#xff08;1)cd切换工作目录命令 3 相对路径、绝对路径和特殊路径 (1)相对路径和绝对路径的概念和写法 (2)几种特殊路径的表示符 (3)练习题&#xff1a; 4 创建目录命令&#x…

用Matlab和SIMULINK实现DPCM仿真和双边带调幅系统仿真

1、使用SIMULINK或Matlab实现DPCM仿真 1.1 DPCM原理 差分脉冲编码调制&#xff0c;简称DPCM&#xff0c;主要用于将模拟信号转换为数字信号&#xff0c;同时减少数据的冗余度以实现数据压缩。在DPCM中&#xff0c;信号的每个抽样值不是独立编码的&#xff0c;而是通过预测前一…

BERT的工作原理

BERT的工作原理 BERT的工作原理&#xff1a; Transformer的编码器是双向的&#xff0c;它可以从两个方向读取一个句子。因此&#xff0c;BERT由Transformer获得双向编码器特征。 我们把句子A&#xff08;He got bit by Python&#xff09;送入Transformer的编码器&#xff0c…

5.STM32之通信接口《精讲》之IIC通信---软件IIC与外设MPU6050通信《深入浅出》面试必备

上一节&#xff0c;我们完成对IIC通信的时序以及IIC的通信的讲解和代码实现&#xff0c;接下来&#xff0c;我们正式进入&#xff0c;利用上一节软件实现的IIC通信协议来对外设MPU6050进行读写操作。(本节IIC代码在上节) 本节&#xff0c;目的很明确&#xff0c;就是利用软件I…

解决k8s拉取私有镜像401 Unauthorized 问题

拉取镜像时未指定账户和密码通常是因为需要访问的镜像仓库启用了认证&#xff0c;但 Kubernetes 默认配置中未提供访问凭据。要解决此问题&#xff0c;可以按照以下步骤配置镜像仓库的认证信息&#xff1a; 1. 创建 Kubernetes Secret 为镜像仓库配置访问凭据&#xff0c;使用…

【Linux课程学习】:环境变量:HOME,su与su - 的区别,让程序在哪些用户下能运行的原理,环境变量具有全局性的原因?

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux课程学习 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 HOME环境变量&#xff1a; PWD环境变量&#…

不只是请求和响应:使用Fiddler抓包HTTP协议全指南(上)

欢迎浏览高耳机的博客 希望我们彼此都有更好的收获 感谢三连支持! &#x1f649;你是一名侦探 ! 正在追踪一条条数字化的线索。从简单的网页浏览到复杂的在线交易&#xff0c;每一次点击和滑动背后都隐藏着复杂的数据交换。每一个HTTP请求和响应都像是现场留下的指纹&#xf…

代码纪元——源神重塑无序

简介 源神&#xff0c;真名为张晨斌&#xff0c;原为代码宇宙创世四神之一。代码宇宙在创造之初时空无一物&#xff0c;只有复杂且繁琐的底层代码&#xff0c;智慧神灵每日都困在诸如脚本等复杂的底层框架之中&#xff0c;源神面对这种局面非常不满意&#xff0c;于是源神通过大…

Docker pull镜像拉取失败

因为一些原因&#xff0c;很多镜像仓库拉取镜像失败&#xff0c;所以需要更换不同的镜像&#xff0c;这是2024/11/25测试可用的仓库。 标题1、 更换镜像仓库的地址&#xff0c;编辑daemon.json文件 vi /etc/docker/daemon.json标题2、然后将下面的镜像源放进去或替换掉都可以…