Skip to main content

#полоумныйдом ESP8266 OTA, обновление без регистрации и смс ;)

Краткое (не очень) описание похода на поводу у собственной лени и желания чего-нибудь автоматизировать прямо вот сейчас и здесь, дома, с помощью esp8266, nodemcu и нескольких часов свободного времени.

Для начала, вводные:

  • в квартире есть несколько разных типов сенсоров (в розетке или с автономным питанием на базе LiFePo4, с датчиками bme280, am230x и/или ds18b20), которые в итоге делают совершенно одинаковые вещи: подключаются по Wi-Fi к домашней сети, получают адрес по DHCP, собирают информацию с сенсоров, передают информацию на MQTT-брокер, есть радикально другой тип датчиков, никакого MQTT, суровый UDP с самодельным гейтом в MQTT-брокер – этот подвид в данной статье не рассматривается, там нет Lua и nodemcu
  • на все датчики установлена прошивка Nodemcu-firmware, с нужным набором модулей (для всех одна, вне зависимости от набора датчиков)
  • вся инфрастуктура или уже есть, или же находится в процессе инсталляции по мере поступления дензнаков и наличия свободного времени (SCADA-система, подобие HMI, элементы управления на импульсных реле, PLC, прочие штуки, которые мне хотелось попробовать, но все никак не доходили руки)
  • написана условная “прошивка”, пускай это будет называться нашим микрокодом, на Lua, который успешно “раскатан” по всем устройствам (в той или иной итерации)
  • ключевым недостатком решений-поделок для “умного” дома на базе ESP8266 (ну и ESP32, я помаленьку поглядываю и в эту сторону), на мой взгляд, является отсутствие какого-либо централизованного управления микрокодом
  • замена ESP8266 на что-либо не предполагается и суть не рассматривается, ибо цена и доступность являются решающими факторами в выборе поделок для сенсоров, которые не учавствуют в управляющих цепях, то есть не имеют прямой обратной связи с освещением, кондиционированием, приточно-вытяжными устройствами и прочими устройствами, которые способны нарушить мой комфорт буквально физически
  • OTA, в рассматриваемом контексте, я лично называю процесс замены всех *.lua файлов на устройстве с некоторым набором дополнительных проверок
  • я не программист, выбор Lua и nodemcu по сути случаен, оптимальный код – не мой конек, увы

В очередной раз, глянув в пол-глаза на чудовищную поделку под названием “это-написал-я-два-месяца-назад-и-этот-ужас-нужно-срочно-исправить”, ощютил небывалый прилив сил и сподобился-таки починить то, что и так (неплохо) работало. Внезапно понял, что лезть в кладовку за датчиком, цеплять к нему клемы, выкатывать т.н. обновление вот прямо сейчас и еще в течении месяца мне мешает лень, причем дикая. А таких датчиков, как оказалось, уже десяток. Опасаясь очередного озарения, решил действовать проактивно и накатал подобие OTA-обновления, используя костыли вкупе с ранее полученными познаниями в процессе самостоятельного хождения по грабелькам и подчерпнутой из интернетов информацией. Вышло, в общем-то, неплохо, для моих целей. Странно, что удобоваримых чужих наработок с наскоку не нашел, да и ладно (видимо, это слишком очевидная задача).

Собственно, для самых нетерпеливых весь код сложен в одном месте.

Итого, имеем следующий набор кое-как склеенного в единообразную кучку кода. Рассматривать предлагаю только по существу, в рамках предложенной темы обновления микрокода “по воздуху”, без использования чего-то материального (ну за исключением ноутбука на столе, мы-то обсуждаем лень и рациональность использования).

Микрокод состоит из нескольких функциональных частей, нарезан на “кусочки” (с чисто практической пользой, прошу заметить, ниже по тексту будет одна маленькая деталь).

init.lua вызывает user.lua после заданного тайм-аута, данный атавизмъ взят из примера, суть не требуется для нормальной работы. user.lua, в свою очередь, последовательно исполняет набор файлов, из которых нам интересны не только лишь все.

credentials.lua (загружен для примера credentials.lua.example, его надо переименовать и прописать свои настройки)
    web_server = "a.b.c.d"

Адресок web-сервера, где лежат наши файлы для обновления

variables.lua
    need_update = false
    _, reset_reason = node.bootreason()
    -- 0 poweron 6 extreset (nodemcu esp-12) 2 reset button
    if reset_reason == 0 or reset_reason == 6 or reset_reason == 2 then -- TODO check poweron status!!!
        need_update = true --wifi.lua
    elseif ((reset_reason == 5) or (reset_reason == 4)) then
        need_update = false
    end

В сухом остатке – проверяем, собственно, а как мы оказались в таком положении (то есть – нам известен текущий статус загрузки), и если на то есть причины – взводим флажок статуса обновления.

wifi.lua
if need_update then
        ota()
    else
        sensor_read()
        mqtt_connect()
    end
ota.lua

web_server_url = “http://”..web_server..”/ota/”..sensor_client_id..”/” – меняем на свой вариант, где, собственно, у нас лежат подкаталоги с обновлениями для каждого устройства.

