alexatts – Rev 4

Subversion Repositories:
Rev:
#!/usr/bin/env nodejs
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2020 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////

// Audioinjector cards: sysdefault:CARD=audioinjectorpi

const Gpio = require('onoff').Gpio
const mqtt = require('mqtt')
const YAML = require('yamljs')
const winston = require('winston')
const picoSpeaker = require('pico-speaker')
const googleTTS = require('google-tts-api')
const player = require('play-sound')((opts = { player: 'mpg123' }))
const https = require('https')
const tmp = require('tmp')
const fs = require('fs')
const lambda = require('was.js').lambda

// Load configuration file.
const config = YAML.load('config.yml')

// Define configuration for pico TTS.
var picoConfig = {
  AUDIO_DEVICE: config.card,
  LANGUAGE: config.language
}

// Initialize with config
picoSpeaker.init(picoConfig)

// Generate GPIO pins for configuration.
var ATTS = {}
for (var i in config.GPIO) {
  if (!config.GPIO.hasOwnProperty(i)) continue

  if (config.GPIO[i] === -1) continue

  ATTS[i] = new Gpio(config.GPIO[i], 'out')
}

// Set up logger.
winston.add(winston.transports.File, { filename: config.log })

// Initiate connection to MQTT.
const client = mqtt.connect(config.mqtt.url)

function mqttSubscribeCallback (err, granted) {
  if (err) {
    winston.info(`Unable to subscribe to MQTT topic ${config.mqtt.topic}`)
    return
  }
  winston.info(`Subscribed to MQTT topic ${config.mqtt.topic}`)
}

client.on('connect', () => {
  winston.info(`Connected to MQTT server ${config.mqtt.url}`)
  client.subscribe(config.mqtt.topic, {}, mqttSubscribeCallback)
})

client.on('error', () => {
  winston.error(`Unable to connect to MQTT server ${config.mqtt.url}`)
})

client.on('message', (topic, message) => {
  if (message.length === 0) return

  // Remove any retained message.
  client.publish(topic, '', { retain: true })

  speakMessage = `${config.alexa}, Simon says, ${message.toString()}`
  winston.info(`Received MQTT message: ${message}`)

  if (config.tts.use === 'google' && speakMessage.length > 100) {
    winston.error(
      `Total spoken message would be too long: ${speakMessage.length} characters out of ${config.tts.google.maxLength}`
    )
    return
  }

  winston.info(`Sending to Alexa: ${speakMessage}`)
  ATTS['ptt'].write(1, err => {
    if (err) {
      winston.error(`Unable to press push-to-talk button: ${err}`)
      return
    }

    lambda.switch(
      config.tts.use,
      tts => {
        winston.error(`Unknown TTS engine selected`)
      },
      tts => tts === 'google',
      tts => {
        googleTTS(speakMessage, 'en', config.tts.google.speed)
          .then(url => {
            winston.info(`Google TTS URL obtained: ${url}`)

            tmp.tmpName((err, path) => {
              if (err) {
                winston.error(`Unable to create temporary file: ${err}`)
                return
              }

              const file = fs
                .createWriteStream(path)
                .on('error', err => {
                  winston.error(
                    `Writing temporary file ${path} failed with error ${err}`
                  )
                })
                .on('finish', () => {
                  winston.info(`Google TTS file stored at ${path}`)
                  winston.info(`Speaking...`)
                  player.play(path, err => {
                    if (err) {
                      winston.error(`Unable to play the sound file: ${err}`)
                    }
                    ATTS['ptt'].write(0)
                    winston.info(`Done`)
                  })
                })
                .on('end', () => {
                  file.close()
                })

              https.get(url, (res, err) => {
                res.pipe(file)
              })
            })
          })
          .catch(err => {
            winston.error(`Google TTS error: ${err}`)
          })

        return true
      },
      tts => tts === 'picotts',
      tts => {
        picoSpeaker.speak(speakMessage).then(() => {
          winston.info('Message ' + speakMessage + ' sent to Alexa.')

          ATTS['ptt'].write(0)
        })

        return true
      }
    )
  })
})