Nodo de monitoreo ambiental 4G basado en Air700E (datos de temperatura, humedad, presión, etc.), carga a la plataforma IoT de Alibaba Cloud mediante MQTT

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/LoRa

Sugerencia: 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

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

Artículos recomendados