Мы занимаемся сбором данных с ИПУ в многоквартирных домах. Наше предпочтение - проводное подключение. Обычно мы подключаем одинаковые типы счетчиков в один канал - Вода, Газ, Электричество.
Однажды к нам перешел дом, на котором в качестве счетчиков электричества стояли "Меркурий 203.2ТL". Особенность этих счетчиков в том, что они используют в качестве интерфейса опроса - CAN шину. И тут важно уточнить, что именно только CAN шину, ничего более от CAN там нет. То есть, если вы купите преобразователь Ethernet<->CAN по типу USR-CANET200
- работать у вас ничего не будет. Причина в том, что подобные преобразователи используют кадры CAN для общения между устройствами, но разработчики счетчиков "Меркурий 203.2ТL" вертели на своем вертеле все это и просто поставили внутри драйвер CAN<->UART.
Преобразователей, способных просто работать в чистом виде по CAN, не так уж и много.
Скажу честно, мы не любим зависимости, поэтому пошли своим путем. Была приобретена Orangepi Zero, USB<>SerialTTL, TTL<>CAN. Все это было слеплено вместе в один преобразователь Ethernet<>CAN. Сразу скажу, что мы пробовали несколько разных драйверов CAN и на практике они оказались довольно капризными, мы пришли к выводу, что лучше использовать драйвер VD230 с UsbSerialTTL-CH340G. На алике выглядят они так:
![](https://habrastorage.org/getpro/habr/upload_files/083/46d/147/08346d147ae33dc3808966fc34695f57.png)
Что в качестве софта? Самый простой способ - использовать ser2net, демон, который согласно конфигурационному файлу пробрасывает данные c serial порта в сеть.
Но использовать ser2net мне не понравилось, вот почему:
Нельзя быстро посмотреть, доступен ли serial порт (может преобразователь не имеет контакта в USB)
Нельзя быстро поменять порт или внести какие-либо изменения в настройки
Нельзя быстро оценить, ходит ли трафик через него
Нельзя использовать какой-либо middleware для трафика
Были попытки найти готовое решение с интерфейсом. Но проблема в том, что эти решения настолько старые, что просто не запускались.
На тот момент мы решили проблему очень простым приложением на nodejs
и забыли про все это.
Но в последнее время, из-за особенности применяемого оборудования, нам приходится ставить по 4 преобразователя типа RS485/CAN<>Ethernet. Отчасти это связано с использованием счетчиков электричества типа CE102R5.1.
Энергомера это вообще отдельная тема разговора, пожалуй, хуже счетчиков не припомню. Прекрасно понимаю, что CE102R5.1 это урод, созданный по нашему ГОСТу, но легче от этого не становится. CE102R5.1 так капризны в плане выбора преобразователя, что нам пришлось всерьез пересмотреть то, что мы берем на дома. Если раньше мы могли обойтись каким-нибудь 2-х канальным Teleofis или набрать по одному USR-TCP232-304
и использовать вообще любой Китай, то сейчас нам приходится брать WaveShare определенной модели, лишь бы эта сволочь нормально работала.
Как уже писал выше, мы не особо любим зависеть от кого-либо, и в этот раз мы тоже пошли своим путем. Правда в железе этот путь выглядит не очень красиво. Вначале мы взяли пару компов, которые раньше использовались как терминалы касс. Но практика показала, что компы у нас под linux просто физически умирали через день-два работы в непрерывном режиме. В чем была причина разбираться не стали и вернулись к использованию уже проверенных средств.
Взяли Orangepi Zero, USB хаб на 4 порта и преобразователи USB<>RS485. Все это выглядит, мягко говоря, непрезентабельно, зато, как показала практика, оно работает.
Для такой сложной схемы было написано уже более полноценное приложение с WEB Интерфейсом - VGranite.
Что может VGranite
VGranite позволяет из интерфейса:
Добавлять serial порты
Добавлять TCP сервера
Добавлять TCP клиенты
Создавать связи между ними. Одна связь работает только в одну сторону
Например, можно создавать такие схемы:
![](https://habrastorage.org/getpro/habr/upload_files/434/3a5/105/4343a51055ff4c8f2042f8f8ab333cdd.jpg)
Тут используется сервер :4001
для работы с serial портом. Сервер :4002
можно использовать для мониторинга ответов от serial порта. Для этого, правда, придется написать минимальный socket client. Если же мы хотим отслеживать весь трафик - можем завести выход :4001
на вход :4002
.
Если какой-то из элементов нашей внутренней сети не может быть запущен по какой-либо причине - VGranite будет пытаться пересоздать элемент каждые 15 секунд до исправления проблемы.
Также для удобства в интерфейсе можно найти:
Авторизацию (для одного пользователя, по умолчанию admin,admin)
Статусы serial портов/серверов/клиентов
Можно добавлять доп описание
Простые графики активности трафика, как отдельных элементов, так и конкретной связи
Убогая схема соединений (ну что уж есть, зато есть)
В целом, интерфейс рассчитан на мобилки. Мне чаще проще зайти с мобилки и проверить, ходит ли трафик, чем с компьютера, поэтому я стараюсь делать интерфейс, в первую очередь, для мобилки.
Выше я уже писал, что хотелось бы иметь какой-либо middleware для трафика. Зачем это нужно? Если собрать преобразователь Ethernet<>Can по схеме выше, каждый раз, когда вы будете отправлять в serial порт данные - вы будете тут же получать их обратно. Это связано с тем, что драйвер, по сути, принимает те же данные, что и отправляет. В VGranite для Serial порта есть такой параметр, как Except request, который исключает посылку из полученных данных.
Еще бывает полезно дождаться получения данных, а не сразу отдавать данные с serial порта. В настройках serial порта есть возможность включить ожидание и установить таймаут.
В целом по функционалу - пока все. Такое вот простое, но полезное для меня приложение.
Установка
Инструкция по установке на англ. можно найти в readme.
Очень важная цель, которую я ставил перед собой - простая установка. Единственное условие - nodejs > 16v. У nodejs, как по мне, сейчас есть серьезные проблемы с установкой на Ubuntu, поэтому VGranite проверялся на работу в среде bun, и вполне успешно. Также был запущен на MacOS и Windows, есть предположение, что будет работать и на Andoid/Termux.
Проще всего ставить его на Ubuntu.
Скачиваем последний релиз
Распаковываем в папку
/opt/vgranite
Запускаем внутри
npm install --only=production
Ну и можем проверить работу приложения
npm run start
Чтобы VGranite запускался при старте системы:
cp /opt/vgranite/vgranite.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable vgranite
systemctl start vgranite
После запуска будет доступен интерфейс по адресу:
http://host:4000 - Web interface - login data - Login:admin Password:admin
http://host:3999 - API doc - VGranite работает полностью через API. При желании вы можете автоматизировать создание преобразователей и тп.
На этом официальная часть статьи закончена. Если вы просто хотите попробовать - этого будет вполне достаточно.
Как работает VGranite
Когда я опубликовал первую статью про VRack и попросил знакомых оценить ее - мне сказали, что VRack это какой-то недоумный дом. Такое недопонимание расстроило меня, в целом они ничего не поняли. Люди видят визуальную схему и думают, что под ней находится визуальный редактор, и все это работает по типу NodeRed.
Но VRack вообще не про это. VGranite хороший пример, как можно использовать концепции VRack для создания сервис-приложения.
Когда я смотрю исходники, например, того же NodeRed - то вижу, в основном, портянки кода с кучей вложенности, которые очень сложно разобрать и понять что в какой момент будет происходить. Не удивительно, ведь какой-нибудь MVC не особо подходит для таких приложений. Мои попытки систематизировать код для работы в событийно-ориентированном стиле привели меня к концепциям, которые я начал использовать в VRack.
Мне намеренно не хотелось смотреть на готовые решения. Цель была создать что-то, что я раньше не видел. Получилось! Но получилось так, что внешне это очень похоже на типичную Flow систему, но работающую абсолютно другим образом.
Во Flow системах обычно используются ноды, у которых есть вход и выход. В основе ноды обычно лежит функция, которая выполняется по приходу данных, а результат отправляется на выход.
В VRack вместо нод используются абстракции в виде устройств, у которых может быть сколько угодно портов, они динамически могут быть добавлены во время запуска сервиса. Устройство может запретить запуск сервиса если, например, необходимый порт не будет подключен, и иметь подобную дополнительную логику.
В основе кода устройств лежат классы. Это очень важно, ведь классы могут полноценно наследоваться и хранить состояние. После запуска сервиса, устройства живут своей жизнью. Они могут выполнять работу сами, хранить состояние и реагировать на входящие данные. У них нет никаких ограничений, они могут использовать любые методы, сторонние библиотеки и т.п. По сути, каждое устройство после запуска является маленьким сервисом, размер которого, на самом деле, архитектурно неограничен.
Также у устройств могут быть порты, которые могут возвращать значение. То есть одно (или несколько устройств) могут использовать другое устройство как сервис, который после обработки полученных данных возвращает результат.
Давайте глянем на фактическую схему работы VGranite, пожалуйста не пугайтесь, сейчас я все объясню:
![](https://habrastorage.org/getpro/habr/upload_files/02a/3f3/a7b/02a3f3a7bb046af361f8cc037c7cffc7.jpg)
Такая схема формируется из файла сервиса, в котором указаны все запускаемые устройства, их параметры и связи между ними. Удобно, когда ваш сервис может быть представлен в виде схемы на уровне кода, не правда ли?
HttpServer
Начнем с HttpServer
. Вот пример как выглядит добавление в файл сервиса:
{
"id": "HttpServer", // Идентификатор
"type": "vgranite.HttpServer", // Класс устройства (чей экземпляр будет создан)
"options": { // Опции определеяет автор устройства, они проходят валидацию и тп
"port": 4000, // Порт, на котором будет работать сервер
// Перечисляем какие выходы мы хотим использовать (их можно найти на схеме)
"requestPorts": ["guard", "users", "converters","netserv","netclients", "serials", "serial", "connector", "database"],
"routes": [
{ "method": "post", "path": "/users/login", "port": "users.request" },
{ "method": "get", "path": "/net/servers/markers", "port": "netserv.request" },
// ... Список всех роутов
]
}
}
Каждый раз, когда пользователь отправляет запрос в HttpServer
, происходит проверка пути, поиск подходящего роута и push
соответствующего порта с ожиданием результата, который потом будет возвращен пользователю. Что-то типа:
try {
const result = await this.ports.output[route.port].push({ req, res, next })
if (result === undefined) return
this.successResponse(res, result)
} catch (error) {
this.errorResponse(res, error)
}
Устройства, которые хотят в удобном виде отвечать на API запросы, наследуют ApiDevice
, который автоматически преобразует приходящие запросы в вызовы методов типа GETLogin
PUTCreate
и т.п. Как по мне - очень даже удобно и наглядно. Но вас никто ни в чем не ограничивает, вы можете реализовывать это как угодно, это моя личная реализация для конкретного проекта.
Guard
Далее у нас идет устройство Guard
, его конфигурация выгляди так:
{
"id": "Guard",
"type": "vgranite.Guard",
"options": {
"guestAccess": ["/users/login"]
}
}
Из конфигурации должно быть понятно, что он может пропускать гостевые запросы только на путь /users/login
. Еще он подключен к устройству Session
двусторонней связью (обратите внимание на стрелочку связи в обе стороны на схеме) для запроса у Session
состояние переданного в запросе токена.
ApiUsers
Его конфигурация:
{
"id": "ApiUsers",
"type": "vgranite.ApiUsers",
"options": {
"username": "admin",
"password": "admin"
}
}
ApiUsers
отвечает за авторизацию. После успешной авторизации ApiUsers
добавляет новую сессию для токена в Session
.
ApiSerials
Далее у нас идут по сути 3 почти одинаковых устройства, мы разберем только одно - ApiSerials
.
Порты этого устройства:
request
- Вход, нужен для обработки Http Api запросов.metric
- Выход, нужен для отправки данных в базу vrack-db для построения графиков.connect
- Вход, получает данные отApiConnector
и отправляет их в Serial порт.connect
- Выход, получает данные от Serial порта и отправляет их вApiConnector
.
Чтобы не писать 2 разных порта одного имени, я просто обернул на схеме в <>
.
ApiConnector
ApiConnector
Занимается тем, что принимает данные и отправляет их согласно внутренней таблице соединений. Через API вы ему говорите - создать соединение между id1 и id2. Даже если этих идентификаторов не существует, он создаст правило и будет ждать, что ему придут данные от id1.
Если какой-то работающий элемент был удален - соединения в ApiConnector
автоматически не удаляются. Можно сделать так, что при удалении зависимого элемента, удаляется его связь, но пока это не реализовано. При условии, что изменять конфигурацию нужно не так часто, пока можно жить и так.
Отличия VRack и flow подхода
VGranit не требует каких-либо визуальных редакторов, имеет минимальный набор зависимостей. Он состоит из конкретных классов, каждый из которых реализует свой необходимый функционал, хранит нужные состояния и выставляет наружу интерфейсы взаимодействия между другими классами. Сервис имеет четкую структуру, которую можно визуально отобразить с помощью генерации.
Мне не встречались flow системы, которые бы работали по такому же принципу. В основном, это все же именно набор функций, веб интерфейс и схемы запуска этих функций. Поэтому на flow системах будет довольно сложно реализовать такой сервис как VGranit (если вообще возможно).
Еще одна из особенностей VRack подхода - хорошая расширяемость без исправления чужого кода.
Обычно, когда проектируется класс-устройство, в нем делаются закладки для его расширения. Например, возьмем устройство ApiDB
, мы видим, что у него есть уже 3 порта для приема метрик. Надо полагать, что порт metric%d
динамический. Если заглянуть в настройки устройства ApiDB
, мы увидим там параметр inputs
, который можно поправить для получения нужного количество входящих портов для отправки метрик в базу.
В VGranit много подобных вещей, например, вы можете добавить в API свое устройство. Для начала, вам нужно будет добавить в массив параметра requestPorts
устройства HttpServer
новое название, например udpserver
.
"requestPorts": ["guard", "users", "converters","netserv","netclients", "serials", "serial", "connector", "database", "udpserver"],
Это добавит новый порт в HttpServer
, к которому можно привязывать роуты:
{ "method": "get", "path": "/udpserver/struct", "port": "udpserver.request" },
Останется только реализовать собственное устройство, в котором будет определен порт request
и добавлен его обработчик с возвращаемым значением.
Концепция VRack позволяет вам не просто расширять сервис, она позволяет в принципе заменять его части. Не так сложно, к примеру, написать устройство HttpsServer
и использовать его вместо HttpServer
, не трогая остальную часть сервиса. Или, например, вы хотите поменять базу данных с VRackDB на ClickHouse - вам нужно будет только написать свой адаптер для метрик, поменять пару строчек в сервис файле и, вуаля, не трогая чужой код, вы выполнили свою задачу.
Хоть внешне подход VRack похож на flow системы, но на практике он очень далек от них.
Я уже давно ищу какой-либо opensource инструмент, который бы мне давал подобные возможности, но, к сожалению, все системы, которые я видел, концептуально работают одинаково - как flow система.
Что я хотел всем этим сказать? Мне кажется, что такой подход в программировании напрасно обходят стороной. Мне хотелось показать, что написанные в таком стиле сервисы вполне жизнеспособны, и VGranite является, к сожалению, одним, но зато публичным подобным примером.
Пожалуйста, если у вас возникнут вопросы - готов ответить на них в комментариях. Я буду очень рад (может не очень конечно) конструктивной критике. Также готов выслушать предложения по улучшению сервиса VGranite.