Иван Иванов   27 января в 17:35

Световая сигнализация на основе 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. Веб-сервис будет использовать MQTT для отправки сообщений на плату Raspberry Pi, которая сможет управлять светодиодами нашей LED-ленты.

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

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

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

Открываем терминал на Raspberry Pi и вводим:

$ sudo apt-get install build-essential unzip wget

Вместо того, чтобы просто использовать терминал для отправки сигналов через Pi, мы создадим небольшую программу в node.js для управления нашим источником света. Создаем новую рабочую область node.js в новом каталоге.

$ npm init

Проходим через весь процесс и далее вводим:

$ npm i pigpio

Это установит библиотеку ввода/вывода, которую мы будем использовать для управления выводами ввода/вывода на 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, чтобы получать все сообщения по этой теме, другие публикуют топики по теме, чтобы отправлять сообщения любому подписчику. Аналог системы тегов в 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

Для запуска на Пи используйте:

$ sudo node index.js

Итог

Спасибо Лукасу Морану с проекта hackster.io за предоставленный урок. На этом всё. Хороших вам проектов.