気象特別警報・警報・注意報のTwitter botのPULL化

約5年前に気象特別警報・警報・注意報のTwitter botを作りました。

気象庁が実証実験として行っている、気象庁防災情報XMLPubSubHubbubによるPuSH通知を利用したものでしたが、この実証実験は今年(2020年)8月で終了とのこと。

現在は、気象庁がPULL型での情報提供を行っているので、この仕組みで防災情報XMLを取得するように改修しました。

改修概要

  1. PUSH通知があったときにXMLを取得していたのを、cronで定期的に取得するようにした。
  2. 詳細XMLの内容が前回の内容と変更があったとき、ツイートするようにした。

 

これで当分サービスを継続でいます (^^)

 気象特別警報・警報・注意報のTwitter botアカウント

地域 注意報 警報 特別警報
東京都新宿区 @shinjuku_wa @shinjuku_ww @shinjuku_wew
東京都品川区 @shinagawa_wa @shinagawa_ww @shinagawa_wew
東京都葛飾 @katsushika_wa @katsushika_ww @katsushika_wew
神奈川県横浜市 @yokohama_wa @yokohama_ww @yokohama_wew
Minato City, Tokyo (English) @minato_ewa @minato_eww @minato_ewew

 

今のところ、気象庁による防災情報の発表から投稿までは、2〜3分程度の遅延が発生しています。

なお、当botTwitterに投稿する情報の範囲は次の通りです。

注意報

大雨注意報、大雪注意報、風雪注意報、雷注意報、強風注意報、波浪注意報、融雪注意報、洪水注意報、高潮注意報、濃霧注意報、乾燥注意報、なだれ注意報、低温注意報、霜注意報、着氷注意報、着雪注意報

警報

暴風雪警報、大雨警報、洪水警報、暴風警報、大雪警報、波浪警報、高潮警報

特別警報

暴風雪特別警報、大雨特別警報、暴風特別警報、大雪特別警報、波浪特別警報、高潮特別警報


※ 河川洪水、土砂災害、地震津波、噴火等の情報は対象外です。

 

私もM5Stack心拍モニタを作って見ました

私もM5Stack心拍モニタを作って見ました ^_^

f:id:tsun226:20181014221926j:plain

 

 当初は脈にあわせてVUメータの針が振れるものを作って、それを胸のあたりに装着し、ドキがムネムネしてるのがわかるようにしようと思ってたのですが、M5Stackを入手したので、Arduino IDEを使ってディスプレイにハートマークを点滅させて簡単に見栄えのするものを作って見ました。

注意:あくまでおもちゃなので、医療用には使えません(^_^)

動画

 

構成

特徴

  • 2つの表示モードがあります。
    + グラフモード           心拍をグラフ表示します。
    + 画像モード               脈に合わせて画像を点滅表示します。
  • Ambient連携機能        20秒間隔で心拍数をambientに送信します。
  • Beep音ON/OFF          脈に合わせて音が鳴ります。
  • ファイルによる設定   WiFi,Ambientの設定をmicroSDから読み込みます。

microSDカード

 microSDカードに設定情報、画像データを用意します。

設定

Ambientに連携するための設定です。

設定情報が不足している場合、連携機能は起動しません。

  • /config/wifi_ssid               WiFi SSID
  • /config/wifi_passwd         WiFi パスワード
  • /config/ambi_channelid   AmbientチャネルID
  • /config/ambi_writekey     Ambient write key
  • /config/ambi_field            Ambient フィールド番号

空白、タブ、改行等を含まないでください。

画像データ

画像モードで表示する画像を用意してください。

  • /images/heart1.jpg     脈拍<80bpmのときに表示する画像
  • /images/heart2.jpg     脈拍≧80bpmのときに表示する画像

サイズ 320x240

画像形式jpeg(プログレッシブ指定をしないでください)

 

操作

  • Aボタン
    グラフモードと画像モードを切り替えます。
  • Bボタン
    Ambientへの計測データの送信をON/OFFします。
  • Cボタン
    脈にあわせたビープ音をON/OFFします。

心拍センサとM5Stackの接続

  • 黒ケーブル → GND
  • 赤ケーブル → 3V3
  • 紫ケーブル → 36

センサ部分は指や耳たぶに装着するものらしいですが、私の場合は掌の親指の下の膨らんでいるあたりがうまく測れました。

f:id:tsun226:20181021081918j:plain

 

sketch

 

#include  <M5Stack.h>
#include "Ambient.h"

// Pulse sensor
#define ST_INTERVAL          (320 - 1)             // count of interval for calculating statistics
#define S_PIN                36                    // Pin No. for Pulse Sensor
#define VALUE_MAX            4096                  // value max. from Pulse Sensor
#define VALUE_CONT_BEAT      5                     // to avoid chattering for judging beat
#define VALUE_THRESH_COEF    20                    // thereshold : coef of (max - avg) for judging beat
#define THRESH_HIGH_BPM      80                    // >=80 high bpm, < 80 low bpm (clolor/sound depends)
// LCD
#define TSIZE_S              2                     // text font size
#define TSIZE_M              3
#define TSIZE_L              7
#define BRIGHTNESS           200                   // LCD brightness
#define FILE_IMAGE_1         "/images/heart1.jpg"  // image file for low bmp
#define FILE_IMAGE_2         "/images/heart2.jpg"  // image file for high bmp
// Sound
#define BEEP_FREQ_1          2000                  // beep frequency for low bpm
#define BEEP_FREQ_2          4000                  // beep frequency for high bpm
#define BEEP_TIME            50                    // beep time
// Configuration files
#define FILE_CONF_WIFI_SSID  "/config/wifi_ssid"
#define FILE_CONF_WIFI_PASS  "/config/wifi_passwd"
#define FILE_CONF_AMBI_CHID  "/config/ambi_channelid"
#define FILE_CONF_AMBI_WKEY  "/config/ambi_writekey"
#define FILE_CONF_AMBI_FIELD "/config/ambi_field"
// misc.
#define TEXT_SIZE            256                   // text buffer size

// display mode (graph or image)
#define MODE_GRAPH  0         // graph mode
#define MODE_IMAGE  1         // image display mode
// sensProc()
#define S_SETUP     0
#define S_LOOP      1
#define S_INTERVAL  10L       // sampling every 10 msec
#define B_INTERVAL  (20000L)  // BPM calculating interval
#define B_MULTI     (60000L / B_INTERVAL)
// dispProc()
#define D_INIT      0
#define D_UPD       1
#define D_BPM       2
// imageProc()
#define I_INIT      0
#define I_UPD       1

// global variables
int          mode = MODE_GRAPH;
bool         soundOn = false;
bool         ambiOn = true;
// WiFi
WiFiClient   client;
char         wifiSsid[TEXT_SIZE];
char         wifiPasswd[TEXT_SIZE];
// Ambidata
Ambient      ambient;
unsigned int ambiChannelId;           // Channel ID
char         ambiWriteKey[TEXT_SIZE]; // Write key
int          ambiField = 1;           // field no.
// image(jpg)
uint8_t *imageHeart1, *imageHeart2;
size_t imageHeart1Len, imageHeart2Len;

// Setup
void setup(){
  delay(1000);
  Serial.begin(115200);
  Serial.println("Setup.");
  M5.begin();
  M5.Lcd.clear();
  M5.Lcd.setBrightness(BRIGHTNESS);
  M5.update();
  dispMssg("Setup.", WHITE, BLACK);
  readConfig();
  if(ambiOn) {
    if(wifiConnection()) {
      ambient.begin(ambiChannelId, ambiWriteKey, &client);
    } else {
      dispMssg("Ambient OFF", WHITE, RED);
      ambiOn = false;
    }
    delay(1500);
  }
  sensProc(S_SETUP, 0);
}

// Loop
void loop() {
  sensProc(S_LOOP, 0);
  checkButtons();
}


// Configuration
void readConfig() {
  size_t len;
  char buf[256];

  SD.begin();
  // read configurations
  len = readFile(FILE_CONF_WIFI_SSID, (uint8_t*)wifiSsid, TEXT_SIZE - 1);
  wifiSsid[len] = '\0';
  len = readFile(FILE_CONF_WIFI_PASS, (uint8_t*)wifiPasswd, TEXT_SIZE - 1);
  wifiPasswd[len] = '\0';
  len = readFile(FILE_CONF_AMBI_CHID, (uint8_t*)buf, TEXT_SIZE - 1);
  buf[len] = '\0';
  ambiChannelId = String(buf).toInt();
  len = readFile(FILE_CONF_AMBI_WKEY, (uint8_t*)ambiWriteKey, TEXT_SIZE - 1);
  ambiWriteKey[len] = '\0';
  len = readFile(FILE_CONF_AMBI_FIELD, (uint8_t*)buf, TEXT_SIZE - 1);
  buf[len] = '\0';
  ambiField = String(buf).toInt();
  if(! wifiSsid[0] || ! wifiPasswd[0] || ! buf[0] || ! ambiWriteKey[0]) {
    ambiOn = false;
  }
  // read images
  imageHeart1Len = readFileMalloc(FILE_IMAGE_1, &imageHeart1);
  imageHeart2Len = readFileMalloc(FILE_IMAGE_2, &imageHeart2);
}

// Read file and save to buffer
size_t readFile(const char *path, uint8_t *buf, size_t buf_len) {
  File file = SD.open(path, FILE_READ);
  size_t len = file.size();
  if(len > buf_len) len = buf_len;
  size_t len1 = file.read(buf, len);
  file.close();
  return(len);
}

// Allocate memory, read file and save
size_t readFileMalloc(const char *path, uint8_t **bufP) {
  File file = SD.open(path, FILE_READ);
  size_t len = file.size();
  *bufP = (uint8_t*)malloc(len);
  size_t len1 = file.read(*bufP, len);
  file.close();
  return(len);
}

// WiFi
boolean wifiConnection() {
  WiFi.disconnect();
  WiFi.begin(wifiSsid, wifiPasswd);
  int count = 0;
  Serial.print("Connecting Wi-Fi");
  while (count < 50) {
    dispMssg("Wi-Fi...", WHITE, BLACK);
    if (WiFi.status() == WL_CONNECTED) {
      String mssg = "Connected.\nIP: " + WiFi.localIP().toString();
      Serial.println();
      Serial.println(mssg);
      dispMssg("Connected", GREEN, BLACK);
      return(true);
    }
    if(M5.BtnB.wasPressed()) {
      dispMssg("Ambient OFF", RED, BLACK);
      M5.update();
      return(false);
    }
    M5.update();
    delay(250);
    dispMssg("", WHITE, BLACK);
    delay(250);
    Serial.print(".");
    count++;
  }
  Serial.println("Timed out.");
  dispMssg("TIME OUT.", WHITE, RED);
  return(false);
}

// Buttons
void checkButtons() {
  static unsigned long last;
  
  if(last + 10 > millis()) {
    return; 
  }
  last = millis();
  if(M5.BtnA.wasPressed()) {  // Button A -> change mode
    if(mode == MODE_GRAPH) {
      imageProc(I_INIT, 0, false);
      mode = MODE_IMAGE;
    } else if(mode == MODE_IMAGE) {
      dispProc(D_INIT, 0, 0, false);
      mode = MODE_GRAPH;
    }
  }
  if(M5.BtnB.wasPressed()) {  // Button B -> ambient switch
    if(ambiOn) {
       dispMssg("Ambient OFF", RED, BLACK);
       ambiOn = false;
    } else {
       dispMssg("Ambient ON", GREEN, BLACK);
       ambiOn = true;
    }
  }
  if(M5.BtnC.wasPressed()) {  // Button B -> sound switch
    if(soundOn) {
       dispMssg("Sound OFF", RED, BLACK);
       soundOn = false;
    } else {
       dispMssg("Sound ON", GREEN, BLACK);
       soundOn = true;
    }
  }
  M5.update();
}

// Read data from sensor and proceed
void sensProc(int cmd, int param) {
  static unsigned long timeLast, timeLastBeat;
  static int valueLast;
  static int countHigh, countLow;
  static int stSum, stMax, stMin, stCount, stAvg, stThresh, stMaxTmp, stMinTmp;
  static int beatCount, bpm;
  static bool beatHigh;
  int value;

  switch(cmd) {
  case S_SETUP:
    pinMode(S_PIN, INPUT);
    dispProc(D_INIT, 0, 0, false);
    timeLast = timeLastBeat = millis();
    bpm = 0;
    stMax = VALUE_MAX;            // inrerim values
    stMin = 0;
    stThresh = stAvg = VALUE_MAX / 2;
    beatHigh = false;
    valueLast = analogRead(S_PIN);
    countHigh = countLow = beatCount = stSum = stCount = 0;
    stMinTmp = VALUE_MAX;         // inrerim values
    stMaxTmp = 0;    
    break;
  case S_LOOP:
    if(millis() >= timeLastBeat + B_INTERVAL) {                 // update bpm
      bpm = beatCount * B_MULTI;
      if(ambiOn) {
        if(WiFi.status() == WL_CONNECTED || wifiConnection()) { // check and reconnect Wifi
          ambient.set(1, String(bpm).c_str());
          ambient.send();
        } else {
          ambiOn = false;
        }
      }
      if(mode == MODE_GRAPH) dispProc(D_BPM, bpm, 0, false);    // update for image
      // initialize for next bpm calculation
      beatCount = 0;
      timeLastBeat += B_INTERVAL;
      if(timeLast + S_INTERVAL < millis()) {                    // in case of reconnect Wifi
        timeLast = millis();
        break;
      }
    }
    if(millis() < timeLast + S_INTERVAL) {
      break;
    }
    value = analogRead(S_PIN);                  // get value from sorsor
    Serial.print(value);Serial.print(" ");Serial.print(stAvg);Serial.print(" ");Serial.print(stMin);Serial.print(" ");Serial.print(stMax);Serial.print(" ");Serial.println(stThresh);
    // statistics
    stSum += value;
    stCount++;
    if(value > stMaxTmp) stMaxTmp = value;
    if(value < stMinTmp) stMinTmp = value;
    if(stCount == ST_INTERVAL) {
      stAvg = stSum / stCount;
      stMax = stMaxTmp;
      stMin = stMinTmp;
      stThresh = ((stMax - stAvg) * VALUE_THRESH_COEF / 100) + stAvg;
      stSum = stCount = 0;
      stMinTmp = VALUE_MAX;
      stMaxTmp = 0;
    }
    if(value > stThresh) {                      // check beat
      countHigh++;
      countLow = 0;
      if(countHigh == VALUE_CONT_BEAT && ! beatHigh) {
        beatCount++;
        beatHigh = true;
        if(soundOn) soundStart(bpm);
      }
    } else {
      countHigh = 0;
      countLow++;
      if(countLow == VALUE_CONT_BEAT) {
        beatHigh = false;
      }
    }
    // update graph/image
    if(mode == MODE_GRAPH) dispProc(D_UPD, valueLast, value, beatHigh);
    if(mode == MODE_IMAGE) imageProc(I_UPD, bpm, beatHigh);
    timeLast += S_INTERVAL;
    valueLast = value;
    break;
  }
}

// Draw graph
#define GRAPH_X_MAX    (320 - 1)
#define GRAPH_Y_OFFSET 40
#define GRAPH_Y_MAX    (240 - GRAPH_Y_OFFSET)
#define GRAPH_SIZING   200
#define GRAPH_BG       BLACK
#define GRAPH_FG       WHITE
void dispProc(int cmd, int vLast, int v, bool flag) {
  static int cursorLast, valueMin, valueMax;
  int cursorNext;

  switch(cmd) {
  case D_INIT:
    M5.Lcd.fillScreen(0);
    M5.Lcd.setBrightness(BRIGHTNESS);
    cursorLast = 0;
    valueMin = 0;
    valueMax = VALUE_MAX;
    break;
  case D_UPD:
    cursorNext = cursorLast + 1;
    M5.Lcd.setTextColor(RED, 0); M5.Lcd.setCursor(0, 0); M5.Lcd.setTextSize(TSIZE_M);
    if(flag) {
      M5.Lcd.print("@");
    } else {
      M5.Lcd.print(" ");
    }
    M5.Lcd.setTextColor(BLUE, 0); M5.Lcd.setCursor(300, 0); M5.Lcd.setTextSize(TSIZE_M);
    if(ambiOn) {
      M5.Lcd.print("A");
    } else {
      M5.Lcd.print(" ");
    }
    // DEBUG
//    Serial.print(valueMin);Serial.print(" ");Serial.print(valueMax);Serial.print(" ");Serial.print(v);Serial.print(" ");Serial.print(disp_pos_y(v, valueMin, valueMax));Serial.print(" - ");
//    Serial.print(v - valueMin);Serial.print(" ");Serial.print(valueMax - valueMin);Serial.print(" ");Serial.print((v - valueMin) * GRAPH_Y_MAX);Serial.print(" ");Serial.println(((v - valueMin) * GRAPH_Y_MAX) / (valueMax - valueMin));
    M5.Lcd.drawLine((int16_t)cursorNext,                // clear next virtical line area
                    (int16_t)GRAPH_Y_OFFSET,
                    (int16_t)cursorNext,
                    (int16_t)(GRAPH_Y_OFFSET + GRAPH_Y_MAX),
                    GRAPH_BG);
    M5.Lcd.drawLine((int16_t)cursorLast,                // draw line
                    disp_pos_y(vLast, valueMin, valueMax),
                    (int16_t)cursorNext,
                    disp_pos_y(v, valueMin, valueMax),
                    GRAPH_FG);
    cursorLast = cursorNext;
    cursorLast %= GRAPH_X_MAX;
    if(cursorLast == (int16_t)0) {
      M5.Lcd.drawLine((int16_t)0,                       // clear 1st virtical line area
                    (int16_t)GRAPH_Y_OFFSET,
                    (int16_t)0,
                    (int16_t)(GRAPH_Y_OFFSET + GRAPH_Y_MAX),
                    GRAPH_BG);
    }
    break;
  case D_BPM:
    int16_t color;
    char buf[20];
    color = (vLast >= THRESH_HIGH_BPM) ? YELLOW : GREEN;
    sprintf(buf, "  BPM: %3d", vLast);
    dispMssg(buf, color, BLACK);
    break;
  }
}

// Subroutine for dispProc()
int16_t disp_pos_y(int v, int min, int max) {
  if(v > max) v = max;
  if(v < min) v = min;
  return((int16_t)((GRAPH_Y_OFFSET + GRAPH_Y_MAX) - (((v - min) * GRAPH_Y_MAX) / (max - min))));
}

// Draw jpg image
void imageProc(int cmd, int bpm, bool beatHigh) {
  static bool lastBeat;
  static int lastImage;
  switch(cmd) {
  case I_INIT:
    M5.Lcd.setBrightness(0);
    M5.Lcd.clear(0);
    lastBeat = false;
    lastImage = 0;
    break;
  case I_UPD:
    if(beatHigh && ! lastBeat) {                    // on or off
      if(bpm >= THRESH_HIGH_BPM && lastImage != 2) {
        M5.Lcd.drawJpg(imageHeart2, imageHeart2Len);
        lastImage = 2;
      }
      if(bpm < THRESH_HIGH_BPM && lastImage != 1) {
        M5.Lcd.drawJpg(imageHeart1, imageHeart1Len);
        lastImage = 1;
      }
      M5.Lcd.setBrightness(BRIGHTNESS);
      lastBeat = true;
    } else if(! beatHigh && lastBeat) {
      M5.Lcd.setBrightness(0);
      lastBeat = false;
    }
    break;
  }
}

// Display text message on top
void dispMssg(const char* mssg, int16_t fg, int16_t bg){
  M5.Lcd.setTextColor(fg, bg);
  M5.Lcd.setCursor(40, 0);
  M5.Lcd.setTextSize(TSIZE_M);
  M5.Lcd.printf("%-14s", mssg);
}

// Beep sound
void soundStart(int bpm) {
  M5.Speaker.tone((bpm >= THRESH_HIGH_BPM) ? BEEP_FREQ_2 : BEEP_FREQ_1, BEEP_TIME);
}

 

ヘボコン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";
?> 

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