Создание своего модуля

Инструкция по созданию собственного и уникального модуля для IoTManager от Mitchel

Общие правила

Модули находятся по пути IoTManager\src\modules\, а именно:

  • в папке sensor – Сенсоры (датчики)
  • в папке exec — Исполнительные устройства
  • в папке virtual — Виртуальные элементы
  • в папке display – Экраны

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

  • Исполняемый файл *.cpp (например AnalogAdc.cpp) названные по наименованию модуля/папки.
  • Обязательный файл описания модуля modinfo.json

Сторонние библиотеки подключаются в файле modinfo.json (см. ниже) или помещаются непосредственно в папку модуля.

Исполняемый файл

Класс модуля должен быть унаследован от IoTItem.
Работа с GPIO
При необходимости работать с GPIO, следует Подключить extern IoTGpio IoTgpio;
Для получения данных с GPIO в коде вызвать функцию IoTgpio.analogRead(_pin);
Для возможности выбора пина GPIO из веба необходимо определить переменную “pin” в modeinfo.json и в конструкторе класса считать данную переменную.
Для получения параметров из веба необходимо запросить параметр:

_pin = jsonReadInt(parameters, "pin"); // читается параметр pin и копируется в переменную _pin

Объявление переменных

В секции private описываем переменные, используемые при работе класса (по аналогии с Arduino переменные используемые в функциях setup и loop)
и переменные загружаемые в/из веб (Конфигурации модуля).
Например unsigned int _pin; можно использовать для установки и получения из веб значения Пина установленного пользователем.

Значение датчика

У модуля есть основной параметр Value – значение отображаемое в веб интерфейсе.
Для получения этого значения в коде программы используется функция String getValue()
Для изменения/сохранения основной параметр Value используются функции:

  • setValue(const String& valStr, bool genEvent = true);
  • regEvent(const String& value, const String& consoleInfo, bool error = false, bool genEvent = true);
  • regEvent(float value, const String& consoleInfo, bool error = false, bool genEvent = true);

Учитывая что модель работы IotManager событийная, без появления события не будет возможности обработать в сценарии значения модуля.
Для появления события на изменение параметра в модуле необходимо как минимум однократно вызвать regEvent или setValue со значением genEvent = true (если genEvent не передавать в функцию то по умолчанию событие будет true)

Основные функции родительского класса модуля

  • String getID() — получение идентификатора данного модуля (параметр id)
  • virtual String getValue() – получение значения данного модуля (параметр val)
  • long getInterval() – получение интервала для данного модуля (параметр int)

Их можно вызвать у класса родителя IoTItem в любом месте выполнения кода.

Вывод в лог

Для осуществления вывода в окно лога на веб страницу (так же дублируется в сериал порт)
SerialPrint(«i», F(«ModuleName»), «User set default value: » + val);
Первый параметр тип сообщения, может быть «i» – информационный, «E» — ошибка (сообщение выделено красным).
Второй параметр – указывается наименование модуля.
Третий параметр – непосредственно сообщение.

Конструктор класса модуля

Служит для инициализации пользовательского модуля/класса и подключаемых библиотек. Аналог setup из arduino, для небольших модулей сюда можно попробовать просто перенести код из функции setup при написании модуля на основе рабочего кода в Arduino.
Функция конструктор пользовательского класса должна быть унаследована от IoTItem и принимать параметры строку, куда будет приниматься строка с параметрами из веба

ExampleModule(String parameters) : IoTItem(parameters)

Для получения параметров из веб необходимо запросить параметр с определенным именем (должны быть заданы в modeinfo.json)

_pin = jsonReadInt(parameters, "pin"); // читается параметр pin и копируется в переменную _pin

Все параметры хранятся в перемененной parameters, вы можете прочитать любой параметр используя возможности jsonRead функции:

  • jsonReadStr – вернет значение параметра в виде строки,
  • jsonReadBool — вернет значение параметра в виде true|false,
  • jsonReadInt — вернет значение параметра в виде числа.

Основные параметры модуля:

  • интервал опроса (int)
  • идентификатор (id)
  • значение (val)

Считывать их не обязательно. Всегда можно получить у класса родителя IoTItem в любом месте выполнения кода соответствующими функциями:  

  • String getID() — получение идентификатора данного модуля (параметр id)
  • virtual String getValue() – получение значения данного модуля (параметр val)
  • long getInterval() – получение интервала для данного модуля (параметр int)

