История про то как добавить терминалу (Uni-Ubi) то, чего у него не было от рождения с помощью M5Stack на ESP32?

Uni-Ubi и M5Stack
Uni-Ubi и M5Stack

В наши ковидные времена стало прилетать все больше вопросов про термометрию и бесконтактную биометрию. Не смотря, на то, что до недавнего времени мы, как и многие ИТ-компании с удовольствием не касались этой темы, считая её больше СБ-шной, волею обстоятельств вопрос-таки пришел и к нам. Не прямым путем, но пришлось вникнуть.

Являясь подрядчиком одной из выставочной компании по сетевым делам, нас периодически привлекают к совершенно непрофильным, на первый взгляд, задачам. Видимо, руководствуясь мнением, что все, включаемое в локальную сеть – это к айтишному подрядчику.  
Итак, перед очередной выставкой «умных технологий и IoT» от наших выставочников поступило ТЗ, которые мы сразу даже рассматривать не стали. Однако клиент настаивал и пришлось взяться.

Задача формулировалась так: необходимо было усовершенствовать, имеющуюся у них, систему контроля температуры на базе терминалов биометрического распознавания лиц, добавив явную визуальную индикацию в виде светодиодной ленты. При этом индикация на ленте должна зависеть от температуры распознаваемого объекта.

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

На первый взгляд задача показалась более чем простой – замкнуть в терминале реле №1, если температура нормальная и замкнуть реле №2, если температура повышенная. Белую LED линейку можно просто разрывать при подаче питания на реле 1 или реле 2.

Но, как водится, если вы обрадовались простоте задачи, — ждите подвоха. Так и вышло.

Когда к нам привезли на пробы терминал доступа по лицу (им оказался Uni-Ubi Uface 8T-Temp) мы поняли, что рано радовались.
…у него оказался ровно ОДИН! релейный выход. И вот это было фиаско, рубившее на корню всю идею быстро что-то прикрутить, быстро сдать и при этом чего-то заработать.

К самому терминалу 8t-Temp по итогам первичных ознакомительных тестов у нас вопросов не было. Это достаточно популярная штука c болометром «во лбу». По субъективным впечатлениям очень быстрый, интерактивный, лицо ловит метров с двух – достаточно мимо проходя чуть попасть его поле зрения. Температуру измеряет тоже достаточно точно, контроль наличия маски имеет. Сценарии на реле также отрабатывает четко. Но нашей задачи это не решает. Следовательно, надо лезть внутрь и разбираться как можно развязаться логически с помощью внешних устройств.

Изучаем пациента и ищем варианты!

Пришедшая к нам выставочная конструкция представляла из себя следующий набор:
- Блок автономного питания терминала с аккумуляторами на много часов работы
- Металлическая трубостойка, прикрученная к блоку автономного питания
- Собственно сам терминал Uface 8T-Temp.

Вдоль трубостойки по всей высоте была нехитрым образом приклеена зеленая LED- подсветка с принципом работы, простым как палка – она загоралась по замыканию реле терминала, только если температура нормальная, а если нет – не загоралась. В принципе это вполне рабочая схема, минусом является только то, что когда на входе в зал стоит таких стоек не одна, а 20, то издали не понятно почему терминал не дает прохода посетителю: то ли он сам «залип», то ли температура неправильная, то ли индикация отвалилась.
Иначе говоря, вы подходите к терминалу, он вас измеряет, обнаруживает, что у вас повышенная температура и на экране выдается не очень приметная надпись о том, что «у вас повышенная температура». При этом с индикацией ничего не происходит. То есть, если вы идете быстро, а контролер на входе отвернулся в этот момент, то вы спокойно проходите мимо, и никто этого не заметит. Вероятно, наши заказчики это вполне осознали и стали срочно искать варианты более понятной визуализации события.

Первое что, что пришло в голову как-то повлиять на сам терминал и получить желаемый логический вариант ресурсами его процессора. Под капотом терминала Uni-Ubi стоит проц ARM Cortex-A7. Принципиально его бы хватило на что угодно, но ввиду самозамкнутости и защищенности изделия никакого доступа к его логике извне нет. Терминал работает на базе Linux, поверх которого крутится их собственное Юни-Юбишное проприетарное ядро распознавания лиц, GUI и контроль обвеса, включая интерфейсы. Все закрыто и забито гвоздями от любопытствующих, поэтому исполнить свой код внутри терминала не представлялось возможным от слова совсем.

К счастью, у Юни-Юби оказался крайне обширный API (есть и SDK). Он полностью открытый и в нем есть описания примерно на 99% процедур и событий, обрабатываемых на терминале.

Таким образом задача сводилась к тому, чтобы изучить API, получить описания требуемых переменных, получаемых по call-back ответам на запросы и далее и далее введя параметры оценки, полученных значений отработать их по оговоренному сценарию.

Принципиально это достаточно простая задача, даже для начинающего программера. И мы решились попробовать.

Оставалось выбрать то, на чем будем генерировать запросы, собственно запрашивать и обрабатывать полученное в ответ от терминала. Первое, что пришло в голову, — это конечно платка на базе Arduino, но мы пока не понимали, где должно крепиться готовое устройство, поэтому выбор склонялся в сторону чего-то в культурном собственном корпусе. Времени и желания на конструирование корпуса у нас не было.

