Node 和 Lambda 的回调和异步问题

Callback & Async problems with Node & Lambda

我正在为 Alexa 智能家居技能创建一个 lambda 函数来控制我的接收器。我正在使用 Amazon 提供的样板示例代码,但我是节点的新手,在等待我的回调设置响应时遇到了很多麻烦。我一直在尝试操作,好像他们的一般结构在这里是正确的。

我可以调用它,它可以很好地执行 POST 请求,但它总是 returns null(Alexa 不喜欢)。它也没有在 response.on('end' 循环中记录任何内容。似乎它正在通过 marantzAPI 调用爆发。我尝试在 marantzAPI 函数(命令,回调)中使用回调,我最终结果相同。我觉得这里有些东西我没有得到。

这是 lambda 函数:

'use strict';

var http = require('http');
var qs = require('querystring');

/**
 * We're not discovering our devices, we're just hardcoding them.  Easy!
 */
const USER_DEVICES = [
    {
        applianceId: 'marantz-sr6010-shield',
        manufacturerName: 'Marantz nVidia',
        modelName: 'SR6010 Shield',
        version: '1.0',
        friendlyName: 'Shield',
        friendlyDescription: 'nVidia Shield via Marantz SR6010',
        isReachable: true,
        actions: ['turnOn', 'turnOff'],
    }
];

/**
 * Utility functions
 */

function log(title, msg) {
    console.log(`[${title}] ${msg}`);
}

/**
 * Generate a unique message ID
 *
 * 
 * This isn't UUID V4 but it's good enough for what we're doing
 */
function generateMessageID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
    });
}

/**
 * Generate a response message
 *
 * @param {string} name - Directive name
 * @param {Object} payload - Any special payload required for the response
 * @returns {Object} Response object
 */
function generateResponse(name, payload) {
    return {
        header: {
            messageId: generateMessageID(),
            name: name,
            namespace: 'Alexa.ConnectedHome.Control',
            payloadVersion: '2',
        },
        payload: payload,
    };
}

/**
 * This is a lot easier when I'm just hard-coding my devices
 */
function getDevicesFromPartnerCloud() {
    return USER_DEVICES;
}


/**
 * The meat and potatoes, I'm butchering just the things I Need from the solid work done by Nathan Totten:
 * https://github.com/ntotten/marantz-avr/blob/master/lib/avreciever.js
 */

function marantzAPI(commands, apiCallback) {
    log('DEBUG', `MarantzAPI Invoked: ` + JSON.stringify(commands));
    var postData = {};

    // format commands for the Marantz POST (cmd0: cmd1: etc)
    // note: may need to send commands one at a time??
    for (var i=0; i<commands.length; i++) {
        postData['cmd' + i] = commands[i];
    }

    log('DEBUG', `MarantzAPI POST Data: ` + qs.stringify(postData));

    var serverError = function (e) {
        log('Error', e.message);
        apiCallback(generateResponse('UnexpectedInformationReceivedError', e.message));
    };    

    var httpCallback = function(response) {
        response.on('end', function () {
            log('DEBUG', `API Request Complete`);
            apiCallback(generateResponse('APIRequestComplete', postData));
        });        

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

    var apiRequest = http.request({
        hostname: process.env.receiverIp,
        path: '/MainZone/index.put.asp',
        port: process.env.receiverPort,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': Buffer.byteLength(qs.stringify(postData))
        },
    }, httpCallback);  

    apiRequest.on('error', serverError);
    apiRequest.write(qs.stringify(postData));
    apiRequest.end();    
} 


/**
 * Main logic
 */

function handleDiscovery(request, callback) {
    log('DEBUG', `Discovery Request: ${JSON.stringify(request)}`);

    const userAccessToken = request.payload.accessToken.trim();

    const response = {
        header: {
            messageId: generateMessageID(),
            name: 'DiscoverAppliancesResponse',
            namespace: 'Alexa.ConnectedHome.Discovery',
            payloadVersion: '2',
        },
        payload: {
            discoveredAppliances: getDevicesFromPartnerCloud(userAccessToken),
        },
    };

    log('DEBUG', `Discovery Response: ${JSON.stringify(response)}`);

    callback(null, response);
}

function handleControl(request, callback) {
    log('DEBUG', `Control Request: ${JSON.stringify(request)}`);

    const userAccessToken = request.payload.accessToken.trim();
    const applianceId = request.payload.appliance.applianceId;

    let response;
    var commands = [];

    switch (request.header.name) {
        case 'TurnOnRequest':
            // turn on the device
            commands.push('PutZone_OnOff/ON');

            // set the input
            switch (applianceId) {
                case 'marantz-sr6010-shield':
                    commands.push('PutZone_InputFunction/MPLAY');
                    break;
            }

            // I guess?  Not even sure if it actually does all this.
            commands.push('aspMainZone_WebUpdateStatus/');

            marantzAPI(commands, function(response) {
                callback(null, response);
            });
            break;

        default: {
            log('ERROR', `No supported directive name: ${request.header.name}`);
            callback(null, generateResponse('UnsupportedOperationError', {}));
            return;
        }
    }

    // I think I need to remove these, because response is not set at execution time
    // log('DEBUG', `Control Confirmation: ${JSON.stringify(response)}`);
    // callback(null, response);
}

exports.handler = (request, context, callback) => {

    switch (request.header.namespace) {
        case 'Alexa.ConnectedHome.Discovery':
            handleDiscovery(request, callback);
            break;

        case 'Alexa.ConnectedHome.Control':
            handleControl(request, callback);
            break;

        default: {
            const errorMessage = `No supported namespace: ${request.header.namespace}`;
            log('ERROR', errorMessage);
            callback(new Error(errorMessage));
        }
    }
};

Node js 环境默认是一个异步环境,你的 function marantzAPI(commands) 方法是异步执行的,所以当这个函数被执行时,控制不会就此停止,而是继续执行下一个函数并最终执行apiRequest.end(); 而前面的功能尚未完成导致空白响应。这是最可能的答案,我建议您访问 This SO thread 以获得详细说明和可能的解决方案。

我已经开始工作了。这是我更新的 marantzAPI 函数。看起来 response.on('data' 对 HTTP 请求的成功至关重要?不知道。

function marantzAPI(commands, apiCallback) {
    var postData = {};

    // format commands for the Marantz POST (cmd0: cmd1: etc)
    // note: may need to send commands one at a time??
    for (var i=0; i<commands.length; i++) {
        postData['cmd' + i] = commands[i];
    }

    log('DEBUG', `MarantzAPI Called w Data: ` + qs.stringify(postData));

    var serverError = function (e) {
        log('Error', e.message);
        apiCallback(false, e.message);
    };    

    var apiRequest = http.request({
        hostname: process.env.receiverIp,
        path: '/MainZone/index.put.asp',
        port: process.env.receiverPort,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': Buffer.byteLength(qs.stringify(postData))
        },
    }, function(response) {

        response.setEncoding('utf8');
        response.on('data', function (chunk) {
            log('DEBUG', 'CHUNK RECEIVED');
        });

        response.on('end', function () {
            log('DEBUG', `API Request Complete`);
            apiCallback(true, '');
        });        

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

    apiRequest.on('error', serverError);
    apiRequest.write(qs.stringify(postData));
    apiRequest.end();    
}