Периодическое выполнение кода модуля

В эти функции в случае простых модулей можно попробовать вставить код просто из функции loop на основе рабочего кода в Arduino
Вариант 1: функция doByInterval

Основной метод реализации периодически повторяемых действий. Это аналог loop из arduino, но вызываемый каждые int секунд заданные в настройках (параметр «int» настраиваемый в вебе и описанный в modeinfo.json).
Здесь Вы должны выполнить чтение вашего сенсора, получение данных от датчика. Не нужно задумываться о задержках delay или проверках таймера millis. Код в данной функции выпоняется периодически вызовом из ядра IoTManager.
Для регистрации события обновления данных датчика необходимо вызвать функцию
regEvent(value.valD, «exapmleVal»); с указанием переменной где хранится полученное значение датчика и имени параметра датчика (описывается в modeinfo.json)

Здесь так же доступны все переменные из секции переменных, и полученные в setup, если у сенсора несколько величин то делайте несколько regEvent.
Не используйте delay — помните, что данный loop общий для всех модулей. Если у вас планируется длительная операция, постарайтесь разбить ее на части и выполнить за несколько тактов

Вариант 2: функция loop

полный аналог loop() из arduino. Нужно помнить, что все модули имеют равный поочередный доступ к центральному loop(), поэтому, необходимо следить за задержками в алгоритме и не создавать пауз.
Кроме того, данная версия перегружает родительскую, поэтому doByInterval() отключается. При необходимости можно в loop сделать расчет интервалов и обеспечить явный вызов периодических действий doByInterval().

 void loop() {

   //здесь код пользователя 

//Ниже Блок вызова функции периодических действий

       currentMillis = millis();

       difference = currentMillis - prevMillis;

       if (difference >= getInterval()) {

           prevMillis = millis();

           this->doByInterval();

       }

   }

Обработка команд сценария

Для обработки команд, вызываемых в сценарии необходимо переопределить функцию execute.
Внутри функции проверяется имя команды и происходит обработка параметров при необходимости. Параметров может быть несколько.
Используемые команды должны быть описаны в файле modeinfo.json (см. соответствующий раздел)
В сценарии вызывать команды по имени (например, command2(11,”test”);)

 IoTValue execute(String command, std::vector<IoTValue> ¶m){
        if (param.size() > 0) {
            if (command == "command1"){
                if (param.size()){
                    SerialPrint("i", F("ModuleName"), "User set default value: " + param[0].valS);
                }
            }
            else if (command == " command2"){
                if (param.size()){
                    SerialPrint("i", F("ModuleName"), "User set default value: " + param[0].valS + param[1].valS);
                }
            }
        }
        return {};
    }

Обработка кнопок из конфигурации модуля

Для добавления кнопки в блок конфигурации на страницу в веб интерфейсе, необходимо прописать её в файле modeinfo.json см. соответствующий раздел
Также необходимо переопределить функцию onModuleOrder.
В данной функции проверяется имя кнопки приписанной в modeinfo.json без префикса «btn-»
value – значение введенное в поле рядом с кнопкой

void onModuleOrder(String &key, String &value){
        if (key == " Calibration ") //имя кнопки «btn-Calibration»{
//пользовательский код по нажатию кнопки
        }
    }

Деструктор класса

Функция деструктор модуля, вызывается в случае удаления модуля из конфигурации

 ~ExapmleModule(){};

Функция создания модуля

Функция пользовательского модуля для его обработки в ядре IoTManager обязательная для инициализации модуля из ядра.
В данной функции непосредственно создаются объекты модуля по их имени (subtype) описанные в файле modeinfo.json

void* ExapmleModule(String subtype, String param) {

   if (subtype == F("ExapmleModule ")) {

       return new ExapmleModule(param);

   } else {

       return nullptr;

   }

}

Файл описания модуля

На основе описания файла modeinfo.json также формируется справка на сайте iotmanager.org

Блок типа модуля

В блоке «menuSection» определяется тип модуля, могут быть «Сенсоры», «Исполнительные устройства», «Экраны», «Виртуальные элементы»

Блок элементов модуля