Посмотрев на разные варианты было решено использовать продукт на базе микроконтроллера ESP32 как достаточный с запасом и гибко программируемый. Ввиду, того, что нужно было сделать всё быстро и аккуратно, без торчащих проводов, мы применили модули на ESP-32 от M5Stack. Аргументом в их пользу стала цена ниже Raspberry PI, очень простая соединяемость без пайки и наличие собственного корпуса у большинства девайсов этого «конструктора для взрослых».

Компоненты системы:

Компоненты проекта
Компоненты проекта

Обмен данными между терминалом и контроллером:

Терминал Uni-Ubi имеет несколько проводных интерфейсов для отправки данных, среди них был выбран, на мой взгляд, самый простой – Wiegand. С него в обычном случае мы получаем коды лица (карты) и пр., но получим ли мы с него переменные по температуре -- это вопрос. Идея первоначально была в том, чтобы либо сразу после измерения температуры получать значения температуры на Wiegand вместе с FaceID автоматом, либо инициировать это действие по Json запросу от M5Stack по некой синхронизации с событием измерения температуры.

Чтобы, понять, что мы получаем на Wiegand терминала по запросу необходимо посмотреть на посылку данных при распознавании лица и найти там сведения о температуре. Для это я написал простенькую программы вывода в serial получаемой информации:

Подключение M5Stack к Uni-Ubi по Wiegand
Подключение M5Stack к Uni-Ubi по Wiegand
#include <Wiegand.h>
#include <M5Stack.h>

WIEGAND wg;

void setup() {
  Serial.begin(115200); //Инициализируем сериал порт
  wg.begin(2, 5); //подключенные Wiegand D0 и D1 к пинам 2, 5
  M5.begin();
}

void loop() {
  if (wg.available()) //Если по Wiegand пришла посылка
  {
    Serial.print(wg.getCode()); //выводим ее в Serial
  }
}

Тут я столкнулся с проблемой, в посылке присутствовал только идентификационный номер клиента. В настойках терминала нельзя добавить дополнительную информацию для передачи по Wiegand. Однако у Uni-Ubi есть встроенный модуль реле, настроив его на срабатывание при отрицательном результате распознавания, система будет скомплексирована. Таким образом при получении данных по Wiegand и по срабатывании реле можно однозначно определить какая индикация будет на светодиодной ленте. Правда это выглядит не как оптимальное решение, а как «колхоз», да и реле может в дальнейшем понадобиться для других задач, например, если терминалы будут врезаны в выставочные турникеты.

Web-server на M5Stack и получение POST-запросов:

От идеи с Wiegand пришлось отказаться и отдать предпочтения сильным сторонам ESP32, а именного встроенному модулю Wi-Fi, благо в терминале Uni-Ubi WIFI модуль тоже есть. Осталось лишь настроить их взаимодействие.

Отметим, что в дальнейшем, при массовом внедрении, мы все же перешли на проводное подключение, добавив в конструкцию сетевое основание M5Stack LAN-W5500. Так безусловно надежнее и секурнее, но на этапе отладки идеи взаимодействие по Wi-Fi не потребовало никаких добавок и поэтому было выбрано «здесь и сейчас».

На M5Stack с помощью дополнительных библиотек достаточно просто настроить веб сервер.

#include <WiFi.h>
#include <M5Stack.h>

const char *ssid = "******";
const char *password = "*******";

boolean flagEmptyLine = true; // признак строка пустая

WiFiServer server(80);
String header;
char clientAlreadyConnected = false;

void setup(void) {
  Serial.begin(115200);
  M5.begin();
  WiFi.begin(ssid, password);
  Serial.println("");

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  server.begin();
  Serial.println("HTTP server started");
}

void loop(void) {
  WiFiClient client = server.available(); // начинаем прослушивать входящих клиентов:
  if (client) { // если подключился новый клиент
    Serial.println("New Client.");
    flagEmptyLine = true;
    String currentLine = ""; // создаем строку для хранения входящих данных от клиента
    while (client.connected()) { // цикл while() пока клиент подключен к серверу
      if (client.available()) { // если у клиента есть данные
        char c = client.read(); // считываем байт
        Serial.write(c);
        header += c;
        if (c == '\n') { //если байт это символ новой строки – отправляем ответ
          if (currentLine.length() == 0) { // + информация о типе контента
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close"); //  "Соединение: отключено"
            client.println(); // конец HTTP-ответа – пустая строка
            break; // выходим из цикла while:
          } else { // если получили новую строку, то очищаем currentLine
            currentLine = "";
          }
        } else if (c != '\r') { // если получили что-то, кроме символа возврата каретки,
          currentLine += c; // добавляем эти данные в конец строки «currentLine»
        }
      }
    }
    header = ""; // очищаем переменную «header»
    client.stop(); // отключаем соединение:
    Serial.println("Client disconnected.");
    Serial.println("");
  }
}

