Думаю, я затронул большинство модных словечек в этом названии. Может, я смогу втиснуть «Смарт» и «Лазеры», чтобы закончить!

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

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

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

В случае с этим проектом это означало создание быстрого примера для:

  • Распознавание речи и обнаружение намерений
  • Распознавание лиц и объектов
  • Чтение физических датчиков

Как только я все это проработал, я был готов все собрать воедино.

Этот пост не является полным пошаговым руководством. Это больше похоже на введение, которое поможет вам понять, как можно создать такой сложный проект. Здесь определенно слишком много информации, поэтому вот исходный код:
https://github.com/webondevices/geroge-ii

А теперь посмотрим, как я построил этот говорящий завод:

Распознавание речи и обнаружение намерений

Это определенно кажется непосильной задачей ...

Разве не было бы замечательно, если бы можно было просто задействовать возможности существующего голосового помощника, такого как Alexa? Это определенно сэкономит нам много времени!

Что ж, это именно то, что мы можем делать с Amazon Lex!

Lex - это сервис AWS (https://aws.amazon.com/), который позволяет использовать голосовые возможности Alexa. Фактически вы можете начать работу, не написав ни единой строчки кода.

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

В Лексе все - намерение. Выше мое getMood намерение, и вы можете видеть, как легко я могу добавить образцы предложений, чтобы вызвать это намерение. Чем больше вы добавляете, тем легче Лексу становится понять, когда должно сработать намерение.

Самое замечательное в этом то, что ваши образцы предложений не обязательно должны полностью совпадать: Lex будет использовать какой-то умный алгоритм машинного обучения в фоновом режиме и выяснит, что вы имели в виду, даже если вы сформулировали предложение немного иначе.

Затем вы также можете добавить список предложений, которые нужно отправить обратно пользователю, когда ваше намерение сработало. Если мое getMood намерение сработает, Лекс случайным образом выберет предложение из моего списка ответов:

Легкий!

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

Как бы круто это ни было, мы довольно ограничены. Например, как мне реализовать намерение getTime и вернуть текущее время, когда пользователь его запрашивает?

Как и в предыдущем примере, я добавил образцы предложений для разных способов, которыми люди могут запросить время, но вместо того, чтобы возвращать фиксированные предложения, я запускаю облачную функцию Lambda. На самом деле это просто файл JavaScript, который находится в облаке и ждет, когда вы его вызовете!

После входа в консоль AWS перейдите в службу Lambda, добавьте новую Lamba и создайте в ней файл index.js:

Я назвал свою Lamba roppySmartRobot, и это имя я выбрал в Lex для вызова при срабатывании моего getTime.

Ниже приведен код, который вы можете добавить в свой файл JavaScript Lamba для обработки намерения getTime и возврата текущего времени:

'use strict';
var AWS = require('aws-sdk');
function close(sessionAttributes, fulfillmentState, message) {
  return {
    sessionAttributes,
      dialogAction: {
        type: 'Close',
        fulfillmentState,
        message,
    },
  };
}
function getTime (intentRequest, callback) {
  const now = new Date();
  callback(
    close(intentRequest.sessionAttributes,
      'Fulfilled',
      {
        contentType: 'PlainText',
        content: `It is ${now.getHours()} ${now.getMinutes()} right now!`
       }
     )
  );
}
function dispatch(intentRequest, callback) {
  const intentName = intentRequest.currentIntent.name; 
  if (intentName === 'getTime') {
    return getTime(intentRequest, callback);
  }
  throw new Error(`Intent with name ${intentName} not supported`);
}
exports.handler = (event, context, callback) => {
  try {
    process.env.TZ = 'Europe/London';
    if (event.bot.name !== 'roppySmartRobot') {
      callback('Invalid Bot Name');
    }
    dispatch(event, (response) => callback(null, response));
  } catch (err) {
    callback(err);
  }
};

Есть много шаблонного кода, окружающего важные вещи: if condition внутри dispatch функции, которая получает имя намерения от Lex, и функцию для обработки намерения.

Функция getTime настолько проста, насколько это возможно: она извлекает время из объекта Date и возвращает час и минуту в литерале шаблона.

Передача сообщений о намерениях на завод

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

Наша облачная функция Lambda не имеет доступа к нашему настольному устройству и подключенным к нему датчикам, поэтому нам нужен трюк.

Уловка заключается в обмене сообщениями MQTT, и вот как это работает:

  1. Я добавляю намерения в Lex для всего, что я хочу обрабатывать локально, в локальной среде Node.js предприятия, где у меня есть доступ к физическим датчикам, камере и микрофону.
  2. В моей функции Lambda JavaScript, где я обрабатываю намерения, я передаю имена намерений в виде сообщения MQTT.
  3. Я подписываюсь на сообщение MQTT в локальной среде Node.js и делаю что-то в зависимости от полученного имени намерения.

Все, что нам нужно сделать в функции Lambda для передачи намерений в сообщениях MQTT, - это обработать намерения в функции dispatch:

function dispatch(intentRequest, callback) {
  const intentName = intentRequest.currentIntent.name;
    
  const controlCommands = [
    'interpretSurroundings',
    'tellAge',
    'tellSex',
    'tellMood',
    'tellTemperature',
    'tellTemperatureComfort',
    'tellSoilComfort',
    'tellLightComfort',
    'tellOwnAge',
    'tellOwnKind',
    'recogniseMe ',
  ];
  if (intentName === 'getTime') {
    return getTime(intentRequest, callback);
  }
    
  if (controlCommands.indexOf(intentName) > -1) {
    return delegateCommand(intentRequest, callback);
  }
  throw new Error(`Intent with name ${intentName} not supported`);
}

А затем создайте эту delegateCommand функцию для отправки сообщения MQTT:

function delegateCommand (intentRequest, callback) {
  const payload = {
    command: intentRequest.currentIntent.name
  };
  const params = {
    topic: 'roppyControl',
    payload: JSON.stringify(payload),
    qos: 0
  };
    
  iotdata.publish(params, function(err, data){
    if (err) {
      console.log(err);
    } else {
      callback(
        close(intentRequest.sessionAttributes,
          'Fulfilled',
          {
            contentType: 'PlainText',
            content: 'Hm'
          }));
      }
    }
  );
}

Очевидно, что для того, чтобы все это работало, вам нужно перейти в раздел IoT-сервисов AWS, зарегистрировать новый проект и скопировать URL-адрес конечной точки приложения в вызов инициализации, который необходимо добавить в начало вашей функции Lambda:

var iotdata = new AWS.IotData({endpoint: 'your.endpoint.url', region: 'eu-west-2'});

Теперь мы можем подписаться на эти сообщения в нашем локальном приложении Node.js и обрабатывать их локально. Это будут простые сообщения, например: tellTemparature или tellSoilComfort.

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



Практически вся логика приложения хорошо видна в этом файле. Я отвел всю сложную логику в отдельные сервисы. В основном мы сосредоточимся на понимании того, как работает приложение, поэтому, если вас больше интересует, как я взаимодействую с отдельными API, аппаратными компонентами и т. Д., Загляните в папку ./services приложения.

На линии 108 подписываемся на входящие сообщения:

// Subscribe to MQTT topic
mqtt.subscribe(config.mqtt.topic, messageHandler);

Название темы в моем случае --roppyControl, это имя приложения IoT, которое я зарегистрировал в AWS.

messageHandler - это объект, по сути, набор функций, из которых mqtt.subscribe могут вызывать элементы в зависимости от входящего намерения.

const messageHandler = {
  recogniseMe: async () => {
    const imageFile = await
      camera.captureImage(config.imageSettings);
    const name = await robot.recognisePersonOnPhoto(imageFile);
    const lastSeenInDays = await memory.getTimeLastSeenPerson(name);
    if (name) {
      await polly.speak(`Yes, I recognise you. You're name is ${name}!`);
      if (lastSeenInDays > 0) {
        await polly.speak(u.say.notSeen(lastSeenInDays));
      } else {
        await polly.speak(u.say.seen());
      }
    } else {
      await polly.speak(`I don't recognise you. ${u.say.neverSeen()}`);
    }
  },
  tellAge: async () => {
    const f = await getFacialFeatures();
    await polly.speak(`The youngest you could be is ${f.ageLow} years old but ${f.ageHigh} is also realistic. My guess is ${f.age}!`);
  },
  tellSex: async () => {
    const f = await getFacialFeatures();
    await polly.speak(`I think you are a ${f.gender}!`);
  },
  tellMood: async () => {
    const f = await getFacialFeatures();
    await polly.speak(`You look ${f.emotion}!`);
  },
  interpretSurroundings: async () => {
    await interpretSurroundings();
  },
  tellTemperature: async () => {
    await polly.speak(`It is ${hardware.temperature} celsius here.`);
  },
  tellTemperatureComfort: async () => {
    await robot.interpretTemperature(true);
  },
  tellSoilComfort: async () => {
    await robot.interpretSoil(true);
  },
  tellLightComfort: async () => {
    await robot.interpretLight(true);
  },
  tellOwnAge: async () => {
    await polly.speak(`I am ${config.age} old.`);
  },
  tellOwnKind: async () => {
    await polly.speak(`I am ${helpers.stringWithArticle(config.kind)}.`);
  },
};

Некоторые из них довольно просты. Например, намерения tellOwnAge и tellOwnKind возвращают предопределенные ответы из файла конфигурации в виде простого предложения. Эти строки затем передаются в polly.speak, который является просто помощником, обернутым вокруг другого сервиса AWS, называемого Polly.

AWS Polly - это сервис, который выполняет синтез речи: мы отправляем строку в API и получаем обратно аудиофайл. Моя функция Speak делает это за нас, а также немедленно воспроизводит возвращенный файл MP3.

У нас есть другие намерения, которые запрашивают информацию о физических датчиках завода: tellTemperatureComfort, tellSoilComfort, tellLightComfort. Все эти намерения вызывают функции из модуля robot, и они также будут просто отправлять Полли разные строки, чтобы она говорила вслух в зависимости от показаний датчика.

Работа с электронными датчиками

У растения есть датчик влажности почвы, датчик освещенности, датчик температуры, кнопка и три светодиода состояния. Все эти электронные компоненты подключены к Arduino UNO, который отправляет показания в приложение JavaScript через порт USB.

Работа с этими компонентами на самом деле относительно проста, и начало работы со всем этим не должно стоить вам более 10 фунтов стерлингов, если вы будете делать покупки на eBay.

Если вы хотите узнать больше об этом, прочтите мой вводный пост о JavaScript Arduino Electronics здесь:



Распознавание лиц и объектов

Растение может распознавать людей и предметы перед камерой. Он будет знать, является ли человек мужчиной или женщиной, счастливым или грустным, а также может угадать возраст. Для этого мы будем использовать еще один сервис AWS под названием Rekognition.

Я создал несколько вспомогательных функций, чтобы упростить работу с этим API:

  • camera.captureImage делает снимок с камеры устройства и возвращает вам изображение в формате JPG.
  • rekognition.findPerson находит человека на изображении JPG и пытается сопоставить его с любым из людей из вашего заранее определенного списка людей.
  • rekognition.getFacialFeatures находит человека на изображении JPG и отправляет вам список характеристик: эмоции, возраст, пол и т. Д.
  • rekognition.detectLabels находит объекты на изображении JPG и отправляет вам список имен объектов.

Все это асинхронные функции, поэтому вы можете легко использовать их с синтаксисом async .. await:

const imageFile = await camera.captureImage(config.imageSettings);
const [name, facialFeatures] = await Promise.all([
  recognisePersonOnPhoto(imageFile),
  findFacialFeaturesOnImage(imageFile)
]);
if (name) {
  await polly.speak(`Hello ${name}!`);
} else {
  await polly.speak(`Hello stranger!`);
} 
if (facialFeatures) {
  const { age, gender, emotion } = facialFeatures;
  await polly.speak(
    `You look ${emotion},
     you are a ${gender}
     and you seem to be ${age} years old.`
  );
}

Возможность использовать такие сложные API-интерфейсы - это супер!

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

Заключительные мысли

Определенно предстоит многое сделать, но цель этой статьи заключалась в том, чтобы помочь вам понять, как можно относительно легко реализовать такой сложный проект, используя существующие облачные сервисы, как AWS.

Если вам понравился этот проект или вы хотите, чтобы я написал еще что-то подобное, пожалуйста, поддержите меня аплодисментами! 👏