Блок «configItem» определяет элементы данного модуля, может быть несколько см. п. Особенности реализации модулей
Каждый элемент может содержать следующие стандартные параметры которые будут отображаться в вебе при конфигурации, справа имя параметра, слева значение по умолчанию.
Также можно добавлять свои пользовательские параметры для возможности их конфигурирования на веб странице

«global»: 0, — Будет ли данный элемент виден в сети другими Юнитами (устройствами IotManager) (1 – виден всем IoTManager устройствам, 0 – не виден никому)
«name»: «BME280 Температура», — Наименование элемента модуля, обычно наименование датчика и параметра датчика если их несколько (например температура, давление, влажность и т.д.)
«type»: «Reading», — Тип элемента (УТОЧНИТЬ)
«subtype»: «Bme280t», — Краткое наименование элемента (подтип), обычно наименование датчика и параметра датчика если их несколько (например температура, давление, влажность и т.д.)
«id»: «Tmp», — Идентификатор элемента модуля (используется например в сценариях). Здесь задается только префикс, ядром IoTManager будет добавлено в конец случайное число. При конфигурации идентификатор должен быть уникальный
«widget»: «anydataTmp», — Тип виджета для отображения в вебе или приложении. Возможные виджеты определены в файле data_svetle/widgets.json. Там же можно добавить свои виджеты
«page»: «Сенсоры», — Имя блока на веб странице или страница в приложении. Служит для объединения отображения модулей на странице
«descr»: «Температура», — Описание элемента модуля
«int»: 15, — Интервал выполнения периодической функции doByInterval
«round»: 1, – Количество отображаемых знаков после запятой (возможные варианты 0,1,2,3 – количество знаков после запятой)
«needSave»: 0, — Указывает необходимость сохранять в энергонезависимую память основное значение Value данного элемента модуля, 1- сохранять. 0- не сохранять
«map»: «1,1024,1,100», — Преобразовать (промапить) основное значение Value исходной от 1 до 1024, преобразовать в значения от 1 до 100
«plus»: 0, — Смещение. Прибавить к основному значению Value указанное число (число может быть отрицательным)
«multiply»: 1, — Множитель. Умножить основному значению Value на указанное значение, Если необходимо поделить, то указать число обратное. Т.е. для деления на 100, необходимо указать множитель 0.01.
«btn-defvalue»: 0, — Кнопка отображаемая в конфигурации модуля с возможность задать значение для обработки, указано значение по умолчанию. После префикса «btn-» указывается произвольное имя (см. функцию обработки кнопок)
«btn-reset»: «nil» — Кнопка отображаемая в конфигурации модуля без передаваемых значений. После префикса «btn-» указывается произвольное имя (см. функцию обработки кнопок)

Блок описания модуля

Общее описание

  «defActive»: false, — Параметр указывающий входит ли данный модуль в прошивку по умолчанию (Используется при формировании настроек сборки проекта myProfile.json)

Блок   «about» – содержит описание модуля

"about": {
    "authorName": "NAME", - Имя автора
    "authorContact": "https://t.me/@NAME", - Контакт автора
    "authorGit": "https://github.com/NAME", - Ссылка на GitHub автора
    "specialThanks": "NAME", - отдельное спасибо другим разработчикам
    "moduleName": " MyModule ",
    "moduleVersion": "0.1", - Версия данного модуля
    "usedRam": { - Память занимаемая данным модулем в платах (узнается экспериментальным методом)
    "esp32_4mb": 15,
    "esp8266_4mb": 15
    },
    "title": "ЗАГОЛОВОК", - Заголовок Модуля
    "moduleDesc": "ОПИСАНИЕ", - Описание модуля.
    "retInfo": "Дополнительная ИНФОРМАЦИЯ", - Дополнительная информация по данному модулю

Блок описания элементов

"propInfo" - Блок описания параметров всех элементов данного модуля. Здесь разработчик описывает пользовательские параметры добавленные в в блоке configItem
"propInfo": {
   "pin": "Укажите GPIO номер пина для чтения состояний подключенной кнопки",
}

Блок описания пользовательских функций

"funcInfo" – Блок описания пользовательских функций вызываемых в сценарии
    "funcInfo": [
      {
        "name": "get", - Наименование функции, вызов в сценарии например get(“http…”)
        "descr": "Отправить http запрос методом GET.", - Описание функции
        "params": [ - Наименование параметра передаваемого в функцию
          "URL"
        ]
      },
      {
        "name": "post", - Наименование функции, вызов в сценарии например post (“http…”, “ok”)
        "descr": "Отправить http запрос методом POST.", - Описание функции
        "params": [ - Наименование параметров передаваемого в функцию
          "URL","message"
        ]
      }
    ]
  },

