Световая сигнализация на основе Raspberry Pi и светодиодной ленты
Соединяем Raspberry Pi с Google-календарём через веб-сервис, чтобы активировать сигнализацию на основе светодиодной ленты.
Комплектующие
Для реализации данного проекта потребуется набор комплектующих. Основой нашего устройства, безусловно, является «малина».
Raspberry Pi 3 Model B × 1
Макетная плата × 1
Перемычки папа-мама × 4
Перемычки папа-папа × 9
Блок питания на 12В × 1
Мощные MOSFET N-канальные транзисторы × 3
Штекер разъема питания постоянного тока 5,5 мм х 2,1 мм × 1
Светодиодная лента × 1
Практически все основные компоненты доступны в обычных интернет-магазинах вроде AliExpress, а дополнительные элементы для микроконтроллера можно приобрести в отечественных аналогах наподобие digitalreseller.net, где представлен широкий ассортимент деталей для управления «умными домами».
Из программного обеспечения потребуется:
Google Calendar API
Ключевая идея проекта состоит в создании веб-сервиса, получающего события из Google-календаря. Этот веб-сервис будет передавать сообщения на плату Raspberry Pi по протоколу MQTT, что позволит управлять светодиодами нашей LED-ленты.
Схема соединений
В правой части схемы расположен источник питания, подключённый к шинам питания и земли на макетной плате. Также по схеме подключены три транзистора. Всё это соединяется со светодиодной лентой.
Тестирование светодиодов
Откройте терминал на Raspberry Pi и введите:
$ sudo apt-get install build-essential unzip wget
Вместо того чтобы просто отправлять сигналы через Pi из терминала, мы напишем небольшую программу на node.js для управления источником света. Создаём новое рабочее пространство node.js в отдельном каталоге.
$ npm init
Проходим весь процесс инициализации и затем вводим:
$ npm i pigpio
Эта команда установит библиотеку ввода/вывода, с помощью которой мы будем управлять GPIO-выводами на Raspberry Pi.
Управление светодиодами осуществляется путём отправки сигналов на макетную плату через контакты 17, 22 и 24, каждый из которых отвечает за свой цвет. Если задействован только контакт 22, вы сможете включить лишь зелёный светодиод. Если до этого урока вы уже запускали какие-то процессы, рекомендуется на всякий случай выполнить:
$ sudo killall pigpiod
В системе может работать лишь один процесс pigpiod одновременно, в противном случае программа не запустится.
// импортировать пакет pigpio
var Gpio = require('pigpio').Gpio
// инициализировать наши переменные, по одной для каждого цвета
// указать пин и режим (вход или выход)
var ledRed = new Gpio(22, {mode: Gpio.OUTPUT})
// установить красный на полную яркость
ledRed.pwmWrite(255)
// установить красный на половину яркости
ledRed.pwmWrite(127)
// красный выключить
ledRed.pwmWrite(0)
Функция pwmWrite() принимает любое целое число в диапазоне от 0 до 255, где 0 соответствует выключенному состоянию (чёрный цвет), а 255 — максимальной яркости красного. Аналогичным образом инициализируем синий и зелёный.
// импортировать пакет pigpio
var Gpio = require('pigpio').Gpio
// инициализировать наши переменные, по одной для каждого цвета
// указать пин и режим (вход или выход)
var ledRed = new Gpio(22, {mode: Gpio.OUTPUT})
var ledGreen = new Gpio(17, {mode: Gpio.OUTPUT})
var ledBlue = new Gpio(24, {mode: Gpio.OUTPUT})
ledRed.pwmWrite(127)
ledBlue.pwmWrite(255)
ledGreen.pwmWrite(0)
// получаем фиолетовый цвет
Веб-сервис
На основном компьютере выполните шаг 1 из этой страницы сайта Google (руководство по работе с Google API). Создайте новый рабочий каталог и выполните следующие команды npm:
$ npm install mqtt --save
$ npm install googleapis@27 --save
Установив необходимые компоненты, переходим к основной программе.
const fs = require('fs');
const mkdirp = require('mkdirp');
const readline = require('readline');
const {google} = require('googleapis');
const OAuth2Client = google.auth.OAuth2;
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
const TOKEN_PATH = 'credentials.json';
var CREDENTIALS;
var client = {}
/**
* Авторизовать запрос от Google, а затем запросить события календаря
*/
function connectAndGetStuffFromGoogle() {
fs.readFile('client_secret.json', (err, content) => {
if (err) return console.log('Error loading client secret file:', err);
CREDENTIALS = JSON.parse(content)
authorize(CREDENTIALS, listEvents);
});
}
/* Код из руководства Google API */
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
* @param {Object} credentials The authorization client credentials.
* @param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
const {client_secret, client_id, redirect_uris} = credentials.installed;
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uris[0]);
// Проверить ранее сохраненный токен
fs.readFile(TOKEN_PATH, (err, token) => {
if (err) return getAccessToken(oAuth2Client, callback);
oAuth2Client.setCredentials(JSON.parse(token));
callback(oAuth2Client);
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
* @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
* @param {getEventsCallback} callback The callback for the authorized client.
*/
function getAccessToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
console.log('Authorize this app by visiting this url:', authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('Enter the code from that page here: ', (code) => {
rl.close();
oAuth2Client.getToken(code, (err, token) => {
if (err) return callback(err);
oAuth2Client.setCredentials(token);
// Store the token to disk for later program executions
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) console.error(err);
console.log('Token stored to', TOKEN_PATH);
});
callback(oAuth2Client);
});
});
}
/**
* Перечислить следующие 10 событий в основном календаре пользователя.
* @param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
function listEvents(auth) {
const calendar = google.calendar({version: 'v3', auth});
calendar.events.list({
calendarId: 'primary',
timeMin: (new Date()).toISOString(),
maxResults: 10,
singleEvents: true,
orderBy: 'startTime',
}, (err, {data}) => {
if (err) return console.log('The API returned an error: ' + err);
const events = data.items;
if (events.length) {
console.log('Upcoming events:');
events.map((event, i) => {
// if the event is an all day event by definition it isnt an alarm
if(typeof event.start.dateTime != 'undefined') {
const start = event.start.dateTime || event.start.date;
console.log(`${event.summary}`);
console.log('Event starts at ' + start)
// client.publish(ALARM_TOPIC, start.toString())
}
});
} else {
console.log('No upcoming events found.');
}
});
}
Скопируйте данный код. Обратите внимание, что ближе к концу программы одна строка закомментирована. Мы вернёмся к ней чуть позже. Теперь необходимо выполнить:
$ node quickstart.js
Заметьте, что команда sudo здесь не нужна, так как мы не задействуем Piod. При запуске программы в консоли появится сообщение с просьбой перейти по указанной ссылке. Перейдите по ней для авторизации веб-сервиса и предоставления прав на чтение/запись календаря Google. Вы получите access token, и будет создан файл учётных данных, который ваш компьютер станет использовать в дальнейшем.
MQTT функционал
MQTT представляет собой альтернативу HTTP, разработанную для Интернета вещей. Это означает, что протокол отличается лёгкостью и простотой использования. В отличие от HTTP, который всегда возвращает ответ, MQTT обеспечивает одностороннюю связь. Это даёт преимущество в производительности для небольших устройств, поскольку нужно лишь «прослушивать» определённые типы сообщений.
Проще говоря, клиент подписывается на MQTT-топики для получения всех сообщений по данной теме, а другие участники публикуют сообщения в топиках, отправляя их всем подписчикам. Это напоминает систему хэштегов в Twitter — вы публикуете сообщение, и любой подписчик соответствующего хэштега может его увидеть. Впрочем, MQTT, к счастью, не обязательно передаёт данные через Интернет. Необходимо создать собственный MQTT-сервер для доступа к уникальным данным, через который можно подключаться к pub/sub-темам.
npm init
npm install mqtt --save
npm install googleapis@27 --save
Можно воспользоваться сервисом cloudmqtt.com — он бесплатный и легко настраивается. Сервис позиционирует себя как «Брокер сообщений для Интернета вещей». Создаём MQTT-сервер и формируем новый файл под названием mqtt_credentials.json. Вставьте в него следующий код, указав свои данные:
{
"broker_url" : "mqtt://cloudmqtt.com:portnum",
"client_username": "username",
"client_password": "password"
}
Сохраните файл в рабочем каталоге. Далее добавим функциональность MQTT к нашей Raspberry Pi. Вставьте приведённый код сразу после инициализации всех переменных и перед комментарием об авторизации (Authorize):
const TOKEN_PATH = 'credentials.json';
var CREDENTIALS;
var client = {}
...
// Load client secrets from a local file.
fs.readFile('mqtt_credentials.json', (err, content) => {
if (err) return console.log('Error loading client secret file:', err);
// Authorize a client with credentials
var mqtt_cred = JSON.parse(content)
const {broker_url, client_username, client_password} = mqtt_cred
client = mqtt.connect(broker_url, {
username: client_username,
password: client_password
})
// When we connect to the MQTT broker, subscribe to the desired topics and
// publish that the server is listening to DEBUG
client.on('connect', function () {
console.log(DEVICE + ' is connected to MQTT')
client.subscribe(DEBUG_TOPIC)
client.publish(DEBUG_TOPIC, DEVICE + ' began listening to topic: ' + DEBUG_TOPIC + ' at ' + new Date().toString())
client.subscribe(REQUEST_TOPIC)
})
// When we recieve a published message
client.on('message', function (topic, message) {
// if the message is a request then initialize communication with google
if(topic === REQUEST_TOPIC) {
console.log('\n++++++++++begin getting events+++++++++++')
// We must begin with authoriation every time otherwhise we will not
// have access to events added after the first authorization
connectAndGetStuffFromGoogle()
}
else if(topic === DEBUG_TOPIC) // if debug then print message to terminal
console.log('Debug Message: ' + message.toString())
})
});
/**
* Authorize request from google then request calendar events
*/
Кроме того, в начале файла обязательно добавьте:
// includes mqtt
var mqtt = require('mqtt')
// different topics for different purposes
const ALARM_TOPIC = 'alarm'
const DEBUG_TOPIC = 'debug'
const REQUEST_TOPIC = 'request'
// for debug, will always print the origin device
const DEVICE = 'server'
После этого система должна быть полностью работоспособна.
Подключаем Raspberry Pi
Теперь необходимо подключить Pi к MQTT. Скопируйте файл mqtt_credentials.json в рабочий каталог на вашей «малине». Далее добавьте код для прослушивания сообщений и дождитесь их поступления.
var mqtt = require('mqtt')
var Gpio = require('pigpio').Gpio
var fs = require('fs')
const TOPIC = 'alarm' // Все сообщения для передачи тревоги
const DEBUG_TOPIC = 'debug' // Все сообщения для целей отладки
const REQUEST_TOPIC = 'request' // Все сообщения для запрашиваемых событий
const DEVICE = 'RaspberryPi' // Какое устройство
// Создание набора для хранения всех отметок времени активации
var comingEvents = new Set()
// Для каждого цвета необходимо указать, к какому разъему GPIO подключен
// и что мы будем использовать в режиме OUTPUT
var ledRed = new Gpio(22, {mode: Gpio.OUTPUT}); // set the red led to pin #22
var ledGreen = new Gpio(17, {mode: Gpio.OUTPUT});// set the green led to pin #17
var ledBlue = new Gpio(24, {mode: Gpio.OUTPUT});// set the blue led to pin #24
/* В начале */
testLEDs(); // Делаем быстрый тест светодиодов
// Загружаем данные с локального файла
fs.readFile('mqtt_credentials.json', (err, content) => {
if (err) return console.log('Error loading client secret file:', err);
// Авторизуем клиента с учетными данными
var mqtt_cred = JSON.parse(content)
const {broker_url, client_username, client_password} = mqtt_cred
client = mqtt.connect(broker_url, {
username: client_username,
password: client_password
})
// При подключении к серверу подписываемся на основные темы и тему отладки
client.on('connect', function () {
console.log(DEVICE + ' is connected to MQTT')
// Тема отладки
client.subscribe(DEBUG_TOPIC)
client.publish(DEBUG_TOPIC, DEVICE + ' began listening to topic: ' +
TOPIC + ' at ' + new Date().toString())
// Тема предупреждений, сигнализации
client.subscribe(TOPIC)
client.publish(REQUEST_TOPIC, 'RequestEvents')
// Запрос событий
setInterval(() => client.publish(REQUEST_TOPIC, 'RequestEvents'), 30000)
// Проверьте, истек ли срок действия сигналов тревоги, предупреждений
setInterval(checkAlarm, 7000)
})
client.on('message', function(topic, message) {
if(topic == TOPIC) {// if topic is alarm, wait for alarm
if(!(message == 'RequestEvents')) {
console.log(`\nReceived: ${message.toString()}`)
addToEvents(new Date(message))
}
}
// else if(topic == DEBUG_TOPIC) // if topic is debug print debug message
//console.log(message)
})
});
/* В конце */
/**
* вычисляем, как долго Pi должен ждать, прежде чем активировать светодиоды
* @param {Date} startDate Время начала события
*/
function addToEvents(startDate) {
console.log(`look here ${startDate}`)
var startTime = startDate.getTime();
if(!comingEvents.has(startTime)) {
comingEvents.add(startTime)
console.log(`\nAdded ${startTime} alarms`)
}
}
/**
* рекурсивно вызывает себя, чтобы медленно активировать светодиоды
* @param {int} r - Желаемое значение красного | default: 0
* @param {int} g - Желаемое значение зеленого | default: 0
* @param {int} b - Желаемое значение синего | default: 0
* @param {int} speed - Задержка между возрастающим значением | default: 50
*/
function increaseLEDs(r = 0, g = 0, b = 0, speed = 50) {
if (r === 0 && g === 0 && b === 0)
console.log('pew pew lights')
ledRed.pwmWrite(r)
ledGreen.pwmWrite(g)
ledBlue.pwmWrite(b)
if(r < 255) // Сначала увеличивайте значение красного до максимума, затем то же самое для b, g
setTimeout(() => increaseLEDs(r + 5, g, b, speed), speed);
else if(b < 255)
setTimeout(() => increaseLEDs(r, g, b + 5, speed), speed);
else if(g < 255)
setTimeout(() => increaseLEDs(r, g + 5, b, speed), speed);
else {
// время вернуться к черному
setTimeout(() => setLEDs(), 3000)
return;
}
}
/**
* Проверяет, истек ли срок действия сигналов тревоги,
* затем активирует сигналы тревоги, если время их активации истекло
* и отключает их
*/
function checkAlarm() {
setLEDs() // устанавливаем светодиоды на 0 для удобства
console.log('\nChecking Alarms')
var dateNow = new Date() // текущие dateTime
var timeNow = dateNow.getTime() // получить текущую дату в виде отметки времени
// содержит сигналы тревоги, которые мы удалим после их активации
var removeUs = []
// Проверка для каждого времени в наборе временных меток на предмет активации
comingEvents.forEach(function(element) {
// если текущее время больше, чем время предупреждения,
// то пришло время активировать сигнализацию
if(timeNow > element) {
console.log('Activating Alarm')
increaseLEDs();
// показываем, что сигнал тревоги был обработан
removeUs.push(element);
} else { // время до сигнала
const timeLeft = element - timeNow
console.log(`${timeLeft}ms until alarm activation`)
}
});
removeUs.forEach(function(element) { // удалить все активированные тревоги
console.log(`removing ${element} from comingEvents`)
comingEvents.delete(element)
})
}
/**
* установить светодиоды на заданные значения
* @param {int} r - Желаемое значение красного | default: 0
* @param {int} g - Желаемое значение зеленого | default: 0
* @param {int} b - Желаемое значение синего | default: 0
*/
function setLEDs(r = 0, g = 0, b = 0) {
ledRed.pwmWrite(r)
ledBlue.pwmWrite(g)
ledGreen.pwmWrite(b)
}
/**
* Посылает сигналы GPIO, чтобы загорелись светодиоды,
* чтобы проверить их работоспособность
*/
function testLEDs() {
// Установка всех LED на 0 (выкл)
setLEDs()
console.log('Test red');
// Включить красный светодиод, затем подождать 500 мс, прежде чем выключать
ledRed.pwmWrite(255);
setTimeout(function() {
ledRed.pwmWrite(0)
console.log('Test green');
// Включить зеленый светодиод, затем подождать 500 мс, прежде чем выключать
ledGreen.pwmWrite(255);
setTimeout(function() {
ledBlue.pwmWrite(0), 500
console.log('Test blue');
// Включить синий светодиод, затем подождать 500 мс, прежде чем выключать
ledBlue.pwmWrite(255);
setTimeout(() => ledGreen.pwmWrite(0), 500)
}, 500)
}, 500)
// После тестирования выключить
setTimeout(() => increaseLEDs(), 1000);
}
Данный код снабжён подробными комментариями, поэтому должен быть вполне понятен. Поскольку автор является новичком в js, частое использование setTimeout обусловлено незнанием способа обеспечить последовательное выполнение программы без запуска следующих функций до завершения предыдущей.
Для запуска на сервере просто выполните:
$ node quickstart.js
Для запуска на Pi используйте:
$ sudo node index.js
Итог
Благодарность Лукасу Морану с проекта hackster.io за предоставленный урок. На этом всё. Удачных вам проектов.