赤外線リモコンを自作する - その4サーバサイド編

## 目次
1. [赤外線リモコンを自作する - その1データ解析編](https://qiita.com/ayakix/items/3ad454f135fb63a92026)
1. [赤外線リモコンを自作する - その2データ送信編](https://qiita.com/ayakix/items/75adbe742c32689a3cae)
1. [赤外線リモコンを自作する - その3温度/湿度センサー編](https://qiita.com/ayakix/items/69cf14e57dec86f4415a)
1. [赤外線リモコンを自作する - その4サーバサイド編](https://qiita.com/ayakix/items/5161a70ffca3b84e51f3)
1. [赤外線リモコンを自作する - その5クライアント編](https://qiita.com/ayakix/items/ca69c61deff3fb3fdb20)

## 赤外線リモコンを自作する - その4サーバサイド編

今回は、赤外線リモコンであるArduinoに対して外部から命令を出すためのサーバサイドプログラムについて説明します。

## 開発環境
サーバサイドプログラムはAWS上にServerless Frameworkを使ってデプロイします。

- AWS (API GW, Lambda, DynamoDB, SQS)
- Serverless Framework
```sh
sls -v
Running "serverless" from node_modules
Framework Core: 3.19.0
Plugin: 6.2.2
SDK: 4.3.2
```

## 全体の流れ
予め[その1データ解析編](https://qiita.com/ayakix/items/3ad454f135fb63a92026)で取得した赤外線信号を、DynamoDBに保存します。
利用者は、タスク(赤外線信号の送信命令)をサーバに登録します。
Arduinoは定期的にセンサ値（温度、湿度）をサーバにアップロードする。この際、タスクが存在する場合には、サーバは返り値として赤外線信号を返します。
赤外線信号を受信した場合には、Arduinoが赤外線信号を送信し、機器を操作します。

```mermaid
sequenceDiagram
	actor User
	participant Arduino
	participant Server
	participant SQS
	par タスクの作成
		User->>Server: 信号送信タスクの作成
		Server->>SQS: 信号送信タスクをエンキュー
	and センサデータのアップロード
		Arduino->>Server: センサー値のPOST
		Server->>+SQS: 信号送信タスクキューの確認
		SQS-->>-Server: 信号送信タスクのデキュー
		Server-->>Arduino: 信号データの返却
		Arduino-->>Arduino: 赤外線信号の送信
	end

```

## サーバサイドプログラム
package.json
```json
{
  "dependencies": {
    "aws-sdk": "^2.1158.0"
  }
}
```

serverless.yml
```yml
service: homeir
frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs12.x
  stage: v1
  region: ap-northeast-1
  profile: default
  logRetentionInDays: 7
  environment:
    DB_TABLE_COMMANDS: ${self:service}-commands
    DB_TABLE_SENSOR_VALUES: ${self:service}-sensor-values
    SQS_TASK_URL: { Ref: TaskQueue }
  iam:
    role:
      statements:
        - Effect: Allow
          Action: # 必要に応じて制限してください
            - dynamodb:*
            - s3:*
            - sqs:*
          Resource: "*"

package:
    patterns:
        - '!**'
        - handler.js

functions:
  send:
    handler: handler.send
    events:
      - http:
          method: post
          path: /send
          cors:
            origins:
              - '*'
  saveCommand:
    handler: handler.saveCommand
  createTask:
    handler: handler.createTask
    events:
      - http:
          method: post
          path: /tasks
          cors:
            origins:
              - '*'

resources:
 Resources:
  DbTableCommands:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: ${self:provider.environment.DB_TABLE_COMMANDS}
      AttributeDefinitions:
        - AttributeName: key
          AttributeType: S
      KeySchema:
        - AttributeName: key
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
  DbTableSensorValues:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: ${self:provider.environment.DB_TABLE_SENSOR_VALUES}
      AttributeDefinitions:
        - AttributeName: timestamp
          AttributeType: N
      KeySchema:
        - AttributeName: timestamp
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
  TaskQueue:
    Type: "AWS::SQS::Queue"
    Properties:
      QueueName: ${self:service}-task
      MessageRetentionPeriod: 7200 # メッセージ保持秒数
```

handler.js
```js
'use strict';

const aws = require('aws-sdk');
const dynamoDB = new aws.DynamoDB.DocumentClient();
const sqs = new aws.SQS({apiVersion: '2012-11-05'});

module.exports.saveCommand = async (event) => {
  await dynamoDB.put({
    TableName: process.env.DB_TABLE_COMMANDS,
    Item: {
      key: process.env.key,
      command: process.env.command
    }
  }).promise();
  return "OK";
}

module.exports.createTask = async (event) => {
  console.log(event.body);
  const json = JSON.parse(event.body);

  // 送信すべき赤外線データを取得
  const commandItem = await dynamoDB.get({
    TableName: process.env.DB_TABLE_COMMANDS,
    Key: {
      key: json.key
    }
  }).promise();
  if (!commandItem) {
    return {
      statusCode: 404,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ 'success': false }),
    };
  }

  const commandStr = commandItem.Item.command;
  const length = (commandStr.match(/\,/g) || []).length + 1
  await sqs.sendMessage({
    MessageBody: JSON.stringify({
      command: commandStr,
      length: length,
      ceratedAt: Date.now()
    }),
    QueueUrl: process.env.SQS_TASK_URL
  }).promise();

  return {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ 'success': true }),
  };
}

module.exports.send = async (event) => {
  console.log(event.body);
  const json = JSON.parse(event.body);
  // センサデータの保存
  await dynamoDB.put({
    TableName: process.env.DB_TABLE_SENSOR_VALUES,
    Item: {
      timestamp: Date.now(),
      humidity: json.humidity,
      temperature: json.temperature
    }
  }).promise();
  // SQSより赤外線情報を取得
  const sqsMessage = await sqs.receiveMessage({
    QueueUrl: process.env.SQS_TASK_URL,
    MaxNumberOfMessages: 1
  }).promise();
  console.log(sqsMessage);
  var response = {'command': '', 'length': 0};
  if (sqsMessage.Messages) {
    const message = sqsMessage.Messages[0];
    const json = JSON.parse(message.Body)
    response['command'] = json.command;
    response['length'] = json.length;
    await sqs.deleteMessage({
      QueueUrl: process.env.SQS_TASK_URL,
      ReceiptHandle: message.ReceiptHandle
    }).promise()
  }
  console.log(response);

  return {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(response),
  };
};

```


## 関数説明
### saveCommand
saveCommand関数を使って、赤外線信号をDBに保存します。keyに信号名、commandに赤外線信号を設定し、直接Lambdaを呼び出すことでDynamoDBに保存します。

```sh
sls invoke -f saveCommand --env key=workplace_light_off --env command=3502,1732,44...
```

### createTask
利用者が、タスク(赤外線信号の送信命令)をサーバに登録するためのものです。SlackやLINE APIを経由して操作できる想定なので、POSTメソッドにより外部から呼び出せます。
タスクを受け取った場合には、タスク名から赤外線信号を取得し、SQSに命令を登録します。
BodyとしてsaveCommandで登録した信号名を指定します。

```
{
  "key": "workplace_ac_off"
}
```

### send
Arduinoがセンサ値（温度、湿度）をアップロードするためのエンドポイントです。
アップロードの際、タスクが存在する場合には、サーバは返り値として赤外線信号を返します。

## 次回
次回、クライアントであるArduino側のプログラムを解説します。