Показаны сообщения с ярлыком программирование. Показать все сообщения
Показаны сообщения с ярлыком программирование. Показать все сообщения

пятница, 15 декабря 2017 г.

Программа DALI Controller

Для расширения функционала управления освещением квартиры было решено сделать DALI<->GPIO-адаптер и написать собственный сервер.

Почему не использовать уже готовые решения, что должен уметь сервер, подробнее про адаптер и протокол MQTT описано в статье Управление освещением в квартире и другая электрика: техническое устройство (раздел "Устройство сервера").

Здесь описывается устройство и функционал программы (сервера) DALI Controller.

Окружение программы

Сервер должен работать на низкопроизводительном компьютере Raspberry, прослушивать шину DALI и посылать в нее команды через GPIO. В качестве операционной системы выступает Raspbian (один из вариантов Linux для Raspberry). Для удаленного управления сервером поддерживается сетевое подключение.

Выбор инструментов и технологий

Для работы с GPIO в Raspbian существует несколько различных third-party библиотек. В качестве надежной (работа с GPIO по DMA, что резко снижает зависимость от загрузки процессора и прерываний) и продуманной, будет использоваться C-библиотека pigpio (подробнее про нее см. Raspberry Pi 3, pigpio, шина DALI и программа для работы с ней). Причем, в режиме работы - демон (вместо линковки).
Сервер должен поддерживать управление из консоли, командной строки и через MQTT, а также публиковать статус устройств по MQTT.

Сначала ядро программы было написано C, но в какой-то момент я понял, что развитие программы тормозится неудобной организацией межпоточного взаимодействия. Вместо концентрации на функционале программы приходится прилагать много сил, чтобы реализовать это на C. Я решил посмотреть другие варианты и, наконец, попробовать Golang. Это был мой первый подход к языку, и после небольшого исследования возможностей я понял:
  • Go имеет встроенные решения для эффективной многопоточности 
  • на Go можно писать под Raspbian
  • из Go можно вызывать C-код и линковать C-библиотеки
  • Go компилируется в эффективный код
  • для Go существует множество сторонних библиотек

Устройство программы

Сервер состоит из нескольких модулей, которые работают параллельно:
  • прослушивание шины DALI для перехвата команд, отправляемых другими устройствами (выключателями) и ответов на команды. Используется для логирования, отслеживания изменения статусов устройств, запуска настраиваемых на событие JavaScript-скриптов
  • JavaScript-интерпретатор для настраиваемых скриптов
  • MQTT-клиент для получения команд управления и публикации статусов
  • псевдографическая цветная консоль, показывающая лог, список и статусы устройств, прогресс выполнения долгих команд. Имеет поле для ручного ввода команды
  • встроенная помощь по всем командам DALI и другим командам сервера
  • отправка команд в шину DALI (полученных как параметры запуска приложения, через MQTT, ручным вводом в консоли или из JavaScript)
  • дополнительно: модуль опроса устройств DHT-22 (датчик температуры и влажности) с публикацией в MQTT

На снимке экрана видно:
  • лог команд (в скобках указан источник команды: шина DALI, MQTT или JavaScript)
  • иногда возникающие ошибки опроса уличного датчика DHT-22 (слишком длинные провода до него)
  • список устройств с DALI-адресом и статусом. Видно, что часть устройств на шине не имеют имени (они пока не используются)

Это снимок самодельного мобильного приложения, работающего по MQTT. Оно общается с сервером DALI Controller для показа статуса устройств и управления ими.

Работа программы

При запуске программа опрашивает устройства, чтобы выяснить их начальный статус. Затем статус отслеживается путем анализа команд, которые посылаются этому устройству (в том числе путем нажатия настенных выключателей). Любое изменение статуса публикуется по MQTT и все заинтересованные MQTT-клиенты (в т.ч. мобильные приложения) сразу же отражают это изменение у себя.
При управлении (светом) из мобильного приложения (или другого MQTT-клиента), сервер получает команду по MQTT и отправляет ее в шину DALI, отражая, если нужно, изменение статуса.
Консоль программы позволяет, при необходимости, вводить команды вручную. Обычно это требуется при подключении новых устройств для их конфигурирования.


В работе всей этой кухни есть интересная особенность. В арсенале DALI нет команды "переключить светильник". Есть различные варианты "включить" и "выключить", а "переключить" - нет. Это значит, что настенный выключатель должен помнить последнюю команду, которую он посылал, чтобы при следующем нажатии посылать обратную. Да, возможен вариант, когда выключатель отслеживает или опрашивает реальное состояние устройства перед решением, что же ему посылать. Но дешевые выключатели этого точно не делают (про дорогие я не осведомлен).
Представьте картину:
  • включаем свет выключателем (он отправляет команду "вкл" и запоминает факт, что свет включился)
  • выключаем из мобильного приложения
  • хотим снова включить его выключателем, но ничего не происходит. Поскольку выключатель помнит, что свет включен, он пошлет команду "выкл". И чтобы свет все-таки включить, нужно нажать выключатель еще раз (он снова пошлет обратную команду - теперь уже "вкл")