Блок подключаемых внешних библиотек

usedLibs — Блок подключаемых внешних библиотек. При подключении библиотек очень желательно указывать её версию, так как при обновлении PlatformIO перезагрузит библиотеку и не факт, что разработчики там ничего не переделали.
При подключении нескольких библиотек к модулю они указываются через запятую.
Так же здесь указывается для каких плат данный модуль проверен и работает. Если плата не указана, то в данной сборе модуль участвовать не будет даже если его выбрать в настройках проекта myProfile.json
Пример, если НЕТ сторонних библиотек

  "usedLibs": {
    "esp32_4mb": [],
    "esp8266_4mb": [],
    "esp8266_1mb": [],
    "esp8266_1mb_ota": [],
    "esp8285_1mb": [],
    "esp8285_1mb_ota": [],
    "esp8266_2mb": [],
    "esp8266_2mb_ota": []
  }
}

Пример, если используются библиотеки зарегистрированные в PlatformIO

   "usedLibs": {
        "esp32_4mb": [
            "openenergymonitor/EmonLib@1.1.0"
        ],
        "esp8266_4mb": [
            "openenergymonitor/EmonLib@1.1.0"
        ]
    }

Пример, если используются библиотеки c GitHub

    "usedLibs": {
        "esp32_4mb": [
            "https://github.com/JonasGMorsch/GY-21.git"
        ],
        "esp8266_4mb": [
            "https://github.com/JonasGMorsch/GY-21.git"
        ]
    }

Пример подключения нескольких библиотек

    "usedLibs": {
        "esp32_4mb": [
            "https://github.com/robotclass/RobotClass_LiquidCrystal_I2C",
            "marcoschwartz/LiquidCrystal_I2C@^1.1.4"
        ]
}

Особенности реализации модулей


IotManager представляет собой модульную систему поэтому при разработке мудулей необходимо придерживаться следующих правил.
Если датчик имеет несколько параметров, например, может выдавать отдельно или вместе температуру, влажность и т.д., то на каждый параметр датчика необходимо создавать отдельный элемент модуля,
а именно несколько блоков configItem и на каждый элемент датчика необходимо описать свой класс в исполняемом файле.
Как пример датчик Bme280

Метод использования библиотек


Так как запрос в одну библиотеку (при подключении сторонней) происходит из различных классов модуля (Класс для элемента температуры, класс для элемента влажности и т.д.),
то для реализации использования библиотеки и её единичного подключения и инициализации, предлагается использовать следующее:

1. Определить глобальный указатель на библиотеку, например

SensirionI2CScd4x *scd4x = nullptr; // create an object of the CSD40 class

2. Определить глобальную функцию инициализации библиотеки

// Функция инициализации библиотечного класса, возвращает единственный указать на библиотеку
SensirionI2CScd4x *Scd4x_instance()
{
   if (!scd4x)
   { // Если библиотека ранее инициализировалась, т о просто вернем указатель
       // Инициализируем библиотеку
//Здесь описываем всё, что нужно для инициализации библиотеки
// обычно что прописано в функции Setup() в Arduino
       scd4x = new SensirionI2CScd4x();
       Wire.begin();
       scd4x->begin(Wire);
   }
   return scd4x;
}

3. Использовать данную функцию можно в коде любого элемента модуля не задумываясь о том есть ли другой элемент в конфигурации и кто первый из них вызвал библиотеку и её проинициализировал

//Запрашиваем библиотеку 

       int valT = Scd4x_instance()->getTemp ();

4. Так как выше предложенная функция и указатель определяются как глобальные (до каких-либо классов, после определения подключаемых файлов #include), то необходимо их имена указывать уникальными (по наименованию модуля) для избегания конфликтов с другими модулями.
При использовании несколько одинаковых датчиков использующих одну библиотеку и разделенные по адресам (например i2c) можно применить map для хранения нескольких экземпляров библиотек.
См. Пример в модулях bme280, bmp280, Pzem.

Поддержал проект — спас молодого самодельщика! А мы принимаем подарки...

X