Глава 4. Написание кода для управление транспортным средством при помощи обмена сообщений Python и MQTT

В этой главе мы напишем код Python 3 для управления транспортным средством при помощи сообщений MQTT, доставляемых через защищённые соединения (TLS 1.2). Мы напишем код, который будет способен исполняться на различных популярных платформах IoT, например на плате Raspberry Pi 3. Мы разберёмся как мы можем применять свои знания протокола MQTT для построения некоторого решения на основе требований. Мы научимся работе с самой последней версией библиотекой клиента MQTT Python Eclipse Paho. Мы подробно ознакомимся со следующим:

  • Пониманием и основными требованиями управления транспортным средством при помощи MQTT

  • Определением тем и команд

  • Изучением преимуществ применения Python

  • Созданием виртуальной среды для Python 3.x и PEP 405

  • Ознакомимся со структурой каталога для виртуальной среды

  • Активацией виртуальной среды

  • Деактивацией виртуальной среды

  • Установкой paho-mqtt для Python

  • Подключение клиента к присутствующему безопасному серверу MQTT при помощи paho-mqtt

  • Ознакомимся с обратными вызовами

  • Подпиской на темы при помощи Python

  • Настройкой сертификатов для плат IoT, которые выступают в роли клиентов

  • Созданием классов для представления транспортного средства

  • Получением сообщений в Python

  • Работой со множеством вызовов в методе loop

Знакомство с требованиями управления транспортным средством при помощи MQTT

В предыдущих трёх главах мы подробно изучали как работает MQTT. Мы ознакомились с тем как устанавливать некое соединение между клиентом MQTT и сервером MQTT. Мы изучили что происходит когда мы подписываемся на фильтрацию тем и когда некий издатель отправляет сообшения в определённые темы. Мы установили сервер Mosquitto и затем мы сделали его безопасным.

Теперь мы воспользуемся Python в качестве своего основного языка программирования для генерации клиентов, которые будут действовать как издатели и подписчики. Мы выполним соединение клиента MQTT с имеющимся сервером MQTT и мы обработаем команды для управления небольшим транспортным средством при помощи обмена сообщениями MQTT. Наше малое транспортное средство имитирует многие возможности, которые обнаруживаются в дорожных транспортных средствах из реальной жизни.

Мы будем пользоваться шифрованием TLS и аутентификацией TLS, так как мы не желаем чтобы никакой иной клиент MQTT не имел возможности отправлять команды нашему транспортному средству. Мы желаем чтобы наш код Python 3.x запускался на множестве платформ, так как мы применяем одну и ту же основу кода для управления транспортными средствами, которые применяют следующие платы IoT:

  • Raspberry Pi 3 Model B+

  • Qualcomm DragonBoard 410c

  • BeagleBone Black

  • MinnowBoard Turbot Quad-Core

  • LattePanda 2G

  • UP Core 4GB

  • UP Squared

В зависимости от применяемой платформы каждое транспортное средство собирается предоставлять дополнительные функциональные возможности, поскольку некоторые платы обладают более богатыми средствами в сравнении с прочими. Однако мы сосредоточимся на базовых свойствах чтобы сохранять свои примеры простыми и продолжать сосредотачиваться на MQTT. Затем мы будем способны применять данный проект в качестве базовой основы для прочих решений, которые требуют от нас исполнения кода в плате IoT, исполняющей код Python 3.x, подключаемой к некому серверу MQTT и обрабатывающей команды.

Тот код, который запускается на наших платах, управляющих своими транспортными средствами должен иметь возможность исполнять команды, получаемые из сообщений с определёнными темами. Мы будем применять строки JSON в своих полезных нагрузках.

Клиентское приложение также написано на Python и должно обладать способностью управлять одним или более транспортных средств. Мы также напишем определённое приложение клиента на Python, которое будет способно публиковать сообщения при помощи строк JSON в имеющейся теме для каждого транспортного средства. Это клиентское приложение должно отображать полученные результаты от исполнения каждой команды. Все транспортные средства обязаны публиковать сообщение в определённой теме всякий раз после успешного завершения команды.

Определение необходимых тем и команд

Мы будем применять следующие названия тем для публикации своих команд для транспортного средства: vehicles/vehiclename/commands, где vehiclename должно заменяться неким уникальным названием, назначаемом транспортному средству. Например, если мы присвоим vehiclepi01 в качестве имени устройству, которое вооружено платой Raspberry Pi 3 Model B+, мы будем должны публиковать команды в своей теме vehicles/vehiclepi01/commands. Тот код Python, который запускается на этой плате, будет подписан на эту тему для получения сообщений с командами и реагировать на них.

Мы будем применять сдедующее название темы для того чтобы заставлять все устройства публиковать подробности об успешно выполненных командах: vehicles/vehiclename/executedcommands, где vehiclename должно заменяться на уникальное название, назначенное некоторому транспортному средству. К примеру, если мы назначили vehiclebeagle03 в качестве названия для устройства, которое оснащено платой BeagleBone Black, тот клиент, который желает получать информацию об успешной обработке команд обязан подписаться на тему vehicles/vehiclebeagle03/executedcommands.

Все команды будут отправляться в строках JSON с парами ключ- значение. Определение ключа может быть эквивалентно CMD, а его значение должно определять одну из приводимых ниже допустимых команд. Когда определённая команда требует дополнительных параметров, такой параметр должен включаться в следующие клюя и значение для данного параметра в установленном значении для данного ключа:

  • TURN_ON_ENGINE: Включить двигатель данного транспортного средства.

  • TURN_OFF_ENGINE: Выключить двигатель данного транспортного средства.

  • LOCK_DOORS: Закрыть и заблокировать двери данного транспортного средства.

  • UNLOCK_DOORS: Разблокировать и открыть двери данного транспортного средства.

  • PARK: Запарковать данное транспортное средство.

  • TURN_ON_HEADLIGHTS: Включить передние фары данного транспортного средства.

  • TURN_OFF_HEADLIGHTS: Выключить передние фары данного транспортного средства.

  • TURN_ON_PARKING_LIGHTS: Включить габаритные огни данного транспортного средства, также называемые подфарниками.

  • TURN_OFF_PARKING_LIGHTS: Выключить габаритные огни данного транспортного средства, также называемые подфарниками.

  • ACCELERATE: Ускорить данное транспортное средство, то есть нажать на педаль газа.

  • BRAKE: Притормозить данное транспортное средство, то есть нажать на педаль тормоза.

  • ROTATE_RIGHT: Заставить данное транспортное средство повернуть направо. Вы должны определить на сколько градусов мы желаем повернуть направо своё транспортное средство в имеющемся значении для своего ключа DEGREES.

  • ROTATE_LEFT: Заставить данное транспортное средство повернуть влево. Вы должны определить на сколько градусов мы желаем повернуть влево своё транспортное средство в имеющемся значении для своего ключа DEGREES.

  • SET_MAX_SPEED: Установить максимальную скорость, которая допустима для данного транспортного средства. Мы должны определить значение желательной максимальной скорости в милях в час в устанавливаемом значении для ключа MPH.

  • SET_MIN_SPEED: Установить минимальную скорость, которая допустима для данного транспортного средства. Мы должны определить значение желательной минимальной скорости в милях в час в устанавливаемом значении для ключа MPH.

Следующая строка показывает некий пример полезной нагрузки для той команды, которая запускает двигатель вашего транспортного средства:


{"CMD": "TURN_ON_ENGINE"}
 	   

Приводимая далее строка отображает некий пример полезной нагрузки для той команды, которая устанавливает значение максимальной скорости для данного устройства в пять миль в час:


{"CMD": "SET_MAX_SPEED", "MPH": 5}
 	   

Теперь у нас имеются все необходимые подробности для того чтобы приступить к кодированию на Python.

Создание виртуальной среды при помощи Python 3.6.x и PEP 405

В наших последующих разделах и главах мы будем писать различные части кода Python, который будет подписываться на темы, а также будет публиковать в темы сообщения. Как всегда случается, когда мы хотим изолировать среду, которой требуются дополнительные пакеты, удобно работать с виртуальными средами Python. Python 3.3 предложил виртуальные среды с малым весом и они были усовершенствованы в Python 3.4 и последующих версиях. Вы можете прочитать о Виртуальной среде Python PEP 405, которая ввела соответствующий модуль venv.

Все приводимые в данной книге примеры мы проверили в Python 3.6.2 на машинах с macOS и Linux. Эти примеры также были проверены в тех платах IoT, которые мы упоминаем на протяжении всей книги и в их наиболее популярных операционных системах. К примеру, все эти примеры были проверены в Raspbian. Raspbian основывается на Debian Linux и, таким образом, все инструкции для Linux будут работать в Raspbian.

[Замечание]Замечание

Если вы решите воспользоваться воспользоваться популярным virtualenv, сторонней разработкой построителя виртуальной среды или теми параметрами виртуальной среды, которые предоставляются вашим IDE Python, вам всего лишь требуется проверять что вы активировали свою виртуальную среду при помощи соответствующего механизма всякий раз когда это необходимо делать, вместо приводимых ниже шагов, поясняющих активацию виртуальной среды, генерируемой при помощи того модуля venv, который интегрирован в Python.

