Нет предела совершенству. Казалось бы, все работало хорошо, исправлялись мелкие баги и прочее.
Сейчас я расскажу, во-первых, о проблемах, с которыми столкнулся за все время, прошедшее с момента предыдущей статьи, и, во-вторых, о решениях, которые способствовали текущему статусу проекта.
→ Статья про предыдущую версию
Обозначения
bobaos
— npm модуль для взаимодействия с BAOS 83x с помощью UART. Возвращает сырые данные. Используется во всех остальных модулях, перечисленных ниже.
bdsd.sock
— скрипт для работы с KNX объектами. Хранит список датапоинтов, при отправке/получании преобразует значения. Из DPT1 в true/false, из DPT9 — во float. Также слушает Unix Socket для приема запросов от других процессов.
bobaos.pub
— новая версия, использующая redis
для межпроцессного взаимодействия.
KNX объект/датапоинт
— настроенный в ETS объект связи модуля BAOS 83x, которому соответствует(или нет) групповой адрес(а). В текущих версиях железа максимальное количество 1000.
Задача
Основная задача та же, что решала и предыдущая версия. Соединение с серийным портом можно открыть только одно. Скриптов, работающих с KNX, хочется запустить много. В довесок к этому, захотелось реализовать межпроцессное взаимодействие. Т.е. чтобы не только один процесс bdsd.sock
слушал сокет, а каждый запущенный скрипт мог как отправлять, так и принимать запросы.
Идея
В голове родилась идея сделать свой брокер сообщений на node.js поверх юникс сокетов, к которому подключались бы клиенты, подписывались на топики и получали/отправляли сообщения в соответствии с прописанным в них кодом. Я знал, что есть уже готовые решения, про которые в последнее время только ленивый не слышал, изучал, но идея сделать решение свое была навязчивой.
И вот в итоге сервис запущен.
Написал логгер, который сообщения отправляет в топик. Подписчики получают и вольны делать все что угодно, а точнее — что прописано. Удобно — логи с нескольких источников можно смотреть в одном консольном выводе.
Написал, опубликовал в npm пакет bobaos.pub, который, в отличие от bdsd.sock, уже не создает сокет файл, а подключается к брокеру. На первый взгляд все работает как надо.
Проблема
Дальше запустил скрипт, периодически отправляющий запросы в шину KNX, на ночь. Проснувшись утром, по миганию светодиодов, сигнализирующих отправку/передачу данных, понял, что что-то не так. Сообщения не доходили вовремя. Обнаружил, что самописный брокер сообщений занял практически всю доступную из 512МБ(у BeagleBoard Black) оперативной памяти. Дальнейшая работа с nodejs подтвердила, что память — слабое место место js скриптов.
Решение
В результате было принято решение перейти с самописных Unix сокетов на Redis(к слову, он тоже умеет работать с ними). Возможно, стоило разобраться с памятью, найти утечки, но хотелось быстрее запустить.
bobaos подразумевает общение по UART с оборачиванием сообщений в FT1.2, коммуникация у нас синхронная. Т.е. <..сообщение-подтверждение-ответ..>
. Модуль bobaos, ответственный за коммуникацию, хранит все запросы в массиве, выдергивает оттуда по очереди, отправляет в UART, и при входящем ответе разрешает промис, ответственный за этот запрос.
Можно пойти следующим путем: сервис слушает PUB/SUB канал redis, принимает запросы, отправляет в KNX. В этом случае нагрузка на очередь запросов ложится на js модуль bobaos
. Для реализации надо написать простой модуль, подписанный на канал и преобразующий сообщения методом JSON.parse()
. Далее этот модуль можно использовать в остальных скриптах.
Другой вариант, на котором в итоге остановился: использовать существующий менеджер задач поверх redis
. Существует несколько, выбор сделал на bee-queue
.
Тут описано каким образом работает очередь bee-queue
. Если реализовать эту библиотеку для других языков программирования, то можно таким образом сделать клиентские библиотеки и для bobaos
.
Во втором варианте все запросы хранятся в списках redis
, выдергиваются по очереди и отправляются на серийный порт.
Далее, следует переписывание предыдущей версии, но уже все данные по датапоинтам храню в redis
базе данных. Единственное неудобство, которое испытываю — все запросы асинхронны, соответственно, получить массив значений уже чуть тяжелее, чем просто обратиться к массиву.
Были сделаны небольшие оптимизации.
Если раньше были раздельные методы getValue/getValues/readValue/readValues/setValue/setValues
, то теперь getValue/readValue/setValues
принимают как одно значение, так и массив.
Метод getValue([id1, id2, ...])
в прошлой версии отправлял для каждого датапоинта запрос в серийный порт. Но ведь есть возможность отправить запрос для нескольких значений. Ограничения — размер ответа должен быть равен BufferSize
, максимальный — 250 байт; также нельзя выходить за пределы количества объектов, для текущих версий модулей BAOS 83x — 1000.
Длины значений известны, заголовка тоже. Далее достаточно простой алгоритм с циклами while и await-ами =)
- Сортируем массив, удаляем повторяющиеся элементы, если есть. получаем массив
idUniq
. - Запускаем цикл
i < idUniq.length
, в котором делаем следующее:
а) Беремstart: idUniq[i]
, для него считаем максимальное количество значений, которое можем получить. Для примера, если все объекты имеют тип DPT1/DPT5(1 байт), то можем получить значения в количестве 48. Тут есть одно замечание: если, для примера, у нас в ETS настроены объекты#[1, 2, 3, 10, 20]
, то при запросеGetDatapointValue.Req(1, 30)
в ответ будут возвращены нулевые однобайтные значения даже для несуществующих датапоинтов[4, 5, 6, ..., 30]
.
б) Подсчет происходит в новом циклеj < i + max
(гдеmax
— 50, либо, если близко к 1000, то максимально1000 - id + 1
, для 990 это будет 11, для 999 — 2), если в процессе подсчета встречаем элементы массива из исходного запроса, то переменнойi
присваеваем его индекс.
в) Если в циклеj
расчетная длина выходит больше максимальной длины буффера, то формируем элемент "карты" запросов{start: start, number: number}
, закидываем в отдельный массив, увеличиваем переменнуюi
(либо присваиваем индекс найденной в массивеidUniq
), прерываем циклj
, оба цикла запускаются заново.
Таким образом, формируем несколько запросов для bobaos
. Для примера, если отправить запрос getValue([1, 2, 3, 40, 48, 49, 50, 100, 998, 999, 1000])
, то запросы могут быть для частного случая следующие:
{start: 1, number: 48}, // 1, 2, 3, 40, 48
{start: 49, number: 48}, // 49, 50
{start: 100, number: 48}, // 100
{start: 998, number: 3} // 998, 999, 1000
Можно было бы сделать по другому:
{start: 1, number: 48}, // 1, 2, 3, 40, 48
{start: 49, number: 2}, // 49, 50
{start: 100, number: 1}, // 100
{start: 998, number: 3} // 998, 999, 1000
Запросов было бы столько же, данных меньше. Но остановился на первом варианте, поскольку полученные значения сохраняются в базу redis
, соответственно, их можно получить методом getStoredValue
, который я стараюсь использовать чаще, чем getValue
, который отправляет данные по серийному порту.
Для сервисных методов ping/get sdk state/reset
создается отдельная очередь. Таким образом, если с коммуникацией по серийному порту что-то не так(сбился счетчик фреймов, и т.д.) и выполнение остановилось на какой-то задаче, можно послать запрос reset
в другой очереди, и соответственно, перезапустить sdk
.
Клиентская часть — bobaos.sub
Для управления датапоинтами KNX в пользовательских скриптах может быть использован bobaos.sub
модуль.
Следующий пример покрывает все функции модуля:
const BobaosSub = require("bobaos.sub");
// в параметры можно передать опционально:
// redis: объект либо url
// request_channel: "bobaos_req" по умолчанию,
// service_channel: "bobaos_service" по умолчанию,
// broadcast_channel: "bobaos_bcast" по умолчанию
let my = BobaosSub();
my.on("connect", _ => {
console.log("connected to ipc, still not subscribed to channels");
});
my.on("ready", async _ => {
try {
console.log("hello, friend");
console.log("ping:", await my.ping());
console.log("get sdk state:", await my.getSdkState());
console.log("get value:", await my.getValue([1, 107, 106]));
console.log("get stored value:", await my.getValue([1, 107, 106]));
console.log("get server item:", await my.getServerItem([1, 2, 3]));
console.log("set value:", await my.setValue({id: 103, value: 0}));
console.log("read value:", await my.readValue([1, 103, 104, 105]));
console.log("get programming mode:", await my.getProgrammingMode());
console.log("set programming mode:", await my.setProgrammingMode(true));
console.log("get parameter byte", await my.getParameterByte([1, 2, 3, 4]));
console.log("reset", await my.reset());
} catch(e) {
console.log("err", e.message);
}
});
my.on("datapoint value", payload => {
// Имейте в виду, что payload может быть как объектом, так и массивом
// так что необходима проверка Array.isArray(payload)
console.log("broadcasted datapoint value: ", payload);
});
my.on("server item", payload => {
// Имейте в виду, что payload может быть как объектом, так и массивом
// так что необходима проверка Array.isArray(payload)
console.log("broadcasted server item: ", payload);
});
my.on("sdk state", payload => {
console.log("broadcasted sdk state: ", payload);
});
bobaos.tool
Был переписан интерфейс командной строки. О тот как я его реализовывал следующая статья:
Команды стали короче, понятнее и функциональнее.
bobaos> progmode ?
BAOS module in programming mode: false
bobaos> progmode 1
BAOS module in programming mode: true
bobaos> progmode false
BAOS module in programming mode: false
bobaos> description 1 2 3
#1: length = 2, dpt = dpt9, prio: low, flags: [C-WTU]
#2: length = 1, dpt = dpt1, prio: low, flags: [C-WT-]
#3: length = 1, dpt = dpt1, prio: low, flags: [C-WT-]
bobaos> set 2: 0
20:27:06:239, id: 2, value: false, raw: [AA==]
bobaos> set [2: 0, 3: false]
20:28:48:586, id: 2, value: false, raw: [AA==]
20:28:48:592, id: 3, value: false, raw: [AA==]
Послесловие
Получилась рабочая стабильная система. Redis
в качестве бэкенда работает стабильно хорошо. В ходе разработки было набито немало шишек. Но процесс обучения таков, что иногда это неизбежно. Из полученного опыта отмечу, что nodejs
процессы потребляют довольно много оперативной памяти(20МБ на старте) и могут быть утечки. Для домашней автоматизации это критично — потому как скрипт должен работать постоянно, а если он растет с течением времени все больше, то к определенному моменту он может занять все пространство. Поэтому надо внимательно писать скрипты, понимать как работает сборщик мусора и держать все под контролем.
Обновляющаяся документация может быть найдена здесь.
В следующей статье расскажу о том, как с помощью redis
и bee-queue
сделал сервис для программных аксессуаров.
UPD: bobaoskit — аксессуары, dnssd и WebSocket
Буду рад любой обратной связи.