在物联网不求人的上两期的教程当中,我们学会了搭建常见物联网服务器,并控制创客制造神器-3D打印机,知道了如何将ESP32CAM的视频流运用到到服务器当中。最近我看到一个设计巧妙的点阵悬浮时钟结构,觉得还不错,因此用这个结构结合原来的创意点阵时钟与物联网不求人中的服务器做一个整合教大家如何将任意DIY的物联网项目与物联网服务器做有机结合共同组成一个物联网系统,让我们真正做到万物互联相互协同,真正发挥物联网的优势。
下面让我们开始吧,先来看一下演示视频吧:
项目前准备
所有的"悬浮效果"大致分为两类,一类是“真悬浮”通过磁悬浮方式让其悬浮于空中,此类装置需要大量电能用于维持悬浮效果,相对功耗较高;另一类是“伪悬浮”通过巧妙的机械设计或结构达到悬浮效果让人以假乱真,伪悬浮效果常见的有两种形式,一种是使用透明或者半透明的镜面反射光源图像通过与环境的对比让人产生悬浮假象,通过反射的这种其成像与源图像为镜像关系,只有将源图像进行镜像处理才能正常显示。前段时间很火的悬浮小电视时钟原理便是如此,还有一种是将像素做小,周围留出大量空间让光通过,这样只有点亮的区域发光其他没有像素点的区域则透光,小米原来发布的透明电视或者透明OLED原理便是这样。另一种将时针与分针通过巧妙的设计隐藏方式达到悬浮效果例如shiura大神的Hollow Clock系列时钟。本教程中的悬浮时钟模型便来自shiura大神,通过透明亚克力板反射发光点阵的方式达到悬浮效果,这里感谢shiura大神设计的外壳模型。
网络自动校准时间
无网络连接时及时反馈
自定义精美时间显示字体
时间显示动画
时段提示
API亮度调节
API自定义位图显示
家庭自动化
M5 STAMP-PICO
杜邦线(使用下图所示杜邦线可以直插点阵比较方便,使用时将多余部分去除)
4 合 1 点阵模块(根据自己喜好选择不同发光颜色与形状)
3D打印时钟底座(根据自己喜好选择喜欢的耗材颜色)
透明亚克力板(1mm, 165 x 75mm)
USB线(有条件的用带开关的最佳,没条件的用废弃USB线DIY)
STAMP-PICO是M5基于ESP32的最小开发板系统,其主要特点如下
ESP32-PICO-D4(2.4GHz Wi-Fi)
支持UIFlow图形化编程
支持Arduino
多IO引出,支持多种应用形态(SMT,DIP,飞线)
集成可编程RGB LED与按键
迷你尺寸18 *2 4 * 4.6mm
电路连接关系如下:
VCC→5V
GND→GND
DIN→19
CLK→21
CS→22
USB线正极→5V
USB线负极→GND
将所有模块按照电路连接关系使用电烙铁进行焊接如下图所示。
将焊接好的所有模块装入底座并用热熔胶固定(注意STAMP-PICO模块方向如下图所示放置以便下载程序与调试,切记不要搞反)。
最后将点阵两侧如下图所示使用热熔胶固定防止脱落。
最后将亚克力板插入底座便可完成所有结构搭建。
下面开始详细讲解程序设计过程。
我们使用 Aduino 软件来编写本项目的程序,开发板选择 ESP32 类型。至于如何在 Arduino 中配置 ESP32 的开发环境,不在本文的介绍范围,请自行查阅相关资料。
由于STAMP-PICO为最小系统板,本身不带下载电路,因此需要使用USB-TTL烧录器进行程序下载接线方式如下图所示。(如果你用的下载器是STAMP-PICO配套的,那么下载并不需要焊接,只需要将对应的引脚插入,用手按住等待下载即可,其他下载器也是一样的方法。)
作为一个时钟,最重要的功能当然是显示时间啦。那么该如何从网络获取时间呢?
下面的例子演示了如何获取网络时间并将时间保存在变量中,其中 WiFi.h
库的功能是连接网络,NtpClientLib.h
库的功能是获取 NTP 服务器的网络时间,SimpleTimer.h
库是用来设置定时器每秒刷新一次时间。
#include <WiFi.h> #include <NtpClientLib.h> #include <TimeLib.h> #include <SimpleTimer.h> const char* ssid = "***********"; const char* password = "***********"; SimpleTimer timer; const PROGMEM char *ntpServer = "ntp1.aliyun.com"; int8_t timeZone = 8; volatile int hour_variable; volatile int minute_variable; volatile int second_variable; void Simple_timer() { hour_variable = NTP.getTimeHour24(); minute_variable = NTP.getTimeMinute(); second_variable = NTP.getTimeSecond(); Serial.println(hour_variable); Serial.println(minute_variable); Serial.println(second_variable); } void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("Local IP:"); Serial.print(WiFi.localIP()); NTP.setInterval(600); NTP.setNTPTimeout(1500); NTP.begin(ntpServer, timeZone, false); timer.setInterval(1000L, Simple_timer); } void loop() { timer.run(); }
MD_Parola
是 MAX7219 点阵屏的模块化滚动文本显示库,其主要特点如下:
支持点阵屏显示文本时左对齐、右对齐或居中对齐;
具有文字滚动,进入和退出效果;
能够控制显示参数和动画速度;
支持硬件 SPI 接口;
可以在点阵屏虚拟多个显示区域;
用户定义的字体和/或单个字符替换;
支持双高显示;
支持在混合显示文本和图形。
下面的例子简单演示了如何利用 MD_Parola 滚动显示字符串,其中 MD_Parola 对象有 4 个参数:分别为 SPI 管脚 DIN、CLK、CS 及点阵数目。下面我们所做的创意点阵时钟的显示功能均由此库开发。
#include <MD_Parola.h> #include <MD_MAX72xx.h> #include <SPI.h> MD_Parola P = MD_Parola(19, 21, 22,4); //DIN CLK CS MD_MAX72XX mx = MD_MAX72XX(19, 21, 22,4); //DIN CLK CS void setup() { mx.begin(); P.begin(); } void loop() { if (P.displayAnimate()) { P.displayScroll("Mixly", PA_LEFT, PA_SCROLL_LEFT, 50); } }
值得注意的是,原库自带的字体不美观,且通过亚克力板反射后显示的图像是镜像的,因此我们需要自定义一个“镜像字体”,通过显示不同图片的形式,来显示我们想要显示的内容,要在点阵屏中显示图片,首先需要设计点阵图案(位图),然后对图案进行取模操作。点阵取模使用 PCtoLCD2002 取模软件,取模设置如下:
取模方式为阴码、顺向、逐列式,输出方式为 16 进制,注意格式设置为 C51 格式,其余参数按照默认取模方式设置即可。
这里我们取模的数据格式为 uint8_t 数组,我们有自定义字体 0~9 和时间分隔符“:”,再加上一些自定义的图像,这就导致我们有大量的位图。为了方便的管理这些位图,我们使用指针数组 bitmap_data[]
去管理我们的位图。为了显示方便,我们定义了函数 display_bitmap()
,该函数需要 3 个参数,分别为显示横坐标 abscissa、位图宽度 width 及指针数组 bitmap_data[]
中的位置 bitmap_number。需要注意的是我们这里并没有指定位图的高度,因为我们用到的 MAX7219 点阵屏分辨率为 8×32,所以这里我们默认位图高度为 8。(横坐标0为起点,位图序号0为第一幅图像)
#include <MD_Parola.h> #include <MD_MAX72xx.h> #include <SPI.h> MD_Parola P = MD_Parola(19, 21, 22,4); //DIN CLK CS MD_MAX72XX mx = MD_MAX72XX(19, 21, 22,4); //DIN CLK CS uint8_t bitmap_data1[] = {0x3e, 0x2a, 0x3e}; uint8_t bitmap_data2[] = {0x2e, 0x2a, 0x3e}; uint8_t * bitmap_data[] = { bitmap_data1, bitmap_data2, …… }; void display_bitmap(int abscissa, int width, int bitmap_number) { mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF); mx.setBuffer(abscissa, width, bitmap_data[bitmap_number]); mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON); }
MD_Parola 库中,由于字体过大而且不美观,导致显示的时间过长,所以我们需要自定义字体。自定义字体如下图所示,值得注意的是 0~9 的位图宽度是 3,分割符“:”的宽度是 1。
自定义字体取模数据如下所示:
uint8_t Small_font_0[] = {0x7c, 0x44, 0x7c}; uint8_t Small_font_1[] = {0x24, 0x7c, 0x04}; uint8_t Small_font_2[] = {0x5c, 0x54, 0x74}; uint8_t Small_font_3[] = {0x54, 0x54, 0x7c}; uint8_t Small_font_4[] = {0x70, 0x10, 0x7c}; uint8_t Small_font_5[] = {0x74, 0x54, 0x5c}; uint8_t Small_font_6[] = {0x7c, 0x54, 0x5c}; uint8_t Small_font_7[] = {0x40, 0x40, 0x7c}; uint8_t Small_font_8[] = {0x7c, 0x54, 0x7c}; uint8_t Small_font_9[] = {0x74, 0x54, 0x7c}; uint8_t Small_font_10[] = {0x28};
下面我们分析如何显示时间,这里我们只显示小时和分钟。
这里我们有一个小技巧,我们可以把 0~9 的位图放到指针数组 bitmap_data[]
的 0~9 的位置上,时间分隔符“:”放置在数组序号 10 的位置上。由于前面我们定义了一个显示位图的函数 display_bitmap()
,这样我们不需要通过任何映射就可以显示数字了,例如 display_bitmap(22, 3, 0)
就显示 0;display_bitmap(22, 3, 1)
就显示 1,这样是不是很方便呢?
为了分别获取小时和分钟的十位及个位,我们需要对其进行除法和取余操作,例如对小时 9 除 10 得到十位 0(为什么不是0.9?这是因为我们时间变量定义为整数,一个整数除以另一个整数结果只能为整数。还是不懂?那你就该补一下C语言基础知识了。),9 除 10 取余得到个位 9。由分析我们在合适的位置显示时间得到了下面的时间显示函数。
最后,为了显示更加美观,如果小时或分钟只有一位数,我们就需要进行补零操作,将 1:1 补零变成 01:01。显示时间的代码如下:
display_bitmap(22, 3, hour_variable / 10); display_bitmap(18, 3, hour_variable % 10); display_bitmap(14, 1, 10); display_bitmap(12, 3, minute_variable / 10); display_bitmap(8, 3, minute_variable % 10);
时间在流逝,但是我们上面并没有显示秒钟,那我们怎样感知时间的进度呢?为了解决这个问题,我们定义了下面的一系列位图,注意这里定义位图的宽度是 5 不是 8,我们每隔一秒切换一次下面的位图,看起来是不是像秒针在走动呢?
使用取模软件分别对上述点阵图案取模:
uint8_t clock_0[] = {0x38, 0x44, 0x74, 0x44, 0x38}; uint8_t clock_1[] = {0x38, 0x44, 0x54, 0x64, 0x38}; uint8_t clock_2[] = {0x38, 0x44, 0x54, 0x54, 0x38}; uint8_t clock_3[] = {0x38, 0x44, 0x54, 0x4c, 0x38}; uint8_t clock_4[] = {0x38, 0x44, 0x5c, 0x44, 0x38}; uint8_t clock_5[] = {0x38, 0x4c, 0x54, 0x44, 0x38}; uint8_t clock_6[] = {0x38, 0x54, 0x54, 0x44, 0x38}; uint8_t clock_7[] = {0x38, 0x64, 0x54, 0x44, 0x38};
前面我们指针数组 bitmap_data[]
的 0~10 位置都用来放置数字了,我们这里有 8 幅位图,所以放入指针数组 bitmap_data[]
的 11~18 位置,我们定义一个静态局部变量Clock_variable
,设置其初始值为 11,每隔一秒 Clock_variable
变量的值增加 1,并显示对应序号的位图,当 Clock_variable
的值为 19 时,将它重新赋值为 11,这样我们就实现了秒表动画的设计。程序如下:
static int Clock_variable = 11; display_bitmap(4, 5, Clock_variable); Clock_variable = Clock_variable + 1; if (Clock_variable == 19) { Clock_variable = 11; }
上面我们设计了秒表动画,但是还有一个问题,由于点阵屏空间限制,我们没办法用数字显示精确的秒数,那怎么办呢?我们观察到,在点阵屏的底部还空了 2 个像素点的高度,所以我们可以在最后一行通过点数显示精确到秒数。
如上图所示,最后一行前面有 5 个点,后面有 9 个点,因此秒数为 59 秒。显示秒数的代码如下:
if (second_variable / 10) { mx.drawLine(0, 22, 0, (23 - second_variable / 10), true); } if (second_variable % 10) { mx.drawLine(0, 14, 0, (15 - second_variable % 10), true); }
其中 mx.drawLine()
为绘制线段的函数,它有 4个参数,分别为:线段起点横坐标、起点纵坐标、终点横坐标、终点纵坐标,以及显示状态(true 点亮线段;false 熄灭线段)。根据我们使用的 4 合 1 点阵坐标定义,其中横坐标最大为 7,纵坐标最大为 31,下图所示为点阵坐标分布图。
当秒数的个位为 0 的时候将线段清除,重复显示线段即可显示当前秒数了。这里我不对显示线段的位置、长度与秒数的关系进行分析,留给大家活跃一下大脑(此处可以思考为什么显示秒线段的纵坐标为什么是0而不是7)。
为了感知一天时间的变化,我们希望不同时间段用不同的图标进行提示。我们定义了太阳和月亮两个图标,它们的宽度都是 8,样式如下图所示。
使用取模软件取模数据如下:
uint8_t sun[] = {0x24, 0x00, 0xbd, 0x3c, 0x3c, 0xbd, 0x00, 0x24}; uint8_t moon[] = {0x1c, 0x3e, 0x47, 0x03, 0x23, 0x72, 0x24, 0x00};
继续将太阳和月亮的取模数据添加到指针数组 bitmap_data[]
的位置 19 和 20。这里我们定义在 6 点到 18 点之间,在横坐标为 31 处显示太阳,其他时间显示月亮,程序如下:
if ((hour_variable >= 6) && (hour_variable <= 18)) { display_bitmap(31, 8, 19); } else { display_bitmap(31, 8, 20); }
由于我们的时钟依赖网络获取时间进行校正,当网络没有连接时,显示的时间可能不正确,因此我们需要网络连接反馈,当没有联网时显示一个图标用来提示网络无连接,我们定义下图所示位图。该位图的宽度为 19,看上去像是 WiFi 被外星人劫持了,是不是很生动形象!
使用取模软件取模数据如下:
uint8_t wifi[] = {0x20, 0x60, 0xC8, 0xDB, 0xDB, 0xC8, 0x60, 0x20, 0x00, 0x00, 0x0E, 0x18, 0xBE, 0x6D, 0x3C, 0x6D, 0xBE, 0x18, 0x0E};
这里我们使用 !(WiFi.status() != WL_CONNECTED)
语句来判断网络连接是否断开。当 WiFi 连接成功时,!(WiFi.status() != WL_CONNECTED)
返回真,这时我们可以同步时间;当 WiFi 断开时,!(WiFi.status() != WL_CONNECTED)
返回假,我们在点阵屏上显示 WiFi 断开连接提示,然后根据实际情况重启开发板或者重新修改网络设置。代码如下:
if (!(WiFi.status() != WL_CONNECTED)) { hour_variable = NTP.getTimeHour24(); minute_variable = NTP.getTimeMinute(); second_variable = NTP.getTimeSecond(); } else { mx.clear(); display_bitmap(25, 19, 21); delay(2000); mx.clear(); }
第一次配网成功后,在程序正常工作的过程中可能由于网络波动或者是其他偶然原因导致时钟非正常断开网络从而无法校正时间,此时我们可以设置一个超时重启机制,相关代码原理如下:
int cnt = 0; while (WiFi.status() != WL_CONNECTED) { mx.clear(); display_bitmap(25, 19, 21); delay(500); mx.clear(); delay(500); Serial.print("."); if (cnt++ >= 60) {//一分钟无连接将重启 ESP.restart(); } }
为了时钟富有动态感,我们这里为时钟添加一个小狗的动画效果,该动画由两个宽度为 8 的动画帧构成,首先我们先使用取模软件绘制出这两帧图像,最后生成字模即可,如下图所示。
使用取模软件取模数据如下:
uint8_t PROGMEM dog[] = {0x30, 0x30, 0x7f, 0x0c, 0x0c, 0x0c, 0x1f, 0x00, 0x31, 0x32, 0x7f, 0x0c, 0x0d, 0x0e, 0x0f, 0x10};
下面的例子演示将点阵划分为两个区域,区域 0 和区域 1。我们将在区域0显示时间与时间动画,区域1显示时段图标与小狗动画。P.setZone()
函数将点阵划分为不同的显示区域,它有 3 个参数:分别为区域编号、起始点阵及终止点阵。P.begin()
指定区域数量,参数为空默认一个区域,这里我们有两个显示区域,故参数为 2,其中点阵编号与区域的对应关系如下图所示:
P.setSpriteData()
函数为精灵动画的初始化函数,该函数接受 7 个参数:分别为初始化区域、动画开始精灵数据、动画开始精灵宽度、动画开始精灵帧数、动画结束精灵数据、动画结束精灵宽度、动画结束精灵帧数。
P.displayAnimate()
函数有两个作用,分别为反馈显示状态和动画执行函数。当作为反馈状态时,动画显示完成返回 1,未完成返回 0。当作为动画执行函数时,通过不断调用该函数实现动画的流畅运行,因此程序需要不断的调用 P.displayAnimate()
函数。
P.getZoneStatus()
函数作用类似 P.displayAnimate()
函数,不同的是它仅返回区域的显示状态。
P.displayZoneText()
函数为字符串的动画显示函数,该函数接受 7 个参数,分别为:显示区域、显示字符串、对齐方式、动画速度、文本显示时间、动画进入效果、动画退出效果。下面的代码演示了如何在区域显示精灵动画。这里我们显示字符串为空、显示时间为0,显示字符串为空保证了我们仅有小狗动画没有文字,显示时间为 0 保证了小狗动画的连贯性。(这里显示文字也没用因为文字经过亚克力板反射后是镜像的)
void setup() { P.begin(2); mx.begin(); P.setZone(0, 0, 2); P.setZone(1, 3, 3); P.setSpriteData(1, dog, 8, 2, dog, 8, 2); } void loop() { P.displayAnimate(); if (P.getZoneStatus(1)) { P.displayZoneText(1, "", PA_CENTER, 100, 0, PA_SPRITE, PA_SPRITE); } }
当我们睡觉以后我们是不会看时间的,此时降低点阵显示的亮度有助于节能环保,因此我们需要根据时间段自动调节点阵显示的亮度。下面的代码在晚上 0~6 点亮度设置为 0,其他时间亮度设置为 1。P.setIntensity()
函数为区域亮度设置函数,其有两个参数,分别是:显示区域和亮度值,其中亮度值范围为 0~15。(注意当我们通过API请求方式修改亮度时,不需要事先定义不同时段显示不同亮度,只需要按需向时钟提交控制参数修改亮度就好了)
if ((hour_variable >= 0) && (hour_variable < 6)) { P.setIntensity(0, 0);//设置区域0亮度 P.setIntensity(1, 0);//设置区域1亮度 } else { P.setIntensity(0, 1); P.setIntensity(1, 1); }
这里我们希望可以通过对时钟的提交不同参数用来控制亮度或者是显示自定义的位图用来提示,以下代码用于获取提交的网络参数(所有提交的参数不管是名称还是值都为字符串,对参数名称与值进行处理可转换为其他有意义的数据名与数据类型)
#include <WiFi.h> #include <FS.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> const char* ssid = "***********"; const char* password = "***********"; AsyncWebServer server(80); void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } Serial.println(WiFi.localIP()); server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {//采用GET方法提交参数 int paramsNr = request->params(); Serial.println(paramsNr); for (int i = 0; i < paramsNr; i++) { AsyncWebParameter* p = request->getParam(i); Serial.print("Param name: "); Serial.println(p->name()); Serial.print("Param value: "); Serial.println(p->value()); Serial.println("------"); } request->send(200, "text/plain", "message received"); }); server.begin(); } void loop() { }
在这里我们只需要根据提交参数的名称以及值做判断并进行相应处理,便可以控制时钟显示自定义位图或者显示时间及控制亮度,注意这里应当有一个状态变量用来控制显示位图还是时间,当状态变量为真时显示时间,变量为假时显示自定义位图,显示位图必须提交状态变量且状态变量为假,当提交的状态变量为真时恢复时间显示,例如http://192.168.0.110?state=0&image=0x8B,0x8B,0x8B&luminance=0,这里的设备IP地址是192.168.0.110,state(显示状态)为0,luminance(亮度)为0,image(显示位图)数据为0x8B,0x8B,0x8B,提交方式为GET请求。
提交的位图是一个16进制的字符串如0x74, 0x54, 0x7c的形式,在上面我们已经介绍了如何显示位图,因此这里我们只需要定义一个位图显示的中间数组,当收到位图数据后将其拆分并逐项转为10进制既可,最后显示这个中间数组的数据便可得到自定义的图像,在这里我们可以进行全屏显示,即定义一个32位的数组,最后也是显示这个32位数组,当我们收到位图数据时先将位图数组全部赋值为0,这样我们便可以居左显示宽度范围为0-32的位图而无需特殊处理,这里由于篇幅不多做介绍,请自行查看附件的相关代码。
最后,按照上述功能之间的逻辑关系,将代码组合在一起即可,相关代码请查看附件。
这里你可以充分发挥自己的想象力创建自己的自动化流程,下面的Nodered案例将每天早上7点设置亮度为1,下午6点设置亮度为0,点击显示图标将显示断网图标,点击显示时间将恢复正常时间显示,该流程代码附件进行下载,使用Nodered导入流程将对应的IP地址修改为自己设备IP即可。
以上就是物联网不求人-悬浮点阵时钟的全部介绍,如果你想体验演示视频中的项目,那么你可以访问https://docs.m5stack.com/zh_CN/download根据你自己的系统下载M5Burner烧录工具进行安装,打开软件按照下面的步骤进行烧录体验。
下载M5Burner烧录软件
打开软件选择STAMP
下滑到底部选择悬浮点阵时钟plus下载并烧录固件
点击USER CUSTOM登陆或者注册账号
进入用户主页点击BurnerNVS跳出弹窗选择对应的串口并连接
输入网络信息
各数据输入完成确认并保存后单击复位按钮
通过串口监视器(波特率115200)或者路由器后台查看设备IP地址
浏览器访问设备IP地址通过网络参数体验时钟的自定义位图显示或者亮度
从本教程中我们了解了悬浮时钟的原理,以及如何将任意DIY物联网设备接入Nodered以实现自动化处理并与其他设备协同,在这里我们对时钟的控制并不依赖任何服务器,仅靠时钟自身开放API通过对设备IP提交控制参数达到控制的目的,有些时候我们编写一些功能较多的程序,会让我们的程序编写难度增大,但如果将所有功能进行拆分并开放其控制接口,最后统一管理,那么这就让大型项目的协同工作变得更加简单了,每个人都只需要负责自己的一小部分就可以了,希望本期教程能让你对服务器与单片机物联网项目之间的联系感悟更深,以及如何更好的编写物联网项目的控制接口从而实现真正的万物互联。我是默让我们下期再见。
相关代码下载链接https://www.aliyundrive.com/s/ff1Gw8TMRTU
交流qq群713809079