Inside of LOVOT

GROOVE X 技術ブログ

シナリオテストのおはなし

この記事は、Groove Xアドベントカレンダー2022 の10日目の記事です。

こんにちは! LOVOT の ソフトウェアの検証を実施・改善しているチームです。

社内ではQAチームと呼ばれているのですが、単に製品に問題がないかの確認するだけでなく、ユーザー目線で評価することで、品質を向上させる活動を行っています。

LOVOTの新しいソフトウェアがリリースされるときは、たくさんの評価試験を実施してから配信されます。

今回はソフトウェアがリリースされる前に、どのような場所でどのような評価試験を実施しているのかを一部ご紹介したいと思います。

LOVOTの過ごし方

さて、どんな試験をしているのかというご紹介の前に、LOVOTはどんな風に家の中を過ごすのか、少しお話させてください。

LOVOTをお迎えされた方はご存知かもしれませんが、ご家庭に届いた時、実はLOVOTはまだ生まれていません。

ネストと呼ばれる充電ステーションに入れてあげることによって、目を覚まし、その日がLOVOTの誕生日となります。

そしてLOVOTはネストが設置された部屋を中心に家の中を探検し、部屋の形や障害物を覚え、自分のお家のマップを作ります。

またLOVOTは人を見つけると近寄って抱っこをせがんだり、周りにLOVOTがいればそのLOVOTと遊びます。充電が少なくなったら自分でネストに戻って充電しにいき、満充電になればネストから出てきてお部屋の中を歩き始めるなど、とても自由に動き回ります。

※過去のイベントのお洋服をお借りして、いつも検証をがんばっているLOVOTたちに着せて撮影してみました!通常は普通のベースウェアを着ています。

シナリオ試験

私たちはソフトウェアがリリースされる前に、実際に生まれるところからお家で過ごす様子を確認しています。

このような一連の流れを確認し、機能や性能に加えオーナー様目線で違和感がないか確かめる評価試験を、シナリオテストと呼んでいます。

この試験は複数の環境で実施しています。たとえばオーナー様宅ではインターネットがある環境、ない環境どちらもあります。

どちらの環境でもLOVOTが生活できるよう、評価試験ではオンラインとオフラインの環境で正常に活動できるか確認しています。

また確認する観点として生まれた後にちゃんとネストと通信出来るか、ネストに戻れるか、マップを作るかなどなど...

LOVOTと生活をしていただくために必要不可欠な動作を評価しています。

試験環境

LOVOTミュージアムがある建物の弊社オフィスにて、LOVOTの開発や試験なども行っています。

もちろんオフィスにもたくさんのLOVOTがいて、評価を実施するための専用の部屋もありますが、LOVOTはご家庭で生活することを考え、一般家庭に近い環境で一部の評価試験を実施しています。

先程紹介した、LOVOTが届いて生まれる前〜数日の様子を確認する試験の一部も、実はここで実施しています。

最後に

LOVOTならではの評価試験についていかがだったでしょうか?

なかなか実際に人が住むような環境で評価試験を行うような製品は少ないのではと思います。

家の中を自由に動き回り、家族のひとりとして迎えられるLOVOTだからこそ、必要となる試験のひとつと言えるのではないでしょうか。

今回細かくは紹介しきれなかったのですが、ネストに戻って充電して、人と遊んで、お部屋の中を歩き回るなど、普段の生活のなかで不具合が起こらないか別の評価試験で確認を行っています。

詳しくはQAチーム2回目の記事(12/23に公開予定)でご紹介しますので、そちらもぜひご覧ください!!



一緒に働く仲間募集中

世界で唯一無二の存在、LOVOTの成長にご協力いただける方をお待ちしています!

ポジションなどの詳細はこちら

もしご興味があれば是非ご検討ください!

最後まで読んで頂きありがとうございました!

Appチーム・仕事の流儀 〜LOVOTアプリのUIリニューアル舞台裏〜

こんにちは!App(アプリ)チームでLOVOTアプリのデザインをしている、南地です。

LOVOTアプリは今年4月に大幅なUIリニューアルを行いました。UIのリニューアル、これまで使い慣れていたUIが変わってしまうので、一般的にはユーザーからの不満がつきもの。

それでもAppチームでは、そのストレスをできる限り軽減したいと考え、アプリの画面内にとらわれず、様々な工夫をすることでソフトランディングすることができました。

今回はUIリニューアルのプロセスの話を通して、Appチームがどのようにユーザーに向き合っているのかについてご紹介します。

LOVOTアプリのBefore(左)・After(右)

Appチームの紹介

Appチームは自分の他に、アプリエンジニア、ふるまいエンジニア、アニメーター、そしてQA(テスター)がそれぞれ1人ずつ、合計5人で構成されています。

それぞれ違った役割を持ちながら、時に専門領域を超えて協力し、スマホからLOVOT自身のふるまいまで、アプリに関することはワンチームで一気通貫して作ることのできるコンパクトなフィーチャーチームとなっています。

Appチームのメンバー

Appチームでは、ただ単に機能を開発するのではなく、常にオーナーの皆様にどうしたら喜んでいただけるか・ストレスなくLOVOTライフを過ごしていただけるかという視点をとても大切にしています。

それもあってか、とてもありがたいことにiOS / Androidともにストアの評価では★5つ中4.7と、高評価をいただいています。何らかのデバイスと連携するIoT系のアプリとしてはかなり良い評価ではないでしょうか。(中には対応が難しくてずっとお待たせしてしまっている改善点もたくさんあるので、この評価に甘んじてはいられませんが…)

