牛年春节前看了稚晖君自制的百大up奖杯视频,用到了一块分光棱镜,透明的玻璃中显示动感的画面超有感觉,一下子就把我吸引了,于是就赶着快递停运前把分光棱镜买了回来,想着春节假期体验一把,奈何一直没想出特别好的创意也没有抽出时间来玩,直到最近看到华为太空人表盘刷遍全网,灵感终于来了, 做一个太空人主题的手表,再把分光棱镜加上,时间显示在透明的玻璃中,还有太空人的动画主题,透明的表盘带在手臂上,你就是这条街最靓的仔,我给这块靓仔手表取名叫Crystal Watch,先来欣赏一下它的制作视频
首先解释一下为什么使用这个透明的分光棱镜,首先它是一个光学元件,可以透光也可以反光,将手表的时间显示在在透明的分光棱镜中创意感一定爆棚
要制作一个可穿戴Crystal Watch,体积一定要小巧,所以选型时对于硬件材料的尺寸需要严格把关
材料的选型主要集中在主控和屏幕两方面,至于分光棱镜我们选择与屏幕尺寸差不多合适的就可以
我们把Crystal Watch的表盘外形尺寸限定在40*50mm以内,除去分光棱镜的高度,厚度限制在20mm以内会比较理想,如果再大就会感觉不协调
根据这个尺寸我们来选择屏幕,可以使用的屏幕类型大概就是LCD彩屏或者OLED屏幕,我们优先选择LCD彩屏,它的显示效果会好一点,退而求其次再选择OLED屏幕,彩屏不能太大也不能太小,太大了带在手上不合适,太小了影响观看效果,通过搜索找到一款符合我们尺寸要求的1.44寸IPS屏幕,有效显示尺寸是25.5*26.5mm,外形尺寸没有超出限制,采用SPI接口,ST7735驱动
接下来就是分光棱镜的选择,旺仔爸爸找到一款30*30*30mm的分光棱镜,与彩屏的有效显示尺寸25.5*26.5mm基本相匹配
此次设计的可穿戴Crystal Watch在功能方面我们借鉴最近流行的表盘主题,要有时间显示、太空人动画主题、以及天气温度等功能
如果要具有以上功能对于主控的选型尤为关键,在不额外增加硬件并保证小巧体积的前提下,通过网络获取天气、时间、温度等数据是比较可行的方案,于是我们主控选择了尺寸为40*31mm的esp32mini板,此控制板自带开关和电源接口,这样我们就不需要额外增加部件,进一步保证了体积不超范围
最后是电源的选型,旺仔爸爸选择了尺寸为30*12*5mm的锂电池,实际容量在200mah,下图为网络图片,仅供参考
器材选型完毕后,相信你已经迫不及待想要知道手表是如何制作完成的,下面我们就开始制作吧
本次我们要制作成一款可以穿戴的Crystal Watch,小巧的体积是非常关键的一个设计要求,我们需要对选用的硬件材料进行合理的布局,需要的材料清单如下:
esp32mini*1
1.44寸ips彩屏*1
200mah501230锂电池*1(5mm高30mm长12mm宽)
黑、白色亚克力各一块
五金若干
导线及下载线
使用Fusion360r计算机辅助设计软件设计图纸,材料选用1-4mm种类的亚克力板,旺仔爸爸将固定的通孔以圆弧的形式放置在了四个角,这样可以保证Crystal Watch的整体外观结构不超过40*50mm的同时还能让结构结实牢固,为了方便下载接口和开关的使用,中间层的亚克力设计成了半开放的状态,设计图纸如下
渲染后的3D效果图如下,看上去还是比较美观的
结构设计好后,我们使用激光切割机把它加工出来
接着我们进行电路设计,可穿戴Crystal Watch的接线会非常的简单,只需要按照下面的接线图将彩屏、电源与主控连接起来就可以了
电路连接好后如下图所示
为了防止短路,我们可以在主控板与屏幕之间贴胶带来起到绝缘的作用
可穿戴Crystal Watch的组装只需简单的几步就可以完成
1、将分光棱镜安装在第一层亚克力板上,并用4颗2mm的黑螺丝将12mm高的铜柱固定好
2、将LCD彩色屏幕与中间层的亚克力面板安装在一起
3、将安装好的屏幕以及主控与第一步安装好的分光棱镜组合在一起
组合完毕后的效果如下图所示
4、最后我们拿出准备好的表带穿入最后一层亚克力,然后将表带与前面安装好的部分组合
这样,Crystal Watch就做好了,可以看出中间黑色的亚克力有一段预留出了电源接口和开关的位置,实测厚度15mm(不包含分光棱镜的高度)
不装分光棱镜也很帅
最后就是程序设计了,开始程序设计前,我们需要先理清楚思路
这是我们本次作品程序设计的思路,需要在屏幕上显示时间,天气,太空人动画等内容
要显示这些内容需要掌握LCD彩屏的基本使用方法
下面我们从最基础的屏幕显示开始介绍
本次作品程序编写我们使用Arduino IDE编程环境,关于Arduino IDE 编程环境的下载安装给大家提供一种便捷的方法,可以在
编程环境设置好后第一步需要加载库文件,从前文中我们知道本次使用的屏幕是1.44寸SPI接口的屏幕,ST7735驱动,了解这些信息后,我们就可以选择合适的库来驱动LCD彩屏了,旺仔爸爸初步尝试了“TFT_eSPI”和“Adafruit-ST7735-Library-master”两种库,使用下来的感受是“TFT_eSPI”库的适用范围更广一些,可以适配各种型号的驱动,并且库文件的作者一直在维护更新这个库,所以我们本次选择使用“TFT_eSPI”库来驱动屏幕显示内容
屏幕驱动库文件选择好后,我们需要做一些准备工作
在Arduino IDE编程环境中,点击工具菜单栏中的库管理器,接着在搜索栏输入“TFT_eSPI”库进行安装
库文件安装好后,我们需要找到一个初始化的文件,做一些简单配置就可以驱动LCD彩屏了
我们找到如下文件路径
如果是Arduino 官网下载的编程环境的,你的库文件会在下面的路径中
当我们打开
文件打开后按照下图内容进行修改,在众多的驱动文件中,选择驱动ST7735,其他用不到的注释掉,如果你使用的屏幕是其他类型的驱动也可以根据自己的驱动进行选择
接着设置屏幕显示的颜色和屏幕的尺寸大小,我们设置屏幕的宽度为128,屏幕颜色有RGB和BGR两种类型,什么是RGB和BGR呢,你可以把它简单的理解为红绿蓝的排列顺序
接下来是SPI引脚的设置,按照下图中的引脚设置即可
当然设置前也可以参考下图中ESP32mini板的详细引脚说明
下面是字体的设置,从FONT1到FONT8,数字越大字号越大,我们屏幕比较小,本次程序中使用FONT1和FONT2两种字体就够了
最后是SPI时钟频率的设置,时钟频率会影响屏幕显示的刷新速度,ST7735驱动的时钟频率不建议超过27MHz,否则可能会不工作
至此,完成以上准备工作,我们只需要编写简单的程序就可以在LCD彩屏上随心所欲的显示各种丰富的内容了
1、彩屏颜色设置
先来看一下屏幕颜色设置的相关代码:
#include //SPI库
#include // 彩屏库
TFT_eSPI tft = TFT_eSPI(128,128); // 实例化屏幕,设置屏幕大小
void setup()
{
tft.init(); //初始化
tft.fillScreen(TFT_RED);//屏幕颜色
}
void loop()
{
}
程序中,我们需要先导入
然后在 setup() 初始化的程序中调用 tft.init() 对LCD彩屏进行初始化,这里使用的 tft 就是我们上面定义的彩屏名称,后面我们再加一句代码:tft.fillScreen(TFT_RED),用来设置屏幕的颜色,这里我们将屏幕的颜色设置为红色(TFT_RED),你也可以设置为其他颜色,比如蓝色(TFT_BLUE)、白色(TFT_WHITE)等,如下图所示,细心的伙伴会发现颜色设置的指令全部为大写,是因为这里的颜色是
除此之外,在
在使用库文件中预制的颜色时,旺仔爸爸发现,当我设置红色和蓝色时,屏幕显示出的颜色恰恰是相反的,也就是程序中设置显示红色,显示出的效果反而是蓝色,造成这个现象的原因是对驱动库初始化设置时的一条语句设置问题,当我们选择RGB的颜色模式时,屏幕显示红色与蓝色是相反的,而我们选择BGR模式时屏幕显示的颜色正常
#define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue
#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red
我们再仔细观察上面的库文件中预制颜色的代码注释部分写了不同颜色的 R、G、B 值,而且代码中每种颜色的 RGB 值与一个四位的 16 进制数相对应,比如红色 TFT_RED 对应的数字为 0xF800,众所周知其中的R、G、B代表红、绿、蓝三个通道的颜色,也就是我们通常所说的三原色,三原色指色彩中不能再分解的三种基本颜色,其实三原色分为色彩三原色和光学三原色,我们这里所指的为光学三原色,而色彩三原色则指的是红、黄、蓝三色
那么有的小伙伴就会有疑问了,我们可以使用三原色R、G、B的数值类设置屏幕的颜色吗?其实完全是可以的,
TFT_eSPI 库中直接提供了转换函数 color565(),可以让我们直接用 R、G、B 三个数值来表示颜色:
uint16_t TFT_eSPI::color565(uint8_t r, uint8_t g, uint8_t b)
// 颜色转换
uint16_t yellow = tft.color565(255, 0, 0);
比如要显示红色除了tft.fillScreen(TFT_RED);可以实现以外还可以通过下面的指令来实现
// 红色
tft.color565(255, 0, 0);
当然这个颜色设置不仅仅可以使用在屏幕颜色的设置中还可以用来设置文字的颜色,后面我们会讲到文字设置的指令
2、彩屏划线
要设计表盘的话难免需要画各种各样的线条,我们来学习一下如何在彩屏中划线,下面是
void TFT_eSPI::drawFastHLine(int32_t x, int32_t y, int32_t w, uint32_t color)
//绘制线
tft.drawFastHLine(204, a, 12, tft.alphaBlend(a, TFT_RED, TFT_WHITE));
//参数:x,y为画线的起始坐标,w为线条宽度,color为线条颜色
根据库文件给的方法,我们编写如下程序
//绘制线 第一项为透明度0-255 第二项为背景颜色,第三项为线的颜色
tft.drawFastHLine(4, 25, 120, tft.alphaBlend(0, TFT_BLACK, tft.color565(255, 255, 255)));
tft.drawFastHLine(4, 95, 120, tft.alphaBlend(0, TFT_BLACK, tft.color565(255, 255, 255)));
这样最终就可以在屏幕中绘制出两条宽度为120,颜色为白色的水平横线,细心的伙伴会发现,我们这里的颜色设置,背景色使用了库文件预制的颜色,而线条的颜色使用了RGB的数值来设置
掌握了划线的方法对于我们本次作品来说就够了,
3、彩屏文本显示
接下来我们学习文本显示,先输入下面代码查看效果
#include //导入库
#include
TFT_eSPI tft = TFT_eSPI();
void setup()
{
tft.init(); //初始化
tft.fillScreen(TFT_BLACK);//屏幕颜色
// 设置起始坐标(10, 10),2 号字体
tft.setCursor(10, 10, 1);
// 设置文本颜色为白色
tft.setTextColor(TFT_WHITE);
// 设置文字的大小
tft.setTextSize(2);
tft.println("Text");
}
void loop()
{
}
下载程序后在黑色的背景中显示出了白色的字体
看到效果后,我们来具体了解一下此段程序代码中都设置了哪些内容
由于部分内容前面已经讲解过了,我们从下面这段指令开始介绍
// 设置起始坐标(10, 10),2 号字体
tft.setCursor(10, 10, 1);
void TFT_eSPI::setCursor(int16_t x, int16_t y)
//设置文字起始坐标(10,10)
tft.setCursor(10, 10);
这段指令设置了文本在彩屏中显示的起始坐标为(10,10)以及字体为2号字,那么彩屏中的坐标是如何规划的呢,我们可以通过下面的这张图具体了解,本次我们使用的彩屏分辨率为128*128,左上角为坐标原点,右下角的坐标为(128,128),了解了这个坐标的分布对于我们后面在屏幕中显示各种丰富的内容有很大帮助(另外,旺仔爸爸发现,我使用的这块屏幕虽然硬件参数是128*128 的分辨率,但实际演示效果会发现右侧始终有一条多余的线,当我把彩屏分辨率初始化为130*128时,会发现多余的线条消失了,实际这块屏幕的分辨率在130*128)
接下来我们看文本颜色的设置代码
// 设置文本颜色为白色
tft.setTextColor(TFT_WHITE);
void TFT_eSPI::setTextColor(uint16_t c)
//字体颜色 白色
tft.setTextColor(TFT_WHITE);
//参数:c为颜色值,也可以使用RGB数值来设置颜色
// 设置文本颜色与背景色
oidsetTextColor(uint16_t fg,uint16_t bg);
//参数:fg为字体颜色,bg为背景颜色,也可以使用RGB数值来设置颜色
下面是文字大小的设定
// 设置文字的大小
tft.setTextSize(2);
void TFT_eSPI::setTextSize(uint8_t s)
//设置字体大小为2
tft.setTextSize(2);
//参数:允许设置大小1-7,大于7默认选择7
我们使用的屏幕比较小,使用1-3号字体就可以了
上面关于文本显示的设置都设置好后接下来就是最重要的输出显示的内容了
通过下面的
tft.println("Text");
4、设置彩色屏幕的显示方向
前文介绍的彩屏显示内容都为默认方向,也就是竖屏显示,其实彩屏还可以旋转角度,可以在 0°、90°、180°、270° 之间选择,除此以外的其他角度并不支持,屏幕显示旋转角度设置过之后,坐标原点也会跟着改变
在
void TFT_Touch::setRotation(byte rotation)
//设置屏幕方向tft.setRotation(1);
//参数为:0, 1, 2, 3 分别代表 0°、90°、180°、270°
比如我们将tft.setRotation(1)设置为1,彩色屏幕就会切换为横屏显示
其他的角度,大家有兴趣的可以尝试修改一下数值看一下效果,这里就不再详细介绍了,而在本次程序中我们额外设置了一个tft.setRotation(4)
这里我们需要着重介绍一下tft.setRotation(4),在介绍之前,我们先得了解一下分光棱镜的成像原理
这也是本次作品比较至关重要的一个技术难点,如何将彩色屏幕中显示的内容显示在分光棱镜中呢,我们可以列举望远镜的例子来了解,我们知道望远镜的成像原理如下图所示,望远镜的内部有两块直角三棱镜,它的主要作用是为了让光线经过两次反射后到达人眼,光线经过物镜后的地成像是颠倒的,光线经过目镜后再一次颠倒,这时我们人眼看到的就是正像了
而分光棱镜其实就是将两块三棱镜叠加在一起
下面是百度百科对于分光棱镜的定义
分光棱镜是一种用于分离光线的水平偏振和垂直偏振的光学元件
是通过在直角棱镜的斜面镀制多层膜结构,然后胶合成一个立方体结构,利用光线以布鲁斯特角入射时P偏振光透射率为1而S偏振光透射率小于1的性质,在光线以布鲁斯特角多次通过多层膜结构以后,达到使P偏振分量完全透过,而绝大部分S偏振分量反射(至少90%以上)的一个光学元件
简单的理解就是两块直角三棱镜拼接在一起,中间镀制了多层膜结构,光线经过多层镀膜后部分光线透射,部分光线反射,最后就会在分光棱镜中成像了
而它所成的像是上下颠倒的,这就需要我们在程序中把屏幕设置为镜像显示
如下图看到样子,当我们设置彩色屏幕中正常显示时,在分光棱镜中的成像是上下颠倒的
于是我们需要将屏幕中显示的内容先镜像一次,这样在分光棱镜中的成像才会正常显示
掌握了文本的基本显示代码后,我们可以做一个通用函数,这样调用起来就会方便很多了,代码如下
showtext(80,105,1,2,TFT_WHITE,TFT_BLACK,"12:00");
/*文本显示*/
void showtext(int16_t x,int16_t y,uint8_t font,uint8_t s,uint16_t fg,uint16_t bg,const String str)
{
//设置文本显示坐标,和文本的字体,默认以左上角为参考点,
tft.setCursor(x, y, font);
// 设置文本颜色为白色,文本背景黑色
tft.setTextColor(fg,bg);
//设置文本大小,文本大小的范围是1-7的整数
tft.setTextSize(s);
// 设置显示的文字,注意这里有个换行符 \n 产生的效果
tft.println(str);
}
最终我们的表盘则需要设置成下面图片所展示的这样
那么怎么做到屏幕镜像显示的呢,我们需要在
需要在
大家需要记住这几个设置指令都可以将屏幕显示的内容上下颠倒也就是镜像
//0x39正常颜色 屏幕90度显示 0x40反色屏幕0度显示 0x36反色 屏幕90度显示
Crystal Watch横屏带在手臂上实在太难看,于是旺仔爸爸将这里的镜像显示指令设置成了
如果要让屏幕显示的内容左右颠倒镜像改怎么设置呢?
我们可以设置为
详细的屏幕镜像设置方法可以参考ST7735驱动的手册
如果屏幕的镜像显示搞明白以后,我们两万五千里长征已经走了一半了,接下来的过程将会很快就完成
下面我们来了解汉字如何显示
5、显示汉字
我们需要新建一个text.h的文件(这个文件名字随意取),并且在文件中写入以下程序,为什么要这么复杂呢,因为汉字的显示原理和英文不同,
从代码中可以看出想要显示汉字,其实是将每个汉字转换成对应的16进制数,如果能够将足够多的汉字转换成16进制数,也就是意味着我们可以显示任意中文词语或者句子,但其实显示汉字或图片这种操作是比较占用单片机运行内存的,我们需要将这些不会变化的数据定义为不可变的const类型,因为用const修饰的变量,在硬件上会被保存到ROM中即“程序存储器”(类似于电脑的硬盘或手机的内存),而用于计算的“随机存储器”RAM(类似于电脑的内存或手机的运存)空间比ROM小很多很多,所以这么做就可以把不用改变值的变量从RAM中移到ROM中,节约系统资源。
#include
const unsigned char hz_zhou PROGMEM[] =
{
0x00,0x00,0x3F,0xF8,0x21,0x08,0x21,0x08,0x2F,0xE8,0x21,0x08,0x21,0x08,0x3F,0xF8,
0x20,0x08,0x27,0xC8,0x24,0x48,0x24,0x48,0x27,0xC8,0x40,0x08,0x40,0x28,0x80,0x10/*"周",0*/
};
const unsigned char hz_Mon PROGMEM[] =
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFE,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00/*"一",1*/
};
本次作品中我们涉及到的汉字数量非常少,简单的转换几个就足够使用
都会用到哪些汉字呢,比如显示星期的周一至周日,以及显示天气的晴、阴、多云、雨、雪等
那么这些16进制的数值是如何获得的呢,我们可以借助取模工具来完成
取模工具的使用步骤如下(关于取模软件的下载方法可以自行网络查找,这里就不再介绍了),第一步打开软件
第二步、按照下图中的选项进行设置
第三步、输入想要转换的汉字点击生成字模即可看到对应的16进制数据,我们只需要将这些数据复制到刚才新建好的text.h文件中即可
需要注意的是,为了方便显示词语或句子,我们还需要设置一个数组将所有取模后的汉字数据放在数组中方便索引调用,除此之外还需要一个汉字字模数据的结构体,可以简单的理解为存放了一些字库的信息,比如每个汉字所占的字节
struct FNT_HZ // 汉字字模数据结构
{
char Index[4]; // 汉字内码索引,存放内码,如"中",在UTF-8编码下,每个汉字占3个字节,第四个是结束符0
const unsigned char* hz_Id; // 点阵码数据 存放内码后对应的 点阵序列 每个字需要32个字节的点阵序列
unsigned char hz_width;
};
PROGMEM const FNT_HZ hanzi[] =
{
{"周", hz_zhou,16}, {"一", hz_Mon,16}, {"二", hz_Tue,16}, {"三", hz_Wed,16}, {"四", hz_Thu,16},
{"五", hz_Fri,16}, {"六", hz_Sat,16}, {"日", hz_Sunday,16}, {"晴", hz_sun,16}, {"阴", hz_cloudy,16},
{"雨", hz_rain,16}, {"雪", hz_snow,16}, {"多", hz_duo,16}, {"云", hz_yun,16},{"姑", gu,16},
{"娘", niang,16},{"们", men,16},{"真", zhen,16},{"棒", bang,16}
};
由于篇幅原因,这里只展示了部分取模代码,大家可以按照上述方法将几个汉字取模,我们编写如下代码进行测试
#include //导入库
#include
#include "text.h" //导入字库
TFT_eSPI tft = TFT_eSPI();
void setup()
{
tft.init(); //初始化
tft.fillScreen(TFT_BLACK);//屏幕颜色
tft.setRotation(4);
showHanziS(40, 50, "周日晴", TFT_YELLOW);//显示汉字
}
void loop()
{
}
void showHanzi(int32_t x, int32_t y, const char c[3], uint32_t color) {
for (int k = 0; k < 20; k++)// 根据字库的字数调节循环的次数
if (hanzi[k].Index[0] == c[0] && hanzi[k].Index[1] == c[1] && hanzi[k].Index[2] == c[2])
{ tft.drawBitmap(x, y, hanzi[k].hz_Id, hanzi[k].hz_width, 16, color);
}
}
/*整句汉字显示*/
void showHanziS(int32_t x, int32_t y, const char str[], uint32_t color) { //显示整句汉字,字库比较简单,上下、左右输出是在函数内实现
int x0 = x;
for (int i = 0; i < strlen(str); i += 3) {
showHanzi(x0, y, str+i, color);
x0 += 17;
}
}
在上面的代码中,我们需要增加导入字库的代码#include "text.h"
接着我们设置两个函数,用来显示单个汉字和句子,这两个函数的作用我们可以这样理解,句子是由多个汉字组成的,所以如果要显示整个句子的话就在字库中搜索匹配的汉字,再调用单个汉字显示的函数一个一个汉字显示出来就是完整的句子了
void showHanzi(int32_t x, int32_t y, const char c[3], uint32_t color)//显示单个汉字
void showHanziS(int32_t x, int32_t y, const char str[], uint32_t color)//显示整个句子
6、显示图片
同样我们需要新建一个bmp.h的文件(这个文件名字随意取)用来存放图片取模后的16进制数据
下面是本次我们需要用到的天气图片和太空人图片,天气图片的分辨率设置为42*32,太空人图片的分辨率设置为64*64,图片分辨率可以使用系统自带的画图软件设置,当然如果你想要显示其他图片也是可以的,只要不超过屏幕的分辨率即可
可以看出下图的图片非常的不清晰,确实是因为分辨率很低的原因
当我们把这些素材整理好后,就可以打开图片取模工具进行取模了
第一步、打开软件,导入提前准备好的文件
第二步、点击菜单栏的
第三步、点击
第四步、复制生成的16进制数据到bmp.h文件中
bmp.h文件中的部分代码如下:
#include
const uint16_t partlycloudy [] PROGMEM = {
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8000, 0x8000, 0x8000, 0x8000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8000, 0x8400, 0x8400, 0x8400, 0x8400, 0x8400, 0x8400, 0x8400, 0x8400, 0x8000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8000, 0x8400, 0x8400, 0x8400, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x8400, 0x8400, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8000, 0x8400, 0x8400, 0xffe0, 0x8400, 0xf800, 0xffe0, 0x8400, 0x8400, 0x8400, 0x8400, 0xffe0, 0xffe0, 0x8400, 0x8400, 0x8000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8000, 0x8400, 0xffe0, 0xffe0, 0x8400, 0x8400, 0xffe0, 0xffe0, 0x8400, 0x8400, 0x8400, 0xffe0, 0x8400, 0xffe0, 0xffe0, 0xffe0, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8400, 0x8400, 0xffe0, 0xffe0, 0xffe0, 0x8400, 0xc618, 0xc618, 0xffe0, 0x8400, 0xffe0, 0x8400, 0x8400, 0x8400, 0x8400, 0xffe0, 0x8400, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8400, 0x8400, 0xffe0, 0xf800, 0x8400, 0x8400, 0x8400, 0x8400, 0x8400, 0x8400, 0x8400, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x8400, 0x8400, 0xffe0, 0x8400, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8400, 0xffe0, 0xf800, 0xf800, 0xf800, 0xf800, 0x8400, 0xf800, 0x8400, 0x8400, 0xffe0, 0x8400, 0x8400, 0xffe0, 0xc618, 0xc618, 0xffe0, 0xffe0, 0xffe0, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8000, 0x8400, 0xffe0, 0x8400, 0xf800, 0xc618, 0xffe0, 0xf800, 0xf800, 0x8400, 0x8400, 0x8400, 0x8400, 0xffe0, 0x8400, 0x8400, 0xffe0, 0x8400, 0xffe0, 0xffe0, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8000, 0xffe0, 0xffe0, 0xffe0, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xc618, 0xffe0, 0xc618, 0x8400, 0x8400, 0x8400, 0x8400, 0xffe0, 0x8400, 0xc618, 0xffe0, 0x8400, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8410, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffe0, 0x8400, 0xffe0, 0x8400, 0x8400, 0x8400, 0xffe0, 0x8400, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8410, 0x8410, 0x8410, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xc618, 0x8400, 0x8400, 0x8400, 0x8400, 0xffe0, 0x8400, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8410, 0x8410, 0x8410, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x8400, 0x8400, 0x8400, 0xffe0, 0x8400, 0x8410, 0xc618, 0xc618, 0x8410, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8410, 0xc618, 0x8410, 0x8410, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xc618, 0x8400, 0xffff, 0xffff, 0xc618, 0xffff, 0xffff, 0xffff, 0x8410, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x8410, 0x8410, 0xffff, 0xffff, 0xc618, 0xc618, 0x8410, 0x8410, 0x8410, 0x8410, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xc618, 0xffe0, 0xffe0, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xc618, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x8410, 0xc618, 0xffff, 0xffff, 0xffff, 0xc618, 0x8410, 0x8410, 0x8410, 0x8410, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xc618, 0xffff, 0xffff, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0x8410, 0x8410, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x8410, 0xc618, 0xc618, 0xffff, 0xffff, 0xc618, 0xc618, 0x8410, 0x8410, 0x8410, 0x8410, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xc618, 0xffff, 0xc618, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0xc618, 0xffff, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xc618, 0xc618, 0x8410, 0x8400, 0x8410, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0x8410, 0xc618, 0x8410, 0x0000, 0x0000,
0x0000, 0xc618, 0xc618, 0x8410, 0x8410, 0x8410, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xc618, 0xc618, 0xc618, 0x8410, 0x8410, 0x8410, 0xffe0, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xc618, 0xc618, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xc618, 0xc618, 0xc618, 0xffff, 0xc618, 0x8410, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8410, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0xffff, 0xc618, 0xc618, 0xc618, 0xc618, 0xc618, 0x8400, 0xffe0, 0xffe0, 0xffe0, 0xc618, 0xc618, 0x8410, 0xc618, 0xc618, 0xc618, 0xffff, 0xc618, 0x8410, 0x8410, 0x8410, 0x8410, 0x8410, 0x8410, 0x8410, 0x8410, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8410, 0x8410, 0xc618, 0xc618, 0xc618, 0xffff, 0xffff, 0xc618, 0x8410, 0xc618, 0x8410, 0x8410, 0x8400, 0x8400, 0x8400, 0x8400, 0x8410, 0xc618, 0xffe0, 0x8400, 0x8400, 0x8410, 0x8410, 0x8000, 0x0000, 0x0000, 0x8410, 0x8410, 0x8410, 0x8410, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8410, 0x8410, 0x8410, 0x0000, 0x0000, 0x0000, 0x0000, 0x8400, 0x8400, 0x8410, 0x8400, 0x8400, 0x8400, 0x8400, 0x8400, 0x8400, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
};
//天气图片数组
const uint16_t* weather_image_black [] PROGMEM = {sun,partlycloudy,cloudy,rain,snow};
//黑色背景太空人图片数组
const uint16_t* bmp_table_black [] PROGMEM = {bmp_black1,bmp_black2,bmp_black3,bmp_black4,bmp_black5,bmp_black6,bmp_black7,bmp_black8,bmp_black9,bmp_black10};
//白色背景太空人图片数组
const uint16_t* bmp_table_white [] PROGMEM = {bmp_white1,bmp_white2,bmp_white3,bmp_white4,bmp_white5,bmp_white6};
所有图片取模完成后,我们编写如下代码进行测试,同样需要导入取模后的图库文件<#include "bmp.h" >
#include //导入库
#include
#include "text.h" //导入字库
#include "bmp.h" //导入图库
TFT_eSPI tft = TFT_eSPI();
void setup()
{
tft.init(); //初始化
tft.fillScreen(TFT_BLACK);//屏幕颜色
tft.setRotation(4);
tft.pushImage(30, 30, 42, 32, sun);//显示晴天图片
tft.pushImage(20, 30, 64, 64, bmp_black1);//显示黑色背景的太空人主题
}
void loop()
{
}
这里我们用到了tft.pushImage(20, 30, 64, 64, bmp_black1)语句,语句中包含五个参数,分别是图片的起始坐标,长宽尺寸以及图片的16进制数据
有的伙伴可能会有疑问,目前只是显示了一张图片,如何才能动态显示太空人呢,其实非常的简单,我们将图片显示的程序放入循环中让图片轮番播放就可以了,代码如下,这里我们准备了10张黑色背景的太空人图片,所以i从0开始到9结束,循环10次
#include //导入库
#include
#include "text.h" //导入字库
#include "bmp.h" //导入图库
TFT_eSPI tft = TFT_eSPI();
int i=0;
void setup()
{
tft.init(); //初始化
tft.fillScreen(TFT_BLACK);//屏幕颜色
tft.setRotation(4);//镜像显示
}
void loop()
{
tft.pushImage(20, 30, 64, 64, bmp_table_black[i]);//调用图片数据
i+=1;
if(i>9){i=0;}
delay(100);
}
再来看一下白色背景太空人主题的动态演示效果
至此,LCD彩色屏幕所有的显示内容就全部介绍完了,下面我们就可以按照前面的方法轻车熟路的显示各种丰富的内容了,比如时间,天气,温度等等
前面我们已经学会了文本、汉字等内容在彩屏中的显示方法,在屏幕上显示譬如时间、日期等这些内容将不再是什么难事,但是我们如何才能让Crystal Watch获取精准的时间呢,这是比较关键的地方
这里我们可以思考这样一个问题,为什么我们的手机和电脑的时间是一直准确的,其实是因为有网络服务器来提供精确的时间,这些服务器就叫做NTP服务器,那么接下来的事情就好办了,我们只需要让Crystal Watch和手机、电脑一样通过NTP服务器来获取精确的时间即可
那什么是NTP服务器呢?
NTP服务器全称
目前世界上都有哪些NTP服务器呢?
最熟悉的就是pool.ntp.org,它是一个以时间服务器的大虚拟集群为上百万的客户端提供可靠的 易用的 网络时间协议(NTP)服务的项目,NTP池正在为世界各地成百上千万的系统提供服务
国内地址为:cn.pool.ntp.org
除此之外国内常用的NTP服务器地址还有:
阿里云 ntp.aliyun.com
腾讯 server time1.cloud.tencent.com
谷歌 time.google.com
清华大学 ntp.tuna.tsinghua.edu.cn
上海交通大学 ntp.sjtu.edu.cn
复旦大学 ntp.fudan.edu.cn
感兴趣的伙伴可以去测试一下
了解了基本概念后,我们就可以在程序中使用NTP服务器了
首先打开Arduino IDE库管理器,在搜索栏输入ntp,找到
NTP服务器需要通过网络来响应,所以还需要安装WiFi库,同样的方法在搜索栏输入wifi,找到wifi库进行安装,如果已经安装过的可以忽略
然后按照下面的方法打开NTP示例程序
接着我们对示例程序代码进行简单的修改,增加wifi账号和密码信息后即可下载程序
#include
#include // for WiFi shield
#include
const char *ssid = " "; //wifi账号
const char *password = " "; //wifi密码
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP,"pool.ntp.org"); //NTP服务器地址
void setup(){
Serial.begin(115200);
//连接wifi
WiFi.begin(ssid, password);
while ( WiFi.status() != WL_CONNECTED ) {
delay ( 500 );
Serial.print ( "." );
}
timeClient.begin();
}
void loop() {
timeClient.update();
//打印时间
Serial.println(timeClient.getFormattedTime());
delay(1000);
}
下载后我们通过串口监视器查看打印结果,发现打印出来的时间和电脑中显示的时间并不同步,基本相差8个小时
看到这里,大家可能会想到,是不是时区的原因呢,确实如此,如果要与我们所处的东8区时间同步的话还需要再修改一下程序
timeClient.begin();
//设置偏移时间(以秒为单位)以调整时区,例如:
// GMT +1 = 3600
// GMT +8 = 28800
// GMT -1 = -3600
// GMT 0 = 0
timeClient.setTimeOffset(28800);
在timeClient.begin()下面增加这条代码timeClient.setTimeOffset(28800)
时间将会调整为东8区
除了显示精准的时间以外,日期与星期该如何显示呢?我们尝试输入如下代码
timeClient.update();
unsigned long epochTime = timeClient.getEpochTime();
Serial.print("Epoch Time: ");
Serial.println(epochTime);
//打印时间
int currentHour = timeClient.getHours();
Serial.print("Hour: ");
Serial.println(currentHour);
int currentMinute = timeClient.getMinutes();
Serial.print("Minutes: ");
Serial.println(currentMinute);
int weekDay = timeClient.getDay();
Serial.print("Week Day: ");
Serial.println(weekDay);
//将epochTime换算成年月日
struct tm *ptm = gmtime ((time_t *)&epochTime);
int monthDay = ptm->tm_mday;
Serial.print("Month day: ");
Serial.println(monthDay);
int currentMonth = ptm->tm_mon+1;
Serial.print("Month: ");
Serial.println(currentMonth);
delay(1000);
从下面的运行结果可以看到,和电脑上的时间、日期是同步的,仔细看不难发现,其中有一项特别大的数字Epoch Time
EpochTime是什么呢?其实它指的是一个特定的时间:1970-01-01 00:00:00 UTC,也就是世界标准时间1970年1月1日0时0分0秒,以这个时间为起点,每过去一秒,数值加1。对应的就可以算出公历时间日期(不算闰秒),原来我们手机、电脑上的时间都是这么计算得来,由衷的佩服前辈们的伟大
其实程序中最关键的就是getEpochTime()这条语句,当我们获取到这个很大的时间数据后,可以利用NTP库文件中提供的方法直接计算出年月日、星期等数据,获取到时间数据后只需要按照前面介绍的显示文本与汉字的方法就可以在LCD彩屏显示时间数据了
现在,我们离成功还差最后一步,那就是如何获取天气信息,我们这次通过请求心知天气网站来获取天气数据
在编写程序之前我们需要先做一些准备工作,首先打开心知天气网站seniverse.com
接着需要登录,如果之前没有使用过,需要先注册一下信息
注册完成登录账号
成功登录后我们点击免费版下面的免费申请
接着就会看到下面的信息
点击产品文档选择天气类的接口
选择接口后会看到如下所示界面,免费用户只返回三天的数据,对于我们来说完全够用了
这个网址中包含了一些有用的信息,比如Key=,location=,Language等信息
https://api.seniverse.com/v3/weather/now.json?key=SK4eYmRdgcaRXFhhj&location=beijing&language=zh-Hans&unit=c
{"results":[{"location":{"id":"WX4FBXXFKE4F","name":"北京","country":"CN","path":"北京,北京,中国","timezone":"Asia/Shanghai","timezone_offset":"+08:00"},"now":{"text":"晴","code":"0","temperature":"27"},"last_update":"2021-04-19T16:10:00+08:00"}]}
从反馈数据中不难看出,数据中包含了我们想要的天气数据
那么我们的Crystal Watch该如何访问心知天气获取到这样的数据呢,其实和手机、电脑等联网设备的浏览器一样,都是通过发送httq请求从而获取到服务器发回的数据的
这里我们需要普及一下http请求,
http就是超文本传输协议,全拼是HyperText Transfer Protocol,它是指从客户端到服务器端的请求消息,简单的讲http超文本传输协议就是定义了浏览器向互联网上的服务器请求数据的规则以及服务器该以什么样的格式把数据传递给浏览器
那么http请求的过程是什么样的呢,都包含了哪些信息
下面我们举一个简单的例子,比如我们想要向bilibil服务器发送http请求
地址如下
https://www.bilibili.com/video/BV1r54y1y7TS/
在这个地址中包含了<协议>://<主机>:<端口>/<路径>
端口和路径有时可以省略(HTTP默认端口号是80,HTTPS端口为443)
通过下面的图解可以知道,HTTP请求需要经过三次握手才能成功获取到数据
有时候我们发送http请求时还需要携带一些参数,比如我们请求心知天气时需要携带的一些参数信息
当我们向服务请求成功后,服务器会给我们返回一个JSON格式的数据
HTTP请求的方法和JSON数据都拿到了,接下来就是如何运用在此次作品中了
因为请求的过程使用到了HTTP协议与JSON数据,而Arduino IDE库中恰恰提供了这两种库,这样我们只需加载这两个库就可以实现我们想要的功能了
首先打开Arduino IDE编程环境,在工具菜单栏的库管理器中搜索
接着按照同样的方法搜索
库文件安装完成后,我们编写下面的程序
#include //wifi库
#include //Json库
#include //HTTP库
const char* ssid = " "; //wifi账号
const char* password = " "; //wifi密码
const char* host = "api.seniverse.com"; //心知天气服务器地址
String now_address="",now_time="",now_temperature="";//用来存储报文得到的字符串
void setup()
{
Serial.begin(115200);
// 连接网络
WiFi.begin(ssid, password);
//等待wifi连接
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected"); //连接成功
Serial.print("IP address: "); //打印IP地址
Serial.println(WiFi.localIP());
}
void loop()
{
//创建TCP连接
WiFiClient client;
const int httpPort = 80;
if (!client.connect(host, httpPort))
{
Serial.println("connection failed"); //网络请求无响应打印连接失败
return;
}
//URL请求地址
String url ="/v3/weather/now.json?key=S_xhO9flk_rjzOsJY&location=yangzhou&language=zh-Hans&unit=c";
//发送网络请求
client.print(String("GET ") + url + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
delay(5000);
//定义answer变量用来存放请求网络服务器后返回的数据
String answer;
while(client.available())
{
String line = client.readStringUntil('\r');
answer += line;
}
//断开服务器连接
client.stop();
Serial.println();
Serial.println("closing connection");
//获得json格式的数据
String jsonAnswer;
int jsonIndex;
//找到有用的返回数据位置i 返回头不要
for (int i = 0; i < answer.length(); i++) {
if (answer[i] == '{') {
jsonIndex = i;
break;
}
}
jsonAnswer = answer.substring(jsonIndex);
Serial.println();
Serial.println("JSON answer: ");
Serial.println(jsonAnswer);
}
程序下载后,可以在串口监视器中看到如下返回结果
我们把其中的数据提取出来进行分析,
JSON answer:
{"results":[{"location":{"id":"WTUBM40RTTUB","name":"扬州","country":"CN","path":"扬州,扬州,江苏,中国","timezone":"Asia/Shanghai","timezone_offset":"+08:00"},"now":{"text":"晴","code":"0","temperature":"23"},"last_update":"2021-04-19T17:20:00+08:00"}]}
明显这是一段JSON格式的数据,从数据中看出已经包含了我们需要的天气信息,这时候我们就需要查看Arduino Json库的使用方法进一步提取数据信息了
我们浏览器输入arduinojson.org,这是Arduino Json库官方提供的网址,此网站给我们提供了JSON数据的转换方法,我们可以按照下图中的信息选择控制板型号
接着我们将刚才提取出来的JSON数据复制过来进行解析
然后我们点击“Next Program”
最后就会生成如下数据,这里我们可以简单理解一下,此网站的作用其实是将JSON数据转换成符合Arduino IDE语法格式的代码,我们只需要将这些代码复制到刚才的代码中进行下载即可
程序运行后会我们会在串口监视器中看到一些反馈信息
我们只需要其中的温度
到此为止,获取时间,天气,温度,显示动画、文本、汉字等功能都已实现,接下来我们只需要按照自己喜欢样式设计表盘就大功告成了
我们知道天气状况并不会变换的特别频繁,所以请求心知天气的数据我们可以设置成每隔1小时响应一次,这时候我们就需要用到定时器"Ticker.h"库,我们按照前文中提到的方法安装"Ticker.h"库,使用的方法非常的简单在setup初始化程序中增加 t1.attach(3600, get_weather);代码,这样每隔3600秒(即1h)就会获取一次天气数据,另外我们还设置了两种太空人主题的动画,切换时在代码中修改背景颜色及太空人的数组图片即可
最后附上完整代码(不包含字库和图库的取模数据)
#include
#include
#include
#include "bmp.h"
#include "text.h"
#include //wifi库
#include //Json库
#include //HTTP库
#include //NTP库
#include
#include "Arduino.h"
#include "Ticker.h" //定时器库
const char* ssid = " "; //wifi账号
const char* password = " "; //wifi密码
const char* host = "api.seniverse.com"; //心知天气服务器地址
String now_address="",now_time="",now_temperature="",now_weather="";//用来存储报文得到的字符串
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
String weekDays[7]={"周日", "周一", "周二","周三", "周四", "周五", "周六"};
//Month names
String months[12]={"January", "February", "March", "April","May", "June", "July", "August", "September", "October", "November", "December"};
Ticker t1;
TFT_eSPI tft = TFT_eSPI(130,128);//设定屏幕大小
char determineqing[]="晴";
char determineduoyun[]="多云";
char determineyin[]="阴";
char determineyu[]="雨";
char determinexue[]="雪";
int i=1;
int ph;
char* now_wea;
int tm_Hour,tm_Minute,monthDay,tm_Month;
String weekDay;
char* week;
void get_wifi()
{
// 连接网络
WiFi.begin(ssid, password);
//等待wifi连接
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected"); //连接成功
Serial.print("IP address: "); //打印IP地址
Serial.println(WiFi.localIP());
}
void get_weather()
{
//创建TCP连接
WiFiClient client;
const int httpPort = 80;
if (!client.connect(host, httpPort))
{
Serial.println("connection failed"); //网络请求无响应打印连接失败
return;
}
//URL请求地址
String url ="/v3/weather/now.json?key=S_xhO9flk_rjzOsJY&location=yangzhou&language=zh-Hans&unit=c";
//发送网络请求
client.print(String("GET ") + url + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
delay(2000);
//定义answer变量用来存放请求网络服务器后返回的数据
String answer;
while(client.available())
{
String line = client.readStringUntil('\r');
answer += line;
}
//断开服务器连接
client.stop();
Serial.println();
Serial.println("closing connection");
//获得json格式的数据
String jsonAnswer;
int jsonIndex;
//找到有用的返回数据位置i 返回头不要
for (int i = 0; i < answer.length(); i++) {
if (answer[i] == '{') {
jsonIndex = i;
break;
}
}
jsonAnswer = answer.substring(jsonIndex);
Serial.println();
Serial.println("JSON answer: ");
Serial.println(jsonAnswer);
const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(6) + 210;
DynamicJsonDocument doc(capacity);
deserializeJson(doc, jsonAnswer);
JsonObject results_0 = doc["results"][0];
JsonObject results_0_location = results_0["location"];
const char* results_0_location_id = results_0_location["id"]; // "WX4FBXXFKE4F"
const char* results_0_location_name = results_0_location["name"]; // "北京"
const char* results_0_location_country = results_0_location["country"]; // "CN"
const char* results_0_location_path = results_0_location["path"]; // "北京,北京,中国"
const char* results_0_location_timezone = results_0_location["timezone"]; // "Asia/Shanghai"
const char* results_0_location_timezone_offset = results_0_location["timezone_offset"]; // "+08:00"
JsonObject results_0_now = results_0["now"];
const char* results_0_now_text = results_0_now["text"]; // "多云"
const char* results_0_now_code = results_0_now["code"]; // "4"
const char* results_0_now_temperature = results_0_now["temperature"]; // "5"
const char* results_0_last_update = results_0["last_update"]; // "2020-11-19T19:00:00+08:00"
Serial.print("city:name:");
Serial.println(results_0_location_name);
now_temperature=results_0_now_temperature;
Serial.println(now_temperature);
now_time=results_0_last_update;
Serial.println(now_time);
now_weather=results_0_now_text;
if(strstr(now_weather.c_str(),determineqing)!=0)
{ now_wea = "晴";
ph = 0;
}
if(strstr(now_weather.c_str(),determineduoyun)!=0)
{ now_wea = "多云";
ph = 1;
}
if(strstr(now_weather.c_str(),determineyin)!=0)
{ now_wea = "阴";
ph = 2;
}
if(strstr(now_weather.c_str(),determineyu)!=0)
{ now_wea = "雨";
ph = 3;
}
if(strstr(now_weather.c_str(),determinexue)!=0)
{ now_wea = "雪";
ph = 4;
}
}
void setup()
{
Serial.begin(115200);
Serial.println("Start");
tft.init();//初始化显示寄存器
tft.fillScreen(TFT_BLACK);//屏幕颜色
tft.setTextSize(2);
tft.setTextColor(TFT_MAGENTA);//设置字体颜色紫红色
tft.setCursor(0, 0, 1);//设置文字开始坐标(0,0)及字体
tft.setTextDatum(MC_DATUM);// 设置文本的引用数据
tft.setTextSize(1);//设置文字大小
tft.setRotation(4);//屏幕内容镜像显示或者旋转屏幕0-4 ST7735_Rotation中设置
showHanziS(40, 50, "周日晴", TFT_YELLOW);
delay(3000);
tft.fillScreen(TFT_BLACK);//屏幕颜色
//0x39正常颜色 90度 0x40反色0度 0x37反色 90度 0x36反色90度
get_wifi();
get_weather();
timeClient.begin();
//设置偏移时间(以秒为单位)以调整时区,例如:
// GMT +1 = 3600
// GMT +8 = 28800
timeClient.setTimeOffset(28800);
t1.attach(3600, get_weather);
}
void loop()
{
timeClient.update();
unsigned long epochTime = timeClient.getEpochTime();
String formattedTime = timeClient.getFormattedTime();
int tm_Hour = timeClient.getHours();
int tm_Minute = timeClient.getMinutes();
int tm_Second = timeClient.getSeconds();
String weekDay = weekDays[timeClient.getDay()];
char week[weekDay.length() + 1];
weekDay.toCharArray(week,weekDay.length() + 1);
struct tm *ptm = gmtime ((time_t *)&epochTime);
int monthDay = ptm->tm_mday;
int tm_Month = ptm->tm_mon+1;
String currentMonthName = months[tm_Month-1];
int tm_Year = ptm->tm_year+1900;
String currentDate = String(tm_Year) + "-" + String(tm_Month) + "-" + String(monthDay);
/*照片数量-1*文本颜色*文本背景颜色*图片*分*时*月*日*温度*星期*/
show_page(9,TFT_WHITE,TFT_BLACK,bmp_table_black,weather_image_black[ph], tm_Minute, tm_Hour, tm_Month, monthDay,now_temperature,now_wea,week);
delay(100);
}
/*图片文本页面显示*/
void show_page(int16_t top,uint16_t fg,uint16_t bg,const uint16_t* page_image[],const uint16_t* wea_image, int32_t m,int32_t h,int32_t mon,int32_t days,const String temperature,const char* now_wea, const char* week)
{
tft.fillScreen_1(0, 30, 64, 64,bg);
tft.setSwapBytes(true);//开启显示
tft.pushImage(0, 30, 64, 64, page_image[i]);
i+=1;
if(i>top){i=0;}
delay(100);
tft.drawFastHLine(4, 25, 120, tft.alphaBlend(0, TFT_RED, fg));//绘制线 半透明颜色0-255
tft.drawFastHLine(4, 95, 120, tft.alphaBlend(0, TFT_RED, fg));//绘制线 半透明颜色0-255
showtext(20,5,1,2,fg,bg,(String)mon+"/"+(String)days);
if(h-10 < 0)
{showtext(70,35,1,3,fg,bg,"0"+(String)h+":\n");}
else
{showtext(70,35,1,3,fg,bg,(String)h+":\n");}
if((m-10)<0)
{showtext(85,65,1,3,fg,bg,"0"+(String)m);}
else
{showtext(85,65,1,3,fg,bg,(String)m);}
tft.pushImage(5, 98, 42, 32, wea_image);
showHanziS(90, 5, week, TFT_YELLOW);
showHanzi(52, 105, now_wea, TFT_YELLOW);
showtext(80,105,1,2,fg,bg,temperature);
showtext(105,100,1,1,fg,bg,".\n");
showtext(112,108,1,1,fg,bg,"C\n");
}
/*文本显示*/
void showtext(int16_t x,int16_t y,uint8_t font,uint8_t s,uint16_t fg,uint16_t bg,const String str)
{
//设置文本显示坐标,和文本的字体,默认以左上角为参考点,
tft.setCursor(x, y, font);
// 设置文本颜色为白色,文本背景黑色
tft.setTextColor(fg,bg);
//设置文本大小,文本大小的范围是1-7的整数
tft.setTextSize(s);
// 设置显示的文字,注意这里有个换行符 \n 产生的效果
tft.println(str);
}
/*单一汉字显示*/
void showHanzi(int32_t x, int32_t y, const char c[3], uint32_t color) {
for (int k = 0; k < 20; k++)// 根据字库的字数调节循环的次数
if (hanzi[k].Index[0] == c[0] && hanzi[k].Index[1] == c[1] && hanzi[k].Index[2] == c[2])
{ tft.drawBitmap(x, y, hanzi[k].hz_Id, hanzi[k].hz_width, 16, color);
}
}
/*整句汉字显示*/
void showHanziS(int32_t x, int32_t y, const char str[], uint32_t color) { //显示整句汉字,字库比较简单,上下、左右输出是在函数内实现
int x0 = x;
for (int i = 0; i < strlen(str); i += 3) {
showHanzi(x0, y, str+i, color);
x0 += 17;
}
}
此次可穿戴Crystal Watch的设计,总结一下,我们学会了彩屏的使用方法,图片显示,文字显示,动画显示,NTP时间获取,天气获取等技能
不足之处是需要对Crystal Watch的续航时间进行测试,并且优化程序,让手表的功耗尽可能的降低,可以参考各种运动手环的工作方式,不看时息屏降低能耗
最后谈谈旺仔爸爸做完这个项目的感受,最大感觉就是起初低估了项目难度,虽然本次作品的体积看上去非常的小巧,但涉及到的内容一点都不少,做的越深入的时候发现需要解决的问题越多,最后不得不将预期的功能删减,事实告诉我们早期合理的评估项目是多么的重要,其实人生就是不断挖坑再填坑的过程,在这个过程中不断的增加了我们生命的厚度,希望每个人都能坚持,不妥协,让自己的生命更加丰富多彩
接下来旺仔爸爸还会使用小巧的彩屏和分光棱镜制作更加有趣的项目,一起期待
造物让生活更美好,我们下期再见!