Nodo de monitoreo ambiental 4G basado en Air700E de Hezhou (datos de temperatura, humedad, presión atmosférica, etc.), que carga a la plataforma de IoT de Alibaba Cloud mediante MQTT.
Introducción
El módulo 4G Air700E de Hezhou lee sensores (temperatura y humedad, presión atmosférica, etc.) y sube los datos a la plataforma IoT de Alibaba Cloud mediante el protocolo MQTT; los datos también se muestran en una pantalla OLED de 0,96" usando la librería gráfica U8g2.
El módulo WiFi ESP32C3 se suscribe, también por MQTT, a los datos que sube el nodo 4G y los presenta en una pantalla LCD mediante la librería LVGL.
Este es un trabajo de diseño de curso que hice por encima, no está muy pulido.
Requisitos del tema: diseñar y fabricar un sistema de comunicación inalámbrico que, combinando conocimientos del semestre anterior, transmita por radio la temperatura, humedad o distancia medida por ultrasonidos desde el lugar de adquisición al host/receptor y muestre la información en una LCD. Detalles:
⑴ Sistema de transmisión inalámbrica por Bluetooth
⑵ Sistema de transmisión inalámbrica por WiFi
⑶ Sistema de transmisión inalámbrica por ZigBee
⑷ Sistema de transmisión inalámbrica por GPRS/GSM
(5) Sistema de transmisión inalámbrica 4G
(6) Sistema de transmisión inalámbrica NB-IoT/LoRaSugerencia: usar un procesador de 8/16/32 bits como controlador principal, elegir según el contenido integrado del grupo y completar al menos 2 sub-elementos: elegir uno entre (1)(2)(3) y uno entre (4)(5)(6).
Tanto Air700E como ESP32C3 los desarrollé con el sistema LuatOS + scripts Lua.
El firmware de LuatOS lo generé con la nube de compilación de Hezhou.
El flash de 4 MByte que trae el ESP32C3 lo cambié por uno de 8 MByte, porque el firmware supera 4 MB al incluir LVGL y varias fuentes.
Promoción estudiantil de Alibaba Cloud: https://www.aliyun.com/daily-act/ecs/activity_share?userCode=jdjc69nf
Nota: como en la plataforma IoT de Alibaba Cloud los dispositivos no pueden suscribirse a temas de otros dispositivos, solo a los propios, hay que añadir una regla en Reenvío de mensajes → Flujo de productos en la nube para que los mensajes publicados por el nodo 4G se reenvíen a un tema del nodo WiFi.
Enlace open-source en la plataforma LCSC: https://oshwhub.com/zeruns/wen-shi-du-cai-ji-4g-shang-chuan
Grupo de técnicos en electrónía/MCU: 2169025065
Introducción al Air700E de Hezhou
Air700E es un módulo de comunicación LTE Cat.1 bis lanzado por Hezhou, basado en la plataforma EC618 de Yixin, compatible con LTE 3GPP Rel.13. Solo conserva la banda LTE TDD, adaptada a las bandas principales de China Mobile, con encapsulado ultrapequeño y coste extremadamente bajo, ideal para aplicaciones miniaturizadas y de bajo precio.
Características principales:
- Interfaz USIM 1.8/3.3 V
- Puerto serie configurable 1.8/3.3 V
- USB 2.0
- Actualización OTA remota de firmware
- Interfaz de audio digital PSM
- Varios modos de desarrollo: USB-dongle, AT estándar, desarrollo secundario open-CPU (LuatOS, C-SDK), etc.
Air700E integra abundantes protocolos de red, interfaces industriales estándar y controladores para Windows 7/8/8.1/10, Linux, Android, ampliando su uso en M2M: CPE, routers, dongles, tablets, vehículos, videovigilancia, PDAs industriales, etc.
Especificaciones técnicas de Air700E:
Bandas LTE-TDD: B34/B38/B39/B40/B41
Velocidades LTE-TDD:
- Configuración 2: hasta 8 Mbps (DL) / 2 Mbps (UL)
- Configuración 1: hasta 6 Mbps (DL) / 4 Mbps (UL)
Interfaces:
- 1×USB 2.0 High-Speed (hasta 480 Mbps)
- 1×(U)SIM 1.8 V/3.0 V
- 1×NETLIGHT (NET_STATUS)
- 1×I²S digital para códec externo
- 3×UART (principal, general, depuración)
- PWRKEY (activo a bajo nivel)
- 2×ADC
- 13×GPIO + 2×entrada de interrupción
- 1×I²C
Introducción al ESP32-C3 de Hezhou
La tarjeta CORE ESP32, basada en el ESP32-C3 de Espressif, mide solo 21 mm × 51 mm y usa pads de tipo sello para facilitar su uso en cualquier escenario. Soporta UART, GPIO, SPI, I²C, ADC, PWM, etc., a elegir según necesidad.
Recursos hardware:
- Dimensiones 21 mm × 51 mm
- 1×SPI FLASH, 4 MB a bordo, hasta 16 MB
- 2×UART (UART0~UART1); UART0 para descarga
- 5×ADC de 12 bits, 100 kSPS máx.
- 1×SPI lento, modo maestro
- 1×controlador I²C
- 4×PWM, cualquier GPIO usable
- 15×GPIO externos, multiplexables
- 2×LEDs SMD
- 1×botón RESET + 1×botón BOOT
- 1×USB-TTL para descarga y depuración
- Antena PCB 2.4 GHz
Fotos del prototipo
Vídeo demo: https://www.bilibili.com/video/BV1MH4y1B7Df/
Vista general
Nodo 4G
Nodo WiFi
Conectando WiFi:
Tras conectar WiFi, conectando al broker MQTT y sincronizando hora NTP:
Mostrando datos del nodo 4G:
Gráfica de evolución de temperatura y humedad:
Plataforma IoT
Datos en la plataforma IoT de Alibaba Cloud:
Esquemáticos
Nodo 4G
Nodo WiFi
PCB
Nodo 4G
El footprint del módulo 4G no es del todo correcto.
Capa superior:
Capa inferior:
Nodo WiFi
Capa superior:
Capa inferior:
Código
No detallaré cómo descargar el código; consultad la documentación oficial.
Documentación Air700E: https://doc.openluat.com/wiki/44?wiki_page_id=4730
Manual LVGL para LuatOS: https://url.zeruns.com/7z7fN
Documentación ESP32-C3: https://url.zeruns.com/497AP
Tutorial Lua: https://url.zeruns.com/Pc4PA
Nodo 4G
Descarga del firmware: https://url.zeruns.com/2G3K7
Archivo main.lua:
-- LuaTools necesita PROJECT y VERSION
PROJECT = "Nodo de monitoreo ambiental"
VERSION = "1.1.0"
-- Librerías requeridas (escritas en Lua); las internas no necesitan require
sys = require("sys")
aht10 = require "aht10"
bmx = require "bmx"
-- Watchdog para evitar bloqueos
if wdt then
wdt.init(9000) -- 9 s
sys.timerLoopStart(wdt.feed, 3000) -- alimentar cada 3 s
end
print(_VERSION)
log.info(mobile.ipv6(true)) -- habilitar IPv6
local ProductKey = "xxxxxx" -- clave producto
local DeviceName = "xxxxxx" -- vuestro nombre de dispositivo
local DeviceSecret = "xxxxxx" -- vuestro secreto
local client_id, user_name, password = iotauth.aliyun(ProductKey, DeviceName, DeviceSecret)
log.info("Parámetros MQTT", client_id, user_name, password)
local softI2C = i2c.createSoft(29, 31, 2) -- I²C software
local aht10_data, bmx_data
local RH, Temp, Press, High, distance, BAT_Voltage, CPU_T = 0,0,0,0,0,0,0
local RH_C, Temp_C, Press_C, High_C, distance_C, BAT_Voltage_C, CPU_T_C = 0,0,0,0,0,0,0
local AHT10_flag, BMP180_flag, US100_flag, BAT_Voltage_flag, CPU_T_flag = false,false,false,false,false
all_data = {params = {CurrentVoltage = 0, CurrentTemperature = 0, CurrentHumidity = 0,
Atmosphere = 0, Altitude = 0, DetectDistance = 0, CPUTemperature = 0}}
function US_100() -- US-100, lectura distancia ultrasónica
uart.write(1, string.char(0x55)) -- enviar 0x55
sys.wait(50) -- 50 ms
local hData, lData = string.byte(uart.read(1, 2), 1, 2)
uart.rxClear(1)
if hData and lData then
local Distance_mm = tonumber((hData * 256) + lData)
if Distance_mm > 4500 then Distance_mm = 4500 end
return (Distance_mm / 10) -- cm
end
end
-- https://blog.zeruns.com
sys.taskInit(function() -- hilo para leer sensores
sys.wait(500)
aht10.init(softI2C) -- AHT10
bmx.init(softI2C) -- BMP180
uart.setup(1, 9600, 8, 1, uart.NONE)
adc.open(0) -- ADC0
adc.open(adc.CH_CPU)-- temperatura CPU
sys.wait(500)
local RH_sum, Temp_sum, Press_sum, High_sum, distance_sum, BAT_Voltage_sum, CPU_T_sum = 0,0,0,0,0,0,0
local avg_count, avg_count2, avg_count3, avg_count4, avg_count5 = 0,0,0,0,0
while 1 do
aht10_data = aht10.get_data()
bmx_data = bmx.get_data()
distance_C = US_100()
-- Voltaje batería, divisor 4.7 k / 10 k
BAT_Voltage_C = ((adc.get(0) / 1000 / 1007) * 1473)
CPU_T_C = adc.get(adc.CH_CPU) / 1000
-- Media AHT10
if aht10_data.RH and aht10_data.T then
all_data.params.CurrentTemperature = aht10_data.T
all_data.params.CurrentHumidity = aht10_data.RH * 100
if avg_count < 6 then
RH_C, Temp_C = aht10_data.RH * 100, aht10_data.T
RH_sum = RH_C + RH_sum
Temp_sum = Temp_C + Temp_sum
avg_count = avg_count + 1
elseif avg_count == 6 then
RH = RH_sum / 6
Temp = Temp_sum / 6
log.info("AHT10_data.RH: "..(string.format("%.2f", RH)).." %",
"AHT10_data.T: "..(string.format("%.2f", Temp)).." ℃")
AHT10_flag = true
sys.publish("MQTT_P") -- avisar que los datos están listos
avg_count, RH_sum, Temp_sum = 0,0,0
end
end
```--Cálculo de promedio de presión y altitud BMP180
if bmx_data.press and bmx_data.high then
all_data.params.Atmosphere = bmx_data.press
all_data.params.Altitude = bmx_data.high
if avg_count2 < 10 then --Comprobar si es menor que 10, acumular para el promedio
Press_C,High_C = bmx_data.press, bmx_data.high --Leer valores actuales de presión y altitud
Press_sum = Press_C + Press_sum
High_sum = High_C + High_sum
avg_count2 = avg_count2 + 1
elseif avg_count2 == 10 then
Press = Press_sum / 10
High = High_sum / 10
log.info("BMP180_data.press: "..(string.format("%.2f", Press)).." hPa"," BMP180_data.high: "..(string.format("%.2f", High)).." m")
BMP180_flag = true
sys.publish("MQTT_P")
avg_count2,Press_sum,High_sum=0,0,0
end
end
--Cálculo de promedio de distancia US-100 ultrasónico
if distance_C then --Comprobar si se leyeron datos
all_data.params.DetectDistance = distance_C
if avg_count3 < 3 then
distance_sum = distance_C + distance_sum
avg_count3 = avg_count3 + 1
elseif avg_count3 == 3 then
distance = distance_sum / 3
log.info("US-100: "..(string.format("%.1f", distance)).." cm")
US100_flag = true
sys.publish("MQTT_P")
avg_count3,distance_sum = 0,0
end
end
--Voltaje de batería
if BAT_Voltage_C then --Comprobar si se leyeron datos
all_data.params.CurrentVoltage = BAT_Voltage_C
if avg_count4 < 20 then
BAT_Voltage_sum = BAT_Voltage_C + BAT_Voltage_sum
avg_count4 = avg_count4 + 1
elseif avg_count4 == 20 then
BAT_Voltage = BAT_Voltage_sum / 20
log.info("BAT_Voltage: "..(string.format("%.2f", BAT_Voltage)).." V")
BAT_Voltage_flag = true
sys.publish("MQTT_P")
avg_count4,BAT_Voltage_sum = 0,0
end
end
--Temperatura CPU
if CPU_T_C then --Comprobar si se leyeron datos
all_data.params.CPUTemperature = CPU_T_C
if avg_count5 < 20 then
CPU_T_sum = CPU_T_C + CPU_T_sum
avg_count5 = avg_count4 + 1
elseif avg_count5 == 20 then
CPU_T = CPU_T_sum / 20
log.info("CPU_T: "..(string.format("%.2f", CPU_T)).." ℃")
CPU_T_flag = true
sys.publish("MQTT_P")
avg_count5,CPU_T_sum = 0,0
end
end
sys.wait(500)
end
end)
--[[sys.taskInit(function() --Crear tarea, medición ultrasónica, cada 1.5 s
uart.setup(1, 9600, 8, 1, uart.NONE) --Inicializar puerto serie 1
sys.wait(1000)
while 1 do
distance = US_100() --Leer datos US-100
if distance then --Comprobar si se leyeron datos
log.info("US-100: "..(string.format("%.1f", distance)).." cm")
US100_flag = true
sys.publish("MQTT_P")
end
sys.wait(1500)
end
end)]]
sys.taskInit(function() --Crear hilo, inicialización MQTT y reporte de datos
mqttc = mqtt.create(nil, "a1sJbDQiEqr.iot-as-mqtt.cn-shanghai.aliyuncs.com", 1883,true,true)
mqttc:auth(client_id, user_name, password)
mqttc:keepalive(120) -- valor por defecto 240 s
mqttc:autoreconn(true, 3000) -- reconexión automática
mqttc:on(function(mqtt_client, event, data, payload)
if event == "conack" then
sys.publish("mqtt_conack")
log.info("mqtt", "mqtt conectado")
mqtt_client:subscribe("/sys/"..ProductKey.."/"..DeviceName.."/thing/service/property/set")
elseif event == "recv" then
log.info("mqtt", "mensaje recibido", data, payload)
local mqtt_date = json.decode(payload)
elseif event == "sent" then
log.info("mqtt", "sent", "pkgid", data)
end
end)
mqttc:connect()
while true do
sys.waitUntil("MQTT_P", 3000)
if AHT10_flag then
local json_str = json.encode({params = {CurrentTemperature = Temp, CurrentHumidity = RH}}, "2f") -- convertir datos a JSON, 2 decimales
mqttc:publish("/sys/"..ProductKey.."/".. DeviceName.."/thing/event/property/post", json_str) -- publicar propiedad MQTT
mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f")) -- publicar propiedad MQTT
AHT10_flag = false
end
if BMP180_flag then
local json_str = json.encode({params = {Atmosphere = Press, Altitude = High}}, "2f") -- convertir datos a JSON, 2 decimales
mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", json_str) -- publicar propiedad MQTT
mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f")) -- publicar propiedad MQTT
BMP180_flag = false
end
if US100_flag then
local json_str = json.encode({params = {DetectDistance = distance}}, "1f") -- convertir datos a JSON, 2 decimales
mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", json_str) -- publicar propiedad MQTT
mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f")) -- publicar propiedad MQTT
US100_flag = false
end
if BAT_Voltage_flag then
local json_str = json.encode({params = {CurrentVoltage = BAT_Voltage}}, "2f") -- convertir datos a JSON, 2 decimales
mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", json_str) -- publicar propiedad MQTT
mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f")) -- publicar propiedad MQTT
BAT_Voltage_flag = false
end
if CPU_T_flag then
local json_str = json.encode({params = {CPUTemperature = CPU_T}}, "2f") -- convertir datos a JSON
mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", json_str) -- publicar propiedad MQTT
mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f")) -- publicar propiedad MQTT
CPU_T_flag = false
end
end
end)
-- https://blog.zeruns.com
sys.taskInit(function()
u8g2.begin({ic = "ssd1306",direction = 0,mode="i2c_hw",i2c_id=1,i2c_speed = i2c.FAST}) -- direction opcional 0 90 180 270
u8g2.ClearBuffer() --limpiar pantalla
u8g2.SetFont(u8g2.font_opposansm10) --cambiar fuente
u8g2.DrawUTF8("IMEI:", 0, 16)
u8g2.DrawUTF8(mobile.imei(), 0, 32)
u8g2.SendBuffer()
--sincronizar hora por ntp
--socket.sntp()
socket.sntp({"ntp.aliyun.com","ntp1.aliyun.com","ntp2.aliyun.com"}) --servidores ntp personalizados
--socket.sntp(nil, socket.ETH0) --adaptador ntp personalizado
sys.subscribe("NTP_UPDATE", function()
log.info("sntp", "hora", os.date())
sys.publish("NTP_OK")
end)
sys.subscribe("NTP_ERROR", function()
log.info("socket", "error sntp")
socket.sntp()
end)
sys.wait(1000)
sys.waitUntil("NTP_OK", 3000)
u8g2.ClearBuffer() --limpiar pantalla
u8g2.SetFont(u8g2.font_opposansm10) --cambiar fuente
u8g2.DrawUTF8(os.date("%Y-%m-%d"), 0, 16) --mostrar fecha
u8g2.DrawUTF8(os.date("%H:%M:%S"), 0, 32) --mostrar hora
local IP = socket.localIP() --mostrar dirección IP
if IP then
u8g2.DrawUTF8("IP:"..IP, 0, 63)
end
u8g2.SendBuffer()
sys.wait(2500)
while true do
u8g2.ClearBuffer() --limpiar pantalla
u8g2.SetFont(u8g2.font_sarasa_m10_ascii)
if Temp_C then
u8g2.DrawUTF8("T:"..(string.format("%.2f", Temp_C)).."°C", 0, 16)
end
if RH_C then
u8g2.DrawUTF8("RH:"..(string.format("%.2f", RH_C)).."%", 0, 32)
end
if Press_C then
u8g2.DrawUTF8("P:"..(string.format("%.2f", Press_C)).."hPa", 0, 48)
end
if distance_C then
u8g2.DrawUTF8("D:"..(string.format("%.1f", distance_C)).."cm", 0, 63)
end
if High_C then
u8g2.DrawUTF8("H:"..(string.format("%.2f", High_C)).."m", 61, 16)
end
if BAT_Voltage_C then
u8g2.DrawUTF8("B:"..(string.format("%.2f", BAT_Voltage_C)).."V", 66, 32)
end
if CPU_T_C then
u8g2.DrawUTF8("CPUT:"..(string.format("%d", CPU_T_C)).."°C", 63, 63)
end
u8g2.SendBuffer() --actualizar pantalla
sys.wait(200)
end
end)```lua
sys.taskInit(function() --Crear un hilo, parpadear LED cada 500 ms
gpio.setup(27, 0) -- Configurar gpio27 como salida, nivel inicial bajo, usar configuración pull predeterminada de hardware
while true do
gpio.toggle(27) --Invertir nivel de gpio
sys.wait(500)
end
end)
--[[
Fuentes soportadas
["unifont_t_symbols","open_iconic_weather_6x_t","opposansm8","opposansm10","opposansm12","opposansm16","opposansm20",
"opposansm24","opposansm32","sarasa_m8_ascii","sarasa_m10_ascii","sarasa_m12_ascii","sarasa_m14_ascii",
"sarasa_m16_ascii","sarasa_m18_ascii","sarasa_m20_ascii","sarasa_m22_ascii"]
]]
-- Código de usuario finalizado---------------------------------------------
-- Siempre esta línea al final
sys.run()
-- ¡¡¡¡¡No agregar ninguna instrucción después de sys.run()!!!!!
Nodo WiFi
Dirección de descarga del firmware: https://url.zeruns.com/a7eXJ
Archivo main.lua:
-- LuaTools necesita PROJECT y VERSION
PROJECT = "Nodo de visualización de datos"
VERSION = "1.0.5"
-- La biblioteca sys es estándar
_G.sys = require("sys")
require("sysplus")
--Agregar perro guardian para evitar que el programa se bloquee
wdt.init(9000)--Inicializar watchdog a 6 s
wdt_timer_id = sys.timerLoopStart(function()
wdt.feed()
end, 2000) --Alimentar perro cada 2 s
local ProductKey= "xxxxxx" --Clave de producto
local DeviceName= "xxxxxx" --Cambia a tu propio nombre de dispositivo
local DeviceSecret = "xxxxxx" --Cambia a tu propia clave secreta de dispositivo
local client_id, user_name, password = iotauth.aliyun(ProductKey, DeviceName ,DeviceSecret) --Generar trío MQTT
log.info("Parámetros MQTT", client_id, user_name, password)
local local_CPU_T_C = 0;
local all_data = {params = {CurrentVoltage = 0,CurrentTemperature = 0, CurrentHumidity = 0,
Atmosphere = 0, Altitude = 0,DetectDistance = 0,CPUTemperature = 0}};
local key1_flag,key2_flag = false,false
sys.taskInit(function() -- Crear un hilo
log.info("wlan", "wlan_init:", wlan.init()) -- Inicializar wlan
wlan.setMode(wlan.STATION) -- Establecer modo wlan como STATION
wlan.connect("Mate 40 Pro", "123456789", 1) -- Conectar a red wlan
local result, data = sys.waitUntil("IP_READY",6000)-- Esperar evento IP_READY
log.info("wlan", "IP_READY", result, data) -- Imprimir resultado y datos del evento IP_READY
if result then
socket.setDNS(socket.STA, 1, "114.114.114.114")
log.info("wlan", "info", json.encode(wlan.getInfo())) -- Imprimir información detallada de la red wlan
sys.publish("WiFi_connected_OK",data) -- Publicar mensaje, conexión WiFi exitosa
--Sincronizar hora ntp
--socket.sntp()
socket.sntp({"ntp.aliyun.com","time.windows.com","ntp.tencent.com","ntp2.aliyun.com"}, socket.STA) --sntp direcciones personalizadas de servidor
--socket.sntp(nil, socket.STA) --sntp adaptador personalizado
-- Suscribir evento NTP_UPDATE
sys.subscribe("NTP_UPDATE", function()
sys.publish("NTP_OK") -- Publicar mensaje, sincronización NTP exitosa
log.info("sntp", "time", os.date()) -- Imprimir hora del sistema
end)
sys.subscribe("NTP_ERROR", function()
log.info("socket", "sntp error")
socket.sntp()
end)
else
while true do
sys.wait(1000)
end
end
adc.open(adc.CH_CPU)-- Abrir canal ADC-canal de temperatura interna de CPU
local CH_CPU = adc.get(adc.CH_CPU) / 1000 -- Leer temperatura CPU
if CH_CPU then -- Verificar si los datos son válidos
local_CPU_T_C = CH_CPU
end
mqttc = mqtt.create(nil, "a1sJbDQiEqr.iot-as-mqtt.cn-shanghai.aliyuncs.com", 1883,true) -- Crear cliente MQTT
mqttc:auth(client_id, user_name, password) -- Autenticación MQTT
mqttc:keepalive(60) -- Establecer tiempo de keepalive MQTT, valor predeterminado 60 s
mqttc:autoreconn(true, 3000) -- Habilitar reconexión automática
mqttc:on(function(mqtt_client, event, data, payload)-- Definir función callback de eventos MQTT
if event == "conack" then -- Al recibir evento de confirmación de conexión (conack)
sys.publish("mqtt_conack") -- Publicar mensaje mqtt_conack
log.info("mqtt", "MQTT conectado") -- Imprimir información de conexión exitosa
mqtt_client:subscribe("/sys/"..ProductKey.."/"..DeviceName.."/thing/service/property/set") -- Suscribir tema específico
mqtt_client:subscribe("/"..ProductKey.."/"..DeviceName.."/user/get")
elseif event == "recv" then -- Al recibir evento de mensaje (recv)
if data == "/"..ProductKey.."/"..DeviceName.."/user/get" then -- Verificar tema de recepción
all_data = json.decode(payload) -- Analizar contenido del mensaje como JSON
mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", payload) -- Reportar propiedades por MQTT
else
log.info("mqtt", "mensaje recibido", data, payload) -- Imprimir información del mensaje recibido
local mqtt_date = json.decode(payload) -- Analizar contenido del mensaje como JSON
end
elseif event == "sent" then -- Al enviar evento de mensaje (sent)
--log.info("mqtt", "sent", "pkgid", data)-- Imprimir información del mensaje enviado
end
end)
mqttc:connect()
while true do
local CH_CPU = adc.get(adc.CH_CPU) / 1000 -- Leer temperatura CPU
if CH_CPU then -- Verificar si los datos son válidos
CPU_T_C = CH_CPU
end
gpio.toggle(12) --Invertir nivel de gpio
sys.wait(500)
end
end)
sys.taskInit(function() -- Crear un hilo
spi.setup(2, 7, 0, 0, 8, 80 * 1000 * 1000, spi.MSB, 1, 1) -- Inicializar SPI
log.info("lcd.init", lcd.init("st7789",{
port = 2, -- Puerto spi
-- pin_cs = 7, -- CS SPI
pin_dc = 8, -- Pin LCD data/comando
-- pin_pwr = 18, -- Pin retroiluminación LCD opcional
pin_rst = 6, -- Pin reset LCD
direction = 2, -- Orientación LCD 0:0° 1:180° 2:270° 3:90°
w = 320, -- Resolución horizontal LCD
h = 172, -- Resolución vertical LCD
xoffset = 0, -- Desplazamiento x
yoffset = 34; -- Desplazamiento y
}))
log.info("lvgl", lvgl.init()) --- Inicializar LVGL
local style_screen_label_main = lvgl.style_create() -- Crear objeto estilo llamado style_screen_label_main
lvgl.style_set_text_font(style_screen_label_main, lvgl.STATE_DEFAULT, lvgl.font_get("opposans_m_16")) -- Establecer fuente predeterminada de etiqueta
```local Init_scr = lvgl.obj_create(nil, nil) -- crear un objeto de pantalla
lvgl.scr_load(Init_scr) -- cargar el objeto de pantalla y mostrarlo en el monitor
local spinner = lvgl.spinner_create(Init_scr, nil) -- crear un objeto de icono giratorio
lvgl.obj_set_size(spinner, 100, 100) -- establecer el tamaño del icono giratorio
lvgl.obj_align(spinner, nil, lvgl.ALIGN_CENTER, 0, -20) -- alinear el icono giratorio al centro
local label_WiFiConnecting = lvgl.label_create(Init_scr, nil) -- crear un objeto etiqueta
lvgl.label_set_recolor(label_WiFiConnecting, true) -- activar la función de recoloración de la etiqueta, es decir, actualizar automáticamente el color cuando el contenido del texto cambia
lvgl.obj_add_style(label_WiFiConnecting, lvgl.LABEL_PART_MAIN, style_screen_label_main) -- asignar el objeto de estilo a la parte principal de la etiqueta
lvgl.label_set_text(label_WiFiConnecting, "Conectando WiFi...") -- establecer el contenido de texto de la etiqueta
lvgl.label_set_align(label_WiFiConnecting, lvgl.LABEL_ALIGN_CENTER) -- establecer la alineación del texto de la etiqueta al centro
lvgl.obj_align(label_WiFiConnecting, nil, lvgl.ALIGN_CENTER, 0, 46) -- alinear la etiqueta al centro
local result, ip_data = sys.waitUntil("WiFi_connected_OK", 5000) -- esperar el mensaje de conexión WiFi exitosa, con tiempo de espera de 5 segundos
if result == false then
lvgl.label_set_text(label_WiFiConnecting, "#ff0000 ¡Error al conectar WiFi!#\\nReinicie el dispositivo o verifique la red") -- establecer el contenido de texto de la etiqueta (mostrar fallo de conexión WiFi)
lvgl.label_set_align(label_WiFiConnecting, lvgl.LABEL_ALIGN_CENTER) -- establecer la alineación del texto de la etiqueta al centro
lvgl.obj_align(label_WiFiConnecting, nil, lvgl.ALIGN_CENTER, 0, 51) -- alinear la etiqueta al centro
while true do
sys.wait(1000) -- esperar en bucle para evitar que el programa continúe
end
end
lvgl.label_set_text(label_WiFiConnecting, "¡WiFi conectado!") -- establecer el contenido de texto de la etiqueta (mostrar conexión WiFi exitosa)
lvgl.label_set_align(label_WiFiConnecting, lvgl.LABEL_ALIGN_CENTER) -- establecer la alineación del texto de la etiqueta al centro
lvgl.obj_align(label_WiFiConnecting, nil, lvgl.ALIGN_CENTER, 0, 51) -- alinear la etiqueta al centro
sys.wait(500) -- esperar 500 milisegundos
lvgl.obj_del(label_WiFiConnecting) -- eliminar el objeto etiqueta
lvgl.obj_align(spinner, nil, lvgl.ALIGN_CENTER, 0, 0) -- alinear el icono giratorio al centro
local label_Init_log = lvgl.label_create(Init_scr, nil) -- crear un objeto etiqueta
lvgl.label_set_recolor(label_Init_log, true) -- activar la función de recoloración de la etiqueta
lvgl.label_set_align(label_Init_log, lvgl.LABEL_ALIGN_LEFT) -- establecer la alineación del texto de la etiqueta a la izquierda
lvgl.obj_align(label_Init_log, nil, lvgl.ALIGN_IN_TOP_LEFT, 10, 5) -- alinear la etiqueta a la esquina superior izquierda de la pantalla
local Init_log = "#0000ff Registro de inicialización: #\\n¡WiFi conectado!\\n" -- información del registro de inicialización
lvgl.label_set_text(label_Init_log, Init_log) -- establecer el contenido de texto de la etiqueta
Init_log = Init_log.."IP: "..ip_data.."\\nConectando al servidor MQTT...\\n" -- actualizar información del registro
lvgl.label_set_text(label_Init_log, Init_log) -- establecer el contenido de texto de la etiqueta
if sys.waitUntil("mqtt_conack", 10000) == false then
Init_log = Init_log.."#ff0000 ¡Error al conectar al servidor MQTT!\\n¡Reinicie el dispositivo!#\\n" -- actualizar información del registro (mostrar fallo de conexión MQTT)
lvgl.label_set_text(label_Init_log, Init_log) -- establecer el contenido de texto de la etiqueta
mqttc:disconnect() -- desconectar MQTT
mqttc:close() -- cerrar conexión MQTT
while true do
sys.wait(1000) -- esperar en bucle para evitar que el programa continúe
end
end
Init_log = Init_log.."¡Conexión al servidor MQTT exitosa!\\n" -- actualizar información del registro (mostrar conexión MQTT exitosa)
lvgl.label_set_text(label_Init_log, Init_log) -- establecer el contenido de texto de la etiqueta
Init_log = Init_log.."Sincronizando tiempo NTP...\\n" -- actualizar información del registro
lvgl.label_set_text(label_Init_log, Init_log) -- establecer el contenido de texto de la etiqueta
local result = sys.waitUntil("NTP_OK", 3500) -- esperar el mensaje de sincronización NTP exitosa, con tiempo de espera de 3 segundos
if result then
Init_log = Init_log.."¡Sincronización NTP exitosa!\\n" -- actualizar información del registro (mostrar sincronización NTP exitosa)
lvgl.label_set_text(label_Init_log, Init_log) -- establecer el contenido de texto de la etiqueta
else
Init_log = Init_log.."¡Error en sincronización NTP!\\n" -- actualizar información del registro (mostrar fallo de sincronización NTP)
lvgl.label_set_text(label_Init_log, Init_log) -- establecer el contenido de texto de la etiqueta
end
Init_log = Init_log.."Hora actual: "..(os.date("%Y-%m-%d %H:%M:%S")).."\\n" -- actualizar información del registro (mostrar hora actual)
lvgl.label_set_text(label_Init_log, Init_log) -- establecer el contenido de texto de la etiqueta
Init_log = Init_log.."¡Inicialización del sistema completada!\\n" -- actualizar información del registro (mostrar inicialización completada)
lvgl.label_set_text(label_Init_log, Init_log) -- establecer el contenido de texto de la etiqueta
sys.wait(2000)
local alldata_scr = lvgl.obj_create(nil, nil) -- crear un objeto de pantalla para mostrar todos los datos, sin objeto padre ni estilo
lvgl.scr_load(alldata_scr) -- cargar el objeto de pantalla y mostrarlo en el monitor
local label_alldata = lvgl.label_create(alldata_scr, nil) -- crear un objeto Label para mostrar todos los datos
lvgl.label_set_recolor(label_alldata, true) -- activar la función de recoloración de la etiqueta
lvgl.label_set_align(label_alldata, lvgl.LABEL_ALIGN_LEFT) -- establecer la alineación de la etiqueta a la izquierda
lvgl.obj_align(label_alldata, nil, lvgl.ALIGN_IN_TOP_LEFT, 10, 5) -- alinear la etiqueta a la esquina superior izquierda de la pantalla, con margen izquierdo de 10 píxeles y margen superior de 5 píxeles
lvgl.obj_add_style(label_alldata, lvgl.LABEL_PART_MAIN, style_screen_label_main)-- asignar el objeto de estilo a la parte principal de la etiqueta label_alldata
local label_time = lvgl.label_create(alldata_scr, nil) -- crear un objeto Label para mostrar la hora actual
lvgl.label_set_text(label_time, os.date("%Y-%m-%d").."\\n"..os.date("%H:%M:%S"))
lvgl.label_set_align(label_time, lvgl.LABEL_ALIGN_CENTER) -- establecer la alineación de la etiqueta al centro
lvgl.obj_align(label_time, nil, lvgl.ALIGN_IN_TOP_RIGHT, -25, 10)-- alinear la etiqueta a la esquina superior derecha de la pantalla, con margen izquierdo de 10 píxeles y margen superior de 5 píxeles
lvgl.obj_add_style(label_time, lvgl.LABEL_PART_MAIN, style_screen_label_main)
lvgl.obj_del(Init_scr)
local chart_scr = lvgl.obj_create(nil, nil) -- crear un objeto de pantalla para mostrar gráficos, sin objeto padre ni estilo
local chart = lvgl.chart_create(chart_scr, nil);
lvgl.obj_set_size(chart, 310, 150);
lvgl.obj_align(chart, nil, lvgl.ALIGN_CENTER, 0, 0);
lvgl.chart_set_type(chart, lvgl.CHART_TYPE_LINE); --Mostrar líneas y puntos también*/
lvgl.chart_set_point_count(chart, 20)
lvgl.chart_set_y_range(chart, lvgl.CHART_AXIS_PRIMARY_Y, 0, 100)
local ser1 = lvgl.chart_add_series(chart, lvgl.color_make(0xFF, 0x00, 0x00));
local ser2 = lvgl.chart_add_series(chart, lvgl.color_make(0x00, 0x80, 0x00));
-- Imprimir uso de memoria
log.info("mem.lua", rtos.meminfo())
log.info("mem.sys", rtos.meminfo("sys"))
while true do
lvgl.label_set_text(label_alldata, string.format([[#0000ff Temperatura ambiente: %.2f°C#
#ff00ff Humedad ambiente: %.2f%%#
#ff0000 Presión atmosférica: %.2fhPa#
#800080 Altitud: %.2fm#
#0000ff Distancia ultrasónica: %.2fcm#
#ff00ff Voltaje de batería nodo remoto: %.2fV#
#ff0000 Temperatura CPU nodo remoto: %.0f°C#
#800080 Temperatura CPU nodo local: %.0f°C#
]],all_data.params.CurrentTemperature,all_data.params.CurrentHumidity,all_data.params.Atmosphere,
all_data.params.Altitude,all_data.params.DetectDistance,all_data.params.CurrentVoltage,
all_data.params.CPUTemperature,local_CPU_T_C)); -- establecer texto de la etiqueta
lvgl.label_set_text(label_time, os.date("%Y-%m-%d").."\\n"..os.date("%H:%M:%S"))
lvgl.chart_set_next(chart, ser1, all_data.params.CurrentTemperature);
lvgl.chart_set_next(chart, ser2, all_data.params.CurrentHumidity);
lvgl.chart_refresh(chart);
if key1_flag then
lvgl.scr_load(chart_scr)
key1_flag = false
end
if key2_flag then
lvgl.scr_load(alldata_scr)
key2_flag = false
end
gpio.toggle(12) --invertir nivel gpio
sys.wait(300)
end
end)
sys.taskInit(function() --crear un hilo, parpadear LED cada 500 ms
gpio.setup(13, 0) -- configurar gpio13 como salida, nivel inicial bajo, usar configuración de pull predeterminada del hardware
gpio.setup(12, 0) -- configurar gpio12 como salida, nivel inicial bajo, usar configuración de pull predeterminada del hardware
while true do
gpio.toggle(13) --invertir nivel gpio
sys.wait(500)
end
end)
sys.taskInit(function() --crear un hilo, escaneo de teclas
gpio.setup(18, nil, gpio.PULLUP)
gpio.setup(19, nil, gpio.PULLUP)
while true do
if gpio.get(18) == 0 then
sys.wait(20)
if gpio.get(18) == 0 then
key1_flag = true
end
end
if gpio.get(19) == 0 then
sys.wait(20)
if gpio.get(19) == 0 then
key2_flag = true
end
end
sys.wait(50)
end
end)
--[[
Fuentes soportadas ["sarasa_m8_ascii","sarasa_m10_ascii","sarasa_m12_ascii",
"sarasa_m14_ascii","sarasa_m16_ascii","sarasa_m18_ascii","sarasa_m20_ascii","sarasa_m22_ascii"]
]]
-- El código del usuario ha terminado---------------------------------------------
-- Siempre esta última línea
sys.run()
-- ¡¡¡¡¡Después de sys.run() no agregar ninguna sentencia!!!!!
Hardware utilizado
Módulos de hardware utilizados y direcciones de compra:- Módulo 4G: Air700E, https://s.click.taobao.com/gMXE14u
- Módulo WiFi: ESP32-C3, https://s.click.taobao.com/VU6E14u
- Sensor de temperatura y humedad: AHT10, https://s.click.taobao.com/0bmD14u
- Sensor de presión barométrica: BMP180, https://s.click.taobao.com/lj5E14u
- Módulo de medición de distancia por ultrasonidos: US-100 (en modo de comunicación por puerto serie), https://s.click.taobao.com/K0o5X3u
- Pantalla LCD: ST7789 de 1,47 pulgadas, resolución 172×320, https://s.click.taobao.com/qalD14u
- Pantalla OLED: SSD1306 de 0,96 pulgadas, interfaz I2C, https://s.click.taobao.com/otWD14u
- IC de carga y descarga de batería: IP5306-CK (asegúrate de comprar la versión CK; la versión normal corta la salida automáticamente cuando la corriente de descarga es muy baja, por lo que no es adecuada para microcontroladores), https://s.click.taobao.com/eu85X3u
- Batería: EVE 35V-18650, 3500 mAh, https://s.click.taobao.com/uUc5X3u
Recomendación de compra de componentes: tienda LCSC, enlace de registro con descuento: https://activity.szlcsc.com/invite/D03E5B9CEAAE70A4.html
Todos los componentes de la placa están disponibles en la tienda LCSC; en el enlace del proyecto de código abierto, en la lista de materiales (BOM), haz clic en “Agregar al carrito de LCSC” para importar todos los componentes utilizados de una sola vez.
Instrucciones de configuración de la plataforma IoT
Primero, crea un nuevo producto en la plataforma IoT de Alibaba Cloud y selecciona “Dispositivo de conexión directa” como tipo de nodo.
Define las funciones:
A continuación, añade dos dispositivos:
Modifica las claves del producto y otros parámetros en el script, luego descarga el firmware y el script al Air700E y comprueba si el dispositivo se conecta correctamente y sube datos.
Ve a Reenvío de mensajes → Flujo de productos en la nube y crea una fuente de datos:
Haz clic en “Ver” a la derecha de la fuente de datos recién creada, luego añade un Topic: elige “Personalizado” para el primero, tu producto creado para el segundo, tu dispositivo 4G para el tercero y user/update para el cuarto.
Crea un destino de datos, selecciona “Publicar en otro Topic” como acción y elige tu producto creado.
Crea un analizador, luego haz clic en “Ir a editar” o en “Ver” a la derecha.
Asocia la fuente de datos que acabas de crear:
Asocia el destino de datos que acabas de crear:
En el script del analizador, cambia deviceName() por el nombre de tu dispositivo WiFi, como se muestra en la imagen.
Después de editar, haz clic en “Publicar” y luego en “Iniciar”; automáticamente se reenviarán los datos del topic /ProductKey/DeviceName/user/update al topic /ProductKey/DeviceName/user/get.
Otros proyectos de código abierto recomendados- Placa mínima STM32F030C8T6 y luces de desplazamiento (esquemático y PCB): https://blog.zeruns.com/archives/715.html
- Dibujé una placa mínima MSP430F149 y la liberé como abierta: https://blog.zeruns.com/archives/713.html
- Problema de fuente del concurso de electrónica 2007: módulo elevador DCDC ajustable 30-36 V (UC3843): https://oshwhub.com/zeruns/36v-sheng-ya-dcdc-mo-kuai-uc3842
- Placa mínima STC12C5A60S2 / control de temperatura y ventilador con display para MCU 51: https://blog.zeruns.com/archives/721.html
- Plantilla de proyecto STM32F407 con biblioteca estándar y U8g2 portada: https://blog.zeruns.com/archives/722.html
- Placa mínima Qinheng CH32V307VCT6 liberada como abierta: https://blog.zeruns.com/archives/726.html
- Módulo de fuente DCDC ajustable auto-elevador-reductor LM25118: https://blog.zeruns.com/archives/727.html
- Módulo elevador de rectificación síncrona de alta potencia EG1164 liberado, eficiencia máxima 97 %: https://blog.zeruns.com/archives/730.html
Artículos recomendados
- Recomendación de VPS/servidores en la nube de alta relación calidad-precio y baratos: https://blog.zeruns.com/archives/383.html
- Tutorial para montar servidor de Minecraft: https://blog.zeruns.com/tag/mc/
- Comparto mi rack de red doméstica, recomendación de equipos para redes hogareñas: https://blog.zeruns.com/archives/732.html
- Orange Pi 3B (RK3566) – revisión y unboxing: https://blog.zeruns.com/archives/729.html
- Actualicé mi PC, cambié la gráfica por una Yeston RTX3070: https://blog.zeruns.com/archives/746.html