Для устранения этой неприятной ситуации в сервере используется возможность написать JavaScript на событие.
Что делает скрипт:
  • при получении команды по шине DALI запускается скрипт
  • если команда послана настенным выключателем (а в данный момент все команды, пришедшие по DALI, от выключателей) и она включает или выключает свет, то команда сверяется с текущим статусом светильника (его правильный статус помнит сервер)
  • если команда от выключателя пытается "повторить" уже существующее состояние света, но скрипт тут же самостоятельно посылает этому устройству обратную команду
Работа этого скрипта устраняет проблему с нажатие выключателя, при котором ничего не происходит. Эта ситуация реально видна на снимке экрана выше:
10:52:43 on спальня (dali)
10:52:43 off спальня (scr)
первая строка - по шине DALI получена команда "включить свет в спальне". Однако скрипт видит, что свет уже включен, а значит нажатием выключателя требовалось его выключить, и он сам посылает новую команду "выключить свет в спальне". Это вторая команда с источником "(scr)" (что означает JavaScript).

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

Несколько месяцев использования освещения в такой конфигурации, с активным использованием как настенных выключателей, так и мобильного приложения, показывают 100% отсутствие ошибок типа "нажал - ничего не происходит".

четверг, 13 июля 2017 г.

Использование C-библиотек из Golang 1.8 с callback-функцией и cross-компиляция

Выяснил для себя, что писать системные программы на Golang гораздо приятнее, чем на С. Расплата - минимальный размер программы в 1 Мбайт, в отличие от десятков Кбайт на чистом C. Жалко, конечно, но C требует слишком много лишнего в современном программировании.
Поэтому перевожу сервер поддержки протокола DALI и управления освещением (под Raspberry) с C на Go.
Сложности: как я писал ранее, в сервере используется библиотека pigpio. Поэтому нужен вызов C из Golang. Но мало того, нужен callback на Go-функцию из C. Настоящий хардкор - все это хочется в cross-компиляции.

Пример на Go с вызовом C и Go-callback

В исходнике ниже три примера:
1) вызов функции setCallback C-библиотеки extclib из Go
2) передача в С указателя на Go-функцию goCallback
3) создание C-функции myCFunc прямо в Go-исходном файле с последующим вызовом ее из Go

package main
/*
// (1) подключение внешней C-библиотеки к линковке
// (1) объектный файл extclib, скомпилированный под целевую платформу,
// (1) а также extclib.h, должны быть доступны локально (см. ниже
// (1) "Настройка cross-компиляции")
#cgo LDFLAGS: -lextclib
// (1) подключение заголовочного файла внешней C-библиотеки
// (1) в нем описана функция setCallback (см. ниже)
#include <extclib.h>
#include <stdint.h>
// (2) описание callback-функции на Go в C, чтобы можно было передать в C указатель на нее
extern void goCallback(unsigned data);

#ifndef MYCINCL
#define MYCINCL 1
// (3) static inline - необходимо для предотвращения ошибки компиляции
// (3) о повторном объявлении функции myCFunc
static inline void myCFunc(uint32_t data){
// здесь C-код
}
#endif
*/
import "C"


// (1) вызов C-функции setCallback с передачей указателя на функцию Go
func installCallback(){
C.setCallback((*[0]byte)(C.goCallback))
}

// (2) ниже комментарий "//export goCallback" указывает Go, что функция будет видна из С
// (2) внимание! Пробелов между // и export быть не должно, иначе Go не сделает экспорт
//export goCallback
func goCallback(data C.uint){
}

// (3) вызов C-функции из Go
func callCFunc(data uint32){
C.myCFunc(C.uint32_t(data))
}

Настройка cross-компиляции