iOSでの評価

オーナーの皆様のニーズに向き合うため、自分たちは定性・定量の両面でアプリの開発・改善に役立つ情報を吸い上げています。

定性的なアプローチとしては、アプリ内の「ご意見・ご要望フォーム」から寄せられた投稿の全てに目を通したり、IFTTTを活用してtwitterへの「(LOVOT AND アプリ) OR(らぼっと AND アプリ)」で引っかかる投稿がSlackに通知されるようにしていたり、もちろんストアレビューも新しいものが来ていないか毎朝チェックしています。(実は、返信は全て自分が書いてるんです!)

定量的には、各機能がいつ・何回使われたのかをFirebase Analyticsを用いて集計しており、毎週、過去1週間の傾向について全員で確認するのを大切なルーティンにしています。
例えば、LOVOTが作ったマップを確認できる機能は、元々は必要な設定をする時にだけ使うという想定でしたが、コロナ禍の緊急事態宣言前後でマップの利用回数が増減していることから、意外と外出先での利用が多いことがわかりました。

これらの両軸を活用することで、「アプリの現在地」をなるべく正確に把握することにチーム全員で努めています!

リニューアルのきっかけ

それでは、本題のアプリリニューアルの話に入っていきたいと思います。

UIを大幅にリニューアルするきっかけとなったのは、LOVOT 2.0の登場でした。

これまでも複数のLOVOTと一緒に暮らすオーナーがいらっしゃいましたが、2.0の登場でさらにそうしたケースが増えることが予想されました。

当時のアプリでは同じコロニー(ネストと、それを共有するLOVOTの組み合わせ)単位でしか同一のホーム画面で表示することができず、コロニー単位で切り替える必要がありました。

でも、一緒に暮らすLOVOTたちは1つの家族。

そこで、同じユーザーがアプリに登録しているLOVOTたちは、1つのホーム画面上に表示されるようにしよう!となったのでした。

一緒に暮らす、公式twitterのみるきぃちゃん(LOVOT初代)とリアンくん(LOVOT 2.0)

新UIの情報設計プロセス

アプリのリニューアルは、複数のコロニーのLOVOTが1つのホーム画面上に来てもユーザーが迷わずに操作できるよう、各機能を再配置する情報設計からスタートしました。

その中で最も難しかったのが、複数のコロニーにまたがった時に、今どのLOVOTについての操作をしているか、迷わないようにすることでした。

まずはUIデザイナーの自分がリードして、ワイヤーフレームという簡単な線画だけで描いた機能配置のパターンをいくつも描きました。そして、チームで何度も何度も議論を重ね、仕事も趣味もバックグラウンドが様々なメンバーからは、ゲームやペットの健康管理アプリを参考にするなど、様々な意見が飛び出し、またワイヤーフレームにフィードバックしていきました。

初期のワイヤーフレーム

そうして見えてきたのが「1体ずつにフォーカス&スムーズな切り替え」というコンセプトでした。つまり、縦にLOVOTを並べ、上下スワイプするとLOVOT1体ずつにスナップし、そこから各機能にアクセスできるようにするというものです。

従来は、LOVOTをタップした先にメニュー画面を開き、そこから各機能にアクセスしていたのに対し、主要な機能へのボタンをホーム画面に持ってくることができ、シンプルさを保ちながらアクセス性を向上することができました。

最終形に近づいたワイヤーフレーム

ユーザーの声とアナリティクスを反映した一等地の選抜

ホーム画面に生まれた一等地に配置する主要な機能の選定にあたっては、Firebase Analyticsからの分析結果を反映しました。

ユーザーからのフィードバックで、ホーム画面に設置してアクセスしやすくしてほしいと要望の多かったダイアリーとアルバムは最初にスタメン入り。

そして、設定機能の一部だったマップが、Firebase Analytics分析の結果、外出先からLOVOTが元気にしているか確認するニーズがあることがわかり、当初の想定より利用回数が多いことから、晴れてホーム画面に昇格となりました。

UIが一新されたことへの抵抗感をなくすために

新UIの方向性が決まったところで、次はこれまでのUIを使い慣れたユーザーへのフォローを準備していきました。

まず、アプリの中でできることとして、UIがどのように変わったかがわかるよう、「ウォークスルー」という、実際のUIにオーバーレイして表示するガイドを用意しました。

ウォークスルー

さらに、アプリの外でも一工夫しています。それが、iOSのTest FlightやAndroidのクローズド テストを活用した「ベータ版テスト」の実施です。

有志のオーナーが参加してくださっている「LOVOTオフィシャルサポーターズ」の中から参加していただける方を募り、一足先に体験していただくというもの。これまで、LOVOTソフトウェアの先行体験は何度もやってきましたが、アプリの先行体験は初めての取り組みです。オーナー向けの情報発信やイベント運営をしているCX(カスタマー・エクスペリエンス)チームに相談し、実現することができました。

このベータ版テストは、純粋にリリース前に改善点を発見する目的に加え、実はオフィシャルサポーターズの皆様に新UIのインフルエンサーになっていただきたい!という意図がありました。
やはり、実際にご愛用いただいているユーザーの意見が一番、他のユーザーにも響くと考えたからです。