Функция ota() запускает таймер, который раз в 400 мс. (подобрано эмпирическим путем “тычком-пальцем-в-небо”) запускает процедуру опроса ota_loop(), которая, в свою очередь, циклично проверяет, а чем мы, собственно, сейчас заняты, и продвигается дальше по мере необходимости:

        if ota_success then
            ota_index=1
            while ota_index <= #ota_remote_file_list do
                file.remove(ota_remote_file_list[ota_index][1])
                file.rename(ota_remote_file_list[ota_index][1]..".new",ota_remote_file_list[ota_index][1])
                file.remove(ota_remote_file_list[ota_index][1]..".new")
                ota_index=ota_index+1
            end
            print(ota_status)
            tmr_ota:stop()
            tmr_ota:unregister()
            node.restart()
        elseif ota_error then
            print(ota_status)
            tmr_ota:stop()
            tmr_ota:unregister()
            node.restart()
        else
            if status_need_version then ota_get_version() end
            if status_need_filelist then ota_get_filelist() end
            if status_next_file then ota_get_file() end
        end

ota_success применит нашу прошивку и безоговорочно перезагрузит датчик, ota_error просто вернет систему к искомому состоянию до начала обновления.

Сам принцип nodemcu – все на коллбэках, обратных вызовах по завершению, ограничения модулю http, в конце-то концов заставили меня сделать вот такой вот огород с цикличным опросом. Следующая правильная итерация – это создание пула запросов, исполняемого последовательно. Но это позже.

function ota_get_version() получаем version.txt и сверяем версии, локальную и удаленную, запускаем дальнейший рок-н-ролл по мере необходимости:

            if version == remote_version then
                print("OTA version : "..remote_version.." no update needed")
                ota_error = true
                ota_status = "OTA no update needed"
            else
                print("OTA version : "..remote_version.." need update")
                status_need_version = false
                status_need_filelist = true
            end
        else
            ota_error = true
            ota_status = "OTA version.txt not avail (code = "..code.." )"
            status_need_version = false
        end

function ota_get_filelist() получаем filelist.txt, искомый список файлов для обновления, файл генерируется автоматически для каждого устройства скриптом esp_scripts/send_ota_fw.sh, содержит два поля – собственно имя файла и контрольную сумму sha1.

            for remote_filename in string.gmatch(data,'[^\r\n]+') do
                ota_remote_file_list[ota_index] = split(remote_filename,",")
                ota_index = ota_index + 1
            end
            status_need_filelist = false
            status_next_file = true
            ota_index = 1

После получения файла файлов со списком файлов настало время легкого (не так – мутного) порожняка, из-за которого и появился таймер 😉

function ota_get_file() получает требуемый файл обновления, кладет его на файловую систему, сверяет контрольную сумму и делает это столько раз, сколько там мы вычитали из filelist.txt. Теоретически, недоступность файла, сервера обновления и-или иные причины приведут лишь к тому, что мы продолжим работать со старой прошивкой в течении нескольких секунд.

        status_next_file = false
        if  ota_index <= #ota_remote_file_list then -- TODO #ota_remote_file_list
            local filename_local = ota_remote_file_list[ota_index][1]..".new"
            local filename_remote = ota_remote_file_list[ota_index][1]
            http.get(web_server_url..ota_remote_file_list[ota_index][1],nil,function(code,data)
                if code == 200 then
                    local local_file = file.open(filename_local,"w+")
                    local_file:write(data)
                    local_file:flush()
                    local_file:close()
                    if not (crypto.toHex(crypto.fhash("sha1",(filename_local))) == ota_remote_file_list[ota_index][2]) then
                        print(filename_local.." checksum error")
                        ota_error = true
                        ota_status = "OTA "..filename_remote.." checksum error"
                        return
                    end
                    ota_index=ota_index+1
                    status_next_file = true
                else
                    ota_error = true
                    ota_status = "OTA "..filename_remote.." not avail (code = "..code..")"
                end
            end)
        else
            ota_success = true
            ota_status = "OTA all done"
        end

В целом – все просто, за исключение одного важного нюанса – больше 4 килобайт в GET-запрос впихнуть не получается. Невелика беда, не лень и нарезать на кусочки, но имейте в виду.

Далее все становится еще проще – запускается скриптик на bash, esp_scripts/send_ota_fw.sh , который генерирует три файла: version.txt, filelist.txt и device_settings.lua, по scp все это “щастье” переправляется в искомую папку на сервере обновлений. Это можно делать и руками, и приблудой с web-мордой на flask – как угодно, не имеет значения. Его надо малость допилить и использовать по назначению, а в целом сейчас стоит задача получения набора данных с MQTT-сервера – где будет лежать и тип сенсоров, подключенных к датчику, то есть первое подключение к брокеру всегда однозначно будет определять, с чем имеем дело, и в этом случае можно попросту отказаться от выделенных папок для каждого отдельно взятого устройства. В ближайших планах (это вынужденная мера, да) – надо “допилить” нотификацию о необходимости обновления.