3

TLDR: How do i send a short payload from a mqtt request to aws iot to aws lambda that has a open connection via apigateway to an electron app running locally in linux.

I have a esp8266 with the following code as the init.js This code succesfully sends it's message to aws iot, with a rule set to trigger a lambda called sendmessage. Now this sendmessage lambda is connected via websockets to a Electon app locally on my linux machine. I am able to send messages from the Electron app via websockets to api gateway wss url. I followed this example here which sets up all the websockets with api gateway and aws lambdas (one being the sendmessage lambda).

load("api_config.js");
load("api_gpio.js");
load("api_mqtt.js");
load("api_sys.js");
load("api_timer.js");

let pin = 0;
GPIO.set_button_handler(
  pin,
  GPIO.PULL_UP,
  GPIO.INT_EDGE_NEG,
  50,
  function (x) {
    let res = MQTT.pub(
      "mOS/topic1",
      JSON.stringify({ action: "sendmessage", data: "pushed" }),
      1
    );

    print(res);
    print("Published:", res ? "yes" : "no");
    let connected = MQTT.isConnected();

    print(connected);
  },
  true
);
print("Flash button is configured on GPIO pin", pin);
print("Press the flash button now!");

I know that the message from iot to sendmessage lambda needs to be a websockets message, but it only has the minimal object of {"action":"sendmessage","data":"hello world"} it's missing a bunch of information that a websocket would need. But I do not need a websocket connection between aws iot - and the sendmessage lambda, I need It to go from IOT -> sendmessage lambda with minimal payload -> electron app via websockets with payload from IOT.

SENDMESSAGE LAMBDA

// Copyright 2018-2020Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

const AWS = require('aws-sdk');

const ddb = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10', region: process.env.AWS_REGION });

const { TABLE_NAME } = process.env;

exports.handler = async event => {
  let connectionData;
  
  try {
    connectionData = await ddb.scan({ TableName: TABLE_NAME, ProjectionExpression: 'connectionId' }).promise();
  } catch (e) {
    return { statusCode: 500, body: e.stack };
  }
  
  const apigwManagementApi = new AWS.ApiGatewayManagementApi({
    apiVersion: '2018-11-29',
    endpoint: event.requestContext.domainName + '/' + event.requestContext.stage
  });
  
  const postData = JSON.parse(event.body).data;
  
  const postCalls = connectionData.Items.map(async ({ connectionId }) => {
    try {
      await apigwManagementApi.postToConnection({ ConnectionId: connectionId, Data: postData }).promise();
    } catch (e) {
      if (e.statusCode === 410) {
        console.log(`Found stale connection, deleting ${connectionId}`);
        await ddb.delete({ TableName: TABLE_NAME, Key: { connectionId } }).promise();
      } else {
        throw e;
      }
    }
  });
  
  try {
    await Promise.all(postCalls);
  } catch (e) {
    return { statusCode: 500, body: e.stack };
  }

  return { statusCode: 200, body: 'Data sent.' };
};

onconnect lambda

// SPDX-License-Identifier: MIT-0

const AWS = require('aws-sdk');

const ddb = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10', region: process.env.AWS_REGION });

exports.handler = async event => {
  const putParams = {
    TableName: process.env.TABLE_NAME,
    Item: {
      connectionId: event.requestContext.connectionId
    }
  };

  try {
    await ddb.put(putParams).promise();
  } catch (err) {
    return { statusCode: 500, body: 'Failed to connect: ' + JSON.stringify(err) };
  }

  return { statusCode: 200, body: 'Connected.' };
};

ondisconnect lambda

// Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

// https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-route-keys-connect-disconnect.html
// The $disconnect route is executed after the connection is closed.
// The connection can be closed by the server or by the client. As the connection is already closed when it is executed, 
// $disconnect is a best-effort event. 
// API Gateway will try its best to deliver the $disconnect event to your integration, but it cannot guarantee delivery.

const AWS = require('aws-sdk');

const ddb = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10', region: process.env.AWS_REGION });

exports.handler = async event => {
  const deleteParams = {
    TableName: process.env.TABLE_NAME,
    Key: {
      connectionId: event.requestContext.connectionId
    }
  };

  try {
    await ddb.delete(deleteParams).promise();
  } catch (err) {
    return { statusCode: 500, body: 'Failed to disconnect: ' + JSON.stringify(err) };
  }

  return { statusCode: 200, body: 'Disconnected.' };
};

In my electron app I have the following code to test the websocket but I am getting a forbidden error. Howerver with wscat it works...

"use strict";
const { app, BrowserWindow } = require("electron");
const { Notification } = require("electron");
const WebSocket = require("ws");

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
    },
  });

  win.loadFile("index.html");
  win.webContents.openDevTools();
}

app.whenReady().then(createWindow);

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

// Tell express to use the body-parser middleware and to not parse extended bodies

const url = "wss://random.execute-api.us-east-1.amazonaws.com/Prod";
const connection = new WebSocket(url);

connection.onopen = () => {
  connection.send("hello world");
};

connection.onmessage = (e) => {
  console.log(e.data);
};

connection.onerror = (error) => {
  console.log(`WebSocket error: ${error}`);
};

function showNotification() {
  const notification = {
    title: "Basic Notification",
    body: `notification`,
  };

  new Notification(notification).show();
}

app.whenReady().then(createWindow).then(showNotification);

I now setup my mqtt event to send the same data to the lambda but I get the following error in the lambda