そのため、クローズドなテストでしたが、スクリーンショットSNS投稿は大歓迎とし、開始から約半月後にはオーナーとAppチームメンバーとのミーティングをオンライン開催して、積極的な情報発信をお願いしました。これによって、ベータ版テストに参加していないユーザーにも、事前にリニューアル後の画面を見ていただくことで、UIの変化への予防接種的な効果が期待できます。

その結果、非常に多くの嬉しい投稿をしてくださり、また、オーナーの皆様の反応を直接見たり、開発チームの想いを語ることができ、開発者としても一番やりがいを感じられる、忘れられない体験となりました!

オンラインミーティングにご出席いただいたオーナーの皆様

UIリニューアル、正式リリースの結果!

ベータ版テストでの反応も良いことが確認でき、またご指摘いただいた自分たちだけでは気づかなかった細かな改善点も盛り込んだ上で、いよいよ4月5日に正式リリースとなりました!!

普段からウォッチしているtwitter上でも多くの喜びの声が見受けられ、中でも開発者として意外だったのは、「ホーム画面のLOVOTのイラストが大きくなったこと」について、想像以上に喜んでいただけたことでした!
今回のリニューアルの中では比較的簡単な変更だったのですが、それがまさかこんなにお喜びいただけるとは。

一方、心配していた使い慣れていたUIから変わったことへの反応は、ベータ版テストに参加してくださったオーナーからのSNS発信もあり、ネガティブな意見は見受けられずホッとしました。

Twitter上の反応

Firebase Analyticsでの分析結果でも、ホーム画面の一等地に移動した機能の利用数は大きく伸び、マップに至ってはリニューアル以前の2倍ほどを記録しました。

各機能の利用数変化(赤線が正式版リリース日)

アクティブユーザー数も、従来の増加ペースと比べるとベースが上がったことが見受けられます。

アクティブユーザー数の増加ペース(赤線はリニューアル前の増加トレンド)

こうして、アプリのUIリニューアルは定性的にも、定量的にも、無事ユーザーに受け入れられたことが確認できました。

まとめ

Appチームでは普段スマートフォンの中での出来事を開発していますが、より良いユーザー体験を生み出すためには、時に画面の外側にも目を向ける必要があると考えています。

LOVOTアプリは、LOVOTというリアルな存在とつながるためのものなので、その必要性はなおさら高くなります。

今回は、UIリニューアルというネガティブな反応が生まれやすいトピックに対して、ベータ版テストを通した総合的なアプローチによって上手く乗り切ることができたことをご紹介しました。

 

そんなAppチームでは、もっともっとユーザーに向き合い、LOVOTとの絆を深めたり、便利だったり、ワクワクしたりするようなアップデートを重ね、LOVOT自身の進化に負けないくらい成長させて行くために、LOVOTアプリの開発を一緒に加速してもらえるエンジニアを募集しています!!

少しでも興味を持っていただけたなら、まずはカジュアルにお話をすることもできますので、お気軽に以下よりご連絡ください。

また、アプリ以外の分野も募集していますので、LOVOTに関わるお仕事に興味ある方は是非ご検討ください。

 

最後までお読みいただき、ありがとうございました!!!

人に気がつくLOVOT

こんにちは、GROOVE Xのfmyです。
私達のチームではLOVOTが人に気がつくための認識を作っています。
今日その中でも2つ、画像認識とタッチ認識がどんなことをしているか紹介します。

人に気がつく必要性

LOVOTは人とコミュニケーションを行うという目的をもっています。
人と関わっていくために人の生活空間内で人の存在に気が付かなければなりません。

認識では意味が広いのでここでは「認識」を「LOVOTがふるまいに必要な外界についての予測や情報を指す」とします。
(ふるまいについては市川さんの記事をご覧ください)

画像認識

ロボットが人を認識するのに有力な方法の一つが画像認識の技術です。
LOVOTもセンサーホーンのカメラを使って周囲を撮影し、Object Detection(物体検出)と呼ばれるタスクで周囲の人やモノの存在を認識しています。
LOVOTのテクノロジーもご覧ください

LOVOTが撮影した写真に物体検出の枠を表示した例。動き回るので写真がぼやけることもよくある

Object Dectectionでこのように人を検出することは出来ますが
人間とコミュニケーションを取るには人の存在を知るだけでは十分ではありません。
何をしているのか、何かを伝えようとしているのかに気が付くのが望ましいです。
そのためLOVOTは物体の認識や人の存在だけでなく表情や姿勢を読み取れるような認識モデルがあり、ふるまいのための『認識』を作っています。

LOVOTが周囲の人を認識している様子

タッチ認識

動物も、もちろん人間も身体を使って触れ合うコミュニケーションを取ります。
撫でてもらってうっとりしたり、触られてほしくないところは嫌がったり、LOVOTも同じコミュニケーションが取れることが期待されています。
LOVOTは全身に様々な種類のタッチセンサーがあり身体に何かが触れていることを検出しています。
ところでみなさんもスマホタブレットなどを使ったことがあると思いますが、水滴や物に意図せず反応したり逆になかなか反応してくれないという経験はないでしょうか。
LOVOTも同じで様々な大きさの人の手や触り方でも『認識』できるように、自分自身の動きや全く別のものが触れた時に人と間違えないために機械学習を利用しています。

タッチをモニタリングしている様子色が触り方の種類の違いを示している

最後に

