本文主要对NodeMCU固件库以及SmartWifi等相关知识进行整理,并基于DS18B20形成温度监控传感器模块。
NodeMCU编译
首先clone NodeMCU源码
git clone -b dev https://github.com/nodemcu/nodemcu-firmware
在user_modules.h选择相应的模块,然后在user_config.h中设置相应的配置,大致看了一下除了smartconfig外,还有关于debug、I2C、LFS、TLS等的设置,包括此前涉及到的波特率、整数以及浮点的固件版本等都在这里面设置,由此可见之前对ESP8266调试lua代码的波特率的理解是有问题的,实际上还是在固件中就设置好了,正好是115200而已,要修改只能在这里修改
这两个文件都在app/include中
配置好之后,就可以进行编译了
sudo make clean
sudo make
编译过程需要gcc工具组件的支持,期间遇到了很多问题,也是花了不少功夫才结解决,以后有空详细学习整理一下编译相关的知识
编译完成后的二进制固件在bin文件夹里面,会包含两个文件0x00000.bin 和 0x10000.bin,具体这两个文件是什么,目前我还不是特别清楚,以后在看吧
将编译好的固件下载到ESP82666,还是使用之前的工具,即esptool
esptool.py --port /dev/ttyUSB0 write_flash 0x0000 0x00000.bin 0x10000 0x10000.bin
需要注意的是SmartConfig只有在dev版本中才有,一开始没注意到折腾了一会
在user_config.h中设置WIFI_SMART_ENABLE开启,完成上面的步骤后ESP8266中的固件就带有SmartConfig功能了
SmartWIFI
进入到SmartWIFI后,ESP8266模块会进入到监控所有数据包的模式,通过手机中的相应组件,就可以把SSID和密码等信息广播出去,这样ESP8266就可以接受到对应的wifi连接信息,由此完成wifi配置。
nodemcu中的smartwifi比较简单,示例如下:
wifi.setmode(wifi.STATION)
wifi.startsmart(0,
function(ssid, password)
print(string.format("Success. SSID:%s ; PASSWORD:%s", ssid, password))
wifi_connect(ssid,password)
end
)
通过上面的代码,结合手机端的设置,就可以获得SSID和密码。
手机端app可以用Espressif,下载链接https://github.com/espressifapp
NodeMCU的 SmartConfig功能默认是关闭的,需要下载源码进行编译固件完成,直接通过之前网络编译的方法好象没有那个选项
DS18B20
这个传感器上大学的时候就用过,当时使用单片机照着1-wire的总线协议进行位操作形成的驱动程序,当时废了不少劲才成功,现在直接就有现成的可以用,真是方便了好多
NodeMCU中DS18B20的驱动是用lua编的,并没有像其他很多传感器一样是用c编的,这样就需要使用lua文件调用来完成温度采集和转换了
在NodeMCU中下载ds18b20.lua,然后通过下面的程序就可以完成温度采集了
local t = require("ds18b20")
local pin = 3 -- gpio0 = 3, gpio2 = 4
local function readout(temp)
if t.sens then
print("Total number of DS18B20 sensors: ".. #t.sens)
for i, s in ipairs(t.sens) do
print(string.format(" sensor #%d address: %s%s", i, ('%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X'):format(s:byte(1,8)), s:byte(9) == 1 and " (parasite)" or ""))
end
end
for addr, temp in pairs(temp) do
print(string.format("Sensor %s: %s °C", ('%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X'):format(addr:byte(1,8)), temp))
end
-- Module can be released when it is no longer needed
--t = nil
package.loaded["ds18b20"] = nil
end
if not tmr.create():alarm(2000, tmr.ALARM_AUTO, function()
t:read_temp(readout, pin, t.C)
end)
then
print("whoopsie")
end
需要注意的是,这个模块的正常使用需要OW也就是OneWire Module 的支持,OW模块是用c编写的,所以会在固件源码中进行编译。
nodemcu-tool上传和调试
在使用ESPlorer中总是会出现上传不成功或者出错等问题,有其实比较长的代码,不知道是不是我的配置问题,一直没有解决,所以就找了个替代的代码上传工具,即nodemcu-tool
这个工具的应用比较简单,感觉比ESPlorer上传代码还要方便一些
格式化/清除已经上传的文件:
nodemcu-uploader file format
上传代码:
nodemcu-tool upload --port=/dev/ttyUSB0 ds18b20.lua init.lua init_test.lua
执行文件:
nodemcu-tool run init_test.lua
列出已上传文件目录:
nodemcu-uploader file list
显示文件内容:
nodemcu-uploader file print init.lua
显示堆栈大小:
nodemcu-uploader node heap
重启:
nodemcu-uploader node restart
设置默认串口:
set SERIALPORT=/dev/ttyUSB0
删除特定文件:
nodemcu-uploader file remove foo.lua
Node命令
可在串口工具(例如cutecom)中使用的命令,结合上面的工具就可以实现Esplorer的大部分功能
重启:
=node.restart()
执行文件:
=dofile("init.lua")
显示堆栈大小:
=node.heap()
MQTT
之前就提到了可以使用MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)协议作为传感器的通信协议,它工作在TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议,最早由IBM发布。
MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议,它具有以下主要的几项特性:
1、使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合;
2、对负载内容屏蔽的消息传输;
3、使用 TCP/IP 提供网络连接;
4、有三种消息发布服务质量:
a.“至多一次”,消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。
b.“至少一次”,确保消息到达,但消息重复可能会发生。
c.“只有一次”,确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。
其中,有一个比较特别的是遗嘱消息Last Will, 即用于连接丢失后发送的消息。
MQTT支持加密登录功能,可以在服务器端设置用户密码进行登录。
本项目中MQTT分别订阅两种信息,一种用于数据传输,一种用于控制数据采集开关。
通过发送temp send open打开温度采集,发送temp send close关闭温度采集。
MQTT中需要加入处理连接错误的流程。
最终形成的MQTT客户端程序如下:
print("mqtt init")
node.flashindex("_init")() --后续LFS添加的
node.flashindex("ds18b20")()
humi=0.01
tempinfo=""
--****************************************************************
local t = require("ds18b20")
local pin = 3 -- gpio0 = 3, gpio2 = 4
local function readout(temp)
if t.sens then
print("Total number of DS18B20 sensors: ".. #t.sens)
for i, s in ipairs(t.sens) do
print(string.format(" sensor #%d address: %s%s", i, ('%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X'):format(s:byte(1,8)), s:byte(9) == 1 and " (parasite)" or ""))
end
end
for addr, temp in pairs(temp) do
print(string.format("Sensor %s: %s °C", ('%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X'):format(addr:byte(1,8)), temp))
tempinfo=string.format("DHT Temperature:%f;Humidity:%f\r\n",
temp,
humi
)
end
end
--****************************************************************
ismqttconnected = false
temp_flag = false
clientid = "ESP8266-" .. node.chipid()
m = mqtt.Client(clientid, 10)
m:lwt("/lwt", "offline", 0, 0)
m:on("connect", function(client) print ("connected") end)
m:on("offline", function(client)
print ("offline")
ismqttconnected = false
print ("mqtt will be reconnected for offline after 10s.")
tmr.create():alarm(10 * 1000, tmr.ALARM_SINGLE, do_mqtt_connect)
end)
m:on("message", function(client, topic, data)
print(topic .. ":" )
if data ~= nil then
print(data)
end
if string.find(data, "temp send open", 1) then
print("temp_true")
temp_flag = true
tempsendtmr:start()
end
if string.find(data, "temp send close", 1) then
print("temp_false")
temp_flag = true
tempsendtmr:stop()
end
end)
m:on("overflow", function(client, topic, data)
print(topic .. " partial overflowed message: " .. data )
end)
function handle_mqtt_error(client, reason)
print ("mqtt will be reconnected for error after 10s.")
tmr.create():alarm(10 * 1000, tmr.ALARM_SINGLE, do_mqtt_connect)
end
function do_mqtt_connect()
m:connect("host_ip", 1883, false, function(client) --这里是连接服务器的地址
print("connected")
ismqttconnected = true
-- subscribe topic with qos = 0
client:subscribe("/topic", 0, function(client) print("subscribe success") end)
-- publish a message with data = hello, QoS = 0, retain = 0
client:publish("/topic", "hello", 0, 0, function(client) print("sent a topic") end)
end,
function(client, reason)
print("failed reason: " .. reason)
handle_mqtt_error(client, reason)
end)
end
do_mqtt_connect()
--m:close()
--****************************************************************
tempsendtmr = tmr.create()
tempsendtmr:register(3000, tmr.ALARM_AUTO, function()
t:read_temp(readout, pin, t.C)
m:publish("/tempinfo", tempinfo, 0, 0, function(client) print("sent a tempinfo") end)
end)
tempsendtmr:stop()
--****************************************************************
LFS(Lua Flash Store)
按理说上面的问题都已经解决完了,但是当我把所有的程序弄到一起,然后运行的时候就悲剧了,出现了下面的错误
PANIC: unprotected error in call to Lua API (error loading module 'ds18b20' from file 'ds18b20.lua':␍␊
[22:10:00:345] ⇥ not enough memory
看字面意思是RAM不够用了,可能是因为使用了18b20的Lua模块后,RAM需求增加较多导致的
这样就请出了这一节的主角——LFS,下面是Nodemcu关于LFS的介绍
The Lua Flash Store (LFS) patch modifies the Lua RTS to support a modified Harvard architecture by
allowing the Lua code and its associated constant data to be executed directly out of flash-memory
(just as the NodeMCU firmware is itself executed). This now allows NodeMCU Lua developers to create
Lua applications with up to 256Kb Lua code and read-only (RO) constants executing out of flash,
with all of the RAM is available for read-write (RW) data.
有图NodeMCU所执行的Lua程序都是在RAM中运行的(包括代码和数据),这样所执行的代码大小就比较有限,因此如果如果可以将Lua代码写入Flash然后直接执行,那么就可以大大提高Lua程序大小
也就是说LFS使得Lua代码及其常量数据可以直接Flash中执行,就像NodeMCU的固件一样,这样就可以最大允许执行256Kb的Lua代码。
主要通过一下四步
1.LFS的使用需要在编译固件前在这里app/include/user_config.h 通过#define LUA_FLASH_STORE 0x0 调整LFS空间大小
注意需要是4Kb的倍数
2.固件映像(包含需要通过LFS运行的Lua代码)需要通过luac.cross进行编译,这个文件就在NodeMCU固件程序的主目录下
通过下面的命令进行编译
luac.cross -o lfs.img -f *.lua
上面的命令会编译当前文件夹中的所有lua文件,编译中下面这两个文件是必须要包括的
lua_examples/lfs/_init.lua. LFS helper routines and functions.
lua_examples/lfs/dummy_strings.lua. Moving common strings into LFS.
针对本项目,需要编译的文件为
luac.cross -o lfs.img -f _init.lua dummy_strings.lua ds18b20.lua
3.生成的lfs.img需要通过与Lua代码一样的方式上传至ESP8266
为了使用这些固件映像,需要执行载入命令
node.flashreload("lfs.img")
执行完上述命令后ESP8266会自动进行一次重启
4.此后,还需要在调用具体的Lua模块之前执行下面的命令对模块进行注册
node.flashindex("_init")()
node.flashindex("ds18b20")()
这样就可以像普通模块一样执行相应的Lua代码了
按照上述步骤重新运行程序,发现正常了。
再次查看堆栈
[23:26:51:849] =node.heap()
[23:26:51:861] 26744
其它
对于NodeMCU的组成和原理,后面有机会研究,初步看了一下,好像是基于NON-OS SDK和SPIFFS实现的lua语言编程
NON-OS SDK是ESP8266的非操作系统固件库,从名字中就可以看出来这个固件库不是基于操作系统的,而是基于事件驱动的,提供了很多核心功能API,例如数据接收/发送,TCP/IP功能,硬件接口等,与之对应的还有一个ESP8266_RTOS_SDK。
SPIFFS 是一个用于闪存的文件系统,SPI就是SPI接口,这个文件系统用于简单的场景需求,对资源要求较少,因此适用的容量也比较小,而且不支持文件夹功能。
后面有机会再看吧,这块就先到这里了。
完。