#полоумныйдом 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