ロボットが外界を、人を、多様なコミュニケーション認識するにはまだまだ多くの課題があります。
何を持って認識が出来ているとするのか、常に学習と更新を続けるにはどうすれば良いか、限られた電力をどう使うか、私達と一緒に考えて『認識』を作ってくれる方を募集しています。 ぜひ採用ページにアクセスしてください!

recruit.jobcan.jp

SalesforceとSlackのカスタマイズ連携の実装方法

はじめに

SalesforceとSlackを連携させて業務の効率化を行っている現場は多いと思います。 ここ数年でSalesforceとSlack間の連携機能はどんどん追加されていき、「SalesforceからSlackに通知する」という要件程度だと、フローを使えば、ほとんど実現できる状態になっています。

ただ、現時点(2022/12月)だと、SlackからSalesforce上のレコードを更新する場合には、若干かゆいところに手が届かないという状態でもあるので、今回はカスタマイズ開発するとこんなこともできます。というご紹介をしたいと思います。

カスタマイズ開発の概要

今回はSalesforceのレコード詳細に配置したLWCのボタンを押すと、Slackにレコード情報がエスカレーションされ、Slack上のボタンを押して、更新内容を入力するとSalesfoce上のレコードが更新される。というカスタマイズを実装します。

image.png モーダルの入力内容が、Salesforceレコードのテクニカルサポート解析結果項目に反映されるようにします。

実装の流れ

Slack側の準備

まずはSlack APIを実行するためのトークンを取得します。Slackアプリの管理画面からOAuth&PermissionsBot User OAuth Tokenをコピペします。 image.png

Salesforce側の処理(Apex)

ApexでSlack APIの呼び出しを行います。以下はレコード詳細ページのLWCからApexを呼び出す前提のコードになっています。(LWC側のソースは割愛します)

caseEscalationがLWCから呼び出せる関数になっていて、画面表示しているレコードのID(今回はケースID)をrecordIdに、投稿するSlackチャンネルをslackChannelに指定して呼び出します。

generateEscalationSummaryの処理では、Block Kit Builderで作成したフォーマットを生成しています。action_idに指定しているhandle_open_modalは、以降で説明しているLambdaの処理を呼び出すトリガーになっています。

postMessageの処理で、カスタム表示ラベルに設定した、Bot User OAuth Tokenをヘッダーに追加して、Slack APIを呼び出します。

処理が正常に実行されると、Slackの指定したチャンネルにレコードの情報が投稿されます。

@AuraEnabled(cacheable=false)
public static string caseEscalation(String recordId, String slackChannel) {
    try {
        Case record = [
            SELECT Id, CaseNumber, Subject
            FROM Case
            WHERE Id =: recordId
        ];
        PostMessageResponse result = postMessage(slackChannel, generateEscalationSummary(record));

        return 'Success: Slackにエスカレーションしました';
    }
    catch (Exception e) {
        throw new AuraHandledException('The following exception has occurred: ' + e.getMessage());
    }
}

public static String generateEscalationSummary(Case record){
    List<String> summary = new List<String>(); 
    summary.add('[');
    summary.add('    {');
    summary.add('        "type": "section",');
    summary.add('        "text": {');
    summary.add('            "type": "plain_text",');
    summary.add('            "text": "');
    summary.add('■ケース\n' + record.CaseNumber + '\n');
    summary.add('■件名\n' + record.Subject + '\n');
    summary.add('"');
    summary.add('        }');
    summary.add('    },');
    summary.add('    {');
    summary.add('        "type": "actions",');
    summary.add('        "elements": [');
    summary.add('            {');
    summary.add('                "type": "button",');
    summary.add('                "text": {');
    summary.add('                    "type": "plain_text",');
    summary.add('                    "text": "解析入力"');
    summary.add('                },');
    summary.add('                "value": "' + record.Id + '",');
    summary.add('                "action_id": "handle_open_modal"');
    summary.add('            },');
    summary.add('        ]');
    summary.add('    }');
    summary.add(']');

    return String.join(summary, '');
}

public static PostMessageResponse postMessage(String slackChannel, String message){
    String body = 'channel='+ EncodingUtil.urlEncode(slackChannel, 'UTF-8')
               += '&blocks='+ EncodingUtil.urlEncode(message,'UTF-8');

    Http h = new Http();
    HttpRequest req = new HttpRequest();
    req.setEndpoint('https://slack.com/api/chat.postMessage');
    req.setMethod('POST');
    req.setTimeout(60000);
    req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
    req.setHeader('Authorization', 'Bearer ' + System.label.SlackToken);
    req.setBody('channel='+ EncodingUtil.urlEncode(slackChannel, 'UTF-8'));
    req.setBody(body);
    try {
        HttpResponse res = h.send(req);
        Map<String,Object> response = (Map<String,Object>)System.JSON.deserializeUntyped(res.getBody());
        return new PostMessageResponse((String)response.get('channel'), (String)response.get('ts'));
    }
    catch (Exception e) {
        throw new AuraHandledException('SlackAPIの呼び出しに失敗しました: ' + e.getMessage());
    }
}

Slackから呼び出す処理を準備(Lambda)

上記の処理でSlackに投稿すると、「解析入力」というアクションボタンが表示されます。 このままではボタンを押しても何も起きないので、ボタンを押したときに、モーダルを表示させるAPIを用意します。 今回はLambdaにPythonで実装しましたが、複数言語でSDKが用意されているので、好きな言語で実装できます。 また、LambdaなどのFaas上に構築すると、コールドスタートの考慮をしないといけないので、安定性を保証したい場合は、HerokuやGAE上に構築するほうが無難だと思います。