Для cross-компиляции необходимо локальное наличие компилятора под целевую платформу, заголовочных C-файлов, которые будут использоваться и объектных файлов для линковки (скомпилированных под целевую платформу).
Например, можно установить GNU Toolchain for Raspberry (http://gnutoolchains.com/raspberry/tutorial), который умеет забирать заголовочные и объектные файлы, установленные на Raspberry, себе для локальной cross-компиляции.

Дальше можно сделать bat-файл для компиляции или выставить переменные окружения.
Выставить путь к cross-компилятору, который находится внутри toolchain:
@set CC=arm-linux-gnueabihf-gcc

Установить архитектуру. Если этого не сделать, то получим ошибку вроде "unrecognized command line option '-m64'":
@set GOARCH=arm
@set GOARM=6
и операционную систему, иначе ошибка "unsupported GOOS/GOARCH pair windows/arm":
@set GOOS=linux

Выставить CGO_ENABLED, потому что по-умолчанию она выключена для cross-компиляции (и go build в этом случае молча игнорирует файлы, где есть import "C" - даже ошибки в них не проверяет):
@set CGO_ENABLED=1

Запустить сборку:
@call go build

В результате сборки получим список ошибок (как в Go, так и в C-коде) или готовый исполняемый файл, который можно запускать на Raspberry.

понедельник, 17 апреля 2017 г.

Raspberry Pi 3, работа с GPIO в Linux и DALI

см. ранее Освещение на протоколе DALI и его компоненты

Raspberry привлекает не только как маленький и дешевый компьютер, но и наличием GPIO - цифровых интерфейсов ввода-вывода, и с первого взгляда похож на мощный микроконтроллер, вроде замены, Arduino, вкупе с Linux-сервером. Заманчиво, заманчиво...

Я приобрел Raspberry в качестве сервера заботливой ("умной") квартиры, имея ввиду подключение к нему шины управления светом DALI через простой самодельный адаптер, вероятных цифровых датчиков, вроде температуры, видеокамер, как датчиков движения и охраны, ну, и тому подобное.
И вот теперь, начиная писать под Raspbian (официальный Linux для Raspberry), я понимаю свое заблуждение про универсальность.

В общем, Raspberry с Raspbian производит приятное впечатление: работает сразу после копирования системы на SD-карту и подключения hdmi, загружается быстро. Замечая отсутствие хорошей документации, известную медленную работу с SD, пока не пошаманишь и слабый Wi-Fi, начинается понимание, что Raspberry - не зрелый продукт серьезной фирмы, а неплохая реализация неплохой идеи, но, "на коленке". Нет, я только "за" и поддерживаю кошельком, я скорее про завышенные ожидания.

Итак, возвращаясь к настоящей задаче - добавлению квартирному серверу функции управления освещением по интерфейсу DALI.

адаптер собран, к Raspberry подключен (2xGPIO и питание)

Для приема данных DALI нужно уметь считывать состояние цифрового входа не реже 1/2400 секунды или раз в 0,4 мс. Эта простейшая задача для любого копеечного микроконтроллера, однако, под Raspbian сталкивается с серьезными трудностями. Все дело в работающих параллельно моей программе процессах Linux kernel (даже при отсутствии других запущенных программ). Из-за них процесс, читающий состояние GPIO или, что правильнее, вызываемый по прерыванию на фронт сигнала, получает управление нерегулярно и с задержками. Для человека задержка процесса на 2 мс не заметна, и она допустима для настольных многозадачных систем, но для чтения сигнала скоростью более 250 бит/c - почти фатальна.

Начиная программировать на C под Raspbian я узнал, что встроенной в систему поддержки работы с GPIO как бы и нет. И нашел стороннюю библиотеку bcm2835. С ее помощью я проверил, что адаптер работает и сигналы DALI видит. Попытка написать процедуру чтения данных DALI разбилась о тот факт, что bcm2835 не поддерживает прерывания. Вообразите мое разочарование: любой микроконтроллер о шести ножках за 30 центов поддерживает прерывания и GPIO, а Raspberry Pi под официальной Raspbian с официальной поддержкой GPIO, не работает из коробки не только с прерываниями, но и с GPIO.

Новые поиски библиотеки работы с цифровыми входами навели на wiringPi, с прерываниями. И новый опыт показал затруднения программной работы с DALI из-за случайных задержек, описанных выше: при появлении на входе сигнала выставляется флаг прерывания, однако, при параллельно работающих процессах ядра, которые сами активно управляют прерываниями, моя функция периодически вызывается с задержкой, из-за которой часть данных DALI оказывается "не услышана". Если говорить о реальных тестах, то на Raspbian, где работает только программа прослушивания DALI, я получаю 5% ошибок из-за задержек до 2 мс (из которых 2% вполне реально исправить алгоритмом).

Третья по счету библиотека - pigpio - работает с GPIO через DMA (процесс работы с памятью, реализованный аппаратно, поэтому прерывания и другие сложности в ядре процессора ему не должны мешать). Библиотека обещает точность временных интервалов порядка 5 мкс. Такие времена вполне подходят для декодирования DALI. Только нужно понимать, что задержки в вызове программы никуда не денутся. Она будет получать все биты с цифрового входа и точное время появления каждого бита, но получать их с опозданием. Поэтому возможность реализации процесса, где после получения данных требуется ответить за определенный интервал времени, остается под вопросом, благо, в DALI такое поведение необходимо только исполнительным устройствам, но не мастер-контроллеру.