Далее с помощью программы «Postman» настраиваем по API терминал на отправку данных об объекте POST запросом. Необходимо указать локальные адреса и пароль терминала, а также адрес M5Stack веб сервера. Возможность данной настройки прописана в документации на Uni-Ubi.

Настройка термила по API через приложение «Postman»
Настройка термила по API через приложение «Postman»

Смотрим запрос через Serial и пишем парсер.

Нам необходимо получить данные о температуре (1 – нормальная температура, 2 – ненормальная температура, 3 – термометрия отключена). Для этого применим методы очень мощного класса String.

POST запрос на вебсервер M5Stack
POST запрос на вебсервер M5Stack

С помощью метода indexOf(x) (где x = String "temperatureState=") ищем в строке другую строку, при этом метод вернет позицию (номер) первого совпадающего элемента. Далее substring() вернет кусок строки, содержащейся в исходном post запросе , с позиции первого элемента строки "temperatureState=" до позиции "temperatureState="+17. Таким образом функция парсера вернет нужное нам значение температуры.

int parser(String post){
	String temp = "temperatureState=";
	int info = post.substring(post.indexOf(temp), post.indexOf(temp)+17).toInt();
	return info
}

Теперь необходимо, на основе данных из функции parser, выводить анимацию на светодиодную ленту, воспользуемся предлагаем M5Stack библиотекой Adafruit_NeoPixel.

#include <M5Stack.h>
#include <WiFi.h>
#include <Adafruit_NeoPixel.h>

#define PIN        21 //управляющий пин ленты
#define NUMPIXELS  288 //кол-во светодиодов в ленте

//установивем количество светодиодов, номер контакта, тип светодиода
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
const char *ssid = "*******";
const char *password = "*******";

boolean flagEmptyLine = true;

WiFiServer server(80);
String header;
char clientAlreadyConnected = false;

int iterator = 0, jterator = 0; // переменные для анимации ленты

void setup(void) {
  Serial.begin(115200);
  M5.begin();
  WiFi.begin(ssid, password);
  Serial.println("");

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  server.begin();
  Serial.println("HTTP server started");

  pixels.begin();
  pixels.setBrightness(10);
}

void loop(void) {
  WiFiClient client = server.available();
  if (client) {
    Serial.println("New Client.");
    flagEmptyLine = true;
    String currentLine = "";
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        Serial.write(c);
        header += c;
        if (c == '\n') {
          if (currentLine.length() == 0) {
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();
            break;
          } else {
            currentLine = "";
          }
        } else if (c != '\r') {
          currentLine += c;
        }
      }
    }
    int info = parser(header);
    Serial.println(info);
    if ( info == 1 ) {
      pixelState(100, 0, 128, 0);
      pixels.clear();
    }
    else if (info == 2) {
      pixelState(100, 100, 0, 0);
      pixels.clear();
    }
    header = "";
    client.stop();
    Serial.println("Client disconnected.");
    Serial.println("");
  }
  // И небольшая анимация для пассивного состояния ленты
  pixels.setBrightness(10);
  pixels.setPixelColor(iterator++, pixels.Color(127, 199, 255));
  pixels.show();
  if (iterator > 10) {
    pixels.setPixelColor(jterator++, pixels.Color(0, 0, 0));
    pixels.show();
  }
  if (iterator == 288)  iterator = 0;
  if (jterator == 288)  jterator = 0;
}

void pixelState(int brightness, int R, int  G, int B) {
  pixels.clear();
  pixels.setBrightness(brightness);
  for (int n = 0; n < NUMPIXELS; n++) {
    pixels.setPixelColor(n, pixels.Color(R, G, B));
    pixels.show();

  }
}

int parser(String post) {
  String temp = "temperatureState=";
  int info = post.substring(post.indexOf(temp), post.indexOf(temp) + 17).toInt();
  return info;
}

Наконец-то с кодом покончено и можно проверить систему в действии.

Индикация при нормальной температуре
Индикация при нормальной температуре

Разумеется, этот проект — достаточно простой для ESP32 и здесь можно многое доработать. Тем не менее, здесь я описал интересный и реальный кейс добавления нового функционала к биометрическому терминалу.

Хочется сказать несколько слов относительно использованного M5Stack. Свет клином на нем, конечно, не сошелся, более того, для нас это была лотерея, где мы купились на внешний вид и законченность изделий. В принципе, для решения нашей задачи подошел бы любой менее производительный контроллер. Однако, М5-й оставил самые позитивные впечатления. Если вам нужно «соорудить» что-то быстрое как бутерброд — за 5 минут и с гарантированным результатом, то M5Stack – это очень неплохой выбор (как минимум по сравнению с другими аппаратными платформами по типу Arduino и NodeMCU). Ничего не надо паять, никаких макетных плат — воткнул/стэкировал и оно работает. Поэтому, в будущих проектах и кастомных решениях, мы будем использовать возможно именно его.

Дополнительным аргументом «ЗА» можно считать наличие сотен совместимых модулей — датчики, исполнительные устройства, моторы, реле и всё, что только можно представить и внедрить в проекты.

Осталось только всё собрать.
Осталось только всё собрать.

Комментарии (0)