原合集地址:Arduino中文社区
CSDN备份,原合集地址最新↑
打开方式
本系列教程建议在电脑端打开更快
手机端切换文章请点击左下角专题目录
文章内切换章节请点击左下角文章大纲
资料下载、教程持续更新:
点灯开源项目分享群2:QQ 913319679
更新时间:2024年3月14日
前言
本文记录各类调试经验,用于解决调试期间的常见问题(多来自点灯大群)
建议下载Blinker-DHT11-继电器-纯小白入门超详细版全套教程资料.rar查看本文原图(更新进度比网页版慢)
点灯开源项目分享群2:QQ 913319679(此群只提供各类开源分享,不聊天,可向群主投稿或免费咨询,教程等任何问题都可加群主问,看见就回)
新手保姆级教程&本人全部合集Arduino中文社区
目录
可通过左侧目录跳转
一、关于点灯科技
-
1.官网文档提示
-
2.点灯科技概况
-
3.点灯APP网页版
-
4.开发环境搭建
二、常见问题
-
打开APP时刷新数据
-
不用上官网,查看本地示例程序、例程使用方法
-
获取5个免费设备额度
-
烧录(上传程序)失败
-
查看串口调试信息
-
购买专业版后如何查看专业版QQ群号
-
APP实时数据导致崩溃
-
用心跳包实现实时数据
-
点灯库外再加一层断网重启
-
变量类型转换
-
双机通信方法!串口与WiFiUDP等(持续迭代中,预计实现云端本地协议贯通)
-
APP定时怎么定?
-
APP地图不能用
-
APP界面配置
-
APP翻页
-
按钮Button组件万能处理方法
-
报错查看方法
-
免费版专业版商业版、独立设备专属设备区别
-
阿里云设备下线 & 免费额度降低与重新获取
-
连不上WiFi请注意
-
使用mqtt.fxV1.7.1等第三方 连接Blinker MQTT Broker 开放式 API
-
继电器注意事项
没有的问题可以问:
社区GPT-3.5助手 Arduino中文社区
点灯自建GPT套壳站,支持:
GPT-3.5
GPT-4
Gemini-pro
Gemini-vision
GLM
Aily Project- Connect your Device to LLM
一、关于点灯科技
1.官网文档提示
用点灯务必自学、通读官网文档,小白看不懂也硬看,不要怕浪费时间,慢慢就懂了,开发知识都是网状联系的,看得越多越好
开发文档内容解释
官方群301438087
提问前,必须仔细阅读官网开发文档和arduino中文社区的官方教程全文
提问时附带程序、串口调试信息(查看方法)、目标和现状
2.点灯科技概况
点灯科技(成都)有限责任公司
2018年成立 小微企业
法定代表人:陈吕洲
注册资本:30万元
地址:成都市武侯区武兴四路166号西部智谷D区6栋2单元1楼105号(新城管委会),
经营范围:计算机及软件的技术开发、技术咨询、技术服务...
参保人数: 2人
核心成员:2人
陈吕洲 执行董事兼总经理 现任点灯科技(成都)有限责任公司执行董事。
程绮文 监事
陈吕洲
成都人,89年生,高中学历,网名 奈何Col、Coloz
Arduino中文社区 创始人
《Arduino程序设计基础》作者
长期积极参与和推动国内开源硬件及相关社群的发展;硬件创业者,现从事软硬件产品设计与开发工作;
亦是硬件爱好者、机器人爱好者,早期从事机器人竞赛,现在闲来无事喜欢捣鼓硬件,能熟练制造各种烂尾项目。
2002年开始就读于 棠湖中学
2006年全国机器人竞赛第四名
2008年开始就读于 成都理工大学工程技术学院
2009年在校创办 武术社团
2011年3月开始创业
2012年2月创办 成都智能盒子科技有限公司
2012年3月建立 Arduino中文社区
2013年10月创办 成都墨之坊科技有限公司
2014年3月出版《Arduino程序设计基础》
2018年成立点灯科技
预计卒于2031年
个人开源仓库coloz (Lvzhou) · GitHub
以上摘自自我介绍关于我 | XX到此一游
点灯科技网址导航
官方入门教程Arduino中文社区或Arduino中文社区
官网控制台(仅电脑端)点灯管理台
官方开源代码库blinker · GitHub
官方公众号 点灯物联
Arduino社区网址导航(由点灯科技运维)
社区GPT助手 Aily Project- Connect your Device to LLM
社区人工与GPT问答 Arduino中文社区
社区旧版https://arduino.cn(已出售,需chrome打开,部分转移到了新的arduino.me上)
简版的蓝牙物联网应用,不需要服务器,比blinker简单很多,用于中小学教学:Arduino中文社区
3.网页版点灯APP网址
警告:近期正在测试新版APP,旧版APP请做好界面配置备份后,再登陆网页版,新版拖拽编辑器的配置将不支持旧版识别!
如需回到旧版,则在手机APP点击右上角…按钮进入界面配置清空或解绑设备
按F12切换手机模式
不然比例不对
4.开发环境搭建
详见
【新手必看】[Blinker]2023-最新开发环境+温湿度节点+继电器 详细开源教程
二、常见问题
1.心跳包原理
心跳包官方说明Arduino中文社区
1. blinker库自带的mqtt库会在网络协议层维持与服务器的连接;
而心跳包是blinker协议层,是app判定设备"在线"用的,blinker库有默认心跳包机制。
所以即使改掉库代码,不发心跳包,其他按钮之类组件也可以通信,只是app显示"离线"二字。
2. 心跳包是先由app请求,也就是每30秒发送{"get":"state"}请求;
然后blinker库代码才会使设备回复默认心跳包{"state":"online"};
所以app不打开的时候不会发送。
3. 如果开发者注册"用户心跳包"heartbeat()函数,则库代码会把“用户内容”与{"state":"online"}一起发到app
“用户内容”就是:"用户心跳包"所有print方法执行后产生的数据
例如:用户心跳包函数里写刷新按钮状态Button1.print("on"),
库代码就会发送{"state":"online","btn1":{"swi":"on"}}
app调试组件也会出现该条数据
4.综上通过心跳包我们就能实现
打开app后设备状态刷新,之后每30秒app再次请求心跳包来刷新数据
(30秒刷新不够用可以在心跳包设置标志位,这样app请求心跳包后一段时间,每隔5秒以上刷新一次数据)
示例放大看:
心跳包实现实时数据刷新见《心跳包替代实时数据方法》(第8节)
2.1Blinker库自带示例程序
注:此方法打开的所有例程均为Blinker库example文件夹下的本地文件,随时调用,比官网跳转GitHub更快
Arduino IDE>工具栏>文件>示例>鼠标滚轮往下翻>第三方库示例>Blinker>Blinker_widgets组件示例
如下图:
所有函数具体用法解释均在“点灯科技官网》开发文档》设备端开发与SDK》Arduino支持”一文中有说明,务必通读点灯科技-点灯物联网解决方案
2.2例程融合进自己程序的步骤
1.打开
2.剪切粘贴
最前面放定义和全局变量、组件对象实例化
中间复制回调函数或dataread内的语句
setup和loop的也一样复制过去
注意引入多个例程时应避免重复语句
3.多数只有上传功能print的组件(文本text、number)应配合heartbeat心跳包回调函数,并在setup里attachheartbeat(不可直接在loop里print,以防频繁发送被拦截)
上传下载都有的组件(滑块slider、按钮button)最好也在心跳包print状态,开启APP时就能同步状态
(上传print、下载指令用回调函数,setup里用attach注册后,遇到APP发来数据包含其键名,blinker.run就调用其回调函数,并将下发参数填入回调函数的参数表)
https://www.diandeng.tech/doc/arduino-support#app%E7%BB%84%E4%BB%B6
4.关于类库实例化语句的说明(类库便于复用,不要用户反复写底层函数了就,blinker整个就是一个类库)
5.改好后记得将授权码WiFi名密码写入
6.APP界面数值组件设置
2.3图表组件例程使用
1.打开
2.插入datastorage
并将random随机数改为自己要上传图表的全局变量
不用millis限制的话,一直发,默认2分钟一次
3.APP界面设置(温湿度或电流电压为例)
3.设备额度上限
(1) 先确保已经申请过一个免费的
(2)免费增加5个额度:电脑端点灯官网,点击“登录”按钮,登录后进入“管理台”首页,点击“关联账户”的“微信”图标旁边的“GitHub”图标,进入GitHub,注册,输入邮箱,邮箱验证码,并绑定github
GitHub登录不流畅可更换网络,开启VPN或使用Chrome浏览器或反复重试
(3)6个免费额度还不够再前往官网服务购买,购买设备额度,
普通用户只能使用独立设备,
专业版用户可使用专属设备(带统一管理功能,具体看官网开发文档)
(4)2023年6月起,5个直接免费额度降低为1个,之前已经申请的可以继续超额使用,但无法再次添加(需购买或绑定GitHub补齐额度);按上方第2条可再获取5个,这样即能正常添加
(5)购买额度、专业版后如需退款联系群主823569290,以上总结自免费政策官方通知:Arduino中文社区
4.烧录失败
1.解决方法
这个经典报错Timed out waiting for packet header就是烧录超时,arduino没有收到握手包无法开始烧录:
可能原因1:IO0没接低电平、GPIO2没接高电平、GPIO15没接低电平
解决方法:恢复悬空,或接对应电平0和15接低、2接高,然后Arduino点击上传
a.ESP-01S模组等:
使用专用烧录器。
b.自带烧录器的开发板(如Node MCU):
拔掉所有外部电路,只接USB线到电脑。
c.无板载烧录器、无专用烧录器、板载烧录器损坏等情况:
1.将GPIO0接地后插USB上电;
2.长按RST和Boot,先松开RST(开机)再松开Boot。
(有的板子Boot按钮标成IO0,一个意思;Boot用来给IO0引脚接地,RST用来给RST引脚接地)
可能原因2:外部电路
解决方法:拔掉所有其他电路
可能原因3:CH340系列串口驱动没装好或者型号不对或者串口没选对
解决方法:重装驱动CH340SER.EXE(Arduino安装时会自动附带,莫名其妙没了的用CH340SER.EXE重装、重启)
下载地址:Arduino中文社区
CH340驱动独立链接
http://download.openjumper.cn/CH340-drivers%281%29.zip
CP2102等可以自行百度
可能原因4:烧录器没接好
解决方法:TX对RX,RX对TX交叉接,你收我发,你发我收(板载烧录器的不管这条)
可能原因5:电源接反过,或者3.3V电源接到5V导致模块烧毁
终极方法:涂flash(下载地址:工具 | 乐鑫科技)
设置一下速率为低速,选好串口,波特率最低,点击erase
可能原因6:电源接反过,或者3.3V电源接到5V导致模块烧毁
解决方法:重买
2.烧录原理
51单片机烧录是电脑上点击下载后接通电源,单片机上电引导(boot)时会向电脑发一次握手包(包含单片机信息的一组串口消息,所以烧录器Tx、Rx都要接),电脑收到握手包就会开始烧录,握手包只发一次
同样的,esp系列32位SOC上电也要引导:GPIO0输入高电平就引导至flash中的旧程序,输入低电平就引导至烧录模式,串口发一次握手包,电脑收到后开始下载新程序。,握手包只发一次
烧录模式(flash mode) 是一种USB转TTL的串口,经过ESP芯片的SPI转换器,直通flash闪存芯片的SPI总线的模式,所以会在Arduino以及ESP官方烧录程序中找到SPI总线速度的相关设置
烧录模式不执行程序 ,所以不受用户程序卡死的影响,并且没有烧录软件回应的时候会一直向串口发送如图握手包以方便烧录程序判定单片机的有无、波特率、型号、闪存大小
电脑通过TTL转USB收到握手包, 烧录即将开始前,CH340驱动程序会通过USB向烧录器的CH430芯片发送指令使其拉低TRS引脚和DTR引脚,来使单片机RST、boot引脚(ESP芯片的RST、GPIO0)拉低,进入烧录模式。
NodeMCU等板载烧录器、USB口的开发板: 烧录程序时您无需对GPIO0引脚进行操作(例如按Boot以拉低),因为NodeMCU的内置电路(CH340USB转TTL芯片,的DTR、RTS引脚)可以确保GPIO0引脚在工作时连接高电平,而在上传程序时连接低电平。
建议学习参考文章:http://t.csdnimg.cn/aMwa1
5.调试信息查看方法
1. 首先确认程序中有执行
Serial.begin(115200);//硬件串口0初始化,波特率115200
BLINKER_DEBUG.stream(Serial);//开启串口调试流到硬件串口0
BLINKER_DEBUG.debugAll();//打开所有调试信息
2.将开发板用USB- TTL连接至电脑(自带烧录器的直接用USB线连接)
Arduino工具一栏中选择好COM口,
再在工具栏中打开Arduino串口监视器。
3.根据如图所示的程序将串口波特率调整为115200,或9600等数值(setup里设定的多少就是多少),左下角取消自动滚屏,按下rst重启开发版,
4.获得的结果根据后面两张图片进行分析来判断设备卡在哪一步
6.专业版群号
购买后在控制台查看
7.实时数据导致崩溃
旧版点灯库的设备,在APP开启数值组件的实时数据选项时,必须配合设备端rtdata函数不然设备会崩溃,目前新版库已修复:
设备端就会崩溃
解决方法:使用最新版点灯库点灯科技-点灯物联网解决方案下载ArduinoSDK
或取消手机端的实时数据 勾选框。
正确实现实时数据请看下一节↓
8.心跳包替代实时数据方法
官方实时数据要配合rtdata回调函数走HTTP每秒一次上传,APP数据组件开启实时数据,查看官方教程:Arduino中文社区
用MQTT心跳包代替,间隔0-30秒可调↓↓
1自动刷新
1.设变量:APP上次请求时间、心跳包频率限制时间
2.写blinker心跳回调,里面刷新APP上次请求时间
3.loop里根据刷新时间执行两分钟心跳包内容(APP关闭后两分钟自动停止)
4.心跳包内容函数里加一个限时2-10秒
示例,放大看:
getstate是app自动的,改不了间隔,所以自己加这个机制刷新
自动刷新+掉线重连代码(setup和loop的内容分别插入自己程序并调整顺序)
uint32_t hebt_time = 0;//心跳包持续强制发送变量
uint32_t hebt_time_limit = 0;//心跳包发送周期限制,时间戳变量
void heartbeat()//用户心跳包回调函数(APP定时向设备发送心跳包请求{"get":"state"},设备默认返回{"state":"online"},若用户有自定义状态需要在收到心跳包时返回, 可声明并在setup()中注册此回调函数)
{
hebt_time=millis();//APP请求一次心跳后,两分钟内持续发送的标志(赋值当前时间戳)
}
void my_heartbeat()//用户心跳包内容函数(心跳包实际内容,不直接放在回调是因为这样可以在回调执行之后2分钟或更久时间内不停返回心跳包内容,实现数据实时刷新,防止混淆心跳时间戳和心跳频率限制时间戳)
{
if(millis()-hebt_time_limit>10000)//当前减去上次大于10秒才能发,用于计时,最快5秒一次心跳(快于1秒一次会被拦截,串口显示MSESSAGE LIMIT)
{
/*这里放自己的心跳包内容,例如Num1.print之类*/
hebt_time_limit=millis(); //hebt_time_limit用于计时,最快5秒一次心跳
}
}
uint32_t offline_millis = 0;//掉线时间戳
bool offline_flag=false;//掉线触发标志
void setup() //主函数初始化部分(开机引导后第一个执行此函数)
{
Serial.begin(115200); //初始化硬件串口UART0波特率115200bps(调试时需要相应的调整Aruino>工具>串口监视器>波特率为115200)
BLINKER_DEBUG.stream(Serial); //将Blinker库代码调试信息流打印到硬件串口
BLINKER_DEBUG.debugAll(); //开启所有调试信息
Blinker.begin(auth, ssid, pswd); //调用Blinker库的开始成员函数,初始化Wifi连接(参数:授权码、WiFi名、密码)
Blinker.attachHeartbeat(heartbeat); //注册用户心跳包回调函数,Blinker库代码每30秒发送心跳包时会顺带执行此函数(此函数不用返回任何值)
/*这里放自己的其它setup内容*/
}
void loop() //主函数循环执行部分(初始化后循环执行此函数直至断电)
{
Blinker.run(); //运行一下Blinker库代码(库代码确认没有消息收发需求时将自动弹出,执行下方用户代码)(刚开机会先连HTTP服务器鉴权然后用鉴权信息登录MQTT服务器后才能与APP通信)
if(Blinker.connect()) //调用一下Blinker库的连接检测函数,若检测到MQTT服务器连接成功则执行以下用户代码
{
offline_millis=0;offline_flag=false;//在线,所以清空掉线状态
//用户代码放这里:*****************************************************************************
if(millis()-hebt_time<120000)my_heartbeat();//当前减去APP上次请求心跳小于两分钟强制连续发送心跳包
//******************************************************************************************
}
else if(!Blinker.connected()&&millis()>180000)//开机三分钟后,点灯库一旦连不上,就判掉线,触发一次
{
offline_flag=true;//Blinker.connected()触发标志
}
if(millis()>180000&&!offline_millis&&offline_flag)//开机后三分钟后,若触发,则记录第一次掉线时间,之后!offline_millis为假,不再触发
{
offline_flag=false;//Blinker.connected()触发取消
offline_millis=millis();//记录掉线时间戳后!offline_millis为假,不再触发
}
if(millis()>180000&&offline_millis&&millis()-offline_millis>=180000)//开机三分钟后,掉线时间戳不为0,出现三分钟以上断连就重启
{
BLINKER_LOG_ALL(BLINKER_F("******************************************Reatart***************************************"));//串口打印重启消息
ESP.reset();//ESP 硬件重启
}
}
2手动刷新
如图设置一个按钮,和调试框上的等效
9.实时数据+断线重连方法
自动刷新+掉线重连代码(setup和loop的内容分别插入自己程序并调整顺序)
uint32_t hebt_time = 0;//心跳包持续强制发送变量
uint32_t hebt_time_limit = 0;//心跳包发送周期限制,时间戳变量
void heartbeat()//用户心跳包回调函数(APP定时向设备发送心跳包请求{"get":"state"},设备默认返回{"state":"online"},若用户有自定义状态需要在收到心跳包时返回, 可声明并在setup()中注册此回调函数)
{
hebt_time=millis();//APP请求一次心跳后,两分钟内持续发送的标志(赋值当前时间戳)
}
void my_heartbeat()//用户心跳包内容函数(心跳包实际内容,不直接放在回调是因为这样可以在回调执行之后2分钟或更久时间内不停返回心跳包内容,实现数据实时刷新,防止混淆心跳时间戳和心跳频率限制时间戳)
{
if(millis()-hebt_time_limit>10000)//当前减去上次大于10秒才能发,用于计时,最快5秒一次心跳(快于1秒一次会被拦截,串口显示MSESSAGE LIMIT)
{
/*这里放自己的心跳包内容,例如Num1.print之类*/
hebt_time_limit=millis(); //hebt_time_limit用于计时,最快5秒一次心跳
}
}
uint32_t offline_millis = 0;//掉线时间戳
bool offline_flag=false;//掉线触发标志
void setup() //主函数初始化部分(开机引导后第一个执行此函数)
{
Serial.begin(115200); //初始化硬件串口UART0波特率115200bps(调试时需要相应的调整Aruino>工具>串口监视器>波特率为115200)
BLINKER_DEBUG.stream(Serial); //将Blinker库代码调试信息流打印到硬件串口
BLINKER_DEBUG.debugAll(); //开启所有调试信息
Blinker.begin(auth, ssid, pswd); //调用Blinker库的开始成员函数,初始化Wifi连接(参数:授权码、WiFi名、密码)
Blinker.attachHeartbeat(heartbeat); //注册用户心跳包回调函数,Blinker库代码每30秒发送心跳包时会顺带执行此函数(此函数不用返回任何值)
/*这里放自己的其它setup内容*/
}
void loop() //主函数循环执行部分(初始化后循环执行此函数直至断电)
{
Blinker.run(); //运行一下Blinker库代码(库代码确认没有消息收发需求时将自动弹出,执行下方用户代码)(刚开机会先连HTTP服务器鉴权然后用鉴权信息登录MQTT服务器后才能与APP通信)
if(Blinker.connect()) //调用一下Blinker库的连接检测函数,若检测到MQTT服务器连接成功则执行以下用户代码
{
offline_millis=0;offline_flag=false;//在线,所以清空掉线状态
//用户代码放这里:*****************************************************************************
if(millis()-hebt_time<120000)my_heartbeat();//当前减去APP上次请求心跳小于两分钟强制连续发送心跳包
//******************************************************************************************
}
else if(!Blinker.connected()&&millis()>180000)//开机三分钟后,点灯库一旦连不上,就判掉线,触发一次
{
offline_flag=true;//Blinker.connected()触发标志
}
if(millis()>180000&&!offline_millis&&offline_flag)//开机后三分钟后,若触发,则记录第一次掉线时间,之后!offline_millis为假,不再触发
{
offline_flag=false;//Blinker.connected()触发取消
offline_millis=millis();//记录掉线时间戳后!offline_millis为假,不再触发
}
if(millis()>180000&&offline_millis&&millis()-offline_millis>=180000)//开机三分钟后,掉线时间戳不为0,出现三分钟以上断连就重启
{
BLINKER_LOG_ALL(BLINKER_F("******************************************Reatart***************************************"));//串口打印重启消息
ESP.reset();//ESP8266 硬件重启
//ESP.restart();//ESP32 freertos重启
}
}
10.常用变量转换
“多种变量”代表此函数可以输入各种数据类型
老做法:
自动转String
String(多种变量);
to_string(多种变量);
多种变量.toString();
自动转int
atoi(多种变量);
多种变量.toInt();
自动转long int
atol(多种变量);
多种变量.toLong();
自动转float
atof(多种变量);
多种变量.toFloat();
int转ASCII字符
toascii(整形变量);
float转换为字符串,四舍五入
gcvt(浮点变量);
string转long int
strtol(字符串变量);
string转float
strtod(字符串变量);
string转unsigned long int
strtoul(字符串变量);
string转char数组指针(常用于要求输入char*的函数)
const char* c =字符串变量名.c_str();
或
char* c=new char[20];
strcpy(c,s.c_str());
取String第一个字指针
string str = "Hello, world!";
const char* ptr = str.data();
新做法——as
关键字快捷变量转换:
as
关键字并不是C++语言的一部分,而是特定库中定义的一个功能,用于类型转换。
as<T>()
方法允许你将一个数据类型转换为另一个数据类型,尖括号<T>
内的T
代表目标类型,即你希望将数据转换成的类型。这是一种泛型编程的用法。
这种转换通常是安全的,因为它会检查数据是否可以被转换为目标类型,如果不可以,通常会返回目标类型的一个默认值。
在Arduino环境中,这通常见于处理JSON的库,如
ArduinoJson库、
包含ArduinoJson库的Blinker库等
int aa=0;
uint8_t bb=0;
uint16_t cc=0;
uint32_t dd=0;
uint64_t ee=0;
float ff = 0.0; // 用于存储浮点数转换结果
double gg = 0.0; // 用于存储双精度浮点数转换结果
bool hh = false; // 用于存储布尔转换结果
JsonObject ii; // 假设这是一个有效的JsonObject类型,用于存储JsonObject转换结果
void dataRead(const String & data)
{
BLINKER_LOG("Blinker readString: ", data);
aa = data.as<int>(); // 将字符串转换为int
bb = data.as<uint8_t>(); // 将字符串转换为uint8_t
cc = data.as<uint16_t>(); // 将字符串转换为uint16_t
dd = data.as<uint32_t>(); // 将字符串转换为uint32_t
ee = data.as<uint64_t>(); // 将字符串转换为uint64_t
ff = data.as<float>(); // 将字符串转换为浮点数
gg = data.as<double>(); // 将字符串转换为双精度浮点数
hh = data.as<bool>(); // 将字符串转换为布尔值
// 对于JsonObject的转换,你需要根据你使用的库的文档来进行,这里只是一个示例
ii = data.as<JsonObject>(); // 假设这样可以将字符串转换为JsonObject
}
//_t在uint8_t中表示这是一个类型定义(Type Definition),确保了变量的位大小和符号性在所有平台上的一致性。
//as关键字或方法不仅限于处理String类型,它还可以用于处理多种不同的类型,具体取决于它在哪个库中定义以及如何实现。
//在Arduino环境中,特别是在使用类似ArduinoJson这样的库时,as方法常用于将JSON值转换为多种C++基本数据类型,如int、float、bool等,以及库特定的复合类型。
扩展阅读——基本数据类型:
C++提供了一系列的基本数据类型,用于处理不同种类的数据。除了上面已经讨论过的整数类型和浮点数类型,还有以下几种常见的数据类型:
-
字符类型 (
char
): 用于存储单个字符,如'A'
或'3'
。char
类型通常占用1字节的内存空间。 -
双字符类型 (
wchar_t
): 用于存储宽字符或Unicode字符,比char
类型占用更多的内存空间,通常是2或4字节。 -
布尔类型 (
bool
): 用于存储真 (true
) 或假 (false
) 值。 -
双精度浮点类型 (
double
): 用于存储双精度浮点数,比float
类型有更大的范围和精度。通常占用8字节的内存空间。 -
长双精度浮点类型 (
long double
): 提供比double
更大的范围和精度的浮点数。其大小和精度依赖于编译器和平台,通常大于double
。 -
无符号字符类型 (
unsigned char
): 类似于char
,但只用于存储非负数,因此其范围是0到255。 -
有符号字符类型 (
signed char
): 明确表示字符是有符号的,其范围通常是-128到127。 -
整型 (
int
): 用于存储整数。int
类型的大小至少为16位,但在现代计算机上通常是32位。 -
短整型 (
short int
或short
): 用于存储较小的整数,至少16位。 -
长整型 (
long int
或long
): 用于存储较大的整数,至少32位,有些平台上为64位。 -
长长整型 (
long long int
或long long
): 用于存储非常大的整数,至少64位。 -
无符号整型 (
unsigned int
): 类似于int
,但只用于存储非负数。 -
无符号短整型 (
unsigned short int
或unsigned short
): 类似于short int
,但只用于存储非负数。 -
无符号长整型 (
unsigned long int
或unsigned long
): 类似于long int
,但只用于存储非负数。 -
无符号长长整型 (
unsigned long long int
或unsigned long long
): 类似于long long int
,但只用于存储非负数。
这些类型提供了在C++程序中处理各种数据的基础。选择合适的数据类型对于优化内存使用和提高程序性能至关重要。
扩展阅读——泛型编程:
在as<T>()
中,尖括号<T>
内的T
代表目标类型,即你希望将数据转换成的类型。这是一种泛型编程的用法,允许你在调用方法时指定一个或多个类型参数。
泛型编程是一种在编程语言中定义算法时允许其操作的数据类型被指定为一个参数的概念。这意味着你可以编写一个与数据类型无关的代码,然后在需要的时候指定具体的类型。
泛型的主要好处是代码复用:你可以用相同的代码逻辑处理不同的数据类型。
在C++中,泛型主要通过模板实现。模板允许你编写代码来处理任意类型的数据。
你可以定义函数模板或类模板,编译器会根据你使用的具体类型自动生成相应的函数或类。
函数模板示例
template <typename T>
T add(T a, T b) {
return a + b;
}
在这个例子中,typename T
是一个模板参数,代表一个待指定的类型。当你调用add
函数时,可以用任何类型的参数,编译器会为那个特定的类型生成一个add
函数的实例。
类模板示例
template <class T>
class Box {
public:
T content;
void setContent(T newContent) {
content = newContent;
}
T getContent() {
return content;
}
};
在这个例子中,Box
类可以用来存储任何类型的content
。class T
是一个模板参数,表示Box
类可以操作的数据类型。
泛型编程使得代码更加灵活和可重用,但也需要仔细设计接口和考虑类型安全的问题。在使用泛型时,编译器会进行类型检查,确保代码对于任何指定的类型都是安全的。
11.双机通信专题
原理:使用点灯云Blinker.bridge转发两设备间数据时,两设备都要上云,占用设备额度,浪费UI面积
使用本地通信将多个设备数据汇总上传则可以节约设备额度,提高UI信息密度,增强用户感知,提升使用体验,降低服务器端性能开支。
3种方法:
1.UART串口 版用于和8051或STM32等单片机板上通信,实现多个单片机合并上云、8266IO扩展。
A.Serial.print转ASCII码,可读性强,效率低,常用于打印日志log
B.Serial.write直接二进制,可读性差,效率高,常用于数据同步
2.(1)WiFi UDP 极简版 用于测试UDP广播是否能够发送一个int整数型数据。
2.(2)WiFi UDP 协议版 用于WiFi节点间广播通信,实现本地互联,汇聚上云。用户可根据需要自由修改实现一主多从、呼叫应答等控制逻辑。
3.MESH组网(设备管理+人员管理+状态空间+实时同步=智能家居互联动态管理协议)
1.串口通信
串口一般有两种发送方式:
A.将二进制数据转为ASCII码发送:效率低,但是可读性强,可用串口监视器直接看见数据,常用于打印日志log。
如:Serial.print("A");就会发ASCII码十进制65,二进制01000001,十六进制0x41
B.直接发二进制数据帧(一般拼成一个数组):效率高,但是可读性差,串口监视器只能显示二进制字节的十六进制形式,常用于数据同步。
如:单独发一个数据Serial.write(0XCD);就会发二进制11001101,十六进制0xCD
如:循环发送数组for(i=0;i<5;i++)Serial.write(data[i]);就会发数组中5个字节的二进制值
A.串口——基于ASCII字符
硬件UART0用Serial;
硬件UART1改Serial1;
软件串口可以在任意端口,用SSerial定义(自己找教程看下我就用硬件串口0展示了)
(1)全局变量定义:
String dat;
char datachar;
(2)串口初始化:写在setup,可以只要第一句Serial.begin,后面两句会开启Blinker库的调试信息,会影响对方接收,一般调试完Blinker就可关闭,专心调串口
Serial.begin(115200);//硬件串口0初始化,波特率115200,8266可降74880查看开机时bootloader打印的内容,esp32建议115200查看开机时bootloader打印的内容
//BLINKER_DEBUG.stream(Serial);//开启串口调试流到硬件串口0
//BLINKER_DEBUG.debugAll();//打开所有调试信息
(3)发送字符串:写发送机loop函数里
Serial.println(dat);//带入字符串变量
Serial.println("一个固定字符串");//直接代入字符串
Serial.println(String(num));//整数转字符串代入
Serial.print("一个固定字符串");//print不带ln不换行,少一个换行符
(4)接收字符,拼成字符串:写接收机loop函数里
(发送是一个一个字节来的,一个字节8位,代表一个ASCII字符)
void loop() //主函数循环执行部分(初始化后循环执行此函数直至断电)
{
Blinker.run(); // 联网处理一次
if(Blinker.connect()) // 检测连接状态,若连着
{
if(Serial.available())//串口有数据(Arduino串口监视器发给8266)
{
dat="";//清空字符串
while(Serial.available()>0)//当串口有字符
{
datachar=(char)Serial.read(); //读字符
dat+=datachar; //字符加入字符串
}
//Serial.println(dat);//串口回复(Arduino串口监视器可见)
Serial.println("uploaded");//串口回复:已上传(Arduino串口监视器可见)
Blinker.print("8266",dat);//将数据上传到APP调试框
}
}
else // 否则若断开
{Blinker.run();Serial.println("unlinked");} // 再连网
}
B.串口——基于二进制数据帧
Arduino用Serial.write将直接发数据的二进制值,不会像Serial.print那样被转换成ASCII字符的二进制值,相对更高效,不容易转换出错
思路标注:
8266发STM32(可以放loop或单独封装,点灯组件来数据时Change_Flag =true;则触发此函数)
if(Change_Flag == true)//APP 刷新数据,则发给STM32
{
Change_Flag = false;
Serial.write(0xCD);//0
Serial.write(0xDD);//1
Serial.write(gq_Value>>8);//2
Serial.write(gq_Value);//3
Serial.write(yw_Value);//4
Serial.write(sd_Value);//5
Serial.write(Mode_Flag);//6
Serial.write(DJ_Flag); //7
Serial.write(0xDC);//8
Serial.write(0xCC);//9
}
8266收STM32(理论上serialEvent()会被系统调用,没有的话就放loop循环调用就是了)
/* 串口接收中断 */
void serialEvent()
{
Serial_DataCount = Serial.available(); //获取串口接收缓冲区可读字节数
if(Serial_DataCount >= 10){
Serial.read(Serial_Readbuffer,10); //读取一个数据并且将它从缓存区删除
if( Serial_Readbuffer[0] == 0xAB && Serial_Readbuffer[8] == 0xBA)
{
Light_Lux_M = Serial_Readbuffer[2] <<8;
Light_Lux = Light_Lux_M + Serial_Readbuffer[3];
Smokescope = Serial_Readbuffer[4] ;
Temper = Serial_Readbuffer[5] ;
Humi = Serial_Readbuffer[6] ;
while(Serial.read()>= 0){} //清空串口接收缓存
Serial_Readbuffer[10]={0};
Serial_DataCount = 0;
Serial_ReadFlag = true;
}
else{
while(Serial.read()>= 0){} //清空串口接收缓存
Serial_DataCount = 0;
Serial_Readbuffer[10]={0};
}
}
}
STM32发8266 与 收8266
#include "COMM_APP.h"
#include "usart.h"
extern int Light ; //光照度
extern int MQ_Data ; //MQ_2的烟雾浓度数据
extern int fT_value , fH_value ; //计算真正的温湿度数据
u8 i = 0;
u8 UART1_Rx_Buff[10] = {0};
int gq_Value = 1000;
int yw_Value = 30;
int sd_Value = 90;
int Mode_Flag = 0;
int DJ_Flag = 0;
/*
数据帧定义
帧头:0xAB 0xBB
光照度数据:0x_H 0x_L
烟雾浓度:0x__
温度数据:0x__
湿度数据:0x__
舵机开关状态:0x__
帧尾:0xBA 0xAA
*/
unsigned char Send_Data[10] = {0};
void COMM_task(void *pvParameters)
{
/* 帧头 */
Send_Data[0] = 0xAB;
Send_Data[1] = 0xBB;
/* 帧尾 */
Send_Data[8] = 0xBA;
Send_Data[9] = 0xAA;
while(1)
{
Send_Data[2] = (Light>>8) & 0xFF; //Light高8位
Send_Data[3] = Light & 0xFF; //Light低8位
Send_Data[4] = MQ_Data & 0xFF;
Send_Data[5] = fT_value & 0xFF;
Send_Data[6] = fH_value & 0xFF;
Send_Data[7] = 0x00;
for(int i=0; i<10; i++)
{
UART1_Send_Byte(Send_Data[i]);
}
vTaskDelay(3000);
}
}
/* 串口接收中断函数 */
void USART1_IRQHandler(void) //串口1中断服务程序
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断
{
UART1_Rx_Buff[i] =USART_ReceiveData(USART1); //读取接收到的数据
i++;
if(i >= 10)
{
if(UART1_Rx_Buff[8] == 0xDC)
{
gq_Value = (UART1_Rx_Buff[2]<<8)+ UART1_Rx_Buff[3];
yw_Value = UART1_Rx_Buff[4];
sd_Value = UART1_Rx_Buff[5];
Mode_Flag = (UART1_Rx_Buff[6]>0)?1:0;
DJ_Flag = UART1_Rx_Buff[7];
}
i = 0;
}
}
}
2.(1)WiFi UDP 双机通信 极简版
思路标注**(仔细看)**:
程序有详细注释,可直接烧入两台8266测试,一台发,一台收,Arduino串口监视器查看效果。
烧录前请将程序中ssid、password替换为自己的WiFi名称、密码
插入到自己点灯代码进行测试时注意setup里的WiFi.begin以及下面的while不要,只要UDP.begin,以防和Blinker.begin冲突!(下一节可见此情况)
极简版本可在WiFi机的本地局域网发送一个整形数字,并接收打印在串口(WiFi机包括手机热点、电脑热点、路由器、未设置内网隔离的光猫)
如需发多个数据请看下一节协议版。
发送端
#include <ESP8266WiFi.h>//预处理命令:包含WiFi类以使用WiFi实例begin、status等(库内已实例化)
#include <WiFiUdp.h>//预处理命令:包含WiFiUDP类以使用UDP实例(库内未实例化)
WiFiUDP Udp;//实例化语句:创建一个WiFiUDP类的实例,实例名UDP,UDP可用函数begin、beginPacket、parsePacket、readString、remoteIP等
int light;//全局变量:光敏电阻测量值
const char* ssid = "IoT";//全局变量:WiFi名称
const char* password = "1234567890";//全局变量WiFi密码
unsigned int localPort = 4210;//全局变量本地UDP端口
void Send(int a)//UDP发送函数
{
//广播地址255.255.255.255大部分路由都支持,不行就只能写客户端具体地址了
Udp.beginPacket("255.255.255.255", localPort);//1开包,写包的:目标地址,目标端口
Udp.write(String(a).c_str());//2写包的:内容(整数变量套String变字符串然后.c_str()转为字符串第一个字的地址传入UDP.write)
Udp.endPacket();//3封包,发送
}
void setup()
{
Serial.begin(115200);//实例化硬件串口0对象
Serial.printf("Connecting to %s ", ssid);//打印连接WiFi名
WiFi.begin(ssid, password);//开始连WiFi
while (WiFi.status() != WL_CONNECTED)//当没有连上WiFi时1秒一次持续打印点.
{
delay(1000);
Serial.print(".");
}
Serial.println(" connected!");//连上后跳出while循环,打印连接成功!
Udp.begin(localPort);//调用WiFiUDP实例UDP的begin函数开启一个UDP协议客户端,端口4210
Serial.printf("Now sending at IP %s, UDP port %d\n", WiFi.localIP().toString().c_str(), localPort);//打印发送端口
}
void loop()
{
light = analogRead(A0);//读取模拟量输入(光敏电阻)赋值给light
Send(light);//发送光敏值(send函数吃int)
Serial.print("UDP发送");//提示
Serial.println(light);//发送值
delay(3000);//3秒发送一次
}
接收端
#include <ESP8266WiFi.h>//预处理命令:包含WiFi类以使用WiFi实例begin、status等(库内已实例化)
#include <WiFiUdp.h>//预处理命令:包含WiFiUDP类以使用UDP实例(库内未实例化)
WiFiUDP Udp;//实例化语句:创建一个WiFiUDP类的实例,实例名UDP,UDP可用函数begin、beginPacket、parsePacket、readString、remoteIP等
int light;//全局变量:光敏电阻测量值
const char* ssid = "IoT";//全局变量:WiFi名称
const char* password = "1234567890";//全局变量WiFi密码
unsigned int localPort = 4210;//全局变量本地UDP端口
int read(void)//UDP接收函数
{
//1.获取当前解析到的最新的包的字节数,看是否为空包
int packetSize = Udp.parsePacket();
if (packetSize)//判断:字节数>0
{
//2.打印包情况
Serial.printf("\n收到来自远程IP:%s(远程端口:%d)的数据包字节数:%d\n", Udp.remoteIP().toString().c_str(), Udp.remotePort(), packetSize);
//3.读取包数据
String udpStringVal = Udp.readString();//获取包中的字符串
Serial.print("开发板接收到UDP数据中的字符串:");Serial.println(udpStringVal);
//4.字符转整数
int a = atoi(udpStringVal.c_str());//字符串转整形(先.c_str()获取字符串第一个字的地址,然后代入atoi,auto to int,自动转整数)
Serial.print("转换得整形数据:"); Serial.println(a);
return a;//返回UDP获取值
}
return -1;
}
void setup()
{
Serial.begin(115200);//实例化硬件串口0对象
Serial.printf("Connecting to %s ", ssid);//打印连接WiFi名
WiFi.begin(ssid, password);//开始连WiFi
while (WiFi.status() != WL_CONNECTED)//当没有连上WiFi时1秒一次持续打印点.
{
delay(1000);
Serial.print(".");
}
Serial.println(" connected!");//连上后跳出while循环,打印连接成功!
Udp.begin(localPort);//调用WiFiUDP实例UDP的begin函数开启一个UDP协议客户端,端口4210
Serial.printf("Now listening at IP %s, UDP port %d\n", WiFi.localIP().toString().c_str(), localPort);//打印监听地址和端口
}
void loop()
{
light=read();//读取UDP发来的最新光照数值
Serial.println("全局变量赋值:");
Serial.println(light);
Serial.println((light!=-1)?"成功":"失败");
}
2.(2)WiFi UDP 双机通信 协议版(已改用mesh协议,此协议已抛弃)
思路标注**(仔细看)**:
程序有详细注释,可直接烧入两台8266测试:
主机从机相互收发,主机将从机数据上传BlinkerAPP,并同步BlinkerAPP指令给从机,实现一台Blinker主机备带动多个非Blinker开发板!
Arduino串口监视器查看效果。
烧录前请将程序中ssid、pswd替换为自己的WiFi名称、密码
插入到自己代码进行测试时注意:
主机连接WiFi由Blinker.begin函数操作,所以主机端setup里不要WiFi.begin ,
从机因为不直接与Blinker通信所以要WiFi.begin,当然由于协议需要ArduinoJson库,所以从机端也包含了Blinker.h
协议版可在WiFi机的本地局域网互相发送Json字符串,并在接收时用动态Json解析库解析,以便自动化处理长度变化的数据。
用户可以修改代码来实现:主机呼叫后,从机应答,以防设备过多,广播重叠造成网络拥堵或丢包。
(WiFi机包括手机热点、电脑热点、路由器、未设置内网隔离的光猫)
Json字符串协议
以一传感,两按钮,一时间为例:
{"results":[{"data":{"ligh":"光敏值","serv":"true或false","leds":"true或false"},"else":{"time":"UNIX时间戳,例如1702733731"}}]}
主机端
收发函数**(不用细看)**
void UDP_read(void)//UDP接收函数
{
read_success=false; //清零
//1.获取当前解析到的最新的包的字节数,看是否为空包
int packetSize = Udp.parsePacket();
if (packetSize)//判断:字节数>0
{
//2.打印包情况
String payload=""; //声明局部变量:返回字符串payload(有效的载荷,即原始数据,服务器返回的http报文中的json数据部分)
payload +="\n收到来自远程IP:";
payload +=Udp.remoteIP().toString();
payload +="(端口:";
payload +=Udp.remotePort();
payload +=")的数据包,字节数:";
payload +=packetSize;
BLINKER_LOG_ALL(payload);
//3.读取包数据
payload="";
payload += Udp.readString(); //调用http实例的获取字符串成员函数,将返回值放入payload
BLINKER_LOG_ALL(BLINKER_F("==============================")); //串口显示返回字符串
BLINKER_LOG_ALL(payload); //
BLINKER_LOG_ALL(BLINKER_F("==============================")); //
//4.解析数据
DynamicJsonDocument jsonBuffer(1024); //实例化动态json缓冲区对象(1024位)
DeserializationError error = deserializeJson(jsonBuffer, payload); //实例化解码错误对象error,初始化为payload解码到动态json缓冲区的操作结果
if (error)read_success=false; //若操作出错,则天气获取成功为假
else //否则,识别成功,则
{
read_success=true; //天气获取成功为真
JsonObject root = jsonBuffer.as<JsonObject>(); //实例化json对象root,通过调用缓冲区的转json对象成员函数来初始化root对象
//提取json格式、数据中,所需键名的键值
String ligh=root["results"][0]["data"]["ligh"];BLINKER_LOG_ALL(ligh);//提取root对象的“result”键名的数组的第0索引的“data”键名的对象的“ligh”键名的键值(光敏值)并在串口显示
String serv=root["results"][0]["data"]["serv"];BLINKER_LOG_ALL(serv);//提取root对象的“result”键名的数组的第0索引的“data”键名的对象的“serv”键名的键值(舵机开关)并在串口显示
String leds=root["results"][0]["data"]["leds"];BLINKER_LOG_ALL(leds);//提取root对象的“result”键名的数组的第0索引的“data”键名的对象的“leds”键名的键值(灯光开关)并在串口显示
String time=root["results"][0]["else"]["time"];BLINKER_LOG_ALL(time);//提取root对象的“result”键名的数组的第0索引的“else”键名的对象的“time”键名的键值(时间)并在串口显示
//5.传出到全局变量将键值转换为所需整数、浮点数、字符串
light_read=ligh.toInt(); //调用string.h的string to int函数(参数为字符串首字符地址)将光敏值从字符串转为浮点数
//led_state=(leds=="true")?true:false;
//curtain_state=(serv=="true")?true:false;
//UNIX_time=time.toInt();
}
}
}
void UDP_send(void)//UDP发送函数
{
//构造字符串{"results":[{"data":{"ligh":"光敏值","serv":"true或false","leds":"true或false"},"else":{"time":"UNIX时间戳,例如1702733731"}}]}
String a="";
a+="{\"results\":[{\"data\":{\"ligh\":\"";//固定部分
a+=String(light_read);//数据
a+="\",\"serv\":\"";//固定部分
a+=curtain_state?"true":"false";//数据
a+="\",\"leds\":\"";//固定部分
a+=led_state?"true":"false";//数据
a+="\"},\"else\":{\"time\":\"";//固定部分
a+=String(UNIX_time);//数据
a+="\"}}]}";
BLINKER_LOG_ALL("UDP发送:",a);//支持多个字符串参数,递归调用
//广播地址255.255.255.255大部分路由都支持,不行就只能写客户端具体地址了
Udp.beginPacket("255.255.255.255", localPort);//1开包,写包的:目标地址,目标端口
Udp.write(a.c_str());//2写包的:内容(整数变量套String变字符串然后.c_str()转为字符串第一个字的地址传入UDP.write)
Udp.endPacket();//3封包,发送
}
主机端setup、loop**(仔细看)**
/*------------------------------------------- 主函数初始化函数 --------------------------------------------------*/
void setup()//初始化
{
//串口,并开启调试信息
Serial.begin(115200);//硬件串口初始化,波特率为115200
BLINKER_DEBUG.stream(Serial);//开启串口调试流到硬件串口
BLINKER_DEBUG.debugAll();//开启所有调试信息
//初始化blinker
Blinker.begin(auth, ssid, pswd);
//主机UDP通信
Udp.begin(localPort);//调用WiFiUDP实例UDP的begin函数开启一个UDP协议客户端,端口4210
Serial.printf("Now sending at IP %s, UDP port %d\n", WiFi.localIP().toString().c_str(), localPort);//打印发送端口
}
/*------------------------------------------- 主函数循环函数 --------------------------------------------------*/
uint32_t fresh_time=0;//数据刷新时间
void loop()
{
Blinker.run();//负责处理blinker的数据(持续)
UDP_read(); //主机接收(持续)
if(millis()-fresh_time>2000)//主机发送(2秒一次,当前减上次大于2秒执行)
{
UDP_send();//发送
fresh_time=millis();//记录上一次发送时间
}
}
从机端
收发函数**(不用细看)**
void UDP_read(void)//UDP接收函数
{
read_success=false; //清零
//1.获取当前解析到的最新的包的字节数,看是否为空包
int packetSize = Udp.parsePacket();
if (packetSize)//判断:字节数>0
{
//2.打印包情况
String payload=""; //声明局部变量:返回字符串payload(有效的载荷,即原始数据,服务器返回的http报文中的json数据部分)
payload +="\n收到来自远程IP:";
payload +=Udp.remoteIP().toString();
payload +="(端口:";
payload +=Udp.remotePort();
payload +=")的数据包,字节数:";
payload +=packetSize;
Serial.println(payload);
//3.读取包数据
payload="";
payload += Udp.readString(); //调用http实例的获取字符串成员函数,将返回值放入payload
Serial.println("=============================="); //串口显示返回字符串
Serial.println(payload); //
Serial.println("=============================="); //
//4.解析数据
DynamicJsonDocument jsonBuffer(1024); //实例化动态json缓冲区对象(1024位)
DeserializationError error = deserializeJson(jsonBuffer, payload); //实例化解码错误对象error,初始化为payload解码到动态json缓冲区的操作结果
if (error)read_success=false; //若操作出错,则天气获取成功为假
else //否则,识别成功,则
{
read_success=true; //天气获取成功为真
JsonObject root = jsonBuffer.as<JsonObject>(); //实例化json对象root,通过调用缓冲区的转json对象成员函数来初始化root对象
//提取json格式、数据中,所需键名的键值 String ligh=root["results"][0]["data"]["ligh"];BLINKER_LOG_ALL(ligh);//提取root对象的“result”键名的数组的第0索引的“data”键名的对象的“ligh”键名的键值(光敏值)并在串口显示
String serv=root["results"][0]["data"]["serv"];BLINKER_LOG_ALL(serv);//提取root对象的“result”键名的数组的第0索引的“data”键名的对象的“serv”键名的键值(舵机开关)并在串口显示
String leds=root["results"][0]["data"]["leds"];BLINKER_LOG_ALL(leds);//提取root对象的“result”键名的数组的第0索引的“data”键名的对象的“leds”键名的键值(灯光开关)并在串口显示
String time=root["results"][0]["else"]["time"];BLINKER_LOG_ALL(time);//提取root对象的“result”键名的数组的第0索引的“else”键名的对象的“time”键名的键值(时间)并在串口显示
//5.传出到全局变量将键值转换为所需整数、浮点数、字符串
//light_read=ligh.toInt(); //调用string.h的string to int函数(参数为字符串首字符地址)将光敏值从字符串转为浮点数
led_state=(leds=="true")?true:false;
curtain_state=(serv=="true")?true:false;
UNIX_time=time.toInt();
}
}
}
void UDP_send(void)//UDP发送函数
{
//构造字符串 {"results":[{"data":{"ligh":"光敏值","serv":"true或false","leds":"true或false"},"else":{"time":"UNIX时间戳,例如1702733731"}}]}
String a="";
a+="{\"results\":[{\"data\":{\"ligh\":\"";//固定部分
a+=String(light_read);//数据
a+="\",\"serv\":\"";//固定部分
a+=curtain_state?"true":"false";//数据
a+="\",\"leds\":\"";//固定部分
a+=led_state?"true":"false";//数据
a+="\"},\"else\":{\"time\":\"";//固定部分
a+=String(UNIX_time);//数据
a+="\"}}]}";
Serial.print("UDP发送:");
Serial.println(a);
//广播地址255.255.255.255大部分路由都支持,不行就只能写客户端具体地址了
Udp.beginPacket("255.255.255.255", localPort);//1开包,写包的:目标地址,目标端口
Udp.write(a.c_str());//2写包的:内容(整数变量套String变字符串然后.c_str()转为字符串第一个字的地址传入UDP.write)
Udp.endPacket();//3封包,发送
}
从机端setup、loop**(仔细看)**
void setup()
{
//IO初始化
Serial.begin(115200);//实例化硬件串口0对象
//WiFi初始化
//(主机的WiFi初始化由Blinker.begin代为操作,从机要自行初始化WiFi)
Serial.printf("Connecting to %s ", ssid); //打印连接WiFi名
WiFi.begin(ssid, pswd); //开始连WiFi
while (WiFi.status() != WL_CONNECTED) //当没有连上WiFi时1秒一次持续打印点.
{delay(1000);Serial.print(".");}
Serial.println("connected"); //连上后跳出while循环,打印连接成功!
//UDP初始化
Udp.begin(localPort); //调用“WiFiUDP类”的“实例UDP”的“begin函数”开启一个UDP协议客户端,端口4210
Serial.printf("Now sending at IP %s, UDP port %d\n", WiFi.localIP().toString().c_str(), localPort);//打印在哪个端口发送到主机
}
uint32_t fresh_time=0;//数据刷新时间
void loop()
{
UDP_read();//从机接收(持续)
if(millis()-fresh_time>2000)//从机发送(2秒一次,当前时间减上次大于2秒执行)
{
UDP_send();//发送
fresh_time=millis();//记录上一次时间
}
}
3.MESH组网(设备管理+人员管理+状态空间+实时同步=智能家居互联动态管理协议)
【MESH组网】Blinker+painlessMESH组网+ESP8266+DHT11温湿度+继电器 一个额度带几百个设备 简易开源教程1.0
12.点灯APP定时
官网说明点灯科技-点灯物联网解决方案
先确定界面上有做好的按钮(参考16节按钮处理函数)
然后在设备的右上角…菜单里面
动作配置》自动生成动作》保存
再去定时里面定时就可以了
动作配置也可用于APP语音控制(不是智能音箱,是APP右下角语音控制,专业版可用)
2024下半年APP更新之后应该就不用配置动作了
13.APP地图
仅商业版可用
14.APP界面配置备份
复制放入手机备忘录或arduino程序注释中
15.APP翻页
完全可以通过按钮区自己做翻页,通过点击翻页按钮,使得界面上的开关下面的备注文字变化(button.text)就实现翻页了,只是说界面布局不能动,但光靠这个方法就可以复用出很多个按钮、滑块、文本了
案例正在开发中,预计3月前上线教程
16.按钮Button组件标准处理方法
按下立即反馈+重进APP同步状态
将以下内容放入自己程序,同样函数的部分自己合并一下,别定义两个一样的函数。
另外我这个用的是带回调函数注册的组件实例化,setup里不需要attach更简洁。官网也推荐此写法.
多个按钮就复制一下然后用搜索替换将1替换为2、3等(一个一个替换,别搞错了或者全部替换,那样程序就报废了)
//定义GPIO2为1号继电器引脚
#define RELAYPIN_1 2
bool button1_state=false; //按钮1状态 全局变量 开机默认关闭 (按钮多或配合小爱同学语音助手使用时改用布尔数组
void button1_callback(const String & state);//按钮1回调函数声明
//下面这句是类库实例化语句,类名BlinkerButton(按钮组件类),实例名Button1,数据键名“b1”,当blinker.run检测到APP发来数据包含键名“b1”就调用回调button1_callback。
BlinkerButton Button1("b1",button1_callback);//用此句不需要在setup里attach回调了
void button1_callback(const String & state) //按钮1回调函数定义,按下后设备开启继电器1
{
if(state=="tap"){button1_state=!button1_state;}//普通按键:tap反转状态
else if(state=="on"){button1_state=true;} //开关按键:on 短按开启
else if(state=="off"){button1_state=false;} //开关按键:off短按关闭
else if(state=="press"){button1_state=true;} //普通按键或开关按键:press长按开启
else if(state=="pressup"){button1_state=false;}//普通按键或开关按键:pressup松开关闭
Button1.print(button1_state?"on":"off"); //立即反馈状态到APP
digitalWrite(RELAYPIN_1,!button1_state); //输出到继电器
}
//心跳包里调用按钮回调,参数空""即可反馈状态
void heartbeat()//用户心跳包内容函数
{
/*这里放自己的心跳包内容,例如Num1.print之类*/
button1_callback("");
}
//setup函数里加上注册心跳包(与已有的合并!)
void setup()
{
Blinker.attachHeartbeat(heartbeat);
}
断电记忆按钮状态待写
EEPROM待写(ESP32建议SPIFFS,8266可用EEPROM)
参考文章:https://blog.csdn.net/Naisu_kun/article/details/86690493
https://blog.csdn.net/weixin_45499326/article/details/109555894
https://codeantenna.com/a/7a4385xWZP
17.报错查看方法
报错框拉最大,错误一般在最前或最后
若出现大面积飘红则大概率为库不匹配或语法错误,可以先取走(剪切粘贴)部分无关库,或更新点灯库、ESP库
自己语法错误看头尾,一般编译器会用^给你指出,错了这句后面就动不了了,少分号多空格或者中文符号都会报错
只看error不看warning,包括“对应多个库”都不是报错,只是告诉你这款库在arduino自带,你也放了或者ESP库也放了
注意blinker库更新时一定要把旧版删除,不然容易取到旧版
可以参考一下
新手常见错误排查指引(blinker Arduino sdk篇)
18.免费版/专业版/商业版、独立设备/专属设备区别
独立设备/专属设备区别:
明确的叫法应该是独立/量产设备
专属(量产)设备配置一致、支持统一定制APP界面或网页,支持统一OTA固件更新,需要官方审核,具有商业授权。
2024年3-6月专属设备下线,不得新建,旧的继续用,专属与独立合并,仅按版本收费,所以不用管啦。
免费版、专业版共同点:
共享设备端SDK、公共服务器和同一个APP(安卓APP与iOS APP版本号不同,功能一致)。
三者不同点:
免费版仅作公益学习之用,13000用户每个月几万元服务维护费用已经很剧烈,还老被同行攻击,老板写了脚本被攻击自动封IP,有时可能会不稳五分钟。
源代码开源地址:
设备端SDK在 https://github.com/blinker-iot
服务器在 https://github.com/coloz/blinker-lite(据悉已烂尾)
APP在 https://github.com/blinker-iot
专业版多些付费功能例如天气、AQI空气质量、APP自定义背景、APP实时图表等。专业版QQ群点灯老板偶尔回复一下,免费版基本不回,进群方式见本文第6节。其余详见官网文档服务与授权。
(天气可以直接复制Arduino中文社区里面的心知天气代码实现免费天气获取(配置方式见此文第四章)。)
商业版9万元一年才能个性化SDK、独立服务器APP小程序+各种定制服务。(任意官方群里加群主对接)
2024年3-6月APP更新预计加入大模型预测等,并按功能收费。(不过已经从2023-6拖到2024-3了)
19.阿里云设备下线 & 免费额度降低与重新获取
官方通知Arduino中文社区
摘要:
2023年6月起,5个直接免费额度降低为1个,之前已经申请过现在无法使用的可以按教程第五章操作,绑定GitHub获取5个设备额度,即可恢复正常
【入门教程】第五章免费额度获取Arduino中文社区
如果操作后App左侧开发者里面仍然没有显示五个额度,或者设备还不正常就官网换最新blinker库,然后app备份界面配置,并解除绑定,重新申请key,并重新烧录程序
20.连不上WiFi请注意
无论路由器还是手机热点还是 电脑热点,都不可以使用5G Wi-Fi,路由器不可以开启5G优选,或者是双频合一,必须使用路由器2.4G信号,必须使用有密码的Wi-Fi,校园网那种要登陆的必须转接路由器或电脑热点。
21.使用mqtt.fxV1.7.1等第三方 连接Blinker MQTT Broker 开放式 API
1、软件下载
mqtt.fxV1.7.1下载MQTT.Fx 1.7.1 | MQTTFx MQTT Client
2、鉴权信息获取
后面************替换为你手机获取的设备密钥,可以在BlinkerAPP设备的右上角…按钮菜单里查看,点击眼睛图标查看,长按复制
动态鉴权参数去get 或浏览器 https://iot.diandeng.tech/api/v1/user/device/diy/auth?authKey=************
获取Json格式动态鉴权数据(动态:每次获取iotToken都会变化,实现一设备一授权):
{"message":1000,"detail":{"deviceName":"AF2400FE9MSGLG83938Q2U6M","iotId":"AF2400FE9MSGLG83938Q2U6M","iotToken":"8gjGJbDy8leTwdkq6oAxk0Y2OW7p13gi","productKey":"blinker","broker":"blinker","uuid":"e33a3a12006211ed84295254","host":"mqtt://broker.diandeng.tech","port":"1883","realDeviceName":"AF2400FE9MSGLG83938Q2U6M"}}
3、鉴权信息填写到MQTTFX
可以用在线json工具格式化:
JSON在线解析,JSON格式化,JSON解析,JSON 校验(SO JSON)
JSON在线工具 - 在线JSON校验格式化工具(K JSON) - json在线解析|json|在线校验
格式化后,我备注了参数用途:
{
"message": 1000,
"detail": {
"deviceName": "AF2400FE9MSGLG83938Q2U6M",(设备名)
"iotId": "AF2400FE9MSGLG83938Q2U6M",(设备MQTTID,从2024年开始和设备名一样)
"iotToken": "8gjGJbDy8leTwdkq6oAxk0Y2OW7p13gi",(设备MQTT动态密码)
"productKey": "blinker",(量产的专属设备模板名,这里blinker表示非量产的DIY独立设备)
"broker": "blinker",(blinker公司自己APP授权,有些产品是合作企业发布授权)
"uuid": "e33a3a12006211ed84295254",(用户ID也是手机APP)
"host": "mqtt://broker.diandeng.tech",(服务器主机名)
"port": "1883",(端口)
"realDeviceName": "AF2400FE9MSGLG83938Q2U6M"(实际名称,备用)
}
}
MQTTFX设置里面
分别填host(服务器主机名)、port(端口)、deviceName(设备名)、iotId(设备MQTTID)、iotToken(设备MQTT动态密码)到
BrokerAddress 、BrokerPort、ClientID、UserName、Password
如下图
4、按connect连接
绿色表示连接成功,发消息注意后文规范,不然立马被踢下线(踢下线后可以用原来密码登录,无需重新获取鉴权信息)
,
5、确定话题
话题名看设备串口
或官网文档:https://www.diandeng.tech/doc/api-mqtt#%E8%BF%9E%E6%8E%A5&&%E9%89%B4%E6%9D%83
发布话题(send):/device/{deviceName}/s
订阅话题(receive):/device/{deviceName}/r
为啥要两个话题?
设备发布话题会被服务器转发到APP订阅话题
APP发布话题会被服务器转发到设备订阅话题
好比串口RXTX交叉对接,实现双工通信互不干扰,也能支持同一作者uuid其它设备广播进来(看Blinker Bridge)
6、设备发APP
示例发布话题(send):/device/AF2400FE9MSGLG83938Q2U6M/s
示例订阅话题(receive):/device/AF2400FE9MSGLG83938Q2U6M/r
设备发送到APP必须序列化为Json(程序里用ArduinoJson序列化示例)
mqttfx这里我就手写了,用Blinker做过就知道嵌套关系,比如按钮b1的开闭状态用swi表示,颜色clr,图标ico等
{"toDevice":"e33a3a12006211ed84295254","data":{"state":"online","b1":{"swi":"on"}}}
7、APP发设备
手机APP调试框发送到设备会自动序列化成:
{"fromDevice":"e33a3a12006211ed84295254","data":"手机发来的消息"}
手机快速发送自定义键值可用按钮隐藏功能,如图点击空白处即可出现
可以发on/off、tap、press/pressup以外键值
例如键名get,键值(承载内容)state
即可替代调试框的刷新按钮发送{"get":"state"}
8、设备发设备
刚才提到:同一作者(uuid)的其它设备发来
https://www.diandeng.tech/doc/api-mqtt#%E8%BF%9E%E6%8E%A5&&%E9%89%B4%E6%9D%83
官网给出:
blinker Broker以组(Group)进行权限鉴别,在同一组内的设备可以相互通信
例如:两个设备都是同一用户创建的,这两个设备将都在同一用户组中,因此可以相互通信。
{"toGroup":"uuid","data":{"get":"state"}}
经测试toGroup无效,只能toDevice,且必须是自己的设备
示例消息:
{"toDevice":"8C59801BNM4544U74H6PR4B0","data":{"state":"online","b1":{"swi":"on"}}}
其他设备就会收到:
这样就可以云端互联了,和本地UDP广播、TCP对打、WiFi Mesh互联组网、ESPNOW遥控等差不多
22.继电器注意事项
高电平触发继电器
高/低电平触发继电器
无论用哪款继电器模组,记得:
高电平触发的模组平常是低电平0V,单片机输出3.3V高电平才吸合,使用推挽输出:
pinMode(RELAYPIN_1, OUTPUT);
低电平触发的模组平常是高电平5V,单片机输出0V高电平才吸合,避免5V灌入单片机3.3V回路,使用开漏输出:
pinMode(RELAYPIN_1, OUTPUT_OPEN_DRAIN);
参考资料:【NodeMcu-ESP8266】引脚使用参考指南http://t.csdnimg.cn/OtETF