Waldemar Sakalus
Published © GPL3+

Alexa Voice Control for (Almost) Any Amplifier, TV, CD, DVD

A vast quantity of home entertainment devices on market are equipped with a serial port. This controller enables Alexa voice control.

IntermediateFull instructions provided3 hours10,260
Alexa Voice Control for (Almost) Any Amplifier, TV, CD, DVD

Things used in this project

Software apps and online services

AWS Lambda
Amazon Web Services AWS Lambda
Alexa Skills Kit
Amazon Alexa Alexa Skills Kit

Story

Read more

Schematics

Nulsom Ns-RS232 to Particle Photon

Code

krell.ino

Arduino
Code for Particle Photon for the serial RS-232 controller
void setup() {
    Serial1.begin(9600, SERIAL_8N1);
    Particle.function("KrellSerial", controlAmplifier);
}

void loop() {
    
}

int controlAmplifier(String command)
{
    if (command != NULL) {
        Serial1.println(command);
        return 1;
    }
    else return -1;
}

krell.js

JavaScript
Lambda code
var https = require('https');
var serialPortControllerId = "SAKA_Krell_10102017";
var serialPortEchoId = "SAKA_Krell_Echo_10102017"
var particleServer = "api.particle.io";
var particlePath = "/v1/devices/";
var particleId = "put_here_your_particle_device_id";
var powerOn = "1PWRZ";
var powerOff = "0PWRZ";
var volumeUp = "V10UZ"; //up by 10
var volumeDown = "V10DZ"; //down by 10
var setVolumeToXXXLevel = "MVLZ"; //append in front volume level xxxMVLZ
var muteEnable = "MUTZ";
var muteDisable = "UMTZ";
var selectInput1 = "SS1MZ";
var selectInput2 = "SS2MZ";
var selectInput3 = "SS3MZ";

exports.handler = function(event, context) {
    log('Input', event);
    switch (event.directive.header.namespace) {
    
    /**
    * The namespace of "Discovery" indicates a request is being made to the lambda for
    * discovering all appliances associated with the customer's appliance cloud account.
    * can use the accessToken that is made available as part of the payload to determine
    * the customer.
    */
    case 'Alexa.Discovery':
        handleDiscovery(event, context);
        break;
    case 'Alexa.PowerController':
        handleControl(event, context);
        break;

    case 'Alexa.Speaker':
        handleControl(event, context);
        break;

    case 'Alexa.InputController':
        handleControl(event, context);
        break;

    case 'Alexa.ChannelController':
        handleControl(event, context);
        break;

    default:
        log('Err', 'No supported namespace: ' + event.directive.header.namespace);
        context.fail('Something went wrong');
        break;
    }
};
/**
* This method is invoked when we receive a "Discovery" message from Alexa Connected Home Skill.
* We are expected to respond back with a list of appliances that we have discovered for a given
* customer. 
*/
function handleDiscovery(event, context) {
    var payload = {
        "endpoints":[
            {
            "endpointId": serialPortControllerId,
            "friendlyName": "Stereo",
            "description": "Serial port controller for Krell amplifier S-300i",
            "manufacturerName": "SAKA",
            "displayCategories": ["OTHER"],
            "cookie": {
                "deviceId" : particleId
                },
            "capabilities": [
            {
                "type": "AlexaInterface",
                "interface": "Alexa.Speaker",
                "version": "1.0",
                "properties":{ 
                    "supported": [
                        {
                            "name": "volume"
                        },
                        {
                            "name": "muted"
                        }
                    ]
                }
            },
            {
                "type": "AlexaInterface",
                "interface": "Alexa.PowerController",
                "version": "1.0",
                "properties": {
                    "supported" : [
                        {
                            "name": "powerState"
                        }
                    ]
                }
            },
            {
                "type": "AlexaInterface",
                "interface": "Alexa.InputController",
                "version": "1.0",
                "properties": {
                    "supported": [
                        {
                            "name": "input"
                        }
                    ]
                }
            },
            {
                "type": "AlexaInterface",
                "interface": "Alexa.ChannelController",
                "version": "1.0",
                "properties": {
                "supported": [
                        {
                            "name": "channel"
                        }
                    ]
                }
            }
        ]},
    ]};

    /**
    * Craft the final response back to Alexa Connected Home Skill. This will include all the 
    * discoverd appliances.
    */
    var header = event.directive.header;
    header.name = "Discover.Response";
    context.succeed({ event: {
    header: header, payload: payload
    } });
    //log('Discovery', result);
}
/**
 * Control events are processed here.
 * This is called when Alexa requests an action (IE turn off appliance).
 */