Всякая виртуальная среда, которую мы создаём при помощи venv является изолированной средой и она будет иметь свой собственный независимый набор установленных пакетов Python в своём собственном каталоге (папке). Когда иы создаём некую виртуальную среду с venv в Python 3.4 или выше, в эту новую виртуальную среду включается pip. В Python 3.3 требовалась вручную устанавливать pip после создания необходимой виртуальной среды. Отметим, что все приводимые инструкции совместимы с Python 3.4 и выше, включая Python 3.6.x. Последующие команды предполагают что у вас имеется Python 3.5 или старше, установленный в Linux, macOS или Windows.

Вначале нам требуется выбрать необходимую целевую папку или каталог для своей виртуальной среды с малым весом. Ниже приводится тот путь, который мы будем применять в своём примере для Linux и macOS. Такой целевой папкой для нашей виртуальной среды будет папка HillarMQTT/01 внутри нашего домашнего каталога. Например, если в macOS или Linux у нас имеется домашний каталог /Users/gaston, тогда наша виртуальная среда будет создаваться в /Users/gaston/HillarMQTT/01. Вы можете подменить такой предписанный путь на требуемый вам путь в в каждой команде:


~/HillarMQTT/01
		

Ниже приводится тот путь, который мы будем применять в своём примере для Windows. Целевой папкой для виртуальной среды будет папка HillarMQTT\01 внутри папки профиля нашего пользователя. К примеру, если у нашего пользователя папкой профиля является C:\Users\gaston, необходимая виртуальная среда будет создана в C:\Users\gaston\HillarMQTT\01. Вы можете заменять определённый путь в нужном вам пути в каждой команде следующим образом:


%USERPROFILE%\HillarMQTT\01
		

В Windows PowerShell предыдущий путь должен быть следующим:


$env:userprofile\HillarMQTT\01
		

Теперь мы должны воспользоваться параметром -m, за которым следует соответствующее название модуля venv, а также желаемый путь чтобы запустить данный модуль в качестве сценария и создать некую виртуальную среду в предписанном пути. Эти инструкции могут отличаться в различных платформах, в которых мы создаём данную виртуальную среду.

Откройте терминал в Linux или macOS и исполните следующую команду для создания виртуальной среды:


python3 -m venv ~/HillarMQTT/01
		

В Windows в приглашении командной строки для создания виртуальной среды исполните следующую команду:


python -m venv %USERPROFILE%\HillarMQTT\01
		

Если вы работаете в PowerShell Windows для создания виртуальной среды запустите следующую команду:


python -m venv $env:userprofile\HillarMQTT\01
		

Никакая из приведённых выше команд не предоставляет никакого вывода. Наш сценарий создал предписанную целевую папку и установил pip вызвав ensurepip, так как мы не определили параметр --without-pip .

Понимание имеющейся структуры каталога для виртуальной среды

Предопределённая целевая папка имеет некое новое дерево каталога, которое содержит исполняемые файлы Python и прочие файлы, которые указаны для присутствия в виртуальной среде PEP405.

В самом корне для данной виртуальной среды имеющийся файл настройки pyenv.cfg определяет различные параметры для данной виртуальной среды и его присутствие является неким указателем на то, что мы находимся в корневой папке для какой- то виртуальной среды. В Linux и macOS эта папка будет иметь следующие основные вложенные папки: bin, include, lib, lib/python3.6 и lib/python3.6/site-packages. Отметим, что эти назввания папок могут отличаться на основе конкретной версии Python. В Windows данная папка будет иметь следующие вложенные папки: Include, Lib, Lib\site-packages и Scripts. Такие деревья для виртуальной среды в каждой платформе те же самые, что и аналогичная схема установки самого Python в этой платформе.

Следующий снимок экрана отображает папки и файлы в дереве каталога, созданного для виртуальной среды 01 в платформах macOS и Linux:

 

Рисунок 4-1



А в Windows все созданные основные папки в дереве каталога отображены на приводимом ниже снимке экрана:

 

Рисунок 4-2



[Замечание]Замечание

После тогокак мы активировали свою виртуальную среду, мы установим пакеты сторонних разработчиков в этой виртуальной среде и эти модули будут располагаться в папке lib/python3.6/site-packages или Lib\site-packages, в зависимости от конкретной платформы и определённой версии Python. Все исполняемые файлы будут копироваться в ваши папки bin или Scripts, в зависимости от вашей платформы. Те пакеты, которые мы установим, не приведут к изменениям в остальных виртуальных средах или в нашей базовой среде Python.

Активация виртуальной среды

Теперь, когда мы создали некую виртуальную среду, мы запустим особый для платформы сценарий чтобы активировать её. После тогь как мы активируем свою виртуальную среду, мы установим пакеты, которые будут доступны только в этой виртуальной среде. Таким образом мы будем работать в некоторой изолированной среде, в которой все установленные нами пакеты не будут оказывать воздействия на нашу основную среду Python.

В терминале Linux или macOS выполните следующую команду. Отметим, что полученные результаты данной команды будут точными если вы не запустили другую оболочку, чем ту, которая определена по умолчанию в вашем сеансе терминала. Еси у вас имеются сомнения, проверьте настройки и предпочтения своего терминала:


echo $SHELL
		

Данная команда отобразит название той оболочки, которую вы применяете в терминале. В macOS установленной по умолчанию является /bin/bash и это означает, что вы работаете с оболочкой bash. В зависимости от своей оболочки вы можете исполнять различные команды для активации своей виртуальной среды в Linux или macOS.

Если ваш терминал настроен на применение оболочки bash в Linux или macOS, запустите приводимую ниже команду для активации своей необходимой вам виртуальной среды. Данная команда также работает в оболочке zsh:


source ~/HillarMQTT/01/bin/activate
		

Если же ваш терминал настроен на применение одной из оболочек csh или tcsh, для активации своей виртуальной среды выполните такую команду:


source ~/HillarMQTT/01/bin/activate.csh
		

Если ваш терминал настроен на использование оболочки fish, для активции своей виртуальной среды воспользуётесь следующей командой:


source ~/HillarMQTT/01/bin/activate.fish
		

После того как вы активировали свою виртуальную среду, приглашение вашей командной строки отобразит название корневой папки вашей виртуальной среды, заключённой в круглые скобки в качестве префикса вашего установленного по умолчанию приглашения, чтобы напоминать вам что вы работаете в этой виртуальной среде. В данном случае в качестве префикса для своей командной строки вы будете наблюдать (01), так как названием корня папки для активированной виртуальной среды является 01.

Следующий снимок экрана отображает вашу виртуальную среду,которая активирована в терминале macOS High Sierra с оболочкой bash после выполнения предварительно показанных нами команд:

 

Рисунок 4-3



Как мы можем увидеть из предыдущего снимка экрана, наше приглашение на ввод изменилось с Gastons-MacBook-Pro:~ gaston$ на (01) Gastons-MacBook-Pro:~ gaston$ после соответствующей активации нашей виртуальной среды.

В Windows для активации необходимой виртуальной среды вы можете исполнять либо пакетный файл в приглашении командной строки, либо сценарий Windows PowerShell.

Если вы предпочитаете приглашение командной строки, запустите следующую команду в соответствующей командной строке Windows для активации виртуальной среды:


%USERPROFILE%\HillarMQTT\01\Scripts\activate.bat
		

Приводимый далее снимок экрана отображает виртуальную среду, активированную в приглашении командной строки Windows 10 после выполнения указанной выше команды:

 

Рисунок 4-4



Как мы можем обнаружить в предыдущем снимке экрана, наше приглашение изменилось с C:\Users\gaston на (01) C:\Users\gaston после активации соответствующей виртуальной среды.

Если вы предпочитаете Windows PowerShell, для активации виртуальной среды запустите и исполните следующие команды. Заметим, что вы обязаны иметь разрешённым исполнение сценариев в Windows PowerShell чтобы иметь возможность запустить данный сценарий:


cd $env:USERPROFILE
.\HillarMQTT\01\Scripts\Activate.ps1
		

Если вы получите некую ошибку аналогичную приводимой ниже, это означает, что у вас не разрешено исполнение сценариев:


C:\Users\gaston\HillarMQTT\01\Scripts\Activate.ps1 : File
C:\Users\gaston\HillarMQTT\01\Scripts\Activate.ps1 cannot be loaded because
running scripts is disabled on this system. For more information, see
about_Execution_Policies at
http://go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ C:\Users\gaston\HillarMQTT\01\Scripts\Activate.ps1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 + CategoryInfo : SecurityError: (:) [], PSSecurityException
 + FullyQualifiedErrorId : UnauthorizedAccess
		

D Windows PowerShell политикой исполнения по умолчанию является Restricted. Эта политика допускает исполнение индивидуальных команд, но запрещает исполнение сценариев. Таким образом, если вы желаете работать с Windows PowerShell, вам придётся изменить данную политику чтобы позволить исполнения сценариев. Очень важно убедиться, что вы осознаёте все риски политики исполнения Windows PowerShell, которая разрешает вам выполнять сценарии без подписи безопасности. Для получения дополнительной информации о различных политиках обратитесь к следующей веб странице Microsoft {Прим. пер.: или ознакомьтесь с уже упоминавшимся нашим переводом Книгb рецептов администрирования Windows Server 2016 Джордана Краузе.}

Следующий снимок экрана отображает нашу виртуальную среду, активированную в PowerShell Windows 10 {Прим. пер.: аналогичную Windows Server 2016} после исполнение показанной ранее команды:

 

Рисунок 4-5



Деактивация виртуальной среды

Убрать виртуальную среду, созданную в описанном выше процессе чрезвычайно просто. Такая деактивация удалит все изменения, выполненные в ваших переменных среды и изменит текужее приглашение на ввод обратно к своему прежнему сообщению. После деактивации виртуальной среды ва вернётесь обратно к установленной по умолчанию среде Python.

В macOS или Linux просто наберите deactivate и нажмите Enter.

В приглашении командной строки {Прим. пер.: Windows} вам придётся запустить пакетный файл deactivate.bat, содержащийся в папке Scripts. В нашем примере полый путь к этому файлу такой: %USERPROFILE%\HillarMQTT\01\Scripts\deactivate.bat.

В Windows PowerShell вам придётся исполнить сценарий deactivate.ps1 из папки Scripts. Для нашего примера путь этого файла выглядит следующим образом $env:userprofile\HillarMQTT\01\Scripts\Deactivate.ps1. Помните, что вы обязаны иметь разрешённым исполнение сценариев в Windows PowerShell чтобы иметь возможность запустить данный сценарий.

[Замечание]Замечание

Все инструкции в нашем следующем раздее предполагают что наша виртуальная среда была создана и активирована.

Установка paho-mqtt для Python

Проект Eclipse Paho предоставляет реализацию клиента MQTT с открытым исходным кодом. Данный проект включает в себя клиента Python, также именуемого как Paho Python Client или библиотека клиента MQTT Python Eclipse Paho. Этот клиент явился вкладом со стороны проекта Mosquitto и первоначально имел название клиент Python Mosquitto. Вот веб страница проекта Eclipse Paho. А это веб страница версии 1.3.1 библиотеки клиента MQTT Python Eclipse Paho, т.е. модуль paho-mqtt версии 1.3.1.

[Совет]Совет

Мы можем применять paho-mqtt во многих современных платах IoT, которые поддерживают Python с версией 3.x или выше. Нам всего лишь требуется убедится что установлен pip чтобы запросто установить paho-mqtt. Вы можете применять свой компьютер разработки для выполнения примеров или любую из упоминавшихся ранее плат.

Убедитесь что та виртуальная среда, которую мы создали на предыдущих этапах активирована прежде чем перемещаться вперёд к последующим шагам.

Если вы намереваетесь работать с какой- то платой IoT для запуска примера, убедитесь что вы исполняете все команды в соответствующем терминале SSH или в окне терминала, который запущен на самой плате. Если вы применяете свой компьютер разработки, выполняйте все команды в терминале macOS или Linux, либо в приглашении командной строки Windows.

Теперь мы будем применять установщик pip для инсталляции paho-mqtt 1.3.1. Нам всего лишь требуется запустить приводимую ниже команду в терминале SSH или окне локального терминала, который мы применяем со своей платой, или же в компьютере, который мы применяем для установки данного пакета:


pip install paho-mqtt==1.3.1
		
[Совет]Совет

Некоторые платы IoT имеют операционные системы, которые потребуют установить pip прежде чем выполнить приведённую выше команду. В платах Raspberry Pi 3 с Raspbian pip уже установлен. Если вы применяете свой компьютер, ваша установка Python уже содержит pip.

Если у вас есть Python, установленный в соответствующей папке Windows по умолчанию и вы не работаете в какой- нибудь виртуальной среде Python, вам придётся исполнить указанную ваше команду в приглашении командной строки с правами администратора. Если вы не работаете в виртуальной среде Python в вам понадобится запустить указанную выше команду с sudo в качестве префикса: sudo pip install paho-mqtt. Однако, как уже объяснялось ранее, настоятельно рекомендуется применять некую виртуальную среду.

Самые последние строки в выводе указывают что ваш пакет paho-mqtt с версией 1.3.1 был успешно установлен. Ваш вывод будет аналогичен последующим строкам, однако не в точности такой же, так как он очень сильно различается в зависимости от платформ, на которых вы исполняете данную команду:


Collecting paho-mqtt==1.3.1
 Downloading paho-mqtt-1.3.1.tar.gz (80kB)
 100% |################################| 81kB 1.2MB/s
Installing collected packages: paho-mqtt
 Running setup.py install for paho-mqtt ... done
Successfully installed paho-mqtt-1.3.1
		

Настройка сертификатов для бортов IoT, которые будут выступать в качестве клиентов

Вначале мы применим paho-mqtt чтобы создать клиента MQTT, который подключится к нашему серверу Mosquitto. Мы напишем несколько строк кода Python чтобы установить безопасное соединение и подписаться на некую тему.

В Главе 3, Безопасность сервера Mosquitto MQTT 3.1.1, мы сделали свой сервер MQTT безопасным, и следовательно мы будем применять цифровые сертификаты, которые мы создали для аутентификации своего клиента. Большую часть времени мы будем работать с сервером MQYY, который применят TLS и следовательно будет хорошей мыслью установить подключение с TLS и аутентификацией TLS. Гораздо проще установить некое подключение с сервером MQTT с выключенной безопасностью, однако это не будет наиболее общим случаем, с которым мы будем сталкиваться при разработке приложений, которые работают с MQTT.

Для начала нам следует скопировать те приводимые ниже файлы, которые мы создали в Главе 3, Безопасность сервера Mosquitto MQTT 3.1.1, в каталог на своём компьютере или в том устройстве,которое мы будем применять для запуска сценария Python. Мы сохраним эти файлы в каталог с названием mqtt_certificates. Создайте каталог board_certificates в своём компьютере или плате, которую вы вы собираетесь применять в качестве клиента для данного примера. Скопируйте в этот новый каталог три файла:

  • ca.crt: файл сертификата центра авторизации

  • board001.crt: файл сертификата клиента

  • board001.key: ключ клиента

Теперь мы создадим новый файл Python с названием config.py в своей главной папке виртуальной среды. Следующие строки показывают основной код для этого файла, определяющий множество значений настройки, которые будут применяться для установления подключения к серверу MQTT Mosquitto. Таким образом, все значения настройки содержатся в некотором специальном сценарии Python. Вам следует заменить значение /Users/gaston/board_certificates в строке certificates_path на путь к созданному вами каталогу board_certificates. Помимо этого, замените значение для mqtt_server_host на тот IP адрес или имя хоста, который применяет сервер Mosquitto или любой другой сервер MQTT, который вы можете решить применять. Ваш код для примера содержится в папке mqtt_python_gaston_hillar_04_01 в файле config.py:


import os.path

# Замените /Users/gaston/python_certificates на тот путь,
# в которм вы сохранили свой файл сертификата центра авторизации,
# сертификат клиента и ключ клиента
certificates_path = "/Users/gaston/python_certificates"
ca_certificate = os.path.join(certificates_path, "ca.crt")
client_certificate = os.path.join(certificates_path, "board001.crt")
client_key = os.path.join(certificates_path, "board001.key")
# Замените 192.168.1.101 на IP или hostname для вашего Mosquitto
# или иного сервера MQTT
# Проверьте что IP или hostname соответствуют тому значению,
# которое вы применяете для обычного названия
mqtt_server_host = "192.168.1.101"
mqtt_server_port = 8883
mqtt_keepalive = 60
 	   

Этот код определяет значение переменной certificates_path, инициализируемое строкой, определяющей тот путь, в котором вы сохранили соответствующие файл сертификата центра авторизации, файл сертификата клиента и ключ клиента (ca.crt, board001.crt и board001.key). Затем это код объявляет последующие строковые переменные со значениями полного пути к необходимому сертификату и файлам ключей, которые нам требуются для настройки TLS и аутентификации клиента TLS: ca_certificate, client_certificate и client_key.

[Совет]Совет

Вызов os.path.join делает простым присоединение того пути, который определён в переменной certificates_path к самому названию файла и создание полного пути. Данная функция os.path.join работает для любой платформы и следовательно, мы не должны беспокоиться применять ли прямую косую (/) или обратную косую (\) для соединения имени пути с самим названием файла. Иногда мы можем выполнять разработку и проверку в Windows и затем исполнять этот код на плате IoT, которая применяет различные разновидности Unix или Linux, например, Raspbian или Ubuntu. Применение certificates_path делает нашу работу более простой в тех сценариях, в которых мы переключаемся между различными платформами.

Переменные mqtt_server_host, mqtt_server_port и mqtt_keepalive определяют значение IP адреса для вашего сервера MQTT (сервера Mosquitto), значение порта, которое мы желаем применять(8883), а также значение числа секунд, для опции оставления действующим. Очень важно заменить 192.168.1.101 на IP адрес для вашего сервера MQTT. Мы определили 8883 для mqtt_server_port потому что мы применяем TLS, а это значение порта по умолчанию для MQTT поверх TLS, как мы узнали об этом в Главе 3, Безопасность сервера Mosquitto MQTT 3.1.1.

Теперь мы создадим новый файл Python с названием subscribe_with_paho.py в своей главной папке виртуальной среды. Приводимые ниже строки показывают код для этого файла, который устанавливает соединение с нашим сервером Mosquitto, подписывается на фильтр темы vehicles/vehiclepi01/tests, и печатает все получаемые из этого фильтра подписки сообщения. Файл данного кода содержится в папке mqtt_python_gaston_hillar_04_01 в файле subscribe_with_paho.py:


from config import *
import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, rc):
    print("Result from connect: {}".format(
        mqtt.connack_string(rc)))
    # Выполняем подписку на фильтр темы vehicles/vehiclepi01/tests
    client.subscribe("vehicles/vehiclepi01/tests", qos=2)

def on_subscribe(client, userdata, mid, granted_qos):
    print("I've subscribed with QoS: {}".format(
    granted_qos[0]))

def on_message(client, userdata, msg):
    print("Message received. Topic: {}. Payload: {}".format(
        msg.topic,
        str(msg.payload)))

if __name__ == "__main__":
    client = mqtt.Client(protocol=mqtt.MQTTv311)
    client.on_connect = on_connect
    client.on_subscribe = on_subscribe
    client.on_message = on_message
    client.tls_set(ca_certs = ca_certificate,
        certfile=client_certificate,
        keyfile=client_key)
    client.connect(host=mqtt_server_host,
        port=mqtt_server_port,
        keepalive=mqtt_keepalive)
    client.loop_forever()
 	   
[Совет]Совет

Отметим, что данный код совместим с версией 1.3.1 paho-mqtt. Предыдущие версии paho-mqtt не совместимы с данным кодом. Следовательно, убедитесь что вы в точности выполнили описанные ранее шаги по установке paho-mqtt версии 1.3.1.

  Знакомство с обратными вызовами

Наш предыдущий код применял только что установленный модуль paho-mqtt версии 1.3.1 для выполнения шифрованного подключения к имеющемуся серверу MQTT, подписались на фильтр темы vehicles/vehiclepi01/tests и выполнили код при получении сообщений с этой темой. Мы будем применять этот код чтобы ознакомиться с основами paho-mqtt. Этот код является очень простой версией некоторого клиента MQTT, который подписан на фильтр какой- то темы и мы несомненно улучшим его в наших последующих разделах.

Самая первая строка импортирует те переменные, которые мы объявили в своём написанном ранее файле config.py. Вторая строка импортирует paho.mqtt.client как mqtt. Таким образом, всякий раз когда мы применяем сокращение mqtt, мы будем ссылаться на paho.mqtt.client.

Когда мы объявили некую функцию, причём мы передаём эту функцию в качестве некоего аргумента другой функции или методу, любо мы назначаем эту функцию в качестве атрибута, а затем некий код вызывает такую функцию в некий момент времени - такой механизм именуется обратным вызовом (callback). Название обратного вызова применяется по той причине, что данный код выполняет в некий момент времени повторно вызывает некую функцию. Наш пакет paho-mqtt версии 1.3.1 требует от нас работать со множеством обратных вызовов и таким образом важно понимать как они работают.

Наш код определил следующие три функции, которые мы определим позднее в качестве обратных вызовов:

  • on_connect: Эта функция будет вызываться когда ваш клиент MQTT получит отклик CONNACK от своего сервера MQTT, то есть когда успешно установлено некое подключение к серверу MQTT.

  • on_subscribe: Данная функция будет вызываться когда ваш клиент MQTT получит некий отклик SUBACK от своего сервера MQTT, то есть когда успешно выполнена некая подписка.

  • on_message: А эта функция будет вызываться при получении вашим клиентом MQTT сообщения PUBLISH от своего сервера MQTT. Всякий раз когда ваш сервер MQTT публикует некое сообщение, базирующееся на той подписке, на которую подписан клиент, будет вызываться данная функция.

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

 
Отклик от сервера MQTT Подлежащая вызову функция

CONNACK

on_connnect

SUBACK

on_subscribe

PUBLISH

on_message

Наш код для основного блока создаёт некий экземпляр общего класса mqtt.Client (paho.mqtt.client.Client), который представляет некоего клиента MQTT. Мы применяем этот экземпляр для взаимодействия со своим сервером MQTT: Mosquitto. Если мы применяем установленные по умолчанию параметры для создания определяемого нового экземпляра,мы будем работать с MQTT версии 3.1. Мы же желаем работать с MQTT версии 3.1.1, и следовательно в качестве значения для своего аргумента протокола мы определяем mqtt.MQTTv311.

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

 
Атрибут Назначаемая функция

client.on_connect

on_connnect

client.on_message

on_message

client.on_subscribe

on_subscribe

Наш вызов метода client.tls_set выполняет настройку опций шифрования и аутентификации. Очень важно вызвать этот метод перед тем как запущен метод client.connect. Мы определяем полные строковые пути к файлу сертификата центра авторизации, сертификату самого клиента и ключу этого клиента в аргументах ca_certs, certfile и keyfile. Название аргумента ca_certs может быть слегка сбивающим с толку, однако нам требуется определить данную строку пути к файлу сертификата центра авторизации {Прим. пер.: на языке оригинала - certificate authority certificate file}, а не множество сертификатов.

Наконец, наш основной (main) блок вызывает метод client.connect и определяет значения для аргументов host, port и keepalive. Тем самым наш код запрашивает у данного клиента MQTT установление подключения к определённому серверу MQTT.

[Замечание]Замечание

На метод connect запускается в виде некоторого асинхронного исполнения и тем самым выступает в роли вызова без блокировки (неблокирующего вызова).

После того как соединение с сервером MQTT успешно установлено, исполняется определённый в атрибуте client.on_connect обратный вызов, то есть функция on_connect. Данная функция получает значение экземпляра mqtt.Client, который установил данное соединение со своим сервером MQTT в значении аргумента клиента.

[Замечание]Замечание

Если вы желаете устанавливать соединение с клиентом, которое не применяет TLS, вам не требуется вызывать метод client.tls_set. Кроме того, вам потребуется применять соответствующий порт вместо установленного значения порта 8883, который определён для работы с TLS. Помните, что значением порта по умолчанию, когда вы не хотите работать с TLS является 1883.

  Подпись на темы при помощи Python

Наш код вызывает метод client.subscribe с "vehicles/vehiclepi01/tests" в качестве некоторого аргумента для подписки на эту конкретную тему и значение аргумента qos установленное в 2 для запроса 2 уровня QoS.

[Замечание]Замечание

В данном случае мы подписываемся только на одну тему. Однако очень важно знать, что мы не ограничены подпиской на единственный фильтр темы; мы можем подписаться на множество фильтров в единственном вызове метода subscribe.

После того как сервер MQTT подтведит успешность подписки на определённый фильтр темы при помощи отклика SUBACK, будет исполнен заданный в атрибуте client.on_subscribe обратный вызов, то есть функция on_subscribe. Эта функция получит перечень целых значений в значении аргумента granted_qos, который предоставит значения уровней QoS, которые данный сервер MQTT предоставляет для каждой темы запрошенных подписок на фильтры тем. Наш код в функции on_subscribe отобразит полученный уровень QoS, предоставляемый нашим сервером MQTT для определённого нами фильтра темы. В данном случае, так как мы подписались на единственный фильтр темы, наш код схватит самое первое значение из полученного массива granted_qos.

\всякий раз когда получается некое новое сообщение, которое соответствует установленному фильтру темы, на который мы подписаны, исполняется предписанный для нашего атрибута client.on_messsage обратный вызов, то есть функция on_message. Эта функция получает соответствующий экземпляр mqtt.Client, который установил соединение со своим сервером MQTT в значении аргумента клиента и некий экземпляр mqtt.MQTTMessage в значении аргумента msg. Наш класс mqtt.MQTTMessage определяет некое входящее сообщение.

В данном случае, всякий раз, когда исполняется наша функция on_message, значение в msg.topic всегда совпадает с "vehicles/vehiclepi01/tests", так как мы подписаны всего лишь на единственную тему и никакое прочее название темы не соответствует нашему фильтру тем. Однако если мы подписаны на один или несколько фильтров тем, для которых имеется соответствие более чем одной темы, следовало бы проверять какова именно тема в данном сообщении была послана путём просмотра значения атрибута msg.topic.

Наш код в данной функции on_message печатает название темы для того сообщения, которое мы получили, msg.topic, а также строку, представляющую значение полезной нагрузки данного сообщения, то есть значение атрибута msg.payload.

Наконец, наш основной блок вызывает метод client.loop_forever, который в свою очередь вызывает метод loop для нас в некотором бесконечном цикле. К данному моменту мы хотим только запустить свой цикл клиента MQTT в нашей программе. Мы будем получать те сообщения, чьи темы соответствуют той теме,на которую мы подписаны.

[Совет]Совет

Наш метод loop отвечает за обработку всех сетевых событий, то есть, он обеспечивает реальное взаимодействие с сервером MQTT. Вы можете представлять себе метод loop как некий эквивалент синхронизации вашего клиента электронной почты, который получает входящие сообщения и отправляет соответствующие сообщения из блока исходящих писем.

Убедитесь что запущен сервер Mosquitto или любой иной сервер MQTT, который вы можете пожелать для применения в данном примере. Затем выполните следующую строку чтобы запустить наш пример на любом компьютере или устройстве, которое мы хотим применять в качестве клиента MQTT при использовании Linux или macOS:


python3 subscribe_with_paho.py
		

В Windows вам следует исполнить такую строку:


python subscribe_with_paho.py
		

Если вы наблюдаете трассировку с некоторой SSLError, аналогичной в приводимых ниже строках, это означает, что имя хоста или IP адрес нашего сервера MQTT не соответствует определённому в значении атрибута Common Name при генерации файла сертификата нашего сервера с названием server.crt. Убедитесь что вы проверили IP адрес для нашего сервера MQTT (сервера Mosquitto) и повторите генерацию файла сертификата и ключа своего сервера с надлежащим IP адресом или названием хоста для Common Name, как это объяснялось в Главе 3, Безопасность сервера Mosquitto MQTT 3.1.1, раз уж вы работаете с самостоятельно подписываемыми сертификатами, которые мы генерируем. Если вы работаете с с самостоятельно подписываемыми сертификатами, адресами IP и неким сервером DHCP, проверьте также, что ваш сервер DHCP не изменил установленный для вашего сервера Mosquitto IP адрес:


Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/gaston/HillarMQTT/01/lib/python3.6/sitepackages/paho/mqtt/client.py", line 612, in connect return self.reconnect()
  File "/Users/gaston/HillarMQTT/01/lib/python3.6/sitepackages/paho/mqtt/client.py", line 751, in reconnect self._tls_match_hostname()
  File "/Users/gaston/HillarMQTT/01/lib/python3.6/sitepackages/paho/mqtt/client.py", line 2331, in _tls_match_hostname 
    raise ssl.SSLError('Certificate subject does not match remote hostname.')
		

Теперь выполните следующие шаги чтобы воспользоваться утилитой GUI MQTT.fx для публикации двух сообщений в нашу тему vehicles/vehiclepi01/tests:

  1. Запустите MQTT.fx и установите подключение к имеющемся серверу MQTT следуя шагами, которые мы изучили в Главе 3, Безопасность сервера Mosquitto MQTT 3.1.1.

  2. Кликните Publish и введите vehicles/vehiclepi01/tests в ниспадающем меню с левой стороны от данной кнопки Publish.

  3. Кликните по QoS 2 справа от кнопки Publish.

  4. Введите следующий текст в блоке текста под кнопкой Publish: {"CMD": "UNLOCK_DOORS"}. После этого кликните по самой кнопке Publish. MQTT.fx опубликует только что введённый текст в определённой вами теме.

  5. Под кнопкой Publish введите в блоке текста следующий текст: {"CMD": "TURN_ON_HEADLIGHTS"}. Затем кликните по самой кнопке Publish. MQTT.fx опубликует введённый вами текст в заданной теме.

Если ву не хотите работать с утилитой MQTT.fx, вы можете выполнить две команды mosquitto_pub для создания клиентов MQTT, которые опубликуют сообщения в указанной теме. Вам всего лишь понадобится открыть другой терминал в macOS или Linux, либо другое приглашение командной строки в Windows, перейти в каталог с установленным Mosquitto и запустить приводимые ниже команды. В этом случае нет необходимости в определении опции -d. Замените 192.168.1.101 на IP или имя хоста своего сервера MQTT. Не забудьте заменить ca.crt, board001.crt и board001.key надлежащими полными путями к этим файлам, созданным в нашем каталоге board_certificates. Файл кода для этого примера содержится в папке mqtt_python_gaston_hillar_04_01 folder под названием script_01.txt:


mosquitto_pub -h 192.168.1.101 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/tests -m '{"CMD": "UNLOCK_DOORS"}' -q 2 --tls-version tlsv1.2

mosquitto_pub -h 192.168.1.101 -V mqttv311 -p 8883 --cafile ca.crt --cert board001.crt --key board001.key -t vehicles/vehiclepi01/tests -m '{"CMD": "TURN_ON_HEADLIGHTS"}' -q 2 --tls-version tlsv1.2
 	   

Переместитесь к тому устройству или окну, в котором вы исполняете данный сценарий Python. Вы обнаружите следующий вывод:


Result from connect: Connection Accepted.
I've subscribed with QoS: 2
Message received. Topic: vehicles/vehiclepi01/tests. Payload: b'{"CMD": "UNLOCK_DOORS"}'
Message received. Topic: vehicles/vehiclepi01/tests. Payload: b'{"CMD": "TURN_ON_HEADLIGHTS"}'
		

Ваша программа Python успешно установит безопасное шифрованное подключение к серверу MQTT и станет подписчиком на тему vehicles/vehiclepi01/tests с гарантированным уровнем QoS 2. Наша программа отобразит два соответствующих сообщения, которые получены в теме vehicles/vehiclepi01/tests.

Для прекращения исполнения своей прогаммы нажмите Ctrl + C. Созданный вами клиент MQTT закроет данное подключение с имеющимся сервером MQTT. Вы обнаружитесообщение об ошибке, аналогичное такому выводу, так как прервано исполнение вашего loop:


Traceback (most recent call last):
  File "subscribe_with_paho.py", line 33, in <module>
    client.loop_forever()
  File "/Users/gaston/HillarMQTT/01/lib/python3.6/sitepackages/paho/mqtt/client.py", line 1481, in loop_forever rc = self.loop(timeout, max_packets)
  File "/Users/gaston/HillarMQTT/01/lib/python3.6/sitepackages/paho/mqtt/client.py", line 988, in loop socklist = select.select(rlist, wlist, [], timeout)
KeyboardInterrupt
		

  Настройка сертификатов для плат IoT, которые будут выступать в качестве клиентов

Теперь мы напишем код Python, который будет готов к работе в различных платах IoT. Конечно, вы можете работать с отдельным компьютером разработки или с платой для разработки. Нет необходимости запускать этот код в различных устройствах. Мы всего лишь желаем убедиться, что мы способны писать код, который сможем запускать в различных устройствах.

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

Мы сохраним все файлы в каталоге с названием board_certificates в том компьютере или в той плате, которые вы намереваетесь применять в качестве клиента для данного примера. В этот новый каталог скопируйте следующие три файла:

  • ca.crt: Файл сертификата центра авторизации

  • board001.crt: Файл сертификата клиента

  • board001.key: Ключ клиента

Создание класса для представления некоторого транспортного средства

Мы создадим два таких класса:

  • Vehicle: Этот класс будет представлять некое транспортное средство и предоставит методы, которые будут вызываться всякий раз, когда следует обработать некую команду. Чтобы не усложнять наш пример, наши методы будут просто печатать те действия, которые данное тренспортное устройство исполняет после каждого вызываемого метода в вывод своей консоли. Класс из реального мира, который представляет некое транспортное средство, будет взаимодействовать с соответствующими двигателем, фарами, рычагами, датчиками и прочими различными компонентами самого транспортного средства при каждом вызове метода.

  • VehicleCommandProcessor: Данный класс будет представлять некое устройство обработки команд, которое установит соединение с сервером MQTT, подписывается на тему в которой ваш клиент MQTT будет получать сообщения с командами, анализирует все входящие сообщения и уполномочивает исполнение своих команд некому представленному экземпляру заданного класса Vehicle. Наш класс VehicleCommandProcessorопределит много статических методов, которые будут определены в качестве соответствующих обратных вызовов для соответствующего клиента MQTT.

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


# Строки ключей
COMMAND_KEY = "CMD"
SUCCESFULLY_PROCESSED_COMMAND_KEY = "SUCCESSFULLY_PROCESSED_COMMAND"
# Строки команд
# Включить двигатель транспортного средства
CMD_TURN_ON_ENGINE = "TURN_ON_ENGINE"
# Выключить двигатель транспортного средства
CMD_TURN_OFF_ENGINE = "TURN_OFF_ENGINE"
# Закрыть и заблокировать двери транспортного средства
CMD_LOCK_DOORS = "LOCK_DOORS"
# Разблокировать и открыть двери транспортного средства
CMD_UNLOCK_DOORS = "UNLOCK_DOORS"
# Запарковать транспортное средство
CMD_PARK = "PARK"
# Запарковать транспортное средство в безопасном месте, которое предназначено для данного средства
CMD_PARK_IN_SAFE_PLACE = "PARK_IN_SAFE_PLACE"
# Включить передние фары транспортного средства
CMD_TURN_ON_HEADLIGHTS = "TURN_ON_HEADLIGHTS"
# Выключить передние фары транспортного средства
CMD_TURN_OFF_HEADLIGHTS = "TURN_OFF_HEADLIGHTS"
# Включить габаритные огни транспортного средства, также именуемые подфарниками
CMD_TURN_ON_PARKING_LIGHTS = "TURN_ON_PARKING_LIGHTS"
# Выключить габаритные огни транспортного средства, также именуемые подфарниками
CMD_TURN_OFF_PARKING_LIGHTS = "TURN_OFF_PARKING_LIGHTS"
# Ускорить данное транспортное средство, то есть нажать педаль газа
CMD_ACCELERATE = "ACCELERATE"
# Затормозить данное транспортное средство, то есть нажать педаль тормоза
CMD_BRAKE = "BRAKE"
# Заставить данное транспортное средство повернуть направо. Мы должны определить значение в градусах
# на которое мы хотим повернуть транспортное средство вправо для значения ключа DEGREES
CMD_ROTATE_RIGHT = "ROTATE_RIGHT"
# Заставить данное транспортное средство повернуть налево. Мы должны определить значение в градусах
# на которое мы хотим повернуть транспортное средство влево для значения ключа DEGREES
CMD_ROTATE_LEFT = "ROTATE_LEFT"
# Задать максимальное значение скорости, которое разрешено данному транспортному средству. Мы должны определить
# желаемый максимум в милях в час в значении ключа MPH
CMD_SET_MAX_SPEED = "SET_MAX_SPEED"
# Задать минимальное значение скорости, которое разрешено данному транспортному средству. Мы должны определить
# желаемый минимум в милях в час в значении ключа MPH
CMD_SET_MIN_SPEED = "SET_MIN_SPEED"
# Ключ градусов
KEY_DEGREES = "DEGREES"
# Ключ миль в час
KEY_MPH = "MPH
 	   

Переменная COMMAND_KEY определяет строку значения ключа, который определяет какой код будет пониматься в качестве основной команды. Всякий раз когда мы получаем некое сообщение, которое содержит данную строку ключа, мы знаем, что то значение, которое связано с данным ключом в нашем словаре будет указывать ту команду, которую данное сообщение желает запустить код для обработки на соответствующей плате. Наш клиент MQTT не будет получать сообщения в виде словарей и, следовательно, необходимо преобразовывать их из строк в словари, когда они не просто представляют некую строку.

Переменная SUCCESSFULLY_PROCESSED_COMMAND_KEY определяет значение ключа, который задаёт что этот код будет применён в качестве ключа успешно обработанной команды в сообщении отклика, публикуемом для соответствующей темы. Всякий раз когда мы публикуем некое сообщение, которое содержит указанную ключевую строку, мы знаем, что то значение, которое связанно с этим ключом в нашем словаре укажет на команду, которую успешно отработала соответствующая плата.

Создайте новый файл Python с названием vehicle_mqtt_client.py в соответствующей папке главной виртуальной среды. Приводимые ниже строки определяют необходимые импорты и те же самые переменные, которые мы использовали в нашем предыдущем примере для установления подключения к имеющемуся серверу MQTT. Далее эти строки определяют класс Vehicle. Соответствующий файл кода этого примера содержится в папке mqtt_python_gaston_hillar_04_01 в файле vehicle_mqtt_client.py:


class Vehicle:
def __init__(self, name):
self.name = name
self.min_speed_mph = 0
self.max_speed_mph = 10

    def print_action_with_name_prefix(self, action):
        print("{}: {}".format(self.name, action))

    def turn_on_engine(self):
        self.print_action_with_name_prefix("Turning on the engine")

    def turn_off_engine(self):
        self.print_action_with_name_prefix("Turning off the engine")

    def lock_doors(self):
        self.print_action_with_name_prefix("Locking doors")

    def unlock_doors(self):
        self.print_action_with_name_prefix("Unlocking doors")

    def park(self):
        self.print_action_with_name_prefix("Parking")

    def park_in_safe_place(self):
        self.print_action_with_name_prefix("Parking in safe place")

    def turn_on_headlights(self):
        self.print_action_with_name_prefix("Turning on headlights")

    def turn_off_headlights(self):
        self.print_action_with_name_prefix("Turning off headlights")

    def turn_on_parking_lights(self):
        self.print_action_with_name_prefix("Turning on parking lights")

    def turn_off_parking_lights(self):
        self.print_action_with_name_prefix("Turning off parking lights")

    def accelerate(self):
        self.print_action_with_name_prefix("Accelerating")

    def brake(self):
        self.print_action_with_name_prefix("Braking")

    def rotate_right(self, degrees):
        self.print_action_with_name_prefix("Rotating right {} degrees".format(degrees))

    def rotate_left(self, degrees):
        self.print_action_with_name_prefix("Rotating left {} degrees".format(degrees))

    def set_max_speed(self, mph):
        self.max_speed_mph = mph
        self.print_action_with_name_prefix("Setting maximum speed to {} MPH".format(mph))

    def set_min_speed(self, mph):
        self.min_speed_mph = mph
        self.print_action_with_name_prefix("Setting minimum speed to {} MPH".format(mph))
 	   

Как и в предыдущем примере, все наши значения настроек для установления подключения с имеющимся сервером MQTT Mosquitto определены в соответствующем файле Python с названием config.py в нашей папке главной виртуальной среды. Если вы желаете запустить этот пример в другом устройтсве, вам придётся создать некий новый файл config.py с соответствующими значениями и заменить те строки, которые импортируют соответствующие значения из модуля config для применения в этом новом файле настроек. Не забудьте заменить значение /Users/gaston/board_certificates в соответствующей строке certificates_path на путь для созданного нами каталога board_certificates. Кроме того, замените значение для mqtt_server_host на IP адрес или название хоста того сервера Mosquitto или иного сервера MQTT, который вы решите применять.

Мы должны определить название данного транспортного средства в соответствующем необходимом аргументе названия. Нан конструктор, то есть метод __init__, сохранит это полученное название в некотором атрибуте с тем же самым названием. Затем этот конструктор установит начальные значения для двух атрибутов: min_speed_mph и max_speed_mph. Эти атрибуты устанавливают соответствующие значения минимальной и максимальной скоростей для данного транспортного средства, выраженные в милях в час.

Наш класс Vehicle определяет метод print_action_with_name_prefix, который получает некую строку с действием, которое подлежит исполнению в соответствующем аргументе action и печатеает его с тем значением, которое хранится в установленном атрибуте name в качестве префикса. Все прочие методы, определённые в данном классе вызывают этот метод print_action_with_name_prefix для печати сообщений, указывающих те действия, которые следует исполнить самому транспортному средству с названием этого транспортного средства в качестве префикса.

Получение сообщений в Python

Мы будем применять недавно установленный модуль paho-mqtt версии 1.3.1 для подписки на некую определённую тему и запускать код при получении сообщений в этой теме. Мы создадим класс VehicleCommandProcessor в том же самом файле с названием vehicle_mqtt_client.py в своей папке главной виртуальной среды. Это класс будет представлять некий процессор команд, связываемый с каким- то экземпляром написанного ранее класса Vehicle, настроим своего коиента MQTT и необходимую для этого клиента подписку, а также объявим необходимый для обратных вызовов код, который мы собираемся исполнять при возникновении определённых событий, связанных с запущенным MQTT.

Мы расчленим свой код для класса VehicleCommandProcessor на множество фрагментов кода чтобы сделать более простым понимание каждого раздела кода. Вам нужно добавить следующие строки кода в имеющийся файл Python vehicle_mqtt_client.py. Эти идущие внизу строки объявляют сам класс VehicleCommandProcessor и его конструктор, то есть метод __init__. Файл этого примера кода содержится в каталоге mqtt_python_gaston_hillar_04_01 в файле vehicle_mqtt_client.py:


class VehicleCommandProcessor:
    commands_topic = ""
    processed_commands_topic = ""
    active_instance = None

    def __init__(self, name, vehicle):
        self.name = name
        self.vehicle = vehicle
        VehicleCommandProcessor.commands_topic = \
            "vehicles/{}/commands".format(self.name)
        VehicleCommandProcessor.processed_commands_topic = \
            "vehicles/{}/executedcommands".format(self.name)
        self.client = mqtt.Client(protocol=mqtt.MQTTv311)
        VehicleCommandProcessor.active_instance = self
        self.client.on_connect = VehicleCommandProcessor.on_connect
        self.client.on_subscribe = VehicleCommandProcessor.on_subscribe
        self.client.on_message = VehicleCommandProcessor.on_message
        self.client.tls_set(ca_certs = ca_certificate,
            certfile=client_certificate,
            keyfile=client_key)
        self.client.connect(host=mqtt_server_host,
                            port=mqtt_server_port,
                            keepalive=mqtt_keepalive)
 	   

Мы должны определить некое название для своего процессора команд и определённого экземпляра Vehicle, которым будет управлять данный процессор команд в соответствующих необходимых аргументах name и vehicle. Наш конструктор, то есть метод __init__, сохраняет полученные name и vehicle в атрибутах с теми же самыми названиями. Затем наш конструктор устанавливает эти значения для имеющихся атрибутов класса commands_topic и processed_commands_topic. Наш конструктор применяет полученное полученные name для определения самого названия темы для этих команд и для всех успешно обработанных команд, основываясь на тех определенях, которые мы обсуждали ранее. Наш клиент MQTT будет получать сообщения в этом названии темы, сохраняемом в соответствующем атрибуте класса commands_topic и будет публиковать сообщения в название темы, сохраняемом в атрибуте класса processed_commands_topic.

Затем наш конструктоп создаёт некий экземпляр класса mqtt.Client (paho.mqtt.client.Client), который представляет какого- то клиента MQTT и который будет использоваться для взаимодействия с неким сервером MQTT. Данный код назначает этот экземпляр соответствующему атрибуту client (self.client). Как и в нашем предыдущем примере, мы желаем работать с MQTT версии 3.1.1 и, следовательно, мы определяем mqtt.MQTTv311 в качестве значения для своего аргумента протокола.

Данный код также сохраняет ссылку на этот экземпляр в соответствующем атрибуте класса active_instance, так как мы должны выполнять доступ в своих статических методах, который наш конструктор будет определять как обратные вызовы для имеющихся различных событий, которые создаёт клиент MQTT. Мы хотим иметь все эти методы связанными со своим процессором команд транспортного средства в своём классе VehicleCommandProcessor.

Затем наш код назначает статические методы атрибутам соответствующего экземпляра self.client. Приводимая таблица суммирует эти назначения:

 
Атрибут Назначаемый статический метод

client.on_connect

VehicleCommandProcessor.on_connnect

client.on_message

VehicleCommandProcessor.on_message

client.on_subscribe

VehicleCommandProcessor.on_subscribe

[Совет]Совет

Статические методы не получают в своём самом первом аргументе ни self, ни cls, и, следовательно, мы можем применять их в качестве обратных вызовов с требуемым числом аргументов. Отметим, что будем кодировать и анализировать эти статические методы в своих последующих параграфах.

Наш вызов соответствующего метода self.client.tls_set настроит опции шифрования и аутентификации. Наконец, наш конструктор вызывает метод client.connect и определяет необходимые значения для аргументов host, port и keepalive . Таким образом, этот код запрашивает у данного клиента MQTT установление подключения к определённому серверу MQTT. Помните, что сам метод connect запускается как асинхронное исполнение, и, таким образом, это неблокирующий вызов.

[Совет]Совет

Если вы желаете установить некое подключение к серверу MQTT, которое не применяет TLS, вам требуется удалить имеющийся вызов соответствующего метода self.client.tls_set. Кроме того, вам необходимо использовать надлежащий порт вместо значения порта 8883, который определён при работе с TLS. Помните, что значением порта по умолчанию когда вы не работаете с TLS является 1883.

Последующие строки определяют статический метод on_connect, который является частью нашего класса VehicleCommandProcessor. Вам необходимо добавить эти строки в существующий файл Python vehicle_mqtt_client.py. Файл данного кода для этого примера содержится в нашей папке mqtt_python_gaston_hillar_04_01 в файле vehicle_mqtt_client.py.


@staticmethod
def on_connect(client, userdata, flags, rc):
    print("Result from connect: {}".format(
        mqtt.connack_string(rc)))
    # Проверяем 
	Check whether the result form connect is the CONNACK_ACCEPTED connack code
    if rc == mqtt.CONNACK_ACCEPTED:
        # Подписываемся на соответствующий фильтр тем команд
        client.subscribe(
        VehicleCommandProcessor.commands_topic,
        qos=2)
 	   

После того как подключение к имеющемуся серверу MQTT выполнено успешно, будет исполнен тот обратный вызов, который определён в атрибуте self.client.on_connect, то есть статический метод on_connect (помеченный декоратором @staticmethod). Данный статический метод получает значение экземпляра mqtt.Client, которое установило необходимое подключение со своим сервером MQTT в соответствующем аргументе клиента.

Данный код проверяет полученное значение в соответствующем аргументе rc, который предоставляет возвращаемый сервером MQTT код результата CONNACK. Если это значение соответствует mqtt.CONNACK_ACCEPTED, это означает что наш сервер MQTT принял полученный запрос на соединение и, следовательно, данный код вызывает необходимый метод client.subscribe с VehicleCommandProcessor.commands_topic в качестве аргумента для подписки на ту тему, которая определена в соответствующем аргументе класса commands_topic и определяет уровень QoS 2 для данной подписки.

Приводимые далее строки объявляют необходимый статический метод on_subscribe, который является частью нашего класса VehicleCommandProcessor. Вам следует добавить эти строки в уже имеющийся файл Python mqtt_python_gaston_hillar_04_01. Сам файл кода этого примера содержится в папке vehicle_mqtt_client.py в файле vehicle_mqtt_client.py:


@staticmethod
def on_subscribe(client, userdata, mid, granted_qos):
    print("I've subscribed with QoS: {}".format(
        granted_qos[0]))
 	   

Наш статический метод on_subscribe отображает значение уровня QoS, предоставляемый имеющимся сервером MQTT для данного определённого нами фильтра темы. В данном случае мы просто подписываемся на некий единственный фильтр темы, и, следовательно, наш код выхватывает самое первое значение из полученного массива granted_qos.

Далее приводятся строки определения статического метода on_subscribe, который является частью нашего класса VehicleCommandProcessor. Вам нужно добавить эти строки к имеющемуся файлу Python vehicle_mqtt_client.py. Файл с этим кодом имеется в папке mqtt_python_gaston_hillar_04_01 в вайле с названием vehicle_mqtt_client.py:


@staticmethod
def on_message(client, userdata, msg):
    if msg.topic == VehicleCommandProcessor.commands_topic:
        print("Received message payload:
        {0}".format(str(msg.payload)))
        try:
            message_dictionary = json.loads(msg.payload)
            if COMMAND_KEY in message_dictionary:
                command = message_dictionary[COMMAND_KEY]
                vehicle = VehicleCommandProcessor.active_instance.vehicle
                is_command_executed = False
                if KEY_MPH in message_dictionary:
                    mph = message_dictionary[KEY_MPH]
                else:
                    mph = 0
                if KEY_DEGREES in message_dictionary:
                    degrees = message_dictionary[KEY_DEGREES]
                else:
                    degrees = 0
                command_methods_dictionary = {
                    CMD_TURN_ON_ENGINE: lambda:
                    vehicle.turn_on_engine(),
                    CMD_TURN_OFF_ENGINE: lambda:
                    vehicle.turn_off_engine(),
                    CMD_LOCK_DOORS: lambda: vehicle.lock_doors(),
                    CMD_UNLOCK_DOORS: lambda:
                    vehicle.unlock_doors(),
                    CMD_PARK: lambda: vehicle.park(),
                    CMD_PARK_IN_SAFE_PLACE: lambda:
                    vehicle.park_in_safe_place(),
                    CMD_TURN_ON_HEADLIGHTS: lambda:
                    vehicle.turn_on_headlights(),
                    CMD_TURN_OFF_HEADLIGHTS: lambda:
                    vehicle.turn_off_headlights(),
                    CMD_TURN_ON_PARKING_LIGHTS: lambda:
                    vehicle.turn_on_parking_lights(),
                    CMD_TURN_OFF_PARKING_LIGHTS: lambda:
                    vehicle.turn_off_parking_lights(),
                    CMD_ACCELERATE: lambda: vehicle.accelerate(),
                    CMD_BRAKE: lambda: vehicle.brake(),
                    CMD_ROTATE_RIGHT: lambda:
                    vehicle.rotate_right(degrees),
                    CMD_ROTATE_LEFT: lambda:
                    vehicle.rotate_left(degrees),
                    CMD_SET_MIN_SPEED: lambda:
                    vehicle.set_min_speed(mph),
                    CMD_SET_MAX_SPEED: lambda:
                    vehicle.set_max_speed(mph),
                }
                if command in command_methods_dictionary:
                    method = command_methods_dictionary[command]
                    # вызываем соответствующий метод
                    method()
                    is_command_executed = True
                if is_command_executed:
                    VehicleCommandProcessor.active_instance.publish_executed_command_message(message_dictionary)
                else:
                    print("I've received a message with an unsupported command.")
        except ValueError:
                # msg не является словарём
                # Никакой объект JSON не может быть декодирован
                print("I've received an invalid message.")
 	   

Всякий раз когда мы получаем некое новое сообщение в теме, хранимой в нашем атрибуте класса commands_topic, на которую у нас имеется подписка, будет исполнен этот определяемый в атрибуте self.client.on_messsage обратный вызов, а именно предварительно написанный статический метод on_message (помеченный соответствующим декоратором @staticmethod). Этот статический метод получает значение того экземпляра mqtt.Client, который установил подключение к имеющемуся серверу MQTT в своём аргументе клиента и некий экземпляр mqtt.MQTTMessage в значении аргумента msg.

[Замечание]Замечание

Наш класс mqtt.MQTTMessage определяет некое входящее сообщение.

Значедние атрибута msg.topic указывает ту тему, согласно которой будет получаться данное сообщение. Такми образом, наш статическиц метод проверяет соответствует ли полученный атрибут msg.topicустановленному в атрибуте класса commands_topic значению. В нашем случае, всякий раз когда исполняется наш метод on_message, значение msg.topic всегда соответствует установленному значению в соответствующем атрибуте класса темы, потому что мы подписались только на одну тему. Однако, ксли мы подписаны более чем н одну тему, всегда требуется проверка какая именно тема в каом сообщении была отправлена и по этому признаку мы принимаем данное сообщение. Следовательно, мы включаем данный код чтобы получить полное понимание идеи того как проверять значение topic для полученного сообщения.

Наш код печатает значение полезнjq нагрузки полученного сообщения, или иначе, значение атрибута msg.payload. Затем наш код назначает полученный результат функции json.loads для разбора msg.payload в некий объект Python и назначения полученных результатов значению локальной переменной message_dictionary. Если содержимое msg.payload не является JSON, будет перехвачена исключительная ситуация ValueError, значение кода будет выведено на печать сообщения, которое указывает что данное сообщение не содержит раельной команды, и никакой иной код не будет больше исполняться в этом статическом методе. Если же содержимое msg.payload являеься JSON, мы получим некий словарь в своей локальной переменной message_dictionary.

Затем наш код проверит включено ли сохранённое в COMMAND_KEY значение строки в наш словарь message_dictionary. Если полученное значение True, это означает, что данное сообщение JSON преобразовано в некий словарь, содержащий команду, которую мы обработали. Тем менее, прежде чем мы сможем обработать данную команду, нам придётся проверить что это за команда была и, следовательно, получить значения, связанные со значениями ключей, эквивалентными значениям, сохранённым в значении строки COMMAND_KEY. Данный код обладает возможностью запускать определённый код когда его значение является одной из команд, которые мы проанализировали как необходимые.

Наш код код применяет значение атрибута класса active_instance, который располагает ссылкой на значение активного экземпляра VehicleCommandProcessor для вызова всех необходимых методов для тех рассматриваемых транспортных средств, которые базируются на подлежащих исполнению командах. Нам требуется определять обратные вызовы как статические методы, и таким образом мы применяем данный атрибут класса для доступа к активному экземпляру. Когда данная команда успешно выполнена, мы применяем данный атрибут класса для доступа к активному экземпляру. После успешного выполнения данной команды наш код устанавливает значение флага is_command_executed в True. Наконец, наш код проверяет собственно значение этого флага, и если оно эквивалентно True, наш код вызывает publish_executed_command_message для экземпляра VehicleCommandProcessor, сохранённого в значении атрибута класса active_instance.

[Совет]Совет

Естественно, в примере из реальной жизни нам соедует добавить дополнительные проверки. Предыдудщий код упрощён с тем, чтобы позволить нам сосредоточиться на MQTT.

Приводимые далее строки объявляют метод publish_executed_command_message, который составляет часть класса VehicleCommandProcessor. Вам следует добавить эти строки в имеющийся файл Python vehicle_mqtt_client.py. Файл этого кода для нашего образца содержится в нашей папке mqtt_python_gaston_hillar_04_01 в соответствующем файле vehicle_mqtt_client.py:


def publish_executed_command_message(self, message):
    response_message = json.dumps({
        SUCCESFULLY_PROCESSED_COMMAND_KEY:
            message[COMMAND_KEY]})
    result = self.client.publish(
        topic=self.__class__.processed_commands_topic,
        payload=response_message)
    return result
 	   

Наш метод publish_executed_command_message получает необходимый словарь сообщения, который был получен в этой команде в аргументе сообщения. Этот метод вызывает функцию json.dumps со занчением команды в соответствующем аргументе сообщения. Данный метод вызывает имеющуюся функцию client.publish для разбора некоторого словаря в строку в формате JSON с соответствующим сообщением отклика, которое указывает, что данная команда была успешно обработана. Наконец, наш код вызывает имеющийся метод client.publish со значением переменной processed_commands_topic в качестве значения аргумента темы и со значением строки в формате JSON (response_message) в полученном аргументе payload.

[Совет]Совет

В данном случае мы не определяем ответ, полученный из нашего метода publish. Кроме того, мы применяем установленные по умолчанию значения для своего аргумента qos, который определяет желаемый уровень качества обслуживания. Таким образом, мы опубликуем это сообщение с неким уровнем QoS, равным 0. В Главе 5, Проверка и совершенствование нашего решения управления транспортным средством в Python мы выполним работу с более изощрёнными сценариями в которых мы добавим код в обратный вызов on_publish, который запускается когда сообщение успешно опубликовано, как это мы делали в своём предыдущем примере. В данном случае мы применяем уровень QoS только для тех сообщений, которые мы получаем со своими командами.

Работа с множеством вызовов в методе loop

Следующие далее строки объявляют метод process_incoming_commands, который составляет часть класса VehicleCommandProcessor. Вам следует добавить эти строки в имеющийся файл Python vehicle_mqtt_client.py. Файл этого кода для нашего образца содержится в нашей папке mqtt_python_gaston_hillar_04_01 в соответствующем файле vehicle_mqtt_client.py:


def process_incoming_commands(self):
    self.client.loop()
 	   

Наш метод process_incoming_commands вызывает метод loop для своего клиента MQTT и обеспечивает поддержку взаимодействия с с сервером MQTT. Представляйте себе такой вызов метода loop как синхронизацию вашего почтового ящика. Все ожидающие публикации сообщения в его исходящем блоке будут отправлены, а все входящие сообщения придут в его входящий блок, а все события, которые мы анализировали ранее будут возбуждены. Таким образом процессор команд данного транспортного средства получит сообщения и обработает команды.

Наконец, приводимые ниже строки объявляют основной блок (main) кода. Вам следует добавить эти строки в имеющийся файл Python vehicle_mqtt_client.py. Файл этого кода для нашего образца содержится в нашей папке mqtt_python_gaston_hillar_04_01 в соответствующем файле vehicle_mqtt_client.py:


if __name__ == "__main__":
    vehicle = Vehicle("vehiclepi01")
    vehicle_command_processor = VehicleCommandProcessor("vehiclepi01",
        vehicle)
    while True:
        # Обработка сообщений и команд каждую секунду
        vehicle_command_processor.process_incoming_commands()
        time.sleep(1)
 	   

Наш метод __main__ создаёт некий экземпляр класса Vehicle, присваивает транспортному средству название "vehiclepi01" в качестве значения аргумента name. Следующая строка создаёт некий экземпляр класса VehicleCommandProcessor, именует vehicle_command_processor названием "vehiclepi01", а также предварительно созданный экземпляр Vehicle, X, в качестве значений аргументов name и vehicle. Таким образом vehicle_command_processor передаёт полномочия исполнения этих команд экземпляру методов в vehicle.

Наш конструктор для класса VehicleCommandProcessorвыполнит подписку на тему vehicles/vehiclepi01/commands в своём сервере MQTT и, таким образом, нам следует публиковать в эту тему сообщения чтобы отправлять все команды, которые будет обрабатывать наш код. Всякий раз когда команда успешно обрабатывается, в соответствующей теме vehicles/vehiclepi01/executedcommands будет опубликовано новое сообщение. Следовательно нам требуется выполнить подписку на эту тему чтобы проверять те команды, которые исполнены нашим транспортным средством.

Наш цикл while вызывает метод vehicle_command_processor.process_commands и засыпает на одну секунду {Прим. пер.: передаёт управление ядру ОС}. Наш метод process_commands вызывает метод loop для своего клиента MQTT и обеспечивает поддержку соединения с его сервером MQTT.

[Замечание]Замечание

Такще существует некий потоковый интерфейс, который мы можем запускать вызывая метод loop_start для своего клиента MQTT. Тем самым мы можем избегать множества вызовов в свой метод loop. Однако мы вызываем метод loop чтобы сделать более простой отладку нашего кода и разобраться с тем как всё обустроено под капотом. С имеющимися потоковыми интерфейсами мы будем работать в Главе 5, Проверка и совершенствование нашего решения управления транспортным средством в Python .

Проверьте свои знания

Давайте посмотрим, сможете ли вы ответить на следующие вопросы правильно:

  1. Какой из следующих модулей Python является клиентом Paho Python ?

    1. paho-mqtt

    2. paho-client-pip

    3. paho-python-client

  2. Какой из методов вам необходимо вызвать для своего экземпляра paho.mqtt.client.Client перед вызовом connect для установления соединения с сервером MQTT с применением TLS?

    1. connect_with_tls

    2. tls_set

    3. configure_tls

  3. После того как ваш экземпляр paho.mqtt.client.Client установит подключение к своему серверу MQTT, какой из атрибутов с назначенным ему обратным вызовом будет вызван?

    1. on_connection

    2. on_connect

    3. connect_callback

  4. После того как ваш экземпляр paho.mqtt.client.Client получит некое сообщение из одного из фильров тем, на которые он подписан, какой из атрибутов с назначенным ему обратным вызовом будет вызван?

    1. on_message_arrived

    2. on_message

    3. message_arrived_callback

  5. Какой из следующих методов экземпляра paho.mqtt.client.Client вызовет для нас метод loop в бесконечном цикле с блокировкой?

    1. infinite_loop

    2. loop_while_true

    3. loop_forever

Правильные ответы содержатся в Дополнении A, Решения

Выводы

В данной главе мы проанализировали все требования для управления неким транспортным средством при помощи сообщений MQTT. Мы определили те темы, которые мы будем применять и сами команды, которые будут составлять часть полезных нагрузок для управления транспортным средством. Затем мы поработали с клиентом Python Paho с тем, чтобы написать код Python, который подключает клиента MQTT к имеющемуся серверу MQTT.

Мы разобрались с теми методами, которые требуются нам для вызова клиента Python Paho и с параметрами этих вызовов. Мы проанализировали как работает обратный вызов и написали код для выполнения подписки на фильтры тем, а также для получения и обработки сообщений.