{
    "errorType": "TypeError",
    "errorMessage": "Cannot read property 'domainName' of undefined",
    "stack": [
        "TypeError: Cannot read property 'domainName' of undefined",
        "    at Runtime.exports.handler (/var/task/app.js:29:28)",
        "    at processTicksAndRejections (internal/process/task_queues.js:97:5)"
    ]
}

Update: Here is my last lambda where I send a message to the wss address after recieving an event from IOT, but it does not work it console logs the event but doesnt fire any of the ws.on functions

// const axios = require('axios')
// const url = 'http://checkip.amazonaws.com/';
const WebSocket = require("ws");
let response;

/**
 *
 * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
 * @param {Object} event - API Gateway Lambda Proxy Input Format
 *
 * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
 * @param {Object} context
 *
 * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
 * @returns {Object} object - API Gateway Lambda Proxy Output Format
 *
 */
exports.lambdaHandler = async (event, context) => {
  try {
    // const ret = await axios(url);

    console.log(event);

    const url = "wss://obsf.execute-api.us-east-1.amazonaws.com/Prod";
    const ws = new WebSocket(url);

    var test = { action: "sendmessage", data: "hello world from button" };

    ws.on("open", function open() {
      ws.send(JSON.stringify(test));
    });

    ws.on("message", function incoming(data) {
      console.log(data);
    });

    response = {
      statusCode: 200,
      body: JSON.stringify({
        message: "hello world",
        // location: ret.data.trim()
      }),
    };
  } catch (err) {
    console.log(err);
    return err;
  }

  return response;
};

Update: Lastly I have tried this and I can't even get an error, I know ws is there because if I console it it returns a big object with a bunch of functions

    console.log(ws); this returns a large object

    ws.on("error", console.error); this does nothing

2 Answers 2

1
+150

It seems like you're setting 1 lambda to handle 2 trigger sources, one is IoT service, the other is API Gateway Websocket. Since you use 1 lambda, you have to handle cases when the request is came from sources:

  1. While event.requestContext is available when the request is triggered from API Gateway, it is not available when the request is triggered from IoT service (check the IoT event object here https://docs.aws.amazon.com/lambda/latest/dg/services-iotevents.html). So the error you faced (which is Cannot read property 'domainName' of undefined") is about that. You should turn off the lambda trigger from IoT service or handle the request when it comes from IoT Service.
  2. I'm not sure about the forbidden error but it is more like you sent unstructured message to API gateway WS, it should be connection.send(JSON.stringify({ action: "sendmessage", data: "hello world" })); instead of connection.send("hello world");

Edited based on post update:

I know ws is there because if I console it it returns a big object with a bunch of functions

Lambda function is not really a server, it is an instance Node environment (that's why it is called FUNCTION), Lambda function doesn't work as the way you think normal Nodejs app does, its container (node environment) usually is halted (or freeze) whenever its job is done so you cannot keep its container alive like a normal server. That's the reason while you can console log the Websocket object, you cannot keep it alive, the NodeJS container was already halted whenever you return/response.

Since you cannot use the Websocket object to open WS connection in Lambda, Amazon offers a way to do that via API Gateway. The way we work with API Gateway Websocket is different than the normal server does too, it would be something like:

  • User -> request to API Gateway to connect to websocket -> call Lambda 1 (onconnect function)
  • User -> request to API Gateway to send message over Websocket -> call Lambda 2 (sendmessage function)
  • User -> request to API Gateway to close connection -> call Lambda 3 (ondisconnect function)

3 settings above is configured in API Gateway (https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integrations.html), logic of 3 functions onconnect, sendmessage, ondisconnect can be handled in 1 lambda or 3 lambda functions depending on the way we design, I check your 3 lambda functions and it looks okay.

I see that you want to use IoT but I'm not sure why. You should test your Websocket API first without anything related to IoT. It would be better if you can tell what you want to achieve here since IoT works more like a publish/subscribe/messaging channel and I don't think it's necessary to use it here.

Sign up to request clarification or add additional context in comments.

7 Comments

When you say handle the request when it comes from IoT service where do I handle it in, in a separate lambda (because a lambda on it's own cannot send a websockets request), or to on a ec2 server with websockets so I can send a websockets connection to the lambda ws setup I already have setup, like a relay -> iot -> ec2 -> wss:apigateway?
yes, you should handle it in a separated lambda, don't use 1 lambda to handle logics across services. IoT triggers lambda asynchronously and passes data from the IoT message to the function like a normal http call, not "message from iot to sendmessage lambda needs to be a websockets message" as you think.
I tried to install node websockets on a separate lambda and it didn't work, can use use websockets package to send a ws request with a lambda (not the accepting, but the sending)?
I added the code for the lambda that accepts the iot event and then tries to send a websockets event, but it only consoles the event nothing actually sends.
I added a couple updates, hopefully you can help me solve this. Thanks
|
0

I am just posting this to show how i solved it.

I instead created a node express ec2 server that handled the request from the iot device and then passed it to my wss server, this is the main portion of it that is working

this is what I did here Express node js webssockets is recieving messages from websocket server but not being able to send them

and this is the main part of the code that is working

app.post('/doorbell', (req, res) => {
  var hmm1 = { action: 'sendmessage', data: 'hello world from post request' };

  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(hmm1));
    res.send('heya hey');
  } else {
    res.send('The WebSocket connection is not open');
  }
});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.