function handleControl(event, context) {
    var namespace = event.directive.header.namespace;
    var messageId = event.directive.header.messageId;
    var correlationToken = event.directive.header.correlationToken;
    var accessToken = event.directive.endpoint.scope.token;
    var endpointId = event.directive.endpoint.endpointId;
    var deviceId = event.directive.endpoint.cookie.deviceId;
    var name = "";
    var value = "";
    var UTCDate = new Date();
    var jsonDate = JSON.stringify(UTCDate);
    var strDate = JSON.parse(jsonDate); 
    var funcName = "KrellSerial";
    if ((namespace === "Alexa.PowerController") || 
        (namespace === "Alexa.Speaker") ||
        (namespace === "Alexa.InputController") ||
        (namespace === "Alexa.ChannelController")){

        var param = "";
        var index = "0";
        var executeParticle = true;
        if (namespace === "Alexa.PowerController") {
            if(event.directive.header.name == "TurnOn"){
                krellSerialCommand = powerOn;
                name = "powerState";
                value = "ON";
            }
            else if(event.directive.header.name == "TurnOff"){
                krellSerialCommand = powerOff;
                name = "powerState";
                value = "OFF";
            }
        }
        else if (namespace === "Alexa.Speaker") {
            if(event.directive.header.name == "SetVolume"){
                value = event.directive.payload.volume;
                krellSerialCommand = pad(value, 3) + setVolumeToXXXLevel;
                name = "volume";
            }
            else if(event.directive.header.name == "SetMute"){
                var mute = event.directive.payload.mute;
                if (mute) {
                    krellSerialCommand = muteEnable;
                    value = true;}
                else if (!mute) {
                    krellSerialCommand = muteDisable;
                    value = false;}
                name = "muted";
            }
            else if(event.directive.header.name == "AdjustVolume"){
                var volume = event.directive.payload.volume;
                if (volume >= 0) {
                    krellSerialCommand = volumeUp;
                    value = 10;}
                else if (volume << 0) {
                    krellSerialCommand = volumeDown;
                    value = -10;}
                name = "volume";
            }
        }
        else if (namespace === "Alexa.InputController") {
            if(event.directive.header.name == "SelectInput"){
                var inputNumber = event.directive.payload.input;
                if (inputNumber == "1") 
                    {
                        krellSerialCommand = selectInput1;
                    }
                else if (inputNumber == "2")
                    {
                        krellSerialCommand = selectInput2;
                    }
                else if (inputNumber == "3")
                    {
                        krellSerialCommand = selectInput3;
                    }
                else {
                    krellSerialCommand = inputNumber;
                    executeParticle = false;
                    context.succeed(generateControlError(event, 'INVALID_VALUE', 'Input name or number not recognized'));
                    }
                name = "input";
            }
        }

        else if (namespace === "Alexa.ChannelController") {
            if(event.directive.header.name == "ChangeChannel"){
                var channelNumber = event.directive.payload.channel.number;
                value = event.directive.payload.channel;
                if (channelNumber == "1") 
                    {
                        krellSerialCommand = selectInput1;
                    }
                else if (channelNumber == "2")
                    {
                        krellSerialCommand = selectInput2;
                    }
                else if (channelNumber == "3")
                    {
                        krellSerialCommand = selectInput3;
                    }
                else {
                    krellSerialCommand = channelNumber;
                    executeParticle = false;
                    context.succeed(generateControlError(event, 'INVALID_VALUE', 'Input name or number not recognized'));
                    }
                name = "channel";
            }
        }

        if(deviceId == particleId){
            index = "0";
        }
        param = krellSerialCommand;

        var options = {
            hostname: particleServer,
            port: 443,
            path: particlePath + deviceId + "/" + funcName,
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        };

        log(options);

        var data = "access_token=" + accessToken + "&" + "args=" + param;

        log(data);
        var serverError = function (e) {
            log('Error', e.message);
            context.fail(generateControlError(event, 'ENDPOINT_UNREACHABLE', 'Unable to connect to server'));
        };
        if (executeParticle) {
            var callback = function(response) {
                var str = '';
                response.on('data', function(chunk) {
                    str += chunk.toString('utf-8');
                });

                response.on('end', function() {
                log('Return Value');
                log(str);

                var result = {
                    "context": {
                        "properties": [ {
                        "namespace": namespace,
                            "name": name,
                            "value": value,
                            "timeOfSample": strDate,
                            "uncertaintyInMilliseconds": 500
                            } ]
                        },
                        "event": {
                            "header": {
                                "namespace": "Alexa",
                                "name": "Response",
                                "payloadVersion": "3",
                                "messageId": messageId,
                                "correlationToken": correlationToken
                            },
                            "endpoint": {
                                "scope": {
                                    "type": "BearerToken",
                                    "token": accessToken
                                },
                            "endpointId": endpointId
                            },
                            "payload": {}
                            }
                        };
                    context.succeed(result);
                });

                response.on('error', serverError);
            };

            var req = https.request(options, callback);

            req.on('error', serverError);

            req.write(data);
            req.end();
        }
    }
}
/**
 * Utility functions.
 */
function log(title, msg) {
    console.log(title + ": " + msg);
}
function generateControlError(event, code, description) {
    var messageId = event.directive.header.messageId;
    var correlationToken = event.directive.header.correlationToken;
    var accessToken = event.directive.endpoint.scope.token;
    var endpointId = event.directive.endpoint.endpointId;
    var response = {
        "header": {
            "namespace": "Alexa",
            "name": "ErrorResponse",
            "messageId": messageId,
            "correlationToken": correlationToken,
            "payloadVersion": "3"
            },
            "endpoint":{
                "scope":{
                    "type":"BearerToken",
                    "token":accessToken
                },
                "endpointId":endpointId
            },
            "payload": {
                "type": code,
                "message": description
            }
        };
    var result = { event : {response}};
    return result;
}
//Function to format the volume with leading zeros
function pad(num, size) {
    var s = "000" + num;
    return s.substr(s.length-size);
}

Credits

Waldemar Sakalus

Waldemar Sakalus

4 projects • 17 followers

Comments

Add projectSign up / Login