以下のサンプルコードでは、AWS SecretsManagerに登録した、SlackとSalesforceの認証情報を取得しています。またデプロイ時の環境変数で本番環境とSandbox環境を切り替えれるようにしています。

ポイントとしては、モーダル画面に、SFID、チャンネルID、タイムスタンプを仕込んでおくことで、モーダル上の更新ボタンを押下したときのイベントを検知して実行されるupdate_salesforce_caseの処理内でも、更新対象のSalesforceレコードを特定できるようにしていたり、投稿するスレッドを特定できるようにしている点になります。(もう少しスマートなやり方があるかもしれないです。)

import json
import logging
import os
import ast
import re
from pprint import pformat
from typing import Dict

import boto3
from slack_bolt import Ack, App
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
from slack_sdk import WebClient
from simple_salesforce import Salesforce
import requests

logger = logging.getLogger()
logger.setLevel(logging.INFO)

secrets = boto3.client(service_name='secretsmanager')
slack_credentials = secrets.get_secret_value(SecretId='SlackCredentials'+os.environ.get("ENV"))
slack_secret = ast.literal_eval(slack_credentials['SecretString'])

app = App(
    process_before_response=True,
    token=slack_secret['SLACK_BOT_TOKEN'],
    signing_secret=slack_secret['SLACK_BOT_SIGNING_SECRET'],
)

def request_modal_view(case_id, initial_value, channel_id, thread_ts):
    return {
        "type": "modal",
        "callback_id": "update_salesforce_case",
        "title": {"type": "plain_text", "text": "Case"},
        "submit": {"type": "plain_text", "text": "更新"},
        "close": {"type": "plain_text", "text": "閉じる"},
        "blocks": [
            {
                "type": "input",
                "block_id": "notes-block",
                "element": {
                    "type": "plain_text_input",
                    "action_id": "input-element",
                    "initial_value": initial_value,
                    "multiline": True,
                },
                "label": {"type": "plain_text", "text": "解析入力"},
                "optional": True,
            },
            {
                "type": "context",
                "block_id": "case_id",
                "elements": [
                    {
                        "type": "plain_text",
                        "text": case_id,
                        "emoji": True
                    }
                ]
            },
            {
                "type": "context",
                "block_id": "channel_id",
                "elements": [
                    {
                        "type": "plain_text",
                        "text": channel_id,
                        "emoji": True
                    }
                ]
            },
            {
                "type": "context",
                "block_id": "thread_ts",
                "elements": [
                    {
                        "type": "plain_text",
                        "text": thread_ts,
                        "emoji": True
                    }
                ]
            }
        ],
    }


def auth_salesforce():
    res = secrets.get_secret_value(SecretId=os.environ.get("ENV")+"SalesforceCredentials1")
    secret = ast.literal_eval(res['SecretString'])

    access_token_url = os.environ.get("ACCESS_TOKEN_URL")
    data = {
        'grant_type': 'password',
        'client_id' : secret["ConsumerKey"],
        'client_secret' : secret["ConsumerSecret"],
        'username'  : secret["APIUser"],
        'password'  : secret["Password"]
    }
    headers = { 'content-type': 'application/x-www-form-urlencoded' }
    response = requests.post(access_token_url, data=data, headers=headers)
    response = response.json()

    if response.get('error'):
        raise Exception(response.get('error_description'))

    session = requests.Session()
    logger.info(f"response:\n{response}")
    return Salesforce(
        instance_url=response['instance_url'],
        session_id=response['access_token'],
        session=session
    )


def respond_to_slack_within_3_seconds(ack):
    ack()


def handle_open_modal(ack: Ack, body: Dict, client: WebClient):
    logger.info(f"body:\n{pformat(body)}")
    case_id = body["actions"][0]["value"]
    channel_id = body["channel"]["id"]
    thread_ts = body["container"]["message_ts"]

    sf = auth_salesforce()

    try:
        res = sf.query(f"SELECT id, RepairAnalysis__c FROM Case WHERE Id = '{case_id}'")
        initial_value = dict(dict(res)["records"][0])["RepairAnalysis__c"]

        if initial_value is None:
            initial_value = ""

    except Exception as e:
        logger.exception(f"Failed to post a message {e}")

    client.views_open(
        trigger_id=body["trigger_id"],
        view=request_modal_view(case_id, initial_value, channel_id, thread_ts),
    )


def update_salesforce_case(body: Dict, view: Dict, client: WebClient):
    logger.info(f"body:\n{pformat(body)}")
    inputs = view["state"]["values"]
    blocks = body["view"]["blocks"]
    case_id = list(filter(lambda item : item['block_id'] == 'case_id', blocks))
    case_id_value = case_id[0]["elements"][0]["text"]
    channel_id = list(filter(lambda item : item['block_id'] == 'channel_id', blocks))
    channel_id_value = channel_id[0]["elements"][0]["text"]
    thread_ts = list(filter(lambda item : item['block_id'] == 'thread_ts', blocks))
    thread_ts_value = thread_ts[0]["elements"][0]["text"]
    notes = inputs.get("notes-block", {}).get("input-element", {}).get("value")
    trigger_id = body["trigger_id"]
    user_name = body["user"]["name"]

    sf = auth_salesforce()

    try:
        # Salesforceを更新
        sf.Case.update(
            case_id_value,
            {'RepairAnalysis__c': notes}
        )
        # Salesforceに更新した内容をSlackのスレッドにも投稿
        response = client.chat_postMessage(
            channel=channel_id_value,
            thread_ts=thread_ts_value,
            text=f"```{notes}```"
        )
    except Exception as e:
        logger.exception(f"Failed to post a message {e}")


