ESP-8266/ESP32 NodeMCU Lua: азы программирования.

Тема в разделе "ESP8266, ESP32", создана пользователем ИгорьК, 25 июл 2017.

  1. ИгорьК

    ИгорьК Гуру

    Самое распространенное явление - DS18b20. Там между запросом и съемом показаний по максимуму 750 мс.
    Все лень посмотреть, как задержка в Даллас библиотеке сделана.

    Но здесь Lua. Здесь, в основном, все на Callback функциях и таймерах.

    Хотел сделать показ на корутинах, но что-то громоздким показалось. В реальности, при показе погоды каждый шаг смещения по дисплею установлен 35 мс, умножается на количество точек, что надо протащить, и возвращается диспетчеру. Все работает.
     
    Последнее редактирование: 31 июл 2025
  2. Asper Daffy

    Asper Daffy Иксперд

  3. ИгорьК

    ИгорьК Гуру

    Слышу впервые. Он к МК как-то относится? Евгений, я сам DIY, который назавтра забывает что сегодня делал. Это хобби. Это ни хорошо и ни плохо. Это чуть лучше чем лежать на диване, но не так круто, как всходить на Эверест. У всех хобби разные.
     
  4. Asper Daffy

    Asper Daffy Иксперд

    Не понял вопроса. Если про реализацию, то там платформа пофиг, там кросс-компилятор из Т++ в С++.
     
  5. ИгорьК

    ИгорьК Гуру

    Это и есть вопрос новобранца :) Я давно развлекаюсь с Lua, а не с С++.
     
  6. ИгорьК

    ИгорьК Гуру

    ---------------------------------
    Очень дешевый сенсор расстояния LD2402.

    Пытаюсь понять что из него можно достать полезного таким кодом:

    Код (Lua):

    do
        -- Если DEBUG - вывод от 'print' иначе молчок
        PRT = function(...) if DEBUG == true then print(...) end end
        PRT1 = function(...) if DEBUG == 1 then print(...) end end
        PRT2 = function(...) if DEBUG == 2 then print(...) end end
        local prt = PRT or print
        local prt1 = PRT1 or print
        local prt2 = PRT2 or print

        -- Установка порта
        TX = 17
        RX = 16

        MAXDIST = 150                -- Максимальная дистанция для реагирования
        DELTA_DIST = 30              -- Изменение дистанции, когда следует реагировать

        if not dat then dat = {} end -- таблица, для совместимости с другим кодом

        if TX and RX then
            uart.setup(2, 115200, 8, uart.PARITY_NONE, uart.STOPBITS_1, { tx = TX, rx = RX })
            uart.start(2)
            dat.uart2 = true -- Чтобы в других местах знать, что порт установлен
            prt('Set UART at TX, RX', TX, RX)
            TX, RX = nil, nil
        end

        if not wth then wth = {} end -- Таблица, куда собираем необходимые данные

        wth.distance = 0             -- Начальная дистанция
        wth.showdata = 0             -- Чтобы поменьше выводить в консоль, переменная с коророй идет сравнение
        wth.rawdist = 0
        local startUART = false      -- Для улова первого читаемого байта из UART
        local makedata, uartstring, uartdist, rawdist
        local delta = DELTA_DIST
        local maxdist = MAXDIST or 200                        -- Локалим переменную максимпльной дистанции для реагирования
        MAXDIST, DELTA_DIST = nil, nil
        uartstring = ''                                       -- Для сбора текста из UART

        makedata = function(dt)                               -- Обработчик байтов из UART
            if string.byte(dt, 1) ~= 0x0D then                -- Пока не знак возврата каретки - собираем данные в строковую переменную
                uartstring = uartstring .. dt
            else                                              -- Обрабатываем собранные данные
                prt('uartstring:', uartstring)                -- Выводим в консоль что ы итоге получилось
                startUART = false                             -- Говорим UART что будем работать заново
                uartdist = string.match(uartstring, ':(%d+)') -- Выгрызаем числовую составляющую
                prt('Got Distance:', uartdist)

                uartstring = ''
                rawdist = tonumber(uartdist) -- Стринг в число

                if rawdist and wth.rawdist ~= rawdist then  -- Изменения в таблицу
                    -- Если больше 5 см - на печать
                    if math.abs(wth.rawdist - rawdist) > 5 then prt1(wth.rawdist) end
                    wth.rawdist = rawdist
                end

                -- Если ошибка или расстояние больше чем надо - хватит!
                if not rawdist or rawdist > maxdist then
                    wth.showdata = 0
                    return
                end                                                        
                prt('Now Distance:', uartdist)
                wth.distance = rawdist

                -- Реагируем, когда изменение больше чем DELTA_DIST
                if math.abs(wth.distance - wth.showdata) > delta then
                    print('Distance is', wth.distance)
                    wth.showdata = wth.distance
                    -- dofile 'mqttpub.lua'
                end
            end
        end

        uart.on(2, "data", 1,
            function(data)
                local bt = string.byte(data, 1) -- bt - один байт из UART
                prt2(string.format("bt = 0x%02X", bt))
                prt2(data)
                if startUART == false and bt ~= 0x0A then -- Ждем перевода строки
                    return
                elseif startUART == false then            -- Дождались - разрешаем чтение
                    startUART = true
                    return
                end
                makedata(data) -- Отправляем байт в переработку
            end, 0)
    end

     

    Это просто скетч для размышления. Чтобы увидеть все что происходит, дать в консоль "DEBUG = true", убрать данные - "DEBUG = false", так как датчик гонит в UART данные раз в 500 мс. и все ими забивается.

    upload_2025-9-2_11-2-57.png

    Почему датчик по умолчанию считает, что расстояние в данном случае 272 см - вообще не понятно. Перед ним - шкаф в полутора метрах. 145 - 150 - показывает когда я к нему подхожу сантиметров на 90.

    В общем, пока изучаю. Реагирует на движение он стабильно, но, судя по всему, его можно применять лишь как "выключатель" с настройкой "по месту". Понять его данные пока не удается.

    ПС. Датчик лежит на столе, пока с ним "разборки". Оказывается, от расположения верх/низ кое-что зависит.
     
    Последнее редактирование: 3 сен 2025
  7. ИгорьК

    ИгорьК Гуру

    serg3295 нравится это.
  8. ИгорьК

    ИгорьК Гуру

    Вот такую держалку для датчика нарисовал. Файлы Компаса в приложении.

    upload_2025-9-4_9-12-41.png

    upload_2025-9-4_9-25-35.png

    Но напечатал не такую :)

    upload_2025-9-4_9-14-36.png


    Чет она мне напомнила, и я ее перечертил. Хотя все равно чет напоминает :)

    Все в приложении.

    Датчик в такой коробочке стабилизировался, теперь можно размышлять о его деятельности :)
    В верхней части отверстие под фоторезистор. Хочу сделать датчик движения/освещенности.
    Таких датчиков продается пруд пруди, но здесь еще будет выход на реле и, возможно, само реле.

    Алгоритм, полагаю, будет посложнее, чем, обнаружил движение/свет - сообщил. Но это пока замыслы.

    В общем, это просто "держалка" датчиков, а не проект.
     

    Вложения:

    • LD2402.zip
      Размер файла:
      448,8 КБ
      Просмотров:
      19
  9. Credo

    Credo Нуб

    Скажите вообще реально запустить без краша системы? Что пробовал: агресивную очистку памяти, разнести по времени вплоть до 05,10,15,20. Убрать 1н индекс ( хотя не плохо бы 2). Разные сервера получения. Парсинг txt, json, html - вообще мрак крашится. Http сервер не нашел Устанавливал пропустить если размер памяти меньше . Парсинг 1 раз в 60 минут. Монитор показывает
    11:12:33.181 -> [MEM] Weather_str_start: 27192
    11:12:33.181 -> [MEM] Weather_str_end: 27088
    11:12:42.575 -> [MEM] Weather_end: 29824
    11:13:12.592 -> [MEM] Weather_end: 29824
    11:13:33.430 -> [MEM] Weather_str_start: 26416
    11:13:33.430 -> [MEM] Weather_str_end: 26312 на переменные и тд не смотрите , это один из файлов проекта.
     

    Вложения:

    • weather.txt
      Размер файла:
      21,7 КБ
      Просмотров:
      14
  10. ИгорьК

    ИгорьК Гуру

    Здесь не реально.
    Вы не в ту тему вопрос задали, тут про язык Lua.

    ========

    Что касается стабильности, то у меня часы и данные с погодного сервера парсят, и с narodmon, и отправляют данные, и http сервер для настройки поднимают. То есть стабильность достижима.
     
    Последнее редактирование: 21 ноя 2025
  11. ИгорьК

    ИгорьК Гуру

    MAX7219 семисегментный дисплей. ESP32!
    Вот такой:
    max7219md.png

    Модуль:
    Код (Lua):
    -- _max7219md.lua
    local M = {}
    local SSPin = 18
    local DECOD = 0x09
    local INTENS = 0x0A
    local SCAN = 0x0B
    local SHUT = 0x0C
    local DIST = 0x0F
    local ESP = 'esp32'
    local device

    local function sendByte(register, data)
        gpio.write(SSPin, 0)
        device:transfer(string.char(register, data))
        gpio.write(SSPin, 1)
    end

    function M.setup(esp, mosi, sspin, sclk, miso)
        local busmaster
        local lsclk = sclk or 19
        local lmosi = mosi or 23
        local lmiso = miso or 26
        local lesp = esp or ESP

        if lesp == 'esp32' then
            busmaster = spi.master(spi.HSPI, { sclk = lsclk, mosi = lmosi, miso = lmiso })
        elseif lesp == 'esp32s3' then
            -- busmaster = spi.master(spi.SPI2, { sclk = lsclk, mosi = lmosi, miso = lmiso }, 0)
            busmaster = spi.master(spi.SPI2, { sclk = lsclk, mosi = lmosi }, 0)
        else
            print('esp32s3 or esp32 only')
            return
        end

        local device_config = { mode = 0, freq = 10000000 }
        device = busmaster:device(device_config)

        SSPin = sspin or SSPin
        gpio.config({ gpio = SSPin, dir = gpio.OUT })
        gpio.write(SSPin, 1)

        sendByte(SCAN, 7)
        sendByte(DECOD, 0x00)
        -- sendByte(DECOD, 0xFF) -- Scan All Digits
        sendByte(DIST, 0)
        sendByte(INTENS, 1)
        sendByte(SHUT, 1)
        for a = 1, 8 do
            sendByte(a, 0)
        end
        lsclk, lmosi, lmiso, lesp = nil, nil, nil, nil
    end

    function M.setIntensity(intensity)
        sendByte(INTENS, intensity)
    end

    function M.clear()
        for i = 1, 8 do sendByte(i, 0) end
    end

    function M.shutdown(sd)
        local sdRg
        sdRg = sd and 0 or 1
        sendByte(SHUT, sdRg)
        sdRg = nil
    end

    M.sb = sendByte
    return M

    Применять.
    Код (Lua):
    MOSI = 1
    CS = 2
    SCLK = 3
    -- MISO = 5
    ESP = 'esp32s3' --  или 'esp32', тогда править ноги выше
    max7219 = require('_max7219md')
    font = require('font7seg')

    -- function M.setup(esp, mosi, sspin, sclk, miso)
    max7219.setup(ESP, MOSI, CS, SCLK)
    max7219.clear()
    max7219.setIntensity(0)
    SCLK, MOSI, MISO, CS, ESP = nil, nil, nil, nil, nil

    WriteMAX = function(s)
        local d = {}
        local dc = 0
        for i = 1, #s do
            dc = font.GetChar(string.sub(s, i, i))
            if dc == 0x80 then
                if #d == 0
                then
                    d[1] = dc
                else
                    d[#d] = d[#d] + 0x80
                end
            else
                d[#d + 1] = dc
            end
        end
        if #d < 8 then
            for c = 1, 8 - #d do table.insert(d, c, 0x00) end
        end
        for k = 1, 8 do
            max7219.sb(9 - k, d[k])
        end
        if #d > 8 then
            dat.buff = d
        end
    end

    ------


    WriteMAX('21:23 000')

    Фонт в приложении.
     

    Вложения:

    • font7seg.zip
      Размер файла:
      638 байт
      Просмотров:
      9
  12. ИгорьК

    ИгорьК Гуру

    upload_2026-3-17_11-28-15.png
    Датчик качества воздуха PMS9003M для pm2.5 применяется в бытовых воздушных фильтрах, например Xiaomi. У меня будет работать в часах.
    При включении датчик входит в активный режим и начинает сыпать в UART данные. У датчика, кроме UART, есть нога отправки в сон и нога перезагрузки.

    Наработка MTTF по даташиту 15000 часов, что чуть меньше двух лет. Он, конечно, не сравняется с SPS30, у которого заявлено время работы (Life Time) 10 лет, но тоже неплохо.

    В сон датчик можно отправлять командой с UART, поэтому ногу сна можно не задействовать. Если включать датчик на 30 секунд через четверть часа, то, глядишь, проработает больше двух лет.

    Полагаю правильным после подачи питания переключать датчик в пассивный режим, чтобы отправлял данные только по запросу, так и реализовано.

    Итак, модуль. Если на ESP32 памяти не хватает (фигасе!), то в модуле много разбора ответов по UART от датчика, их все можно выкинуть.
    Код (Lua):
    -- pms9003mm.lua - модуль для работы с датчиком PMS9003M на ESP32 (Lua 5.3)
    local mn ='['.. ... .. ']'
    local M = {}
    local prt = PRT or print
    -- UART ID и параметры
    local UART_ID = UART or 2
    local BAUDRATE = 9600

    local TX_PIN = TX or 17
    local RX_PIN = RX or 16
    TX, RX = nil, nil

    -- Переменные парсера
    local data_buffer = {}

    -- Команды Сенсору
    local COMTB = {}
    COMTB.active = "\x42\x4D\xE1\x01\x00\x01\x71"
    COMTB.passive = "\x42\x4D\xE1\x00\x00\x01\x70"
    COMTB.data = "\x42\x4D\xE2\x00\x00\x01\x71"
    COMTB.sleep = "\x42\x4D\xE4\x00\x00\x01\x73"
    COMTB.wake = "\x42\x4D\xE4\x00\x01\x01\x74"

    -- Инициализация UART
    function M.init(tx, rx)
        TX_PIN = tx or TX_PIN
        RX_PIN = rx or RX_PIN
        uart.stop(UART_ID)
        uart.setup(UART_ID, BAUDRATE, 8, uart.PARITY_NONE, uart.STOPBITS_1, { tx = TX_PIN, rx = RX_PIN })
        uart.start(UART_ID)
        uart.on(UART_ID, "data", 1, function(data)
            M.parseByte(string.byte(data))
        end, 0)
        prt(mn, "UART initialized for PMS9003M")
    end

    -- Парсер одного байта
    function M.parseByte(byte_val)
        if #data_buffer >= 32 then data_buffer = {} end
        if #data_buffer == 0 and byte_val == 0x42 then
            data_buffer[1] = byte_val
            return
        end

        if #data_buffer == 1 then
            if byte_val == 0x4D then
                data_buffer[2] = byte_val
            else
                data_buffer = {}
            end
            return
        end

        if #data_buffer >= 2 then
            data_buffer[#data_buffer + 1] = byte_val
            if #data_buffer >= 8 and #data_buffer == data_buffer[4] + 4 then
                M.processCompleteFrame(data_buffer)
            end
        end
    end

    -- Обработка полного кадра данных
    function M.processCompleteFrame(buf)
        if buf[4] == 28 then -- Длина кадра для данных
            M.processSensorData(buf)
        else
            M.processCommandResponse(buf)
        end
    end

    -- Обработка данных измерений
    function M.processSensorData(buf)
        -- Проверка контрольной суммы
        local checksum_received = buf[31] * 256 + buf[32]
        local checksum_calculated = 0
        for i = 1, 30 do
            checksum_calculated = checksum_calculated + buf[i]
        end

        if checksum_received ~= checksum_calculated then
            prt(mn, "!!! Data frame checksum mismatch" )
            return
        end

        -- Чтение значений частиц
        local pm1_0_cf1 = buf[5] * 256 + buf[6]   -- PM1.0 (CF=1)
        local pm2_5_cf1 = buf[7] * 256 + buf[8]   -- PM2.5 (CF=1)
        local pm10_cf1  = buf[9] * 256 + buf[10]  -- PM10  (CF=1)

        -- local pm1_0_atm = buf[11] * 256 + buf[12] -- PM1.0 (атмосферные условия)
        -- local pm2_5_atm = buf[13] * 256 + buf[14] -- PM2.5 (атмосферные условия)
        -- local pm10_atm  = buf[15] * 256 + buf[16] -- PM10  (атмосферные условия)

        -- Вызов пользовательского callback'а (если задан)
        if M.onDataReceived then
            M.onDataReceived({
                pm1_0_cf1 = pm1_0_cf1,
                pm2_5_cf1 = pm2_5_cf1,
                pm10_cf1 = pm10_cf1,
                -- pm1_0_atm = pm1_0_atm,
                -- pm2_5_atm = pm2_5_atm,
                -- pm10_atm = pm10_atm,
            })
        end
        data_buffer = {}
    end

    -- Обработка ответа на команду
    function M.processCommandResponse(buf)
        -- Проверка контрольной суммы для ответа
        local checksum_received = buf[7] * 256 + buf[8]
        local checksum_calculated = 0
        for i = 1, 6 do
            checksum_calculated = checksum_calculated + buf[i]
        end

        if checksum_received ~= checksum_calculated then
            prt(mn, "!!! Command response checksum mismatch")
            return
        end

        -- Анализ ответа
        local command = buf[5]
        local response_data = buf[6]

        if command == 0xE1 then
            -- Ответ на команду смены режима
            if response_data == 0x00 then
                prt(mn, "response: Set to Passive Mode")
            elseif response_data == 0x01 then
                prt(mn, "response: Set to Active Mode")
            else
                prt(mn, "!!! response: Unknown mode response")
            end
        elseif command == 0xE4 then
            -- Ответ на команду сна
            if response_data == 0x00 then
                prt(mn, "response: Sleep activated")
            else
                prt(mn, "[!!! response: Unknown response")
            end
        elseif command == 0xE2 then
            -- Ответ на запрос данных (обычно данные приходят отдельно)
            prt(mn, "!!! response: Data request acknowledged")
        else
            prt(mn, string.format("!!! PMS9003M unknown command response: 0x%02X", command))
        end
        data_buffer = {}
    end

    function M.Ask(com)
        uart.write(UART_ID, COMTB[com])
    end
    return M

    Как опрашивать. Загружаем модуль, инициируем UART, вытаскиваем датчик из сна, даем прочихаться 30 секунд, запрашиваем данные, отправляем датчик в сон, выгружаем модуль.
    Данные помещаются в глобальную таблицу "wth":
    Код (Lua):
    -- Подключение модуля
    local prt = PRT or print
    local mn = '[_Sample_pms9003m]'
    if not wth then wth = {} end

    local wtmr = tmr.create()
    local worktb = { { 1000, 'wake' }, { 30000, 'data', }, { 1000, 'sleep' } }
    local pointer = 1
    PMS = require("_pms9003mm")
    -- Установка функции обратного вызова
    PMS.onDataReceived = function(data)
        prt(mn, "Received new data!")
        for k, v in pairs(data) do
            wth[k] = v
        end
    end
    -- Инициализация UART
    PMS.init(17, 16)
    wtmr:alarm(worktb[pointer][1], tmr.ALARM_SEMI, function(t)
        prt(mn, pointer, worktb[pointer][2])
        PMS.Ask(worktb[pointer][2])
        pointer = pointer + 1
        if pointer > #worktb then
            t:stop(); t:unregister(); t = nil
            package.loaded._pms9003mm = nil
            PMS = nil1
        else
            t:stop()
            t:interval(worktb[pointer][1])
            t:start()
        end
    end)
    wtmr:start()


    -- По умолчанию работает в активном режиме – данные будут поступать автоматически каждые ~2 секунды
    -- PMS.Ask('passive')
    -- PMS.Ask('data')
    -- PMS.Ask('active')
    -- PMS.Ask('sleep')
    -- PMS.Ask('wake')

    При старте устройства переключим датчик в пассивный режим и отправим в сон:
    Код (Lua):
    do
        local mn = '[__pms9003SetSleep]'
        local prt = PRT or print
        local setpassive = true
        local txpin = TX or 17
        local rxpin = RX or 16
        local UARTT = UART or 2
        local init = function(txpin, rxpin, uarrt)
            uart.stop(UARTT)
            uart.setup(UARTT, 9600, 8, uart.PARITY_NONE, uart.STOPBITS_1, { tx = txpin, rx = rxpin })
            uart.start(UARTT)
            prt(mn, "UART " .. UARTT .. " initialized for PMS9003M")
        end

        init(txpin, rxpin, UARTT)

        tmr.create():alarm(1000, tmr.ALARM_AUTO, function(t)
            if setpassive then
                uart.write(UARTT, "\x42\x4D\xE1\x00\x00\x01\x70") -- Passive mode
                setpassive = false
                prt(mn, "PMS9003M At Passive Mode")
            else
                uart.write(UARTT, "\x42\x4D\xE4\x00\x00\x01\x73") -- Sleep
                prt(mn, "PMS9003M Go Sleep")
                t:stop(); t:unregister(); t = nil
                uart.stop(UARTT)
            end
        end)
    end
     
     

    Вложения:

    • PMS9003M.pdf
      Размер файла:
      954,8 КБ
      Просмотров:
      12
    Последнее редактирование: 19 мар 2026
    serg3295 нравится это.
  13. ИгорьК

    ИгорьК Гуру

    Про печать отладочной информации из отдельных модулей, отдельных мест кода.

    Суть явления.

    Есть код, состоящий из модулей. Пока их мало, пользовался такой конструкцией для вывода отладочной информации.

    Код (Lua):
    -- Объявляю глобальную функцию
    PRT = function(...) if DEBUG then print(...) end end

    -- В модуле определяю:
    local prt = PRT or print
    prt(data1, data2)
    Теперь, если глобально "DEBUG = true" идет вывод из данных, если нет DEBUG - вывода нет.

    Однако с ростом числа модулей (в часах их нынче рабочих 47 штук, а если считать малые файлы имен погоды, html окон настроек - еще штук 60!) количество потенциальной ненужной в моменте информации уже напрягает и запутывает. Сделал так.

    Глобально определяю:
    Код (Lua):
    PRT = function(...)
        if not DEBUG then return end
        local args = {...}
        if #args == 0 then return end
        local module_name = args[1]
        if type(DEBUG) == "string" then
            if type(module_name) == "string" and module_name == DEBUG then
                print(...)
            end
        elseif DEBUG then
            print(...)
        end
    end
    В любом модуле вывод данных начинаю стандатртно:
    Код (Lua):
    local mn = "[clock_time]" -- Имя текущего файла/модуля без расширения
    -- в квадратных скобках для удобства чтения
    local prt = PTR or print
    -- ...
    -- ...
    prt(mn, data1, data2)
    Таким образом можно убрать лишний вывод, определив, в данном случае, "DEBUG = '[clock_time]'".
    Вывод будет идти только из модуля "[clock_time]". Если "DEBUG = true" - вывод идет отовсюду, если "DEBUG = nil" - вывода нет.

    Более того, "играя" первым аргументом для печати и значением DEBUG можно очень тонко настраивать вывод отладочной информации и вне зависимости от имен модулей.

    sample.png

    :)
     
    Последнее редактирование: 9 июл 2026
  14. ИгорьК

    ИгорьК Гуру

    SPS30 по i2c.
    Ранее был код работы с модулем по UART. Нынче по i2c, и... написан оный ChatGPT. Для перевода модуля в режим i2c нужно прижать ногу 4, что рядом с GND к этой самой GND.

    Код (Lua):
    --==========================================================
    -- sps30.lua
    --
    -- Sensirion SPS30
    -- ESP32 / NodeMCU Lua 5.3
    --
    -- I2C Software interface
    --
    -- SDA = GPIO16
    -- SCL = GPIO17
    -- Address = 0x69
    --
    -- Result:
    -- global table "wth"
    --==========================================================

    local sps30   = {}
    local I2C_ID  = i2c.SW
    local SDA_PIN = 16
    local SCL_PIN = 17
    local ADDRESS = 0x69

    --==========================================================
    -- Global result table
    --==========================================================

    wth = wth or {}

    --==========================================================
    -- CRC-8
    --
    -- Polynomial : 0x31
    -- Init       : 0xFF
    -- Final XOR  : 0x00
    --==========================================================

    local function crc8(b1, b2)
        local crc = 0xFF
        local data = { b1, b2 }
        for i = 1, 2 do
            crc = bit.bxor(crc, data[i])
            for j = 1, 8 do
                if bit.band(crc, 0x80) ~= 0 then
                    crc = bit.bxor(bit.lshift(crc, 1), 0x31)
                else
                    crc = bit.lshift(crc, 1)
                end
                crc = bit.band(crc, 0xFF)
            end
        end
        return crc
    end

    --==========================================================
    -- I2C initialization
    --==========================================================

    function sps30.init()
        local speed = i2c.setup(I2C_ID, SDA_PIN, SCL_PIN, i2c.SLOW)
        if not speed or speed == 0 then
            print("SPS30: I2C init ERROR")
            return false
        end
        print("SPS30: I2C initialized, " .. speed .. " Hz")
        return true
    end

    --==========================================================
    -- Write pointer / command
    --
    -- IMPORTANT:
    -- SPS30 does NOT support repeated START.
    -- Therefore this function always ends with STOP.
    --==========================================================

    local function write_command(command, data)
        local hi = bit.rshift(command, 8)
        local lo = bit.band(command, 0xFF)
        i2c.start(I2C_ID)

        if not i2c.address(I2C_ID, ADDRESS, i2c.TRANSMITTER) then
            i2c.stop(I2C_ID)
            print("SPS30: I2C device not found")
            return false
        end
        i2c.write(I2C_ID, hi, lo)
        if data ~= nil then
            i2c.write(I2C_ID, data)
        end
        i2c.stop(I2C_ID)
        return true
    end

    --==========================================================
    -- Read command
    --
    -- SPS30 protocol:
    --
    -- START
    -- ADDRESS + WRITE
    -- COMMAND MSB
    -- COMMAND LSB
    -- STOP
    --
    -- START
    -- ADDRESS + READ
    -- DATA
    -- STOP
    --
    -- NO repeated START!
    --==========================================================

    local function read_command(command, length)
        local hi = bit.rshift(command, 8)
        local lo = bit.band(command, 0xFF)

        ----------------------------------------------
        -- Set pointer
        ----------------------------------------------
        i2c.start(I2C_ID)
        if not i2c.address(I2C_ID, ADDRESS, i2c.TRANSMITTER) then
            i2c.stop(I2C_ID)
            print("SPS30: write address NACK")
            return nil
        end

        i2c.write(I2C_ID, hi, lo)
        -- VERY IMPORTANT:
        -- SPS30 requires STOP here
        i2c.stop(I2C_ID)

        ----------------------------------------------
        -- Read response
        ----------------------------------------------

        i2c.start(I2C_ID)
        if not i2c.address(I2C_ID, ADDRESS, i2c.RECEIVER) then
            i2c.stop(I2C_ID)
            print("SPS30: read address NACK")
            return nil
        end
        local data = i2c.read(I2C_ID, length)
        i2c.stop(I2C_ID)
        return data
    end

    --==========================================================
    -- Start measurement
    --
    -- Command 0x0010
    --
    -- 0x03 = Big-endian IEEE754 float
    -- 0x00 = dummy
    -- CRC  = CRC(03 00) = AC
    --==========================================================

    function sps30.start()
        local data = string.char(
            0x03,
            0x00,
            crc8(0x03, 0x00)
        )
        print(
            string.format(
                "SPS30: START, CRC = 0x%02X",
                crc8(0x03, 0x00)
            )
        )
        return write_command(
            0x0010,
            data
        )
    end

    --==========================================================
    -- Stop measurement
    --==========================================================

    function sps30.stop()
        return write_command(
            0x0104
        )
    end

    --==========================================================
    -- Data Ready
    --
    -- Response:
    --
    -- byte 0 = 0x00
    -- byte 1 = 0x01 when ready
    -- byte 2 = CRC
    --==========================================================

    function sps30.data_ready()
        local data = read_command(
            0x0202,
            3
        )
        if data == nil then
            return false
        end

        if #data ~= 3 then
            print("SPS30: Data Ready wrong length = " .. #data)
            return false
        end

        local b1 = string.byte(data, 1)
        local b2 = string.byte(data, 2)
        local crc = string.byte(data, 3)

        if crc8(b1, b2) ~= crc then
            print(
                string.format(
                    "SPS30: Data Ready CRC ERROR " ..
                    "(%02X %02X -> %02X, received %02X)",
                    b1,
                    b2,
                    crc8(b1, b2),
                    crc
                )
            )
            return false
        end
        return b2 == 0x01
    end

    --==========================================================
    -- Read one IEEE754 float
    --
    -- SPS30 sends Big-Endian IEEE754
    --==========================================================

    local function read_float(data, position)
        local value = string.unpack(
            ">f",
            data,
            position
        )
        return value
    end

    --==========================================================
    -- Read measurement
    --
    -- Command 0x0300
    --
    -- Float format = 60 bytes
    --
    -- Each float:
    --
    -- byte 0
    -- byte 1
    -- CRC
    -- byte 2
    -- byte 3
    -- CRC
    --
    -- 6 bytes per value
    -- 10 values = 60 bytes
    --==========================================================

    function sps30.read()
        local data = read_command(
            0x0300,
            60
        )
        if data == nil then
            print("SPS30: measurement read ERROR")
            return false
        end

        if #data ~= 60 then
            print("SPS30: wrong data length = " .. #data)
            return false
        end

        --======================================================
        -- Verify CRC and remove CRC bytes
        --======================================================

        local raw = {}
        local raw_pos = 1
        local data_pos = 1

        for value = 1, 10 do
            ------------------------------------------------
            -- First two bytes
            ------------------------------------------------

            local b1 = string.byte(data, data_pos)
            local b2 = string.byte(data, data_pos + 1)
            local crc1 = string.byte(data, data_pos + 2)
            local calculated1 = crc8(b1, b2)

            if calculated1 ~= crc1 then
                print(
                    string.format(
                        "SPS30: CRC error at byte %d: " ..
                        "%02X %02X -> %02X, received %02X",
                        data_pos, b1, b2, calculated1, crc1
                    )
                )
                return false
            end

            raw[raw_pos] = string.char(b1)
            raw[raw_pos + 1] = string.char(b2)
            raw_pos = raw_pos + 2

            ------------------------------------------------
            -- Second two bytes
            ------------------------------------------------

            local b3 = string.byte(data, data_pos + 3)
            local b4 = string.byte(data, data_pos + 4)
            local crc2 = string.byte(data, data_pos + 5)
            local calculated2 = crc8(b3, b4)

            if calculated2 ~= crc2 then
                print(
                    string.format(
                        "SPS30: CRC error at byte %d: " ..
                        "%02X %02X -> %02X, received %02X",
                        data_pos + 3, b3, b4, calculated2, crc2
                    )
                )
                return false
            end
            raw[raw_pos] = string.char(b3)
            raw[raw_pos + 1] = string.char(b4)
            raw_pos = raw_pos + 2
            data_pos = data_pos + 6
        end

        raw = table.concat(raw)

        --======================================================
        -- Decode values
        --======================================================

        local p = 1
        local keys = { "pm1_0", "pm2_5", "pm4_0", "pm10", "nc0_5", "nc1_0", "nc2_5", "nc4_0", "nc10" }

        for i = 1, #keys do
            wth[keys[i]] = math.floor(read_float(raw, p) * 10 + 0.5) / 10
            p = p + 4
        end
        wth.typical_particle_size = read_float(raw, p)
        return true
    end

    --==========================================================
    -- Start fan cleaning
    --
    -- Command 0x5607
    --==========================================================

    function sps30.clean()
        print("SPS30: fan cleaning")
        return write_command(0x5607)
    end

    --==========================================================
    -- Set automatic cleaning interval
    --
    -- 604800 = 7 days
    --==========================================================

    function sps30.set_cleaning_interval(seconds)
        local b1 = bit.band(bit.rshift(seconds, 24), 0xFF)
        local b2 = bit.band(bit.rshift(seconds, 16), 0xFF)
        local b3 = bit.band(bit.rshift(seconds, 8), 0xFF)
        local b4 = bit.band(seconds, 0xFF)
        local crc1 = crc8(b1, b2)
        local crc2 = crc8(b3, b4)
        local data = string.char(
            b1,
            b2,
            crc1,
            b3,
            b4,
            crc2
        )
        return write_command(0x8004, data)
    end

    return sps30
     
  15. ИгорьК

    ИгорьК Гуру

    Вариант применения:

    Код (Lua):
    if not sps30 then
        sps30 = require("sps30")

        sps30.init()
        sps30.start()
    end

    local readsps = function(t)
        print('\n-----------------')
        if sps30.data_ready() then
            if sps30.read() then
                print("PM1.0 =", wth.pm1_0)
                print("PM2.5 =", wth.pm2_5)
                print("PM4.0 =", wth.pm4_0)
                print("PM10  =", wth.pm10)
            end
        end
        if STOP then
            t:stop()
            t:unregister()
            t = nil
        end
    end

    readsps()

    tmr.create():alarm(3000, tmr.ALARM_AUTO, readsps)
     

    Проверено. Работает с ESP32 уверено.