LoginSignup
3
0

More than 5 years have passed since last update.

シスコビデオ会議システムのマクロ機能で自動運転してみた

Last updated at Posted at 2018-12-23

はじめに

シスコ有志によるCisco Advent Calendar 2018も24日目。クリスマスイブです。昨年のシスコビデオ会議システムのマクロ機能で音楽プレイヤーを作ってみた同様に今流行りのWebRTC だとかは全く関係のないビデオ会議端末ネタで再び切り込みます。

2017年版: https://qiita.com/advent-calendar/2017/cisco
2018年版: https://qiita.com/advent-calendar/2018/cisco

成果予定物と利用した素材

ビデオ会議端末が正常に動作しているかどうかを毎朝 8 時に勝手に発信して発信先が着信し、失敗したらその理由を Cisco Webex Teams ボットからメッセージ送信するマクロを検討します。

Cisco Webex DX シリーズおよびCisco Webex Room シリーズ にはマクロ機能が搭載されています。シスコ製のビデオ会議製品で CE 9.2.1 以降であれば、SX10 を除き今回のマクロを動作させることができます。

マクロ機能について詳しいことはTelePresence EP : CE 9.2 マクロチュートリアル に記載しました。一言で言うと JavaScript で各種設定・状態管理をビデオ会議専用機で実装できるのがマクロ機能です。一般的には標準で足りていない設定の UI や動作を作成するのに利用されます。マクロフレームワークディスカッションにいくつか過去の成果物を投稿しています。Github にさまざまなサンプルが投稿されています・ Cisco Webex にクラウド登録するモードでも10月のリリースからマクロを利用できるようになりました。

マクロ機能の元となった xAPI の全般については英語サイトになりますが Cisco DevNet をご覧ください。

事前準備

Cisco Webex への端末登録 - Cisco Webex Control Hub

Cisco Webex に端末を登録します。既に登録されている場合やオンプレミス利用の場合は飛ばしてください。

Cisco Webex Control Hub にログインし、デバイスから新しいデバイスをクリックします。

1.PNG

利用者に紐づけるか会議室などの場所に紐づけるか聞かれるのでここでは場所を選びます。
2.PNG
既存の場所か新規の場所か聞かれるのでここでは新規の場所を選び、わかりやすい場所の名前を入力します。日本語も入力可能ですが、端末からの検索性や電子メールアドレス形式で生成されるビデオアドレスを考えると最初の文字列は英数字が望ましいです。

4.PNG

その他の Cisco Webex デバイスを選択します。
5.PNG

無料通話を選択します。
6.PNG

Activation Code が発行されます。インターネットに接続したビデオ会議端末に入力することにより、利用可能になります。

7.PNG

Cisco Webex への端末登録 - Cisco Webex DX80

Cisco Webex DX80 を Cisco Webex に登録します。

Change Language を選択します。
dx80-1.png

日本語を選択します。
dx80-2.png
開始を選択します。
dx80-3.png
Cisco Webex を選択します。
dx80-4.png

先ほど Cisco Webex Control Hub から入手した Activation Code を入力します。
dx80-5.png
そのほか端末独自のチュートリアルに従い、セットアップを完了すると利用できるようになります。
dx80-6.png

Cisco Webex 登録時マクロ有効化の方法

Cisco Webex Control Hub からデバイスを選択し、登録した端末を選びます。高度な設定を起動する、を選びます。

8.PNG

端末本体の Web 管理画面に移動します。Control Hub 経由ではなく、端末に直接ログインできるように、ローカルの管理者を作成します。 Security > Users を選択します。

9.PNG

ここでは localadmin というユーザを作成しました。Passphrase がパスワードとなります。
10.PNG

発信・着信マクロの作成

時限マクロ Auto Wakeup を見てみる

Integration > Macro からマクロエディタを起動します。
11.PNG

今回は、次元で動作させたいので Example から参考になりそうなマクロ Auto Wakeup を Load Example で読み込みます。これをベースに編集していきます。
12.PNG

ベースとなるマクロです。 setInterval を使って 1分おきに時刻をチェックし、月-金の8時から3分になる前までであれば、スタンバイから復帰しています。このスタンバイから復帰する動作を発信する動作に変えていきます。

/**
 * Wakes the system up from standby at 8.00 in the morning
 * on weekdays
 */

// library for communicating with video system
const xapi = require('xapi');

// how often to check time
const intervalSec = 60;

// Standard javascript built-ins such as date and timers are included
function checkTime() {
  const now = new Date();
  const weekday = now.getDay() > 0 && now.getDay() < 6;
  const wakeupNow = now.getHours() === 8 && now.getMinutes() < 2 && weekday;

  if (wakeupNow) xapi.command('standby deactivate');
}

setInterval(checkTime, intervalSec * 1000);

発信マクロ

発信と切断の関数を追加します。

function disconnect() {
   console.log('Disconnect');
  xapi.command('call disconnect');
}
function dial(number) {
  console.log('dial', number);
  xapi.command('dial', { Number: number });
}

ベースとなるマクロに発信処理を加えます。現在、通話中であれば切断し、そうでなければ発信するため、 xapi.status.get('Call')でチェックを入れています。const number = ''; に宛先のビデオアドレスを入力します。

/**
 * Auto-Call the system at 8.00 in the morning
 * on weekdays
 */


// library for communicating with video system
const xapi = require('xapi');

// how often to check time
const intervalSec = 60;

// Dial Destination
const number = '';

// Standard javascript built-ins such as date and timers are included
function checkTime() {
  const now = new Date();
  const weekday = now.getDay() > 0 && now.getDay() < 6;
  const wakeupNow = now.getHours() === 8 && now.getMinutes() < 3 && weekday;

  if (wakeupNow) {
      xapi.status.get('Call')
      // Calls already exist. Disconnect
        .then((call) => {
            console.log(callid);
          if (call[0].Status === 'Connected') {
              disconnect();
          }

          else if(call[0].Status != 'Connecting') {
            dial(number);
          }
        })
        .catch(() => {
          console.log('Dial');
          dial(number);
        });
  }
}


function disconnect() {
   console.log('Disconnect');
  xapi.command('call disconnect');
}


function dial(number) {
  console.log('dial', number);
  xapi.command('dial', { Number: number });
}
setInterval(checkTime, intervalSec * 1000);

時間を編集し、動作するかどうか確認します。マクロの有効化を忘れないようにします。

着信マクロの準備

着信側も自動応答にしたかったので同じ時間帯でシステム名が上記の自動発信端末と同じであれば自動着信するマクロです。こちらのマクロは着信側に入力します。

/**
 * Once you are in a call with a specific time & system:
 * Auto-Answer and mute video system.
 */
const xapi = require('xapi');

/* Use your own */
// Webex 登録では発信元SIP URL が通話のたびに変わるため、発信元システム名(ディスプレイ名)を入力

const SystemName = '';

function listenToCalls() {
  const now = new Date();
  const weekday = now.getDay() > 0 && now.getDay() < 6;
  const wakeupNow = now.getHours() === 8 && now.getMinutes() < 4 && weekday;

    xapi.event.on('IncomingCallIndication', () => {
        if (wakeupNow) {
      xapi.status.get('Call')
        .then((call) => {
          console.log(call[0].Status);
          if (call[0].DisplayName.includes(SystemName)) {
            xapi.command('Call Accept');
          }
        });
    }
    });

}

listenToCalls();

Cisco Webex 登録ではステータスから取得可能な SIP URL が発信元にならないため、システム名を利用してチェックしています。このあたりの動作は、SSH などを使ってコマンドラインでログインし、コマンドから見ていくと判別可能です。
CallbackNumberやRemoteNumberがクラウド登録時にビデオアドレスでないことが分かり、場所の名前がDisplayNameとして使われてることが分かります。

xstatus call
*s Call 267 AnswerState: Answered
*s Call 267 CallType: Video
*s Call 267 CallbackNumber: "spark:4929b4ed-31c1-4f34-ad85-7643686c0126"
*s Call 267 DeviceType: Endpoint
*s Call 267 Direction: Incoming
*s Call 267 DisplayName: "TKY-TMT-DX80"
*s Call 267 Duration: 183
*s Call 267 Encryption Type: "AES-128"
*s Call 267 PlacedOnHold: False
*s Call 267 Protocol: Spark
*s Call 267 ReceiveCallRate: 6000
*s Call 267 RemoteNumber: "4b7e28f8-06c5-11e9-affe-024207867246"
*s Call 267 Status: Connected
*s Call 267 TransmitCallRate: 6000
** end

"TKY-TMT-DX80"だとわかったので、ここではSystem Name に格納します。

const SystemName = 'TKY-TMT-DX80';

切断理由が知りたい!

コンソールログに表示する

このままでは、通話が成功したかどうか、また成功したとしてもパケットロスなどがどれくらいあったかわかりません。
まず所定の時間内で発信に失敗したときの切断理由をコンソールに表示します。発信相手のチェックと、通話時間が0 を発信失敗としてログに表示します。なお、function checkTime() と時間の処理が重複していますので改善の余地ありです。

function listenToCalls() {
  xapi.event.on('CallDisconnect', (call) => {
    const now = new Date();
    const weekday = now.getDay() > 0 && now.getDay() < 6;
    const wakeupNow = now.getHours() === 0 && now.getMinutes() < 59 && weekday;

    if (wakeupNow) {
      if (call.RequestedURI === number && call.Duration === '0') {
        var failinfo = "Call was failed:" + "CauseValue: " + call.CauseValue + 'CauseType: ' + call.CauseType + 'CauseString: '+ call.CauseString;
        console.log(failinfo);
      }
    }
  });
}
listenToCalls();

13.PNG

このログはコンソールのほか、 Cisco Webex DX80 にあるログファイルから見ることができます。ただこれだとチェックが面倒なのでさらに外の世界に投げられるように検討します。

切断理由をHTTP POST する

最新の Room OS では HTTP で送信が可能です。

xconfiguration HttpClient Mode: On
** end
OK
xcommand HttpClient Post
AllowInsecureHTTPS:  Header:              Url:

このHTTP POST を HTTPS サーバに投げるためには ルート証明書をアップロードする必要があります。パブリック認証局であっても同様となります。今回、 Cisco Webex Teams の API を利用したいため Go Daddy のレポジトリから GoDaddy Class 2 Certification Authority Root Certificate - G2 gdroot-g2.crt をダウンロードします。

Cisco Webex DX80 のWeb 管理画面から Security > Certificate Authorities からアップロードします。

dx80-7.PNG

証明書を反映させるため、Maintenance > Restart > Restart Device で再起動します。

dx80-8.PNG

 

Cisco Webex Teams Bot 作成

http://developer.webex.com/ にログインします。アカウントがない場合は Sign Up から作成します。
14.PNG

アイコンから My Cisco Webex Teams Apps を選択します。

15.PNG

Create a New App から Create A Bot を選択します。

16.PNG

必要項目を入力し、Add Bot をクリックして Bot を作成します。

17.PNG

Bot's Access Token をコピーし、保存します。この Token は同じものを再発行できません。非常に重要な情報ですので他に漏れないように大切に扱ってください。

18.PNG

Cisco Webex Teams クライアント上で自分のアカウントにBot User Name を加えます。または、新規スペースを作ってそこに追加します。

19.PNG

この部屋のIDを知る必要があるため、developer.cisco.com の Documentation > Rest API > API References > Rooms > List Rooms を選びます。Use personal access tokenをオフにし、Token に先ほどの Bot の Token を入力して Runします。これにより、参加してる部屋(スペース)の一覧とIDが取得可能です。

20.PNG

Responses に部屋の一覧が表示されます。

21.PNG

情報を出力したい部屋の"id"をコピーします。

Cisco Webex Teams にコマンドラインからポスト

Bot's Access Token と Room Idが正しく動作するかどうか、コマンドで確認します。 URL まで入力し終わったら改行、Body 部である {"roomId": "RoomID","text": "aa"} が入力し終わったら ピリオド"."を入力して改行すると HTTP POST が実施されます。マルチライン入力時の実装となります。
BOT_ACCESSTOKENとRoomIdはそれぞれ先ほど入手した値に置き換えます。 Cisco Webex Teams Messages API の Create を利用しています。

xcommand HttpClient Post Header: "Authorization: Bearer BOT_ACCESSTOKEN" Header: "Content-Type: application/json"  url: https://api.ciscospark.com/v1/messages
{"roomId": "RoomId","text": "aa"}
.
OK
*r HttpClientPostResult (status=OK):
*r HttpClientPostResult StatusCode: 200
** end

Cisco Webex Teams にマクロからポスト

この処理をマクロに置き換えた例になります。


var payload; 
const room = "RoomId";
var text ="1";
const url = 'https://api.ciscospark.com/v1/messages';
const token = "Bot_ACCESSTOKENf";

xapi.config.set('HttpClient Mode', 'On');

function postTeamsMessage(room, text,url) {
  var payload = { 
    "roomId":room,
    "text":text
  };
  var auth = 'Authorization: Bearer ' + token;
  xapi.command('HttpClient Post', 
  {
    Header: 
    ['Content-Type: application/json', 
auth],
    Url: url
  },
  JSON.stringify(payload))
  .then((result) => {
    console.log('HTTP Post was success:' + result.StatusCode);
  }).catch((err) => {
    console.log('HTTP Post was failed:' + err.message);
  });
}

切断理由を POST するように処理をfunction listenToCalls() に加えます。

function listenToCalls() {
  xapi.event.on('CallDisconnect', (call) => {
    const now = new Date();
    const weekday = now.getDay() > 0 && now.getDay() < 6;
    const wakeupNow = now.getHours() === 8 && now.getMinutes() < 3 && weekday;

    if (wakeupNow) {
      if (call.RequestedURI === number && call.Duration === '0') {
        var failinfo = "Call was failed:" + " CauseValue: " + call.CauseValue + ' CauseType: ' + call.CauseType + ' CauseString: '+ call.CauseString;
        console.log(failinfo);
        postTeamsMessage(room, failinfo,url);
      }
    }
  });
}

Cisco Webex Teams に切断理由が出るようになりました。

22.PNG

まとめ

ここまでの処理をまとめた例になります。

/**
 * Auto-Call the system at 8.00 in the morning
 * on weekdays
 */


// library for communicating with video system
const xapi = require('xapi');

// how often to check time
const intervalSec = 60;

// Dial Destination
const number = '';

// Webex Teams
var payload; 
const room = "";
var text ="1";
const url = 'https://api.ciscospark.com/v1/messages';
const token = "";

// Standard javascript built-ins such as date and timers are included
function checkTime() {
  const now = new Date();
  const weekday = now.getDay() > 0 && now.getDay() < 6;
  const wakeupNow = now.getHours() === 8 && now.getMinutes() < 3 && weekday;

  if (wakeupNow) {
      xapi.status.get('Call')
      // Calls already exist. Disconnect
        .then((call) => {
          if (call[0].Status === 'Connected') {
              disconnect();
          }

          else if(call[0].Status != 'Connecting') {
            dial(number);
          }
        })
        .catch(() => {
          dial(number);
        });
  }
}


function disconnect() {
   console.log('Disconnect');
  xapi.command('call disconnect');
}


function dial(number) {
  console.log('dial');
  xapi.command('dial', { Number: number });
}


function listenToCalls() {
  xapi.event.on('CallDisconnect', (call) => {
    const now = new Date();
    const weekday = now.getDay() > 0 && now.getDay() < 6;
    const wakeupNow = now.getHours() === 8 && now.getMinutes() < 3 && weekday;

    if (wakeupNow) {
      if (call.RequestedURI === number && call.Duration === '0') {
        var failinfo = "Call was failed:" + " CauseValue: " + call.CauseValue + ' CauseType: ' + call.CauseType + ' CauseString: '+ call.CauseString;
        console.log(failinfo);
        postTeamsMessage(room, failinfo, url);
      }
    }
  });
}

function postTeamsMessage(room, text,url) {
  var payload = { 
    "roomId":room,
    "text":text
  };
  var auth = 'Authorization: Bearer ' + token;
  xapi.command('HttpClient Post', 
  {
    Header: 
    ['Content-Type: application/json', 
auth],
    Url: url
  },
  JSON.stringify(payload))
  .then((result) => {
    console.log('HTTP Post was success:' + result.StatusCode);
  }).catch((err) => {
    console.log('HTTP Post was failed:' + err.message);
  });
}

xapi.config.set('HttpClient Mode', 'On');
setInterval(checkTime, intervalSec * 1000);
listenToCalls();

おまけ

あまりうまい処理ではないのですが、接続できた後の通話ステータスをとる処理を加えたバージョンになります。ジッター値やパケットロスなどを取得可能です。 取れてしまう値が多いので、直接 Cisco Webex Teams に送るのではなく、どこか別サーバでロギングしてから何か問題がある時だけ Cisco Webex Teams に投げたほうがよさそうですね。音声、メインビデオ、資料共有の上りと下り分表示されます。

function checkTime() {
  const now = new Date();
  const weekday = now.getDay() > 0 && now.getDay() < 6;
  const wakeupNow = now.getHours() === 8 && now.getMinutes() < 3 && weekday;

  if (wakeupNow) {
      xapi.status.get('Call')
      // Calls already exist. Disconnect
        .then((call) => {
          var callid = call[0].id;
          if (call[0].Status === 'Connected') {
                 var command = 'MediaChannels Call ' + callid + ' Channel';
                 xapi.status.get(command)
                    .then((mediaId) => {
                    console.log(mediaId);
                    postTeamsMessage(room, JSON.stringify(mediaId),url);
                });
              disconnect();
          }
          else if(call[0].Status != 'Connecting') {
            dial(number);
          }
        })
        .catch(() => {
          dial(number);
        });
  }
}

23.PNG

今日はここまで。2018年お疲れさまでした。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0