ヘボコン2018に参戦したロボットに搭載したシステム(2018.07.28更新):備忘録として

息子がデイリーポータルZ主催のヘボコン2018(2018年6月30日、於東京カルチャーカルチャー)に参戦しました。

息子のロボットにはArduinoベースのリモートコントロール(Wifi)システムを搭載してます。

 

システム構成

  • ロボット側 ESP8266(ESPr Developer)
  • コントローラ ESP32(M5Stack Gray)
  • Wifi親機 iPhoneテザリング(2018.07.28削除)

基本動作

  1. ESP8266, M5StackともWifi親機に接続。ESP8266をWifi親機、M5StackをWifi子機として接続。(2018.07.28更新)
  2. mDNSによりM5StackはESP8266のIPアドレスを取得。
  3. WebSocketによりM5StackからESP8266に指示を伝える。

2018.07.28更新

ロボット搭載システムは、以前クラウド・サービス(Twilio)を使っていたものを流用したためiPhoneテザリング接続する作りでしたが、ESP8266をWifi親機としてM5Stackが直接接続するように変更しました。

ESP8266が制御するロボットの動き

  1. 熊の生首(ポンポン)
    高輝度LEDを点灯させ、サーボモータにより首を左右に振らせます。
  2. プリズムの回転
    PWMで回転速度を制御します。
    超低速で回転させたかったので、周波数を低くしてます。そのためぎこちない動きとなってしまいました。

おまけの機能

E5Stack Grayは加速度センサー搭載なので、躍度(加加速度)が閾値を超えた時にブザーが鳴る機能を付けました。音が大きいので鳴るとびっくりするという意味不明の機能です。

回路とスケッチ

息子はArduinoスターターキットとGenuinoスターターキットを持っています(ヘボコン・ワールドチャンピオンシップ2016のArduino賞を受賞した時の副賞です)。

モータをPWMでドライブする回路のスイッチング素子として、最初はスターターキットに入っていたIRF520Nを流用したのですが、出力電圧3.3VのESP8266ではゲートターンオンできませんでした。そこで検索してデータシートを調べ、2SK2232というパワーMOS FETを購入し代わりに使いました。

回路図

f:id:tsun226:20180625214330p:plain

ESP8266スケッチ(2018.07.29更新)

#ifdef ESP8266
extern "C" {
#include "user_interface.h"
}
#endif

#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WebSocketsServer.h>
#include <Servo.h>

 

// Wi-Fi parameters
const char* ssid = "{SSID}";
const char* pass = "{PASSWORD}";
const IPAddress ip_address({Wifi Station IP address});  // 2018.07.28 added
const IPAddress ip_subnet({Subnet mask});                   // 2018.07.28 added
const char* hostString = "{HOST NAME}";

 

#define LED1 12
#define LED2 14
#define IO_SERVO 16
#define IO_MOTOR 13

#define BEAR_RESET 0
#define BEAR_START 1
#define BEAR_STOP 2
#define BEAR_LOOP 3
#define BEAR_SWING 50 // bear swinging degree

#define MOTOR_RESET 0
#define MOTOR_UP 1
#define MOTOR_DOWN 2
#define MOTOR_KEEP 3
#define MOTOR_OFF 4
#define MOTOR_LOOP 5
#define MOTOR_MAX 1000 // 0 - 1023?
#define MOTOR_ACCELL 2
#define MOTOR_PWM_FREQ 10

 

// WebSocket Server
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length);
WebSocketsServer webSocket = WebSocketsServer(80);

 

// Servo
Servo servo;

 

// Setup
void setup() {
  Serial.begin(115200);
  Serial.println("Setup.");
  wifiConnection();
  setupMDNSServer();
  setupWebSocketServer();
  bear(BEAR_RESET);
  motorControl(MOTOR_RESET);
}

 

// Loop
void loop() {
  if(WiFi.status() == WL_CONNECTED || wifiConnection() ||       setupMDNSServer() || setupWebSocketServer()) {
    webSocket.loop();
  } else {
    Serial.println("NO WORK");
    delay(10);
  }
  bear(BEAR_LOOP);
  motorControl(MOTOR_LOOP);
}

 

// WiFi 2018.07.28 modified
boolean wifiConnection() {
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(ip_address, ip_address, ip_subnet);
  WiFi.softAP(ssid, pass);
  return(true);
}

 

// mDNS Server
bool setupMDNSServer() {
  if (!MDNS.begin(hostString, WiFi.localIP())) {
    Serial.println("Error setting up MDNS responder!");
    return(false);
  }
  Serial.println("mDNS responder started");
  MDNS.addService("ws", "tcp", 80);
  return(true);
}

 

// WebSocket Server
bool setupWebSocketServer() {
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
  return(true);
}

 

// WebSocket Event Prosessing
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
  char buff[4];
  int len;

 

  switch (type) {
  case WStype_DISCONNECTED:
    Serial.println("WS disconnected.");
    break;
  case WStype_CONNECTED:
    Serial.println("WS connected.");
    break;
  case WStype_TEXT:
    len = min(3, (int)length);
    for(int i = 0; i < len; i++) {
      buff[i] = (char)payload[i];
    }
    buff[len] = '\0';
    Serial.println("WS: Text Received: " + String(buff));
    parseRequest(buff);
    break;
  }
}

 

void parseRequest(char *buff) {
  static boolean bear_status = false;
  static boolean motor_status = false;


  if(buff[0] == '1') { // button A pressed
    bear_status = ! bear_status;
    if(bear_status) {
      bear(BEAR_START);
    } else {
      bear(BEAR_STOP);
    }
  }
  if(buff[1] == '1') { // button B pressed
    motor_status = ! motor_status;
    if(motor_status) {
      motorControl(MOTOR_UP);
    } else {
      // motorControl(MOTOR_DOWN);
      motorControl(MOTOR_OFF);
    }
  }
  if(buff[1] == '2' && motor_status) { // button B released
    motorControl(MOTOR_KEEP);
  }
}

 

void bear(int command) {
  static int mode = 0; // 0:off, 1:on
  static int status = 0; // 0:foward -> 1:left -> 2:forward -> 3:right
  const int deg[] = {90, 90 - BEAR_SWING, 90, 90 + BEAR_SWING};
  static unsigned long last = 0L;


  switch(command) {
  case BEAR_RESET:
    Serial.print("Bear RESET: ");
    pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT);
    digitalWrite(LED1, HIGH); digitalWrite(LED2, HIGH);
    servo.attach(IO_SERVO);
    delay(500);
    // no break
  case BEAR_STOP:
    Serial.println("Bear STOP");
    servo.write(deg[status]);
    digitalWrite(LED1, LOW); digitalWrite(LED2, LOW);
    mode = 0;
    status = 0;
    servo.write(deg[status]);
    break;
  case BEAR_START:
    Serial.println("Bear START");
    mode = 1;
    status = 0;
    last = millis();
    digitalWrite(LED1, HIGH); digitalWrite(LED2, HIGH);
    break;
  case BEAR_LOOP:
    if(! mode || last + 2000L > millis()) {
      break;
    }
    last = millis();
    status = (status + 1) % 4;
    Serial.print("Bear angle "); Serial.println(deg[status]);
    servo.write(deg[status]);
    break;
  }
}


void motorControl(int command) {
  static int status = 0; // 0:off, 1:up, 2:keep, 3:down
  static int speed = 0;
  static unsigned long last = 0L;


  switch(command) {
  case MOTOR_RESET:
    Serial.print("Motor RESET: ");
    pinMode(IO_MOTOR, OUTPUT);
    analogWriteFreq(MOTOR_PWM_FREQ);
    analogWriteRange(MOTOR_MAX);
    // no break
  case MOTOR_OFF:
    status = 0;
    speed = 0;
    analogWrite(IO_MOTOR, speed);
    Serial.println("Motor OFF: ");
    break;
  case MOTOR_UP:
    Serial.println("Motor UP");
    status = 1;
    speed += MOTOR_ACCELL;
    last = millis();
    analogWrite(IO_MOTOR, speed);
    break;
  case MOTOR_DOWN:
    Serial.println("Motor DOWN");
    status = 3;
    speed -= MOTOR_ACCELL;
    last = millis();
    analogWrite(IO_MOTOR, speed);
    break;
  case MOTOR_KEEP:
    Serial.println("Motor KEEP");
    status = 2;
    break;
  case MOTOR_LOOP:
    if(status == 0 || status == 2 || last + 300L > millis() || speed <= 0 || speed >= MOTOR_MAX) {
      break;
    }
    if(status == 1) {
      speed += MOTOR_ACCELL;
    } else if(status == 3) {
      speed -= MOTOR_ACCELL;
    }
    Serial.print("Motor ACCELL "); Serial.println(speed);
    last = millis();
    analogWrite(IO_MOTOR, speed);
    break;
  }
}

 M5Stackのスケッチ

#include <Arduino.h>

#include <M5Stack.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <WiFiClientSecure.h>
#include <ESPmDNS.h>
#include <WebSocketsClient.h>
#include "utility/MPU9250.h"
#include "utility/quaternionFilters.h"

 

WiFiMulti WiFiMulti;
WebSocketsClient webSocket;

 

// Wi-Fi parameters
const char* ssid = "{SSID}";
const char* pass = "{PASSWORD}";
const char* wsHost = "{ESP8266 HOST NAME}";
const char* hostString = "{MY HOSTNME}";
IPAddress esp8266Ip;

 

// MPU9250
#define AHRS true // Set to false for basic data read
MPU9250 IMU;

#define JERK_THR 700.0f
#define BEEP_FREQ 880
#define BEEP_TIME 500
#define TSIZE_S 2
#define TSIZE_ST 3
#define TSIZE_L 7

boolean mpu2950_active = true;
boolean network_active = true;
boolean beep_mode = false;

 

// Setup
void setup() {
  Serial.begin(115200);
  Serial.println("Setup.");
  M5.begin();
  M5.Lcd.setTextSize(TSIZE_S);
  M5.Lcd.fillScreen(0); M5.Lcd.setTextColor(WHITE, 0);   M5.Lcd.setCursor(0, 0);
  M5.Lcd.println("Setup.");
  delay(1000);
  if(M5.BtnA.isPressed()) { // Button B -> network disable
    network_active = false;
    M5.Lcd.println("Network disabled.");
  }
  if(M5.BtnB.isPressed()) { // Button A -> mpu2950 disable
    mpu2950_active = false;
    M5.Lcd.println("MPU2950 disabled.");
  }
  if(mpu2950_active) {
    setupMPU9250();
  }
  if(network_active) {
    wifiConnection();
    retrieveEsp8266Ip();
    setupWebSocketClient();
  }
  display_hebocon();
}

 

// Loop
void loop() {
  if(mpu2950_active) {
    checkMPU9250();
  }
  if(network_active) {
    if(WiFiMulti.run() == WL_CONNECTED || wifiConnection() ||           setupWebSocketClient()) {
      webSocket.loop();
      sendStatus();
    } else {
      Serial.println("NO WORK");
      M5.Lcd.fillScreen(RED); M5.Lcd.setTextColor(WHITE, RED);       M5.Lcd.setCursor(0, 0);
      M5.Lcd.println("NO WORK");
    }
  }
}

 

// MPU2950
void setupMPU9250() {
  Wire.begin();
  byte c = IMU.readByte(MPU9250_ADDRESS, WHO_AM_I_MPU9250);
  Serial.print("MPU9250: I AM ");
  Serial.print(c, HEX);
  Serial.print(" : ");
  Serial.println(0x71, HEX);

  // Start by performing self test and reporting values
  IMU.MPU9250SelfTest(IMU.SelfTest);

  // Calibrate gyro and accelerometers, load biases in bias registers
  IMU.calibrateMPU9250(IMU.gyroBias, IMU.accelBias);
  IMU.initMPU9250();
}

 

void checkMPU9250() {
  static float lax = 0.0f, lay = 0.0f, laz = 0.0f;
  float dax, day, daz, accl;

 

  if (IMU.readByte(MPU9250_ADDRESS, INT_STATUS) & 0x01) {
    IMU.readAccelData(IMU.accelCount); // Read the x/y/z adc values
    IMU.getAres();

    // Now we'll calculate the accleration value into actual g's
    // This depends on scale being set
    IMU.ax = (float)IMU.accelCount[0]*IMU.aRes; // - accelBias[0];
    IMU.ay = (float)IMU.accelCount[1]*IMU.aRes; // - accelBias[1];
    IMU.az = (float)IMU.accelCount[2]*IMU.aRes; // - accelBias[2];
  } // if (readByte(MPU9250_ADDRESS, INT_STATUS) & 0x01)

  // Must be called before updating quaternions!
  IMU.updateTime();

  IMU.delt_t = millis() - IMU.count;
  if (IMU.delt_t > 100) {
    // Print acceleration values in milligs!
    Serial.print("X-acceleration: "); Serial.print(1000*IMU.ax); Serial.print(" (");     Serial.print(1000 * lax);
    Serial.print(" mg ");
    Serial.print("Y-acceleration: "); Serial.print(1000*IMU.ay); Serial.print(" (");     Serial.print(1000 * lay);
    Serial.print(" mg ");
    Serial.print("Z-acceleration: "); Serial.print(1000*IMU.az); Serial.print(" (");     Serial.print(1000 * laz);
    Serial.println(" mg ");
    if(lax != 0.0f && lay != 0.0f && laz != 0.0f) { // not initial
      dax = IMU.ax - lax;
      day = IMU.ay - lay;
      daz = IMU.az - laz;
      accl = sqrt*1 * 1000;
      Serial.print("JERK: "); Serial.println(accl);
      M5.Lcd.setCursor(0, 210);
      M5.Lcd.print("JERK: ");
      M5.Lcd.print(accl);
      if(beep_mode) {
        M5.Lcd.print(" B");
        if(accl > JERK_THR) {
          M5.Lcd.print("EEP ");
          M5.Speaker.tone(BEEP_FREQ, BEEP_TIME);
        }
        M5.Lcd.println(" ");
      } else {
        M5.Lcd.println(" ");
      }
      IMU.count = millis();
    }
    lax = IMU.ax;
    lay = IMU.ay;
    laz = IMU.az;
  }
}


// WiFi
boolean wifiConnection() {
  WiFiMulti.addAP(ssid, pass);
  int count = 0;
  Serial.print("Connecting Wi-Fi");
  M5.Lcd.print("Connecting Wi-Fi");
  while (count < 50) {
    if (WiFiMulti.run() == WL_CONNECTED) {
      String mssg = "Connected.\nC: " + String(hostString) + "\nIP: " +       WiFi.localIP().toString();
      Serial.println();
      Serial.println(mssg);
      M5.Lcd.fillScreen(BLUE); M5.Lcd.setTextColor(WHITE, BLUE);       M5.Lcd.setCursor(0, 0);
      M5.Lcd.println(mssg);
      return(true);
    }
    delay(500);
    Serial.print(".");
    M5.Lcd.print(".");
    count++;
  }
  Serial.println("Timed out.");
  M5.Lcd.fillScreen(RED); M5.Lcd.setTextColor(WHITE, RED);   M5.Lcd.setCursor(0, 0);
  M5.Lcd.println("WiFi connecting timed out.");
  return(false);
}

 

// retrieve ESP8266 IP address by mDNS
void retrieveEsp8266Ip() {
  if (!MDNS.begin(hostString)) {
    Serial.println("Error setting up MDNS responder!");
    M5.Lcd.println("Error setting up MDNS responder!");
  }
  Serial.println("mDNS started.");
  for(int i = 0; i < 10; i++) {
    esp8266Ip = MDNS.queryHost(wsHost);
    Serial.println("mDNS res = " + esp8266Ip.toString());
    if(esp8266Ip[0] != 0) {
      Serial.println("OK");
      M5.Lcd.println("S: " + esp8266Ip.toString());
      return;
    }
    delay(500);
  }
  Serial.println("ESP8266 not found.");
  M5.Lcd.fillScreen(RED); M5.Lcd.setTextColor(WHITE, RED);   M5.Lcd.setCursor(0, 0);
  M5.Lcd.println("ESP8266 not found.");
}


// WebSocket Client
bool setupWebSocketClient() {
  webSocket.begin(esp8266Ip.toString(), 80, "/");
  webSocket.onEvent(webSocketEvent);
  webSocket.setReconnectInterval(5000);
  Serial.println("WebSocket acrivated.");
  M5.Lcd.println("WS activated.");
  return(true);
}


// WebSocket Event Prosessing
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
  switch (type) {
  case WStype_DISCONNECTED:
    Serial.println("WS disconnected.");
    break;
  case WStype_CONNECTED:
    Serial.println("WS connected.");
    break;
  case WStype_TEXT:
    String payload_str = String*2 {
    return;
  }
  last = millis();

  if(M5.BtnA.wasPressed()) { // Button A
    st[0] = '1';
  }
  if(M5.BtnA.wasReleased()) {
    st[0] = '2';
  }
  if(M5.BtnB.wasPressed()) { // Button B
    st[1] = '1';
  }
  if(M5.BtnB.wasReleased()) {
    st[1] = '2';
  }
  if(M5.BtnC.wasPressed()) { // Button C
    st[2] = '1';
    beep_mode = ! beep_mode;
  }
  if(M5.BtnC.wasReleased()) {
    st[2] = '2';
  }
  if(! String(st).equals("000")) {
    webSocket.sendTXT(st);
    Serial.println("Sent: " + String(st));
    M5.Lcd.setCursor(0, 170);
    M5.Lcd.println("Sent: " + String(st));
  }
  M5.update();
}

 

// display HEBOCON LOGO on LCD
void display_hebocon() {
  delay(500);
  M5.Lcd.setBrightness(0);
  M5.Lcd.fillScreen(YELLOW);
  M5.Lcd.setTextSize(TSIZE_L);
  M5.Lcd.setTextColor(BLUE, YELLOW);
  M5.Lcd.setCursor(20, 20);
  M5.Lcd.println("HEBOCON");
  M5.Lcd.println(" 2018");
  M5.Lcd.setTextSize(TSIZE_ST);
  delay(100);
  M5.Lcd.setBrightness(100);
  delay(400);
  M5.Lcd.setBrightness(0);
  delay(400);
  M5.Lcd.setBrightness(100);
  delay(400);
  M5.Lcd.setBrightness(0);
  delay(400);
  M5.Lcd.setBrightness(100);
}

 

*1:dax * dax) + (day * day) + (daz * daz

*2:char*) payload);
    Serial.println("WS: Text Received: " + payload_str);
    M5.Lcd.setCursor(0, 202);
    M5.Lcd.println("Received: " + payload_str);
    parseRequest(payload_str);
    break;
  }
}

 

void parseRequest(String payload_str) {
}

 

void sendStatus() {
  static unsigned long last;
  char st[4] = "000"; // BtnA,BtnB,BtnC

  if(last + 10 > millis(

Twilio Notifyの使いみち

Twilio Notifyは適切なタイミングで適切な通知を行うAPIとの触れ込みですが、今ひとつ具体的な使用イメージがついていませんでした。

2018年3月にTwilio NotifyがGAになり使ってみて、少し使い方がわかってきました。

(自分はSMSとFacebookMessengerで試してみました。)

この記事ではドキュメントから読み取って実際に検証してない事項もありますので、ご了承ください。

 

大きく次の2つの使い方があるようです。

1. Twilio側に予め通知先情報を登録し、通知するときにその中から選択して通知する。

2. 通知するときに通知先情報を与える方法(一斉通知)

いずれの場合も基本的にはチャネル(SMS,FacebookMessenger,...)の種類を意識せず通知することができます。

 

以下、この2つの使い方について概要を説明します。

通知に使用するチャネルはインストール済みであることを前提とします。

 

Ⅰ Twilio側に予め通知先情報を登録しておく方法

次の表のような通知先情報をTwilio側に登録しておき、その中からユーザとチャネルを指定して通知する例を考えてみます。

ユーザID SMS電話番号 FBメッセンジャーID 優先チャネル
001 +818011111111 0123456789012345 sms
002 +818022222222 9876543210987654 facebook-messenger
: : : :

 手順

    1. Notify Service作成
      Twilioポータルで、プロパティにSMSや事前に設定してあるチャネル(FacebookMessengerPage等)を選択し、Serviceに紐づけておきます。

    2. Binding登録
      REST API(Binding)を使い、各ユーザのチャネル毎に上記のServiceに登録していきます(例では4回APIを呼び出すことになります)。
      優先チャネルのTagには「preferred_device」と設定しておきます(Tagの使い方の一例)。
      項目
      Identity 001
      BindingType sms
      Adress +818011111111
      Tag preferred_device
      項目
      Identity 001
      BindingType facebook-messenger
      Adress 0123456789012345
      項目
      Identity 002
      BindingType sms
      Adress +81802222222
      項目
      Identity 002
      BindingType facebook-messenger
      Adress 9876543210987654
      Tag preferred_device

 

  1. 通知
    REST API(Notify)を使い、通知します。

    1. 指定ユーザの全チャネルに通知する場合
      以下のようにIdentityだけを指定すると、該当ユーザの全チャネル(例ではSMSとFacebookMessenger)に対してメッセージが通知されます。
      送信元は、それぞれServiceに紐づけてある電話番号、FacebookPageとなります。
      項目
      Identity 001
      Body メッセージ内容
      MediaUrl {画像のURL}


    2. 指定ユーザの優先チャネルに通知する場合
      以下のようにIdentityとTagを指定すると、両方が一致するユーザのチャネル(例ではユーザID 001のSMS)にメッセージが通知されます。
      項目
      Identity 001
      Tag preferred_device
      Body メッセージ内容
      MediaUrl {画像のURL}

      同様にIdentity=002, Tag=preferred_deviceを指定すると、ユーザID 002のFacebookMessengerに通知されます。

      Tagは最大20指定できます。活用することによりいろんな応用ができるでしょう。

    3. 指定ユーザの指定チャネルに通知する
      暗黙的に、TagにBindingTypeが登録されていると見なされます。
      ユーザID 002のSMSに通知したい場合は、Tagにsmsを指定します。
      項目
      Identity 002
      Tag sms
      Body メッセージ内容
      MediaUrl {画像のURL}

Binding登録したSMS電話番号が解約されて通知できなかった場合など、Bindingリストから自動的に抹消されるそうです。

Ⅱ 通知するときに通知先情報を与える方法

一斉通知するときに便利な方法です。

通知先のチャネルを混在させることもできます。

 手順

  1. Ⅰと同様にServiceを作成します。
    Bindingの登録は不要です(Binding登録されているServiceを流用することもできます)。

  2. 通知
    REST API(Notify)を使い、通知します。
    ToBindingにはチャネルと宛先をJSON形式で指定します。最大10,000件(全サイズが1MBを超えない範囲)指定できます。
    項目
    ToBinding {"binding_type":"sms","address":"+818011111111"}
    ToBinding {"binding_type":"facebook-messenger","address":"9876543210987654"}
    Body メッセージ内容
    ToBindingを指定するときは、MediaUrlパラメータは使えないようです(チャネル固有のパラメータを使って画像等を添付することは可能です)。

 

ToBindingを使った一斉送信なんかは、営業目的ですぐ使えそうですね。

Twilio Channels FacebookMessenger有効期限2ヶ月

去年の11月にTwilio ChannelsのFacebookMessengerによりProgrammable SMSを数回試した後、ずっと放置し、3月にやろうとしたところエラーになってしまいました。

(GAになったNotifyでFacebookMessengerを試したかったのです。)

 

Debuggerには次のような記録があり、アクセストークンの有効期限が過ぎてしまったのが原因だったようです。

description "Channel provider error response: 400 - {\n \"error\" : {\n \"message\" : \"Error validating access token: Session has expired on Tuesday, 02-Jan-18 21:13:25 PST. The current time is Saturday, 10-Mar-18 05:30:04 PST.\",\n \"type\" : \"OAuthException\",\n \"code\" : 190,\n \"error_subcode\" : 463,\n \"fbtrace_id\" : \"GISMqEFc77Q\"\n }\n}"description "Channel provider error response: 400 - {\n \"error\" : {\n \"message\" : \"Error validating access token: Session has expired on Tuesday, 02-Jan-18 21:13:25 PST. The current time is Saturday, 10-Mar-18 05:30:04 PST.\",\n \"type\" : \"OAuthException\",\n \"code\" : 190,\n \"error_subcode\" : 463,\n \"fbtrace_id\" : \"GISMqEFc77Q\"\n }\n}"isPassthrough "false" 

 検索して調べたところ、Facebookのアクセストークンの有効期限は2ヶ月で、Facebookに時々アクセスしてれば1日1回アクセストークンが更新されるのですが、アクセスしないでいると2ヶ月経過した時点で失効するようです。

失効してしまったら、ChannelsのFacebookMessengerを一旦アンインストールしてからもう一度インストールしなければなりません。

LogOut→LogInではダメでした。

 

(Notifyできました:-)

Twilio Speech RecognitionのpartialResultCallbackオプション

Twilioの新機能Speech Recogniton(音声認識)は、従来のTwiMLの<Gather>の拡張として実装されています。

<Gahter>はDTMFを認識する機能で、終了のキーの押下またはタイムアウトのタイミングで押されたキーの情報を取得することができます。

Speech Recognitionの場合も同様にタイムアウトのタイミングで、音声認識されて生成された文字列を取得できます。このような仕様なので、音声をリアルタイムに認識しつつ何かをやるようなアプリケーションを作るのは難しそうです。

しかし、ドキュメントによるとSpeech Recognitionをする場合の<Gather>のオプション・パラメータにpartialResultCallbackというのがあり、ここで指定したURLにTwilioはリアルタイムに部分的な音声認識の結果を送信するとのこと。

この機能を使うことにより、リアルタイムに処理を行うアプリケーションを作ることができないかと思い、partialResultCallback指定先にどのように情報が送られるのか試してみました。

送られる情報

  • SequenceNumber … シーケンス番号(五月雨式に送信されて順番が入れ替わって届くことがあるため)
  • Stability … 試した範囲では0.01か0.9のいずれかでした。
  • UnstableSpeechResult
  • StableSpeechResult … 試した範囲では常に空だったので、出力しませんでした。

動画

partialResultCallback指定先では上記情報をファイルに追記するようにし、tail -f の出力を画面に表示しています。

タイムアウト後、actionで指定したURLに送信される認識結果SpeechResultを最後にファイルに追記しています。 

出力結果

ファイルに出力された内容です。

Sequence Number = 3
Stability = 0.01
UnstableSpeechResult = '昔々'

Sequence Number = 2
Stability = 0.01
UnstableSpeechResult = '昔の'

Sequence Number = 1
Stability = 0.01
UnstableSpeechResult = '昔'

Sequence Number = 6
Stability = 0.01
UnstableSpeechResult = '昔々あると'

Sequence Number = 5
Stability = 0.01
UnstableSpeechResult = '昔々ある'

Sequence Number = 4
Stability = 0.01
UnstableSpeechResult = 'むかしむかし'

Sequence Number = 0
Stability = 0.01
UnstableSpeechResult = '床'

Sequence Number = 7
Stability = 0.01
UnstableSpeechResult = '昔々あるとこ'

Sequence Number = 9
Stability = 0.01
UnstableSpeechResult = '昔々あるところに'

Sequence Number = 11
Stability = 0.01
UnstableSpeechResult = '昔々あるところにおじい'

Sequence Number = 10
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお'

Sequence Number = 12
Stability = 0.01
UnstableSpeechResult = '昔々あるところにおじいさ'

Sequence Number = 14
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんと'

Sequence Number = 8
Stability = 0.01
UnstableSpeechResult = '昔々あるところ'

Sequence Number = 16
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さん'

Sequence Number = 19
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んで'

Sequence Number = 13
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さん'

Sequence Number = 17
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが'

Sequence Number = 18
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住ん'

Sequence Number = 15
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆'

Sequence Number = 22
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいました'

Sequence Number = 21
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいまし'

Sequence Number = 20
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んで今'

Sequence Number = 23
Stability = 0.9
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいました'

Sequence Number = 24
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじい'

Sequence Number = 29
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山'

Sequence Number = 27
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじいさんは'

Sequence Number = 26
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじいさん'

Sequence Number = 25
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじいさ'

Sequence Number = 30
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ'

Sequence Number = 28
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたおじいさんぱ家'

Sequence Number = 31
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へし'

Sequence Number = 33
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈り'

Sequence Number = 32
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へしば'

Sequence Number = 34
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りに'

Sequence Number = 35
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにお'

Sequence Number = 37
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあ'

Sequence Number = 38
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにお婆さん'

Sequence Number = 39
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは'

Sequence Number = 41
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川'

Sequence Number = 36
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りに'

Sequence Number = 43
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川伊勢'

Sequence Number = 42
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ'

Sequence Number = 44
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯'

Sequence Number = 40
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは彼'

Sequence Number = 49
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きました'

Sequence Number = 48
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きまし'

Sequence Number = 47
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きま'

Sequence Number = 46
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行き'

Sequence Number = 45
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に'

Sequence Number = 50
Stability = 0.9
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きました'

Sequence Number = 52
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさん'

Sequence Number = 53
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさんが'

Sequence Number = 51
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きました小正'

Sequence Number = 58
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で'

Sequence Number = 55
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさんの顔'

Sequence Number = 59
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川です'

Sequence Number = 54
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさんなか'

Sequence Number = 61
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯'

Sequence Number = 60
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で水'

Sequence Number = 56
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川'

Sequence Number = 63
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯して'

Sequence Number = 57
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばさんが変わっ'

Sequence Number = 62
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯し'

Sequence Number = 64
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯してい'

Sequence Number = 66
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると'

Sequence Number = 65
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯している'

Sequence Number = 67
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯しているとか'

Sequence Number = 71
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上'

Sequence Number = 72
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上か'

Sequence Number = 73
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から'

Sequence Number = 69
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると変わっ'

Sequence Number = 70
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると彼は彼'

Sequence Number = 68
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると彼は'

Sequence Number = 74
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃'

Sequence Number = 75
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃が'

Sequence Number = 77
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこ'

Sequence Number = 76
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃が飛ん'

Sequence Number = 78
Stability = 0.9
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこ'

Sequence Number = 79
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこ'

Sequence Number = 82
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れ'

Sequence Number = 83
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れて'

Sequence Number = 81
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらことな'

Sequence Number = 80
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと'

Sequence Number = 84
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてき'

Sequence Number = 87
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきました'

Sequence Number = 85
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきま'

Sequence Number = 86
Stability = 0.01
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきまし'

Sequence Number = 88
Stability = 0.9
UnstableSpeechResult = '昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきました'

Speech Result:
'昔々あるところにお爺さんとお婆さんが住んでいましたお爺さんは山へ芝刈りにおばあさんは川へ洗濯に行きましたおばあさんが川で洗濯していると川上から桃がどんぶらこどんぶらこと流れてきました'

 

 最後に

この結果だけ見るとpartialResultCallbackが使い物になるかどうかは定かではありませんが、TwilioのSpeech Recognitionは裏でGoogle APIを呼び出しているということなので、Google APIの使われ方を検索するとうまい応用があるかもしれませんね。

参考

Twilio TwiML <Gather>の仕様 … TwiML™ Voice: <Gather> - Twilio

Twilio Blog … Introducing Speech Recognition - Public Beta Now Open

 

ESP8266とTwilio音声認識でヘボコン・ロボット用IoTモジュール作成

先に作ったヘボコン・ロボット用IoTモジュールを改良しました。

今回はTwilio SIGNAL 2017で発表された新機能、Speech Recognition(音声認識)とFunctionsを使ってみました。

仕様

ロボットが攻撃を受けたら自動的に操縦者に電話をかけ、音声合成で報告し、操縦者の音声による指示を認識して、ロボットのサーボモータを動かします。

デモ動画

 システム

f:id:tsun226:20170625215130p:plain

ESP-WROOM-02WiFi機能を持ったAdruino互換CPU(スイッチサイエンス製ESPr Developerを使用)

R … ルータ(iPhoneテザリングを利用)

Twilio … (REST APIによりFunctionsを起動)

Web Server … Dockerコンテナ上にnginx,php-cgi,twilio-php-master環境を用意(Twilioから音声認識された文字情報を受け取り保存し、またESP-WOOM-02にこの文字情報から生成したサーボモータ制御情報を渡す)

ロボットモジュール配線図

f:id:tsun226:20170625215148p:plain

動作

  1. ロボットのスイッチに敵ロボットが当たる。
  2. ESP-WROOM-02WiFi→ルータ→インターネット経由でTwilio REST APIに接続し、架電のリクエストをする。
  3. TwilioはFunctionsに登録されている指示に従い、操縦者に架電し音声合成によりメッセージを伝え、操縦者の音声を認識してWeb Serverに文字情報として伝える。
  4. Web ServerはTwilioから受け取った文字情報を保存する。
  5. ESP-WROOM-02は定期的にWeb Serverをポーリングし、4.で保存された文字情報から生成されたサーボモータ制御用情報(角度を表す数値)を取得し、サーボモータの角度をコントロールする。

ESP-WROOM-02スケッチ

定期的にWeb Serverをポーリングし、取得した値をサーボモータに出力します。

スイッチがONになるとTwilio REST APIに架電リクエストをPOSTします。

#ifdef ESP8266
extern "C" {
#include "user_interface.h"
}
#endif

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <Servo.h>

ADC_MODE(ADC_VCC);

WiFiClientSecure client;
Servo servo;

// Twilio Call parameters
const char* asid = “{Twilio ASID}”;
const char* authorization = "Basic {TwilioのBASIC認証データ(BASE64)}”;
const char* twimlUrl = “{TwilioのFunctionsのURL}”;
const char* to = “{宛先電話番号}“;
const char* from = “{発信者電話番号(Twilio側電話番号)}}”;

// HeboCall parameters
const char *hebocallAuthorization = "Basic {Web ServerのBASIC認証データ(BASE64)}”;
const char *hebocallUrl = “{Web Serverサーボモータ制御用情報取得URL}“;

// Wi-Fi parameters
const char* ssid = “{ルータのSSID}”;
const char* pass = “{ルータのパスワード}“;

// Twilio REST API endpoint
const char* twilioHost = "api.twilio.com";
const char* twilioPath0 = "/2010-04-01/Accounts/";
const char* twilioPath1 = "/Calls.json";

// HeboCall endpoint
const char* hebocallHost = “{Web ServerホストのFQDN}“;
const int hebocallPort = {Web Serverのポート番号};
const char* hebocallPath = “{Web Serverのサーボモータ制御用情報取得ファイルパス}”;

#define IO_SWITCH 14
#define IO_LED0 13
#define IO_LED1 12
#define IO_SERVO 16
String content;
String hebocallContent;

void setup() {
  Serial.begin(115200);
  Serial.println("Setup.");
  pinMode(IO_SWITCH, INPUT);
  pinMode(IO_LED0, OUTPUT);
  pinMode(IO_LED1, OUTPUT);
  servo.attach(IO_SERVO);
  digitalWrite(IO_LED0, LOW);
  digitalWrite(IO_LED1, LOW);
  digitalWrite(IO_SERVO, 0);
  wifiConnection();
  makeContent();
  makeHebocallContent();
}

void loop() {
  if(digitalRead(IO_SWITCH) == HIGH) {
    if(WiFi.status() == WL_CONNECTED || wifiConnection()) {
      httpsPost();
      for(int i = 0; i < 500; i++) {
        led(10);
        delay(10);
      }
    }
  }
  led(50);
  checkAction(500);
  delay(10);
}

boolean wifiConnection() {
  WiFi.begin(ssid, pass);
  int count = 0;
  Serial.print("Connecting Wi-Fi");
  while (count < 50) {
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println();
      Serial.println("Connected. IP ADDRESS: " + WiFi.localIP().toString());
      return(true);
    }
    delay(500);
    Serial.print(".");
    count++;
  }
  Serial.println("Timed out.");
  return(false);
}

// POST request to Twilio
void httpsPost() {
  Serial.println("Connecting to Twilio API endpoint...");
  if (client.connect(twilioHost, 443)) {
    client.println(content);
    delay(10);
    Serial.print("Waiting Twilio API response...");
    String response = client.readString();
    int bodypos = response.indexOf("\r\n\r\n") + 4;
    Serial.println();
    Serial.println("RESPONSE: " + response.substring(bodypos));
    return;
  } else {
    Serial.println("ERROR");
    return;
  }
}

void makeContent() {
  String data = "Url=" + (String)twimlUrl + "&To=" + (String)to + "&From=" +   (String)from;
  String header = "POST " + (String)twilioPath0 + asid + (String)twilioPath1 + " HTTP/1.1\r\n" +
  "Host: " + (String)twilioHost + "\r\n" +
  "Authorization: " + (String)authorization + "\r\n" +
  "User-Agent: ESP8266/1.0\r\n" +
  "Connection: close\r\n" +
  "Content-Type: application/x-www-form-urlencoded;\r\n" +
  "Content-Length: " + data.length();
  content = header + "\r\n\r\n" + data;
  Serial.println(content);
}

void checkAction(int wait) {
  static int count = 0;
  String result;
  int angle;

  count++;
  if(count < wait) {
    return;
  }
  count = 0;
  result = httpsGet();
  angle = result.toInt();
  servo.write(angle);
  Serial.println("Servo angle = " + String(angle));
}


// GET Action from Web Server
String httpsGet() {
  String line;
  int i;
  int p, pos[2];

  Serial.println("Connecting to Web Server ...");
  if (client.connect(hebocallHost, hebocallPort)) {
    client.println(hebocallContent);
    delay(10);
    Serial.print("Waiting HeboCall response...");
    line = client.readString();
    p = line.indexOf("\r\n\r\n") + 4;
    for(i = 0; i < 2 && p < line.length(); i++) {
      p = line.indexOf("\r\n", p) + 2;
      pos[i] = p;
    }
    Serial.println();
    Serial.println("HEBOCALLRESPONSE: " + line.substring(pos[0], pos[1]));
    return(line.substring(pos[0], pos[1]));
  } else {
    Serial.println("ERROR");
    return("89");
  }
}

void makeHebocallContent() {
  hebocallContent = "GET " + (String)hebocallPath + " HTTP/1.1\r\n" +
  "Host: " + (String)hebocallHost + "\r\n" +
  "Authorization: " + (String)hebocallAuthorization + "\r\n" +
  "User-Agent: ESP8266/1.0\r\n" +
  "Connection: Keep-Alive\r\n\r\n";
  Serial.println(hebocallContent);
}

void led(int wait) {
  static int flag = 0;
  static int count = 0;

  count++;
  if(count < wait) {
    return;
  }
  count = 0;
  if(flag == 0) {
    digitalWrite(IO_LED0, HIGH);
    digitalWrite(IO_LED1, LOW);
    flag = 1;
  } else {
    digitalWrite(IO_LED0, LOW);
    digitalWrite(IO_LED1, HIGH);
    flag = 0;
  }
}

Twilio Functionsコード

メッセージを音声合成し送信し、受信した音声を認識してその結果(文字列)を{Web Server文字情報保存のURL}にPOSTします。

(この機能はWeb Serverに置いても、TwiML Binsを使っても可能ですが、新機能Functionsを使ってみたかったのです:-)

exports.handler = function(context, event, callback) {
    const message = "攻撃されています。攻撃されています。指示をしてください。"
    const messageNoRes = "指示を受け取れませんでした。"
    let twiml = new Twilio.twiml.VoiceResponse();
    let gatherParams = {};
    gatherParams.input = "speech";
    gatherParams.language = "ja-JP";
    gatherParams.timeout = "3";
    gatherParams.action = ‘{Web Server文字情報保存のURL}’;
    gatherParams.method = "POST";
    let sayParams = {};
    sayParams.language = "ja-JP";
    sayParams.voice = "alice";
    twiml.gather(gatherParams).say(sayParams, message);
    twiml.say(sayParams, messageNoRes)
    callback(null, twiml);
};

Web Server文字情報受け取りPHPコード

パラメータSpeechResultの値として渡されたTwilioにより認識された文字列を、ファイル /mnt/data/speech_text に保存します。

<?php
require_once dirname(__FILE__) . "/twilio-php-master/Services/Twilio.php";

$message_post = "。了解しました";
$speech_result = $_POST["SpeechResult"];
syslog(LOG_ERR, "speechresult = '" . $speech_result . "'");

# save file
$file_path = "/mnt/data/speech_text";
$result = file_put_contents($file_path , $speech_result . "\n");
if($result == FALSE) {
    syslog(LOG_ERR, "ERROR: Couldn't save speech result (" .  $speech_result . ") to file(" . $file_path . ")");
}

# make TwiML
$twiml = new Services_Twilio_Twiml();
$twiml->say($speech_result . $message_post,
    array(
        'language' => "ja-JP",
        'voice' => "alice"
    ));
print $twiml;

?>

Web Serverサーボモータ制御用情報取得PHPコード

ファイル /mnt/data/speech_text 保存されている文字列に含まれる文字により、0, 89 または 179 の角度情報を返します。

<?php
$file_path = "/mnt/data/speech_text";
$speech_result = file_get_contents($file_path);
if(preg_match('/(怒|怖|恐|攻|撃|襲|発|射|始|ang|aggr|att)/i', $speech_result) == 1) {
    print "179\n";
    exit;
}
if(preg_match('/(笑|楽|元|停|止|終|smile|laugh|easy)/i', $speech_result) == 1) {
    print "0\n";
    exit;
}

print "89\n";
?> 

果たして、このモジュールをどこかのヘボコンで披露する機会は来るのでしょうか。

ESP-WROOM-02(Arduino互換)とTwilioで電話をかける

まえがき

(読み飛ばしてかまいません)

2016年夏、息子がヘボコン・ワールドチャンピオンシップにに参加し、Arduino AGのファウンダーの一人であるDavid Cuartielles氏によりArduino賞に選ばれました。そして、副賞としてArduino/Genuinoスタータキットを2セット頂きました。

このイベントはとても盛り上がり、家族のいい思い出となりました。

自分は小学生の頃(40年以上前^^;)から電子工作を始めており、仕事はソフトウェア開発であるものの、ここ十数年PIC、ArduinoRaspberry Pi等いろんなことができるデバイスが簡単に入手できる環境になってきて、手を出したいと思っていました。そこに息子がArduinoをもらうというきっかけがあったので、これ幸いと息子と一緒にArduinoをいじり始めました。

電話を始めとするコミュニケーション・クラウド・サービスであるTwilioにも馴染みがあるので、ヘボコンArduinoTwilioというキーワードの組み合わせで何かできないかと思い、考えたシナリオはこうです。ヘボコンが攻撃を受けたら搭載されたArduinoがTwilio経由で電話を操縦者にかけて、攻撃されていることを音声合成で報告するというものです。

システム 

全体のシステムは下図の通りです。

 

f:id:tsun226:20170206213636p:plain

構成:

ESP-WROOM-02WiFi機能を持ったAdruino互換CPU(今回は開発用のスイッチサイエンス製ESPr Developerを使用)

R … ルータ(iPhoneテザリングを利用)

Twilio … (REST APIによりTwilioデベロッパーセンターのTwiML Binを利用)

動作:

  1. ヘボコン上のスイッチに敵のヘボコンが当たり、プログラムが起動する。
  2. Arduino(互換)がWiFi→ルータ→インターネット経由でTwilio REST APIに接続し、架電のリクエストをする。
  3. Twilioは事前に登録されている指示に従い架電し、音声合成によりメッセージを伝える。

 この記事では、Twilioの設定(TwiML)とESP-WROOM-02(Arduino互換)で動作させるプログラム(スケッチ)について主に説明します。

 

Twilioの設定

Twilioの基本的な使い方については、説明を省略します(Twilioのサイトを参照してください)。

TwiMLの作成

  1. Twilioサイトにログインし、デベロッパーセンター → TwiML Bins を選択します。
  2. 「+」マークをクリックし、TwiML Binを新規作成します。
  3. ConfigurationのFRIENDLY NAMEに任意の名前を、TWIMLには下記内容を記載します。
    「メッセージ」の部分が音声合成されます。任意のメッセージに置き換えてください。

    <?xml version="1.0" encoding="UTF-8"?>
    <Response>
    <Say voice="alice" language="ja-JP" >
    メッセージ
    </Say>
    </Response> 

  4. Save をクリックして登録します。

  5. たった今作成したTwiML Binをクリックしてもう一度内容を表示させると、URLが生成されています。
    このURLはスケッチで使われますので、コピーしておいてください。
  6. Cancelをクリックして閉じます。

ESP_WROOM-02のスケッチ

Mac上のArduino IDE 1.8.1を使いました。

  1. Arduino IDEESP-WROOM-02を開発するための準備をします。
    次のページを参考にしました。
    ESP-WROOM-02開発ボードをArduino IDEで開発する方法
  2. 次の内容のスケッチを作成します。

    #ifdef ESP8266
    extern "C" {
      #include "user_interface.h"
    }
    #endif

    #include <ESP8266WiFi.h>
    #include <WiFiClientSecure.h>

    ADC_MODE(ADC_VCC);

    WiFiClientSecure client;

    // Twilio Call parameters
    const char* asid = "アカウントSID";
    const char* authorization = "Basic ベーシック認証情報";
    const char* twimlUrl = "TwiML BinsのURL";
    const char* to = "通知先電話番号";
    const char* from = "発信元電話番号";

    // Router
    const char* ssid = "ルータのSSID";
    const char* pass = "ルータのパスワード";

    // Twilio REST API endpoint
    const char* twilioHost = "api.twilio.com";
    const char* twilioPath0 = "/2010-04-01/Accounts/";
    const char* twilioPath1 = "/Calls.json";


    void setup() {
      Serial.begin(115200);
      if (wifiConnection()) {
        httpsPost();
      }
    }

    void loop() {

    }

    boolean wifiConnection() {
      WiFi.begin(ssid, pass);
      int count = 0;
      Serial.print("Connecting Wi-Fi");
      while (count < 50) {
        if (WiFi.status() == WL_CONNECTED) {
          Serial.println();
          Serial.println("Connected. IP ADDRESS: " + WiFi.localIP().toString());
          return(true);
        }
        delay(500);
       Serial.print(".");
        count++;
      }
      Serial.println("Timed out.");
      return(false);
    }

    void httpsPost() {
      Serial.println("Connecting to Twilio API endpoint...");
      if (client.connect(twilioHost, 443)) {
        Serial.println("Connected.");
        String data = "Url=" + (String)twimlUrl + "&To=" + (String)to + "&From=" + (String)from;
        String header = "POST " + (String)twilioPath0 + asid +       (String)twilioPath1 + " HTTP/1.1\r\n" +
        "Host: " + (String)twilioHost + "\r\n" +
        "Authorization: " + (String)authorization + "\r\n" +
        "User-Agent: ESP8266/1.0\r\n" +
        "Connection: close\r\n" +
        "Content-Type: application/x-www-form-urlencoded;\r\n" +
        "Content-Length: " + data.length();
        client.println(header);
        client.println();
        client.println(data);
        delay(10);
        Serial.print("Waiting Twilio API response...");
        String response = client.readString();
        int bodypos = response.indexOf("\r\n\r\n") + 4;
        Serial.println();
        Serial.println("RESPONSE: " + response.substring(bodypos));
        return;
      } else {
        Serial.println("ERROR");
        return;
      }
    }

     

動作の様子(デモ動画)

 上記のシステムを少しアレンジしたものの、動作の様子です。

 

とても簡単に、小さなモジュールが電話をかけ音声合成でメッセージを伝えるシステムを作ることができました。

WiFi機能を持つArduino(互換)を使えばインターネットに接続できるので、インターネット・サービスを利用するIoTモジュールを簡単に試作することができますね。

Twilioを使った気象情報架電通知システムの試作

1. 概要

気象情報の変化を市区町村別、かつ注意報/警報/特別警報別のアカウントに投稿する、既存のツイッターbotを拡張し、登録してある電話番号に架電して音声合成で通知するシステムを試作しました。

ユーザは下記情報を登録し、該当する気象情報が発生した時に、架電通知されます。

  • 電話番号
  • エリア(市区町村)
  • 通知して欲しい情報
    ー 注意報/警報/特別警報別
    ー 発表/解除/継続

 

Pubsubhubbubプロトコルにより気象庁の実証実験で提供されている気象情報が当システムに通知され、その情報を解析してメッセージを構成し、Twilio APIに架電と音声合成による通知を要求し、Twilioがユーザに対し架電通知を行います。

f:id:tsun226:20160416140220p:plain

 

2. 入力

気象庁の実証実験では、Googleが運用するAlert Hubを介してpubsubhubbubプロトコルによるPush方式で、都道府県単位の気象情報の変化の概要がAtom形式で通知されます。

② この概要情報には詳細情報(市区町村単位の注意報/警報/特別警報)へのリンクが含まれており、概要情報に含まれる都道府県名が、当システムが架電通知またはツイートするエリアを含むときに、HTTP GETによりXML形式の詳細情報を取得します。

f:id:tsun226:20160416140253p:plain

 

3. システム

 4つのDockerコンテナにより構成されています。

いずれのコンテナにおいてもnginxが稼働しており、HTTPアクセスによりperlまたはphpスクリプトが起動され、処理されます。

 今回、既存システムにPhoneコンテナDockwilioコンテナを追加して、システムの拡張を行いました。

f:id:tsun226:20160416194919p:plain

 

3.1. AlertSubコンテナ(既存)

2.で述べたAlert Hubや気象庁からの情報を受け取り、解析して、エリア毎*1に通知する要素(発表/解除/継続、注意報/警報/特別警報、注意報警報の内容)を構成し、PhoneコンテナTweetコンテナに通知します(図の③、⑥)。

*1  架電通知またはツイート対象として登録されているエリアのみ対象にします。

 

3.2. Phoneコンテナ

登録ユーザ情報から、通知して欲しい条件がAlertSubコンテナから通知されたエリア、発表/解除/継続、注意報/警報/特別警報に一致するユーザを検索し、通知要素を元に構成したメッセージと電話番号をDockwilioコンテナに通知します(図の④)。

 

3.3. Dockwilioコンテナ

Phoneコンテナから通知された情報に従い、架電するようにTwilio APIに要求し(図の⑤)、その後TwilioからのHTTPS GETアクセスに対しメッセージを音声合成して伝える旨通知します(図の⑥)。

  • Twilioは通話をはじめとする各種コミュニケーション手段を提供するクラウドサービスです。
  • DockwilioコンテナはTwilioを使った架電と音声合成メッセージ通知を簡単にできるDockerコンテナです。

 

3.4. Tweetコンテナ(既存)

AlertSubコンテナから通知された情報のエリア、注意報/警報/特別警報に対応するツイッターのアカウントに、構成したメッセージを投稿するようにTwitter APIに要求します(図の⑦)。

 

AlertSub, Phone, Tweetのコンテナは、emerry/jmaをカスタマイズしたものです。

Dockwilioコンテナのリポジトリemerry/dockwilioです。

当システムは試作システムで、一般にサービスを提供していません。