Световая сигнализация на основе Raspberry Pi и светодиодной ленты

Световая сигнализация на основе 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-ленты.

Схема соединений

Схема соединений LED-ленты с Raspberry Pi

В правой части схемы расположен источник питания, подключённый к шинам питания и земли на макетной плате. Также по схеме подключены три транзистора. Всё это соединяется со светодиодной лентой.

Тестирование светодиодов

Откройте терминал на 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 за предоставленный урок. На этом всё. Удачных вам проектов.