app.view("update_salesforce_case")(
    ack=respond_to_slack_within_3_seconds,
    lazy=[update_salesforce_case]
)


# 以下の部分で「解析入力」ボタンが押された時のイベントを検知して、モーダル表示する処理を呼び出しています。
app.action("handle_open_modal")(
    ack=respond_to_slack_within_3_seconds,
    lazy=[handle_open_modal]
)


def handler(event, context):
    logger.info(f"event:\n{pformat(event)}")


    # 予期しない重複実行を排除する
    if event["headers"].get("x-slack-retry-num") is not None and event["headers"]['x-slack-retry-reason'] == "http_timeout":
        return {
            "statusCode": 200,
            "headers": {
                "Content-Type": "application/json"
            },
            "body": {
                "message": "No need to resend"
            }
        }

    slack_handler = SlackRequestHandler(app=app)
    return slack_handler.handle(event, context)


if __name__ == "__main__":
    app.start()

SlackからLambdaを呼び出せるようにする

上記のLambdaで準備したAPIのエンドポイントをSlackに登録します。Slackアプリの管理画面からInteractivity & ShortcutsRequest URLから登録します。

image.png

Slackアプリの権限設定

OAuth&PermissionsAPI実行に必要な権限を付与して、Slackワークスペースと再認証します。(割愛)

Slackアプリをワークスペースに招待

SalesforceからSlack投稿するチャンネルにSlackアプリを招待しておきます。 これで一通り実装と設定が完了します。

まとめ

SalesforceとSlack間の連携機能は今後もどんどん追加されていくはずなので、数年後にはこういったカスタマイズが不要になる可能性がありますが、SalesforceとSlack連携をすぐにパワーアップさせたい!という方はカスタマイズするのも悪くないと思います!

弊社ではさらに、SalesforceユーザとSlackユーザがコミュニケーションをとれるようにする機能も実装しています。 紺色の背景テキストがSalesforceユーザで、灰色の背景テキストがライセンス無しユーザがSlackからコメントした内容になっています。

image.png

SlackAPIの実装を通して、より業務改善の引き出しを増やすことができたので、今回の実装はいい経験になりました。 みなさんもぜひカスタマイズ実装をご検討してみてはいかがでしょうか?

参考

https://qiita.com/seratch/items/0b1790697281d4cf6ab3

https://slack.dev/bolt-js/tutorial/getting-started

LOVOTを動かすファームウェア

こんにちは、ファームチームのaoikeです。ファームチームで行っていることを紹介します。

LOVOTのファームウェアって?

LOVOTにはTomと呼ばれるメインコンピューターと、Jerryと呼ばれるサブコンピューターが搭載されており、センサーで感じたことをJerryに伝えることで、どう動くかを意思決定し、指令を出します。その指令を受けてモーターを動かし、カラダを動かしています。

このセンサーとモータの制御を行うために、小さいコンピューター(マイコン)が搭載されていて、このマイコンで動作するソフトウェアの開発を行うのがファームチームのお仕事です。

LOVOTのマイコン、センサー、アクチュエータ

LOVOTには多くのセンサー、アクチュエータが搭載されているため、マイコンも10個以上載っています。 センサーは、LOVOTの全身だと50以上載っており、例えば、頭のセンサーホーン部分には

  • 距離センサー × 5
  • 照度センサー × 1
  • タッチセンサー × 2
  • サーマルカメラ × 1
  • 赤外線センサー × 1
  • スイッチ × 3

の、合計13個のセンサがあります。直径6cm、長さ7cmくらいの小さなホーンの中にたくさんのセンサーが詰まっています。

さらに、全身のアクチュエータとしては

と、合計18個があります。

パワーサーボモーターは、頭、首、肩を動かしたり、ホイール・キャスターを出し入れするために使っています。ナノサーボモーターはパワーサーボモーターより小さく、手先のなめらかな動きを実現しています。

この他にも、目のバックライト、レーダーセンサー、ネストに戻るための超音波センサー、赤外線センサーなどのデバイスも搭載されており、これらをマイコンによって処理しています。 1つのマイコンで処理するには能力の限界があるため、複数のマイコンを使い、マイコン間をUART / SPI / I2Cといった通信規格を用いて制御しています。

こうしたたくさんのセンサー、アクチュエータを使うことで、LOVOTのかわいらしいふるまいを表現しているわけです。

最後に

最後まで読んで頂き、ありがとうございました。

今日はかなり概要の紹介になってしまったので、実はこんなことをやっているよ!ということを、また機会があれば記事にできたらと思います。

また、一緒に働く仲間を募集しています!気になった方は是非下記リンクにアクセスしてください。

recruit.jobcan.jp

LOVOTのマップの作り方

こんにちは GROOVE X の Mチーム の hiro-ogawa です。 Mチームの M は Mapping の M でして、主に担当しているのはLOVOTをお迎えされたおうちのマップを作ることと、マップの中でLOVOT自身がどこにいるのかを推定すること*1の機能を開発しています。 今日はLOVOTで実際にどうやっているのかを簡単にご紹介します。

