Python вещей: первые шаги
Эта статья является адаптацией моего доклада «Python of things» с конференций Web Camp 2016 и SE2016.
Интернет вещей (англ. Internet of Things, IoT) — концепция вычислительной сети физических предметов («вещей»), оснащённых встроенными технологиями для взаимодействия друг с другом или с внешней средой.
Если вы, как и я, любите Python, программируете на Python, и вас не оставляет идея применить ваши навыки в мире физических вещей, то это статья именно для вас. Считаю необходимым сразу сделать оговорку, что речь пойдет не про суровый продакшн и выпуск IoT гаджетов миллионными тиражами, а скорее про всевозможные pet-проекты, которые приятно создавать на выходных в промежутке между дедлайнами.
Железо: что предлагает рынок
Итак, мы хотим микроконтроллер или одноплатный компьютер с возможностью программирования его на Python. Что нам может предложить рынок? Скажем прямо, что рынок не особо изобилует предложениями для таких запросов, но кое-что все же есть. А если точнее, то есть 3 основных варианта:
1) PyBoard — это плата с микроконтроллером STM32F405RG, программируемая на MicroPython. MicroPython — это форк третьего питона, оптимизированный для запуска на микроконтроллерах. Основной плюс данного варианта — это то, что тут, в отличие от следующих двух вариантов, мы имеем возможность программировать непосредственно микроконтроллер, у нас нет никаких операционных систем между нашим кодом и железом. То есть мы получаем быстрое и — самое главное — прогнозируемое время отклика на любую команду, не зависящее от загруженности операционной системы, запущенных в фоне процессов и тому подобное.
Минусы — это, как видно на фото, отсутствие какой-либо периферии из коробки, то есть Wi-Fi, Bluetooth, LAN etc нужно докупать, вооружаться паяльником и припаивать самостоятельно. Также к минусам можно отнести относительно высокую цену.
Если вы уже являетесь обладателем платы с микроконтроллером других производителей, то на некоторые из них тоже есть возможность установить MicroPython. С полным списком поддерживаемых плат можно ознакомиться здесь.
2) Intel GALILEO — это уже полноценный одноплатный компьютер на базе процессора Intel® QuarkTM SoC X1000 (это
Это устройство не является микроконтроллером в прямом смысле этого слова, но обладает всеми необходимыми цифровыми и аналоговыми выходами и входами для подключения внешних устройств. При этом оно «Arduino certified», то есть поддерживает все Arduino shield и прочие внешние устройства, совместимые с Arduino, которых на рынке немало. И что наиболее важно для нас, доступ ко всем этим входам и выходами можно получить при помощи Python. Также из коробки вы получаете Mini-PCI Express, USB и LAN (RJ45) интерфейсы.
3) Raspberry/Orange/Banana Pis — последнее в нашем списке, но не последнее по значению. Это плодово-ягодное семейство, о котором мы и будем более детально говорить в этой статье. Raspberry Pi — родоначальник данного семейства, представляет собой одноплатный компьютер на базе ARM процессора, на котором можно запустить различные сборки Linux, Android и даже Windows 10 (версия для Surface с ARM процессором). Конфигурация железа (память, процессор), а также наличие внешних интерфейсов — разное в разных версиях данного продукта.
Популярность «малины» не заставила долго ждать появления толп подражателей и тут же из поднебесной поехали всевозможные ее клоны — наиболее известные это Orange Pi и Banana Pi. Все они совместимы между собой и отличаются лишь конфигурацией железа и наличием/отсутствием тех или иных внешних интерфейсов из коробки. Далее речь в этой статье пойдет про программирование именно Raspberry Pi, но если у вас есть любой из его клонов, все что вы тут прочтете с 99% вероятностью будет релевантно и для вас.
Работа с Raspberry Pi
Итак, мы хотим использовать Raspberry Pi для подключения всевозможных внешних датчиков, кнопок, моторов, лампочек, переключателей и прочих атрибутов реального мира. Давайте разберемся, что у нас для этого есть.
Мы имеем
Еще сразу же в глаза бросается тот факт, что в отличие от плат конкурентов, тут ни один pin не подписан. Так что надо либо запоминать, какой из них что делает, либо подписать (можно просто распечатать на бумажке подписи и надеть сверху, а можно приобрести специальные насадки с уже сделанными подписями для каждой ножки).
По функционалу все ножки можно разделить на 5 групп:
● GPIO (General-purpose input/output) — это программируемые пины, которые доступны вам для работы;
● Ground — земля;
● 3.3v и 5v — питание;
● EEPROM — для подключения EEPROM микросхем памяти (в принципе, их можно переопределять и использовать для других нужд, но без особой необходимости это делать не рекомендуется).
Также каждый пин имеет свой порядковый номер, по которому можно к нему обращаться из программы. Причем есть два способа нумерации — физический, где все номера расставлены по порядку, и второй, по словам создателей Raspberry, интуитивно понятный, где все номера расставлены по какому-то недоступному мне принципу:
Какой из способов использовать, вы решаете при инициализации GPIO в своей программе, после чего можете обращаться к пинам по их номеру.
Пример: зажигаем диод
Пора переходить от слов к делу, так что давайте для примера рассмотрим, пожалуй, самую простую схему, которую можно собрать — у нас есть диод, кнопка, и мы хотим, чтобы пока кнопка была нажата, диод светился, и чтобы переставал, когда мы ее отпускаем. Схема будет выглядеть так (на схеме изображена первая модель Raspberry, у которой было лишь 26 пинов):
Кнопка подключена с одной стороны к земле, а с другой — к программируемому пину (номер 16). Диод подключен одной ножкой к земле, а другой — к программируемому пину (номер 23). В данном примере при подключении диода использован 270Ω резистор, чтобы не сжечь диод (вычислить необходимое сопротивление резистора вы всегда можете сами, вспомнив несложные законы из школьной физики). Темы коротких замыканий и сжиганий мы еще коснемся, ибо сжечь все, в том числе и Raspberry, достаточно просто, к сожалению об этом я знаю не понаслышке :)
Программу, реализующую данный функционал мы разобьем на несколько логических частей.
Инициализация GPIO:
import RPi.GPIO as GPIO //(1) GPIO.setmode(GPIO.BOARD) //(2) # or GPIO.setmode(GPIO.BCM) GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP) //(3) # or pull_up_down=GPIO.PUD_DOWN GPIO.setup(23, GPIO.OUT) //(4)
(1) Если вы используете Raspbian — сборку Debian для Raspberry, которую предоставляют производители, то прямо из коробки вы получаете Python библиотеку RPi.GPIO, для доступа к PinBoard.
(2) Инициализируем PinBoard, при этом указываем вид нумерации пинов, который хотим использовать. GPIO.BOARD — физический, то есть пины нумеруются по порядку, GPIO.BCM — другой, «интуитивно понятный» вариант.
(3) Каждый из доступных для программирования пинов может быть определен как вход или выход, причем его «роль» можно изменять в процессе выполнения программы. Для определения состояния пина используется функция GPIO.setup, в которую мы передаем номер пина (в соответствии с выбранным нами ранее типом нумерации) и его состояние (GPIO.IN — вход, GPIO.OUT — выход).
Также для «входов» есть дополнительный параметр pull_up_down, который может принимать два значения — GPIO.PUD_UP и GPIO.PUD_DOWN. Разница в том будет ли подан ток на этот вход или нет, то есть мы можем использовать входы по-разному. В данном случае кнопка одним контактом подключена к земле, а вторым ко входу, на вход постоянно подается напряжение (pull_up_down = GPIO.PUD_UP). При нажатии кнопки оно замыкается с землей, и напряжение на входе падает. Падение напряжения и будет являться индикацией нажатия кнопки. Но мы можем сделать и наоборот: подключить кнопку одним контактом к источнику тока, а вторым ко входу и не подавать на него ток (pull_up_down = GPIO.PUD_DOWN). Тогда при нажатии кнопки ток на входе вырастет, и индикацией нажатия кнопки будет именно рост напряжения. Какой из вариантов выбрать, зависит от конкретной ситуации.
(4) Ну и
Жизненный цикл программы:
try: while True: if GPIO.input(18) == False: GPIO.output(23, GPIO.HIGH) else GPIO.output(23, False) finally: GPIO.cleanup()
Весь жизненный цикл программы — это бесконечный цикл, в процессе которого мы постоянно опрашиваем наш вход и ждем False, то есть момента, когда напряжение на нем упадет. Если бы использовали вариант с pull_up_down = GPIO.PUD_DOWN, то мы ждали бы True, то есть когда напряжение вырастет. И если напряжение упало, то мы подаем на наш выход ток (GPIO.HIGH или True), то есть зажигаем диод. А когда кнопку отпускают, и напряжение снова вырастает, мы перестаем подавать ток на диод (GPIO.LOW или False).
Очень важное замечание по поводу cleanup: эта функция убирает напряжение со всех программируемых пинов, которые были под напряжением — иначе после завершения работы вашей программы, все пины, которые были под напряжением, по прежнему останутся под напряжением, что может привести к нежелательным последствиям. Например, самое банальное — вы сами случайно их чем-то замкнете, или более вероятный сценарий — внутри вашей схемы произошло короткое замыкание. В таком случае может быть два исхода: либо Raspberry успеет это заметить по резко выросшему потреблению энергии вашей программой и завершит ее по exception, либо не успеет заметить и сгорит. Во втором случае вашу «малину» уже ничто не спасет, а вот в первом, если вы использовали cleanup, то подача тока прекратится. Так что рекомендую всегда заканчивать ваши программы этой строчкой.
Использование callback-ов и PWM
Давайте слегка усложним задачу. Теперь я хочу, чтобы лампочка горела всегда, но по нажатию кнопки начинала гореть не так ярко. И — для разнообразия — оформим это в виде callback-ов.
Как уже говорилось выше, аналоговых выходов тут нет, и мы не может регулировать напряжение, но мы можем использовать PWM (Pulse-Width Modulation) и таким образом регулировать частоту и жизненный цикл сигналов.
PWM — это генератор импульсов, то есть ток подается не постоянно, а импульсами. И у него есть два основных параметра — это частота (сколько импульсов будет подаваться в течение секунды) и жизненный цикл (время жизни одного импульса, который может меняться от 0 до 100%). Например, мы установили частоту 500 Гц — это значит, что в течение 1 секунды будет произведено 500 импульсов, и жизненный цикл 100% — это значит, что импульсы полностью заполнят собой эту секунду, а значит, каждый из них будет длиться 1/500 секунды. А если я изменю жизненный цикл на 50%, жизненный цикл одного импульса сократится в два раза и составит лишь 1/1000 секунды, а значит, полсекунды диод будет гореть, а полсекунды — нет. А если быть более точным, то 1/500 секунды гореть, а потом 1/500 нет, и такое быстрое мигание создаст у нас впечатление, что он начал гореть не так ярко.
import RPi.GPIO as GPIO GPIO.setmode(GPIO.BOARD) # or GPIO.setmode(GPIO.BCM) GPIO.setup(12, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(16, GPIO.OUT) pwm_led = GPIO.PWM(23, 500) //(1) pwm_led.start(100) GPIO.add_event_detect(18, GPIO.FALLING, callback=press, bouncetime=100) //(2) GPIO.add_event_detect(18, GPIO.RISING, callback=unpress, bouncetime=100) def press(): pwm_led.ChangeDutyCycle(50) //(3) def unpress(): pwm_led.ChangeDutyCycle(100) try: while True: pass //(4) finally: GPIO.cleanup()
Начало такое же точно, как в предыдущем примере, но далее начинаются различия.
(1) Привязываем PWM с частотой 500 Гц к пину 23 и запускаем его с жизненным циклом, равным 100%.
(2) Объявляем два callback для пина 18, один на падение напряжения (GPIO.FALLING) и один на рост напряжение (GPIO.RISING). Падение и рост напряжения означает, что оно опустилось ниже или поднялось выше 1.65v, то есть половины от максимального напряжения на пине. Callback — это некий callable объект, который будет вызван в каждом конкретном случае.
И есть еще один аргумент — bouncetime, в нашем случае он не нужен, но я добавил его специально, чтобы была возможность о нем упомянуть. Представьте себе, что у нас стоит немного другая задача — нужно на одно нажатие включать лампочку, на следующее включать, а на следующее опять выключать и так далее. Так как мы перешли в мир реальных вещей, тут действуют законы физики, и когда вы нажимаете на кнопку, она не сразу замыкается, а, на самом деле, слегка вибрирует, то есть вместо одного нажатия мы получаем несколько подряд, что может слегка испортить работу нашей программы. Так вот параметр bouncetime — это количество миллисекунд после события, вызвавшего callback, в течение которых программа не будет реагировать на аналогичные события. Обычно 100 миллисекунд хватает, чтобы контакты перестали дрожать и продуцировать нажатия.
(3) Объявляем две функции, которые будут вызваны в случае нажатия и отпускания кнопки. Одна из них меняет жизненный цикл PWM на 50%, тем самым делая лампочку тусклее, другая возвращает все обратно.
(4) Без бесконечного цикла и здесь некуда, иначе программа закончится, так и не успев начаться. Ну и очистка напряжения на всех пинах после окончания программы.
Дополнительные внешние устройства
На самом деле это все операции, которые доступны нам как программистам, но их более чем достаточно, чтобы реализовать практически любой проект, в разумных пределах, конечно.
Однако любой интернет вещей, кроме самого микроконтроллера, требует наличия каких-либо внешних устройств — чаще всего это всевозможные датчики света, влажности, температуры, радиации, шума и так далее. Многие из этих датчиков представляют из себя сложные реостаты, на которые подается ток, и по разнице напряжений на входе и на выходе можно определить показания датчика.
И тут мы сталкиваемся с проблемой, что мы никак не умеем измерять ток, ведь, как обсуждалось ранее, все наши входы цифровые, а аналогового ни одного нет. Но выход есть всегда, и в данном случае выход выглядит так — adafruit.com.
Эту даму зовут LadyAda (по паспорту Лимор Фрид), она закончила MIT, после чего организовала свою фирму под названием AdaFruit, которая занимается выпуском всевозможной электроники, в том числе периферии для Raspberry Pi. Также она стала первой женщиной, попавшей на обложку журнала Wired, и получила еще кучу всевозможных наград. Конечно, это не единственный выход из нашей ситуации, с таким уже успехом на ее месте могла быть фотография какого-то китайца с AliExpress, но то, что это самый импозантный выход — это уж точно :)
В каталоге ее магазина порядка 350 наименований всевозможных устройств, которые можно подключить к Raspberry Pi, в том числе и аналого-цифровой преобразователь, который поможет вам решить проблему отсутствия аналоговых портов.
Но это даже не самое главное: такой же точно преобразователь можно купить где угодно, а вот тот факт, что для каждого своего продукта они выпустили библиотеку на Python, дает этой компании много очков форы в глаза питонистов. Все эти библиотеки недоступны в pip, поэтому приходится их брать прямо у них на GitHub.
Отдельно хочется остановиться на интерфейсах для подключения внешних устройств. Raspberry аппаратно поддерживает несколько интерфейсов подключения: SPI, I2C, а также
Серверная часть: полезные сервисы
Еще пару слов про велосипеды, и как их не изобретать :) Речь у нас шла об интернете вещей, то есть вещи, подключенной к интернету, значит, там в интернете есть нечто, куда вещь отправляет какую-то информацию, будь то показания датчика или сообщения в Твиттер. Хочу посоветовать два отличных сервиса, которые позволят вам в 90% случаев не заморачиваться с написанием серверной части вашего интернета вещей.
IFTTT (if this, than that) — сервис из категории «must have» для всех, а не только для программистов IoT. Сервис позволяет создавать простые, как они их называют, «рецепты», по типу «если произошло это, сделай то». Например, если появилась новая картинка в Instagram, положи ее ко мне в Dropbox; если пришел email из Хогвартса, затвить об этом и тому подобное. На данные момент IFTTT поддерживает работу с несколькими сотнями всевозможных сервисов и позволяет строить как простые, так и крайне замысловатые рецепты.
В нашем случаем он может быть полезен возможностью построить рецепт типа: если пришел HTTP запрос такой-то, сделай то-то. То есть вы можете твитить, постить в Facebook, складывать информацию в Google drive, отправлять письма и делать еще кучу всего, просто отправляя запросы со своего Raspberry с соответствующей информацией. И вам не нужно для всего этого изучать API Твиттера, Вконтакте и всех остальных.
ThingSpeak — этот сервис направлен только на IoT. Он предоставляет массу возможностей, которые частично пересекаются с IFTTT, то есть тут тоже можно настроить некоторые действия в ответ на ваши HTTP запросы. Но, кроме этого, ThingSpeak предоставляет в ваше распоряжение облако, которые вы можете использовать для сохранения ваших данных, их анализа и последующей реакции. Самый простой вариант — вы используете его только как базу данных для собранной вами информации, например, каждые 30 секунд отправляете ему влажность воздуха в помещении, а потом просто выгребаете все данные в виде Excel или CSV файла.
Также ThingSpeak предоставляет доступ к Matlab, где вы можете настроить всевозможный анализ и визуализацию ваших данных в виде графиков и диаграмм. Плюс к этому вы можете настроить график анализа ваших данных, чтобы каждый день в конце дня получать на email сводную таблицу изменения влажности за последние сутки. И, в добавок, можно настроить триггеры, что если среднесуточная влажность упадет ниже какого-то уровня, то сделать что-нибудь — например, включить пожарную сигнализацию, чтобы приехали пожарные, все залили и таким образом повысили влажность, например :)
Вместо заключения: история из личного опыта
Заканчивая с теоретическими примерами сферических IoT в вакууме, хочу привести пример из реальной жизни, как Raspberry достаточно сильно облегчил жизнь лично мне.
Довольно продолжительное время я работал учителем в школе, и одной из главных проблем современной украинской педагогики, кроме зарплаты, является обилие бумажной работы. Особенно меня напрягало заполнение журнала. Даже не сам процесс, а куча дополнительных действий, которые необходимо было совершать. В наш век интернета и высоких технологий мне надо было ногами идти, брать какую-то книгу, вписывать в нее руками что-то... И главное, что ошибаться ни в коем случае нельзя, ибо за каждое исправление надо было писать объяснительную.
Поэтому раз в две недели специально обученные прилежные ученицы заполняли журнал за меня, чтобы все было красиво и без исправлений. Но информацию на протяжении этих двух недель надо было как-то собирать и где-то хранить. Особенно остро встал вопрос учета посещаемости, ведь, как я уже говорил, на дворе
И тут-то Raspberry и пригодился — при помощи него и NFC reader было собрано устройство учета посещаемости. В разобранном виде оно выглядело примерно так:
Каждый ученик имел карточку с NFC чипом, благо в Харькове все карточки метро оборудованы NFC чипами, так что проблем с этим не было. Я использовал NFC reader на базе чипа PN532, который поддерживал подключение через интерфейс SPI. Каждая карточка имела свой уникальный ID, который считывался и отправлялся на сервер, после чего и я, и родители видели, пришел их ребенок на урок информатики вовремя, опоздал или же не пришел вообще.
К сожалению, тогда я еще не знал про существования LedyAda и того, что у нее есть уже готовая библиотека для работы c PN532 NFC reader, поэтому мне пришлось самостоятельно читать документацию и изобретать велосипед. В общем, не повторяйте моих ошибок.
На этом, пожалуй, все. Надеюсь, что получилось интересно. Также хочется верить, что эта статья поможет тем, кто давно хотел попробовать сделать нечто свое в этой области, но все время откладывал, всё-таки решиться попробовать. А те, кто даже не думал об этом, задумаются и, может быть, тоже решатся.