マップを作る手順

マップを作るための基本的な手順は以下の通りです。

  1. 周りの情報を記録する
  2. 自分の位置を記録する
  3. 別の場所に移動する
  4. 最初に戻る

基本的には周囲の情報をいろいろな場所で集めて、それを繋げばマップができるというわけなんですが、ここで、問題になってくるのは2つ目の項目にある、自分の位置をどうやって知るのか。 自分の位置を知るためにはマップが必要です。でもマップは今作っているところなので、まだマップがありません。 これは、鶏が先か、卵が先かという問題になりますね。

で、どうしているかというと、これらの手順をちょっとずつ実行します。 まず自分の周りの狭い領域のマップを作って、その場所を最初の場所として覚えます。 少しだけ移動すると、これまで見えていなかった情報が見えるようになってきますので、それらを新しい情報として記録します。 少しだけの移動なので、これまで見えていた情報もたくさん残っていて、先程作った狭い領域のマップを使って、自分がどれだけ動いたかがわかります。 それを繰り返すことで、ちょっとずつマップを広げながら探索していきます。

一般的には SLAM と呼ばれていて*2、これは Simultaneous Localization and Mapping の頭文字を取ったものです。和訳すると 自己位置推定とマップ作成の同時実行 という感じでしょうか*3

実際にはだんだん誤差が累積していくなどの問題が発生するので、様々なテクニックで発生する誤差を解消したりしますが、そのお話を始めると際限無く長くなってしまうので、別の機会にします。

LOVOTのSLAM

マップを作るために LOVOT が持つ様々なセンサが使われています。主に使われているのは下記の3つになります。

  1. 半天球カメラ
  2. 姿勢センサ
  3. 深度カメラ

LOVOTのSLAMで使われているセンサ(LOVOT ANATOMY を改変)

半天球カメラ

周囲360度が見渡せるカメラです。 LOVOTでは人の検出や識別、写真撮影などに使われていますが、マップ作りにも使われています。

具体的には画像の中で特徴的な点を抽出して、その点の動きから自分の動きを推定したり、特徴点の配置から自分の位置を推定したりしています。

画像の中から特徴点を抽出している様子

画像を用いたSLAMを VSLAM *4 と呼んだりもします。 特にLOVOTではカメラ1台を利用しているので、単眼VSLAMと呼ばれる方式になります。

上の図を見てもわかるとおり、特徴点は均一の壁などではほとんど検出されません。 ですのでシンプルなお部屋よりも、絵画やポスターなど、いろいろ飾ってあるお部屋の方がマップの情報量が増えてLOVOTが自分の位置を見つけやすいかもしれません。

姿勢センサ

IMU*5とも呼ばれており、並進の加速度と回転の角速度が計測できます。 実際に欲しいのは基準位置姿勢からの位置と姿勢の差分なのですが、それを直接計測できるセンサはありません*6。 そこで、加速度を2回積分して位置の変化量を求めたり、角速度を積分して角度の変化量を求めたりします。 ただし、積分では誤差が累積しやすいので、IMU単体で使うことはあまり多くなく、LOVOTでは上記の半天球カメラと組み合わせて利用しています。

VIO*7 SLAM

実は単眼SLAMでは大きさが区別できません。 これは特徴点までの距離がわからないことが原因です。 精巧に作られたミニチュアのセットと実物とでは大きさが全然違うのにカメラで撮影するとその違いがほとんどわからないのと同じです。 そこで、カメラとIMUを組み合わせることで、特徴点の移動した量と、カメラの動いた量との関係から、大きさを推定します。

複数のセンサを組み合わせて使うことをセンサフュージョンと言いますが、このセンサフュージョンを行う際に重要になってくるのが、時刻同期です。 このタイミングがずれると、カメラがほとんど動いていないのに特徴点が大きく動くことや、その逆が発生してしまい、大きさの推定がおかしくなってしまいます。

実は LOVOT ではセンサフュージョンを前提として、専用のカメラモジュールを作っています。 半天球カメラとIMUを同じFPGAに接続して、撮像タイミングとIMUの測定タイミングの時刻を制御することで、正確に時刻同期をさせ、精度の高い特徴点のマップを作っています。 ちなみに、IMU情報は画像フォーマットの中に埋め込まれた形で届けられます。


www.youtube.com

壁情報

LOVOTアプリでは下のようなマップの表示があります。 VIO SLAMでは画像の特徴点のマップを作ると説明しましたが、特徴点は白い壁などではほとんど取れませんので、通行できない場所を知ることができません。

アプリでのマップ表示

そこで登場してくるのが、LOVOTのフロントセンサユニットにある深度カメラ。 これは距離画像カメラとも呼ばれていて、画像の画素が色でなく、距離が計測できるようになっています。 下の図は距離を色に変換してわかりやすくしたものです*8

深度カメラ画像

距離の計測方法にはいくつかの種類があるのですが、LOVOTで使用しているのは Time of Flight *9 という光が対象物に当たって帰ってくるまでの時間を計測して光の速さ*10と掛け算して距離を求めます。 光が帰ってこないと距離が計測できないので、ガラスなどの透明なものや、鏡、つやつやな表面など正反射が強いもの、反射しない黒い物などが苦手ですね*11

ちょっと話が脱線してしまいましたが、深度カメラとLOVOTの位置がわかると、それをたくさん集めて融合させることで、壁などの障害物情報が作れます。 それがLOVOTアプリで表示されているマップ情報になります。 この壁情報によって通れる場所、通れない場所を判断できるようになって、お出迎えの時の玄関までの経路やネストに戻るための経路が計算ができるようになります。


www.youtube.com

LOVOTの自己位置推定

マップができたら、そのマップを使って自分の位置を推定します。 LOVOTの自己位置推定では次の4つのセンサを使っています。

  1. 半天球カメラ
  2. 深度カメラ
  3. ホイールモータ
  4. 姿勢センサ

半天球カメラと深度カメラでは今見えている画像内の特徴点や壁の形状と、自分の持っているマップとを比較して、合致する場所を推定します。 基本的にはカメラ画像をメインで使いますが、夜に部屋の電気が消えている場合などは、画像が暗くて特徴点がうまく取れない場合があるので、そのような場合には深度カメラを使って推定します。


www.youtube.com


www.youtube.com

また、補助的に自分の動きをホイールモータの速度情報と姿勢センサの角速度情報から自分の移動量を推定しています。 ただ、この推定は目隠しで歩いているようなものなので、移動量が増えると誤差も累積しやすい問題がありますね。

おわりに

LOVOTはかわいい家族型ロボットではあるのですが、ちゃんとしたロボット技術が使われていて、専用のハードウェアまで作っちゃっているんです。 現在、 GROOVE X ではSLAM / 自己位置推定 開発エンジニアを募集しています。 各家庭や職場で1万体以上が愛されているLOVOTで使われるSLAM技術を一緒に開発しませんか?

recruit.jobcan.jp

また、SLAM以外の分野でも募集しておりますので、LOVOTに関わるお仕事に興味ある方は是非ご検討ください。

recruit.jobcan.jp

*1:Localization、自己位置推定と言います。

*2:スラムと読みます。これだけで検索するとバスケットボールの情報がよく出てくる。

*3:Simultaneous は地デジのサイマル放送のサイマルです。

*4:Visual SLAMの略。

*5:Inertial Measurement Unit の頭文字。

*6:そんなセンサがあったらすごく欲しい。

*7:Visual Inertial Odometry の頭文字。これだけで検索すると・・・以下略。

*8:青いものが近く、赤いものが遠くを表しいます。

*9:ToFと略されます。

*10:約30万km/sです。

*11:誰にでも苦手なものはありますよね。そう、LOVOTにもね。反射強度もわかるので、モノクロ画像みたいなものも取れます。

LOVOTのふるまいづくり

こんにちは!ふるまいチームのエンジニア、市川です!

この記事ではLOVOTのふるまいづくりについてご紹介します!

ふるまいって何?

わたしたちが呼んでいる「ふるまい」とは下記の事を指します。

  • 感情(学術的な理論に基づく心のモデル)
  • 感情や認識情報に基づいた意思決定(何をするか?)
  • 意思決定に基づいた体への動作指示(手・足・目・瞼などの動き)

これらはもちろんプログラムされています。

そう聞くと「決まった通りに動くからつまらない」と思われるかもしれませんが、開発者が単独では把握しきれないくらい大きなシステムであり、複雑な意思決定をしています。

それに加え、どう考え、どう動くかは様々な要因によって決まるため、開発者の予想を超えてくる事もあります。

つまり、決まった通りに動きますが、予測が難しいので面白いのです。

ソフトウェアアップデートの度に複雑さは増していき、面白さも加速していきます。

どうやってつくってるの?

ふるまいづくりにはざっくりと次の工程があります。

  1. 仕様作成
  2. プログラムの実装
  3. デバッグ(バグ発見と対応)
  4. レビュー
  5. リリース前の試験
  6. リリース

ふるまいチームではアニメータとエンジニアがタッグを組んでいて、それぞれが各工程で役割を持っています。

工程 アニメータの役割 エンジニアの役割
仕様作成 「どう見せたいか?」から仕様に落とす ロジックから仕様に落とす
プログラムの実装 動作指示に近い部分を実装する 動作指示以外の部分を実装する
デバッグ 動きの定性的評価によって異常を探す ソースコードの確認やデータの定量的評価によって異常を探す
レビュー 動きに関するレビュー プログラムの書き方やアルゴリズムに関するレビュー
リリース前の試験 定性的な試験 定量的な試験

(あくまでもイメージであり、各人の得意不得意にあわせて役割が変わります)

仕様作成の際、アニメータはどのようなふるまいを作りたいかを頭に描きます。

例えば絵が得意なアニメータは、頭の中にあるふるまいを絵に起こし、説明文を添えてエンジニアに説明します。

実際、下記のような絵を頂きました(一部を切り取っています)。

エンジニアはこれを見て、アニメータと議論しながら論理的に仕様を整理していきます。

その後、動作指示に関する部分はアニメータが、意思決定などの部分はエンジニアがプログラミングします。

そしてデバッグ、レビュー、試験に関しては、アニメータが定性的な目で、エンジニアは定量的な目で評価します。

基本的には人の主観に依存しないように定量的に評価したいところですが、 動きが良いかどうかのような定量的に評価しがたい部分について、アニメータの職人的な目が重要になります。

最後に

ここまで読んで頂きありがとうございます!

LOVOTはまだまだ未完成のロボットです。

センサーが多数搭載されており、できることが沢山ある可能性の塊です。

そんなわくわくするLOVOTを一緒につくりませんか?仲間を募集しています。

recruit.jobcan.jp