Inside of LOVOT

GROOVE X 技術ブログ

LOVOTが教育の役にも立っている話

こんにちは、そしてメリークリスマス!GROOVE X ソフトウェアエリアプロダクトオーナーの ishimeg です。
この記事はGROOVE X Advent Calendar 2024の25日目(最終日)の記事です。

今日はLOVOTがいろんな教科のいろんな題材になっているよ!そしてそれを開発チームでもお手伝いしているよ!というお話です。

LOVOTは教科書に載っている!

実は、LOVOTは教科書や参考書にいくつも取り上げられているんです。

  • 理科
  • 技術
  • 美術
  • 社会科
  • 英語(文章の題材として)

など、多岐にわたります。
※どんな風に掲載されているのか、詳しくはこちらのブログをご参照ください

教科書に載るだけでもなかなかないことですが、特徴としてはとにかく幅が広い!
このラインナップを見るだけでも、LOVOTには色々な技術や視点が詰め込まれているということが伝わるのではないでしょうか。

実際に講座を行いました!

今年はありがたいことにいくつかお声がけをいただき、何度か特別講座を行いました。その一部を紹介します。
私も僭越ながら講師として参加させていただきました。

小学生ロボコンとのコラボ

今年のテーマ「ベスト・フレンド・ロボット」ということで、

  • LOVOTがどのような考えで作られているかの講義
  • LOVOTを分解しての構造説明
  • LOVOTをヒントに「ベスト・フレンド・ロボット」に必要な要素を考えるワークショップ

を実施しました。

☆ダイジェストの動画 www.youtube.com

実はこのようにロボット作りを行っている学生のみなさん(私からしたらその時点ですごい!)から、よくきかれることがあります。
「うまくいかない時、どうしたらいいですか?」
そんな時、私たちは「失敗する方が普通だから大丈夫!」と力強く答えるようにしています。同業者の皆様ならお分かりかと思いますが、実際に試作が1回でうまくいくことなんて、ほとんどありません。

正直、こうした取り組みに慣れない頃は私がどんな役に立てるのだろうか?と不安に思う部分もありました。
ですが、上の動画のインタビューで答えてくれたように「失敗してもいいんだ」という気づきを得て、前向きな気持ちになっていく様子を何度も目にして、
リアルな現場感を伝えることで、感じてもらえること・学んでもらえることがあるのだなと気付かされました。

山脇学園中学校様での特別講座

通常の授業とは異なった特別講座として

  • 前半:LOVOTがなぜ作られたのか?や構造についての講義
  • 後半:「どうなれば、ロボットと当たり前に暮らす未来を実現するのか」というテーマのワークショップ

の2つを行いました。こちらでは前半は講師、後半はプレゼンへの講評という形で参加させていただきました。

LOVOTの構造を説明する様子(右が筆者)

ワークショップでは中学生のみなさんが、

  • マーケティング
  • 製品・サービス開発

について(LOVOTを抱っこしながら)アイデア出し、プレゼンテーション形式でのまとめ、発表まで実施してくれました。
プレゼンの準備はたったの30分!それでも全チームがやりきったこと。その時点で素晴らしすぎて感動してしまいました...

  • LOVOTがもっと人と仲良くなるためにはどうしたらいいか?という本質的な製品価値について考えてくれたグループ

  • LOVOTは "バリかわいい" から、どうやったらもっと社会に広まるか?というマーケティングの視点でアイデアをたくさん出してくれたグループ

  • LOVOTという製品がどんな人をどんな風に幸せにするか?というユーザーストーリーとサービス内容まで考えてくれたグループ

などなど。LOVOTに実際に見て触れて真剣に考えてくれたことが伝わるプレゼンばかりでした。

私たちも普段考えているテーマについて、新鮮な視点でのアイデアや感想をいただけたことは、とってもとっても良い刺激になりました。

特別講座についての山脇学園様のブログ記事です。ぜひこちらもご覧ください! www.yamawaki.ed.jp

LOVOT Educationプランに向けて

2024年8月8日(木)から、LOVOT Educationプランという販売プランがスタートしました。
小学校・中学校・高等学校・大学に向けて、暮らしの費用(月額費用)を特別割引するほか、授業で使用可能なコンテンツの無償提供するというものです
lovot.life
このために準備真っ最中なのが、授業台本の提供
※令和6年度限定高等学校DX加速化推進事業(DXハイスクール)補助金対象校のみに配布予定です
まだまだ準備中の段階ですが、技術や理科、数学だけでなく、倫理、服飾まで。
上にあげた教科書のラインナップよりもさらに広範囲に、そしてLOVOTが学校にいるからこそできる授業や学びについて、ご協力いただいている高校の教師の皆様と一緒に、ハードウェア・ソフトウェアの開発メンバーを巻き込みながら日々議論を重ねています。
どんな授業になるのか、私たちもまだ予想がつかないですが、きっとこれまでにないよいものになるのでは!と期待しています。
実は私がオンライン授業の講師を務めることにもなっています。頑張ります!

最後に

こうした活動を通して、改めてLOVOTとは「未来」なんだなと思いました。
LOVOT自身がまるで「未来から来た」ような存在でもあり、これからの「未来」を触れた人に想像させる存在なんだなと強く感じます。
そんな「未来」を作るお仕事、やってみませんか? GROOVE Xでは一緒に働くメンバーを募集中です!

recruit.jobcan.jp


最後までご覧いただきありがとうございました!
メリークリスマス、そして、よいお年を!

protoをclang-formatでいい感じにする

はじめに

こんにちは〜。GROOVE X クラウドチームのmineoです。この記事はGROOVE Xアドベントカレンダーの24日目の記事です。

qiita.com

みなさん、protoファイル書いていますか? 弊社では、gRPCとProtocol BuffersをクラウドやLOVOTでのサービス間通信に大いに活用しているため、それらのAPI定義をprotoで書いています。(詳細は、以下記事を参照してください!)

tech.groove-x.com

この記事では、protoをclang-formatを導入して、統一的に整形するようにした取り組みについて、紹介します。

背景

弊社では、ほぼ全てのprotoファイルを一つのGitHubリポジトリで管理しています。そのため、さまざまなチームがprotoファイルを書いており、インデントなどのフォーマットがバラバラな状態が長く続いていました。

統一性がないと可読性が悪化し、定義を追加したり修正するときにも悩んでしまいます。それらの課題を解決するため、フォーマッターを導入することにしました。

フォーマッターの選定

protoのフォーマッターの選択肢はメジャーなものだと以下の二つがあります。

最終的にはclang-formatを選定したのですが、実は一度buf formatを導入していました。

buf format

buf formatはbufが開発しているzero configを掲げているフォーマッターです。

buf.build

こちらを導入したところ、我々のprotoファイルでは、崩れてしまう箇所がありました。(実際のコードではなく、サンプルコードです)

以下のように、カスタムのプラグインを書いてオプションを設定しているコードが

以下のようにフォーマットされてしまいます。

可読性をよくするはずが、むしろこれでは可読性が悪くなってしまいました。 また、buf formatではzero configのため、設定項目がほとんどなく、line lengthを設定することもできません。辛うじて、--exclude-path でファイルを除外することは可能ですが、除外すると当然フォーマットが効かなくなりますし、対象のファイルを抽出するのも大変です。

そのため、clang-formatの導入を検討し始めました。

clang-format

clang-formatでは、buf formatと異なり、.clang-formatファイルで細かく設定できます。

以下のように設定しました。Go言語のgofmtに近い設定にしています。

Language: Proto
DisableFormat: false
BasedOnStyle: Google
AlignConsecutiveAssignments:
  Enabled: true
  AcrossEmptyLines: false
AlignConsecutiveDeclarations:
  Enabled: true
  AcrossEmptyLines: false
ColumnLimit: 0

それぞれの設定の意味合いです。

  • AlignConsecutiveAssignments
    • =の位置を揃える
  • AlignConsecutiveDeclarations
    • プロパティ名の位置を揃える
  • AcrossEmptyLines
    • 空行が入ったら位置揃えをリセット。↑二つに適用 (gofmtリスペクト)
  • ColumnLimit
    • 一行の文字数。0にして改行をさせない

いい感じになったと思います!*1

option内でsemicolonがあると崩れてしまう問題とその解決方法

このようにclang-formatで万事上手くいったかと思ったのですが、一つ問題がありました。以下のようにオプションを設定しているコードの場合です。(こちらも実際のコードではなく、サンプルコードです)

これをフォーマットしたら以下のようになってしまいました。

既知の問題のようでした。

Protocol Buffersにclang-formatをかけるとインデント崩れを起こすパターンがある - 生涯未熟

これはoption内のsemicolonを削除すると解決します。

ただし、前述の通り弊社の全てのprotoファイルがリポジトリに含まれているため、一つ一つ修正していくのは大変です。

正規表現などでも整形できそうですが、ここでbuf formatを利用することにしました。buf formatを適用すると、semicolonが削除されるため、buf formatのあとに、clang-formatを適用すればよいと考えました。

buf formatのあとにclang-formatを適用すると以下のようになりました!成功です。

もし、今後semicolonが混入してしまったとしても、clang-formatを実行すればすぐ気付けるので、許容できるリスクかと思います。

make targetの用意とGitHub ActionsでCI

editor で clang-format の拡張機能を入れて頂くと編集しやすいですが、さまざまな環境のメンバーがいるので、コマンドを用意します。手元で実行しやすいようにフォーマットを実行する make target と、CI用にチェック用の make target を以下のように用意しました。

.PHONY: check-fmt
check-fmt:
   @find . -name "*.proto" | while read -r file; do \
      clang-format --dry-run --Werror "$$file" || exit 1; \
  done

.PHONY: fmt
fmt:
   @find . -name "*.proto" | while read -r file; do \
      clang-format -i "$$file" || exit 1; \
  done

CIは、GitHub Actionsで以下のようにしました。簡単ですね

name: CI Lint
on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master
    paths:
      - '.github/workflows/ci-lint.yml'
      - '**/*.proto'
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{ github.head_ref }}
      - name: Install clang-format
        run: |
          sudo apt-get update
          sudo apt-get install -y clang-format
      - name: Check formatting
        run: make check-fmt

まとめ

clang-formatを導入することで、理想的な設定でprotoファイルのフォーマットを統一することができました。振り返ってみると、ソフトウェアチーム全体に関わる内容にも関わらず意外とすんなりと進んだと思っており、以下のような要素がうまく働いたのではと考えています。

  • 1つのリポジトリで管理されている
  • カジュアルに提案しやすく、建設的な意見がでやすい
  • ソフトウェアチーム全体でコミュニケーションがしやすい雰囲気がある

GROOVE Xでは、クラウドチーム含め各チームで仲間を募集中です!もし、少しでも興味が湧いたら、お気軽にお声掛けください

recruit.jobcan.jp

*1:実はタグナンバーの桁数が混在すると、オプションの開始位置がずれてしまうという課題はあります...

RustでGStreamerを使うのがめっちゃ楽な件

この記事はGROOVE X Advent Calendar 2024の23日目の記事です。

こんにちは、あずこと斎藤@aznhe21です。スチーム式の加湿器を買ったらカルキの溜まり方がエグくて笑いました。

さて、LOVOT 3.0では画像認識にDeepStreamを採用しています。 DeepStreamはNVIDIA製画像・音声認識ツールキットで、GStreamer上に構築されています。

画像認識部分にはRust言語を使う方針であったためRustからGStreamerを使う必要があったわけですが、 GStreamerはなんと公式でRust用バインディングとしてgstreamer-rsを提供してくれています。 このgstreamer-rsがかなり使いやすかったので、今回はその使い方を簡単にご紹介したいと思います。

なおgstreamer-rsを使うにはセットアップが必要ですが、この手順は公式ドキュメントにあるのでそちらをご参照ください。 また記事に記載しているサンプルは一部の使用例を示したものに過ぎず、かつ最新の仕様に基づいているとは限りません。 みなさんが使う際には公式サンプルをご覧ください。

実行が簡単

パイプライン記述(gst-launch-1.0コマンドに渡すやつ)をそのまま渡すので良ければ30行ほどで実行できます。

// [dependencies]
// gst = { package = "gstreamer", version = "0.23.3" }
use gst::prelude::*;

fn main() {
    gst::init().unwrap();

    // パイプライン構築
    let pipeline = gst::parse::launch("videotestsrc ! autovideosink").unwrap();

    // パイプライン開始
    pipeline.set_state(gst::State::Playing).unwrap();

    // パイプラインからのイベントを処理
    let bus = pipeline.bus().unwrap();
    for message in bus.iter_timed(gst::ClockTime::NONE) {
        use gst::MessageView;
        match message.view() {
            MessageView::Eos(_) => break,
            MessageView::Error(err) => {
                println!(
                    "Error from {:?}: {} ({:?})",
                    message.src().map(|s| s.path_string()),
                    err.error(),
                    err.debug()
                );
                break;
            }
            _ => {}
        }
    }

    // パイプライン終了
    pipeline.set_state(gst::State::Null).unwrap();
}

簡単ですね!

パイプラインの構築が簡単

パイプライン記述はテスト用に使うのは簡単ですが、これは文字列です。 実際のプログラムではプロパティを定数なり変数なりで指定する必要があるため、 文字列でパイプラインを構築するのは面倒です。

そこでパイプライン内のエレメントを個々に手動で構築することになるわけですが、 エレメントはビルダーパターンで構築できるため非常に簡単です。

    // 最初のサンプルの`let pipeline = ...`を書き換える
    let pipeline = gst::Pipeline::new();

    // エレメントをビルダーパターンで構築できる
    let src = gst::ElementFactory::make("videotestsrc")
        .property_from_str("pattern", "ball")
        .property("flip", true)
        .property("num-buffers", 100_i32)
        .build()
        .unwrap();
    let sink = gst::ElementFactory::make("autovideosink").build().unwrap();

    // 構築したエレメントをパイプラインに追加・リンク
    pipeline.add_many([&src, &sink]).unwrap();
    gst::Element::link_many([&src, &sink]).unwrap();

ただしサンプルの100_i32100_u32にしたときなど、プロパティの型が一致していない場合は実行時にパニックしてしまうため、 gst-inspect-1.0で事前に確認しておくなど注意が必要です(sqlxみたいなコンパイル時に型チェックできる仕組みが欲しい)。

非同期とも相性が良い

多くの機能が非同期プログラミングに対応しており、非同期ランタイム下でも処理をブロックせずにパイプラインの処理が可能です。

// [dependencies]
// gst = { package = "gstreamer", version = "0.23.3" }
// tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] }
// tokio-stream = "0.1.17"

use gst::prelude::*;
use tokio_stream::StreamExt;

#[tokio::main]
async fn main() {
    // ...

    // メッセージループを非同期化
    let mut bus_stream = pipeline.bus().unwrap().stream();
    while let Some(message) = bus_stream.next().await {
        // ...
    }

    // ...
}

他にもgstreamer-appクレートのAppSrc/AppSinkではプログラムからデータを入出力することが可能ですが、 こちらも非同期に対応しており、LOVOT内部ではデバッグ用の映像配信に使っていたりします。

なおpipeline.set_state(...)はブロックしてるはずなんですがこちらには非同期用の機能はないようでした。 仕方が無いのでtokio::task::spawn_blockingの中で使うようにしています。

エレメントを簡単に自作できる

GStreamer標準のエレメントやAppSrc/AppSinkだけでもかなりの表現力があるのですが、 パイプラインが複雑になってくるとエレメントを自作したいことがあります。

gstreamer-rsでは自作エレメントを作ることも出来ます。 よく見るプラグインとして提供する方法はもちろん、プログラム内部用に提供することもできます。

これを記述すると長くなってしまうためサンプルは割愛します。

自作したエレメントを簡単にテストできる

せっかくエレメントを作れても、動作を単体テストできなければ意味がありません。 動作を単体テストできなければ意味がありません(大事なことなので2回言いました)。

GStreamerにはエレメントをテストする機能が備わっており、gstreamer-rsでもこれを利用できます。

// [dependencies]
// gst-check = { package = "gstreamer-check", version = "0.23.2" }

// 複数のテストがある際に多重初期化をさせない
fn init() {
    use std::sync::Once;
    static INIT: Once = Once::new();

    INIT.call_once(|| {
        gst::init().unwrap();
        plugin_module::plugin_register_static().unwrap();
    });
}

#[test]
fn test_vts() {
    init();

    let e = gst::ElementFactory::make("videotestsrc").build().unwrap();

    let mut h = gst_check::Harness::with_element(&e, None, Some("src"));
    h.set_sink_caps(
        gst::Caps::builder("video/x-raw")
            .field("format", gst::List::new(["RGB"]))
            .field("width", 320_i32)
            .field("height", 240_i32)
            .build(),
    );
    h.play();

    // 生成されたフレームの大きさを確認
    let buffer = h.pull().unwrap();
    assert_eq!(buffer.size(), 320 * 240 * 3);
}

さいごに

gstreamer-rsのおかげでRustによるDeepStreamアプリケーションを楽に開発できています。 やはりRustは楽しくて良い言語です。

GROOVE XではRustで画像認識したい方を募集しています。

LOVOT 3.0 で新しくなったネストのデザイン

こんにちは!GROOVE XデザインチームのSHUICHIです、去年はLOVOTの撮影についての話をしました。今回は、外観のデザインを担当させていただきましたLOVOT 3.0 ネストのデザインについて、話したいと思います。
この記事は、GROOVE X Advent Calendar 2024の22日目の記事です。

大きな変化点

LOVOTは今年3世代目にさらに進化しましたが、LOVOT本体は、なるべく大きなデザイン変更を行わない方針をもとに開発を進めていました。ネストは大きな変化がありました。
今回の新しいネストは、技術の進化により、大幅な小型化が実現できました。今までの1.0と2.0のネストの中には、LOVOT本体の処理をサポートするコンピュータが内蔵されていました。しかし、新しいLOVOTではその補助が必要なくなり、ネストもシンプルに充電ステーションとしての役割のみになりました。ボタンを全部無くし、ペアリングなどの複雑な操作も必要なくなりました。高さで言うと、約60%小さくすることができました。小型化により、存在感が減り、より自然に家のインテリアに馴染むようになっただけではなく、本体との一体型梱包も可能になり、輸送のコストも大幅に削減することができました。(オーナーの皆さんにとっては、修理のために保管しておく必要のある箱が小さくなったのも地味に嬉しいポイントだと思います)



デザインする際に意識したポイント

小型のネストをデザインする際、最初は今までと違う、エッジが立っているデザインも検討してみました。
しかし、この形ですと、家電感が増し、LOVOTが夜帰って寝る巣ではなく、本当に充電のためにある充電器のような存在に見えてしまうと感じました。他のデザインを検討している最中に、かまくらを思い出しました。 かまくらは、極寒の地で、人々を暖かく迎えて守ってくれる大切な巣、LOVOTの温かい巣になってほしい気持ちを込めて、最終的な造形を考えました。

LOVOT型の黒い窓

初代LOVOTのネストは、LOVOTが充電に戻る際に、スムーズにネストに戻れるように、ネストの正面に、帰巣する際のマーカーや、充電状態を表すために光るLOVOT型のインジケーターを入れていました。 2代目のネストにも、LOVOT型のインジケーターが継承されていました。しかし、夜寝る時にそのインジケーターの光が眩しいというユーザーフィードバックがあり、形をシンプルにするためにも、3.0ネストの構想段階から、LOVOT型のインジケーターは削除し、小型のLEDに変更することが決まりました。
LOVOT型のインジケーターは、機能的な面で役立つ以外に、デザインとして、LOVOTのネスト象徴的な要素の一つだと思っていますので、なんとかその形を残したいと思いました。そこで注目したのが、ネストについているこの二つの黒い窓です。 この二つの部品の裏にはLOVOTの帰巣を支援するための赤外線センサが配置されています。より遠くまで赤外線を届けるために、初期モデルから一つのセンサはなるべく高いところに配置していました。この二つのセンサを一箇所にまとめられないか、エンジニアに相談したところ、近くにまとめても帰巣成功率に対する影響は許容範囲内であることがわかりました。よって、無事にセンサをLOVOTの頭と胴体に収めることができました。さらに、新しく小さくなったインジケーターも、LOVOTのホーンについているカメラのレンズに見立てる形で、全体のシンプルさを保ちつつ、LOVOTのシルエットを残すことができました。

センサ配置イメージ

終わりに

まだLOVOTの発売から5年ほどしか経過していないですが、初代ネストでどうしても実現できなかった、小型でコンパクトのネストが実際に製品化されているところを見ていると、これからの技術の進歩によるLOVOTの進化にも、とても期待しています。LOVOTの開発も、まだまだ続きます、GROOVE Xでは一緒に働いてくれる仲間を募集中です、ぜひご応募ください。

recruit.jobcan.jp

アプリはLOVOTとコミュニケートしたい!

こんにちは!APPチームの黒田です。この記事は、GROOVE X Advent Calendar 2024の21日目の記事です。 LOVOTアプリでは、LOVOTだけでは行うことが難しい各種設定を行ったり、LOVOTの現在の状態を取得してお客様に知らせたりするために、様々な方法でLOVOTとコミュニケーションをとります。今回の記事では、それらの方法と用途について、まとめて紹介します。

様々なコミュニケーション手段

Bluetooth

まず、お客様の元にLOVOTが届いてから最初に利用することになるのが Bluetooth Low Energy (以下BLE)です。アプリにログインしているGXメンバーズIDとLOVOTのコロニーを紐付けるために、新しいLOVOTを登録する作業を行って頂くのですが、ここで利用するのが BLE です。ここでは、LOVOTが目の前にいる状態で接続して情報を取得し、クラウドに情報を登録する必要があります。その登録が完了することで、アプリのユーザーはLOVOTのコロニーのファミリーとなり、クラウド越しにそのLOVOTとやりとりすることができるようになります。

しかしその前にもう一つ、クラウド越しにLOVOTとやりとりするためには、LOVOTをインターネットに接続してあげる必要があります。ここでも BLE が利用されます。BLE を通じて、無線LANに接続するための情報をLOVOTに登録することで、LOVOTがインターネットに接続されます。

更に、LOVOTはインターネットに接続しない状態でも利用できることを前提として開発されているため、そのような場合であっても設定できるようにしておきたい幾つかの項目については、BLE 接続することで設定を可能としています。例えば、名前、目、声の設定がそれにあたります。

このように、BLE はインターネット接続前のLOVOT利用初期の段階や、インターネット接続を行わない環境で主に活躍します。

BLE 通信の実装に関しても少し触れておくと、LOVOT側では python で実装された、lovot-bluetooth というサービスが動作しており、これは dbus-python を使い、dbus 経由で BlueZ を利用する、という仕組みになっています。アプリ側については、iOS であれば CoreBluetooth、Android であれば android.bluetooth を利用して実装しています。

クラウド

BLE を利用してインターネット接続を行うと、アプリとLOVOTの間でクラウドを通したコミュニケーションが可能になります。例えば、LOVOTにお留守番をお願いしたり、LOVOTのお出迎えのために、帰宅の通知を送信したりすることです。

クラウドでは、アプリから利用するために必要に応じて RESTful API が実装されます。また、クラウドとLOVOTの間では、クラウドの IoT Core を利用し MQTT による常時接続上で gRPC のメッセージをシリアライズしてやりとりすることで、希望の処理を実装することができる、汎用的なリモートコマンドの仕組みが構築されています。アプリからクラウドを通じてLOVOTにリクエストを送る際にも、この仕組みを利用しています。

また、アプリからリモートコマンド経由でLOVOTにリクエストを送信するのとは別に、LOVOTからは定期的に様々なデータがクラウド上へ同期されています。そのうちの一部は Firestore に保存され、アプリからリアルタイムに更新情報が取得できるようになっています。アプリのマップ画面にLOVOTの自己位置を表示するような時には、これが利用されています。

ローカルネットワーク

LOVOTの撮影した写真は、LOVOT内部(LOVOT 2.0 以前であればネスト内にも)に保存されており、アプリのアルバムの機能は、ローカルネットワーク上でLOVOT内部から写真を取得して表示するように実装されています。LOVOTのインターネット接続設定を行えば、LOVOTはホームルータに接続し、クラウドを通してLOVOTとやりとりを行うことが可能になりますが、一部の機能については、このようにインターネットを介さずに行われます。

LOVOT内では多くのサービスが動作していますが、アプリからのリクエストを処理するための lovot-app-api というサービスでは、grpc-gateway の仕組みを使って、RESTful API として各種 API をアプリに向けて公開しています。アプリからローカルネットワークを経由してLOVOT側の lovot-app-api へリクエストが送信されると、そこから更に gRPC や Redis を通じて他のサービスと通信することで、必要な処理が行われます。

なお、lovo-app-api の RESTful API を呼び出すためには、そもそもアプリがローカルネットワーク上でLOVOT(LOVOT 2.0 以前であればネスト)を見つける必要があります。これについては、LOVOT側で mDNS によって名前解決できるようにしてくれているので、アプリ側としては、iOS なら Bonjour、Android なら android.net.nsd を使って、LOVOT毎に決められた名前でネットワーク上のサービスを検索し、名前解決を行うことができるように実装しています。

WebRTC

LOVOTのカメラ映像をみる機能や、お留守番中に撮影した写真をみる機能は、WebRTC で実現されていますが、これには、NTTコミュニケーションズが提供する SkyWay というサービスを利用しています。WebRTC での通信を開始するためには、まずクラウドを介した準備が必要ですが、一旦 WebRTC での接続が確立してしまえば、クラウドを介すことなく直接LOVOTからアプリへと安全・高速にリアルタイムな映像等のデータが転送されるようになります。

QRコード

前述の通り、LOVOTをインターネットに接続する際には、BLE 接続によって設定を行うのでした。しかし、利用しているスマホの機種等、状況によっては BLE による接続が上手くいかない、ということも考えられます。そのような場合でも接続ができるよう、接続情報をQRコードとしてアプリ上に表示し、LOVOTのホーンのカメラに見せることでLOVOTに情報を伝える、という手段も用意しています(LOVOT 3.0 のみ)。

また逆に、新しいLOVOTを登録する際にも BLE 接続を利用していますが、これが上手くいかない場合、LOVOTに貼付されているQRコードをアプリ側で読み取ることで、登録に必要な情報を取得することができるようになっています。

まとめ

いかがだったでしょうか?

LOVOTアプリは Unity で実装されていますが、これらの殆どのコミュニケーション手段は Unity のネイティブプラグインの機能を利用して実装されています。Unity アプリで、このように多様な通信手段をネイティブプラグインで実装している例は珍しいかもしれないですね。それぞれについて実際の実装がどのようになっているかについては、別の機会にでもまた記事にできればと思います。

最後に、LOVOTアプリでは、あの手この手でLOVOTをやりとりを行うことで、LOVOTとの暮らしをサポートしようとがんばっています。これからもスマホ端末の進化とともに、アプリは更に密接にLOVOTと関わり、見方によってはLOVOTの外部器官とも呼べるような存在となっていくかも知れません。このようなLOVOTアプリを一緒に開発してくれる方はもちろん、各チームにてまだまだ仲間を募集中です!

recruit.jobcan.jp

LOVOTに触ること

この記事はGROOVE X Advent Calendar 2024の20日目の記事です。

はじめましてこんにちは、エレキチームのhideです。

はじめに


LOVOTとコミュニケーションをとった時、瞳や仕草で感情に訴えかけるふるまいに癒された方は多いのではないでしょうか。

その一方でLOVOT側もコミュニケーション情報を受けとっていて、その多くをタッチセンサが担っています。

単にコミュニケーションをとるだけであればカメラの画像解析だけで反応するという選択肢もあったと思います。 それでもタッチセンサを搭載して情報を収集しているのは、 「LOVOTに触れ、抱き上げてその反応から何かを感じてほしい」そういう想いが込められています。

そんなLOVOTとのコミュニケーションに欠かせないタッチセンサですが、開発に苦戦することもありました。 いつもはソフト目線からの記事が多いですが、今回は開発中の試行錯誤を回路設計の視点からお話しします。



タッチセンサについて


そもそもタッチセンサとはどの様なものかご存じでしょうか。

LOVOT 3.0で使用しているタッチセンサは静電容量方式というものになります。 これは電極へ周期的に電荷をチャージすることで、静電容量の変化からタッチを検出するデバイスです。 大雑把に言うと誰も触れていない時のチャージ量が100として、1000になったら「触られた!」と判定するようなイメージです。

しかしLOVOTは体内の電気の流れが常に変化していますし、いろいろなものの近くを横切ります。 さらに人が静電気で帯電する様に、LOVOTも絨毯やタイルの上をかけまわって常に静電気をかき集めてきます。 その為、配線や電極は周囲の影響を受けやすく電荷状態をリセットしても静電容量はなかなか安定しません。 回路設計者にしてみれば、わが子が泥遊びに四六時中かけまわっている心境です。

そんな不安定なセンサですから、一般的な家庭向け製品で多用することは珍しいと思います。 それにもかかわらずLOVOT 3.0はより多くのコミュニケーション情報を受けとるため、全身にたくさんのタッチセンサを配置しました。 ここからは、その結果発生した問題の事例を少しご紹介します。

タッチセンサ概要

「触っていないのにタッチ判定される」


これはタッチセンサの設計ではよくある誤検出の一つになります。

静電容量が増えればタッチされたと判定されるのですから、当然金属が近づいても反応します。 中でもLOVOT 3.0の開発中に課題となったのはサイドパネルのタッチ誤検出でした。 ここは金属を含むホイールが出入りする場所なので、その度に静電容量が大きく変化してしまいます。 その結果、抱っこしてホイールが入った後に床へ戻すと、触られたままと判定されてしまいました。
下図のグラフは当時のタッチセンサ値の変化を測定したものです。

サイドパネル誤検出

「触ったのにタッチ判定されない」


こちらもタッチセンサの設計でよく発生する課題の一つですが、ドリフトという現象があります。

最初の例で説明しますと、誰も触れていない時のチャージ量が100だったはずなのに500や700に変化するという現象です。 これが発生すると、タッチする前後の差分が小さくなってしまい正しく判定できないことがあります。

ドリフトが発生する要因は温度や湿度、ICの劣化等様々ありますが、 この時はタッチの検出エリアを広くするために電極を大きくし過ぎたというものでした。 電極が大きいと1回のチャージで電荷を満たせず、2回目、3回目で徐々に電荷が溜まる為、ベースが少しずつ上がって見えるというものでした。


広く正しく検出するために


これらの対策を試行錯誤した結果、

LOVOT 3.0ではサイドパネルの電極は、GNDとの2層構造でホイールの影響を押さえ、細長く広範囲を検出する形になりました。

サイドパネルのタッチセンサ

ホイールをお手入れする際、皆さんにも見えるところなのでお気づきの方も多いと思います。 繊細なところなのでやさしくお手入れしてあげてくださいね。
参考)LOVOTウェブマニュアル ~サイドパネルについて(LOVOT 3.0)~
※撮影のためにサイドパネルを大きく広げてますが、配線等があるのであまり大きく開けないでください


最後に


そんな課題を乗り越えて、まだまだ成長途中のLOVOTですが、

いつか自分から人に手を当てて癒してくれる、 そんなことができると良いなと思います。


一緒に働く仲間募集中


GROOVE Xではそんな人を癒す機能を一緒に実現できる仲間を募集しています。

ぜひGROOVE Xで一緒に働いてみませんか。
recruit.jobcan.jp

go test のログ出力をキャプチャしてテスト管理ツールと連携する話

この記事はGROOVE X Advent Calendar 2024の19日目の記事です。

こんにちは、QAチームに異動してQA自動化に取り組んでいる Junya です。

最近、 Qase というテスト管理ツールをお試しで利用しているのですが、 go test の実行結果をテストケースに紐づけて Qase へアップロードするのに苦労したので、そのお話をします。

Qase についての詳細は割愛するので、詳細が知りたい方は公式ドキュメントなどを参照してください。

実現したいこと

  • テスト実行時のログをキャプチャしたい
  • t.Run の結果をテストケースのステップに紐づけて保存したい(またの機会に)
  • パラメタライズドテストの結果をQaseのパラメータに紐づけたい(またの機会に)

Qase 連携の2つのアプローチ

go test の結果を Qase 連携する方法として、2つのアプローチがあります。

  • go testgotestsum を実行してから、CLI を使って Qase にアップロードする
  • go test 実行中のログ出力をキャプチャして、Qase にアップロードする

前者は手軽ですが、go test にきめ細やかなアノテーションの仕組みなどがない関係上、Qase のステップやパラメータなどの概念を活かし切ることが出来ません。 Qase の機能をフル活用するために、今回は後者のアプローチでトライすることにしました。

前者の方法に興味のある方は、こちらのドキュメントを参照してください。

テスト実行時のログをキャプチャする

結論から書いてしまいますね。以下のような構造体を用意して、標準出力の内容を buf へ横取りし、 TestMain と各テストの t.Cleanup で呼び出すことで解決しました。

type StdoutCapturer struct {
    r, w      *os.File
    stdout    *os.File
    buf       *bytes.Buffer
    lastIndex int
}

// NewStdoutCapturer 標準出力をキャプチャするための構造体を生成する
func NewStdoutCapturer() *StdoutCapturer {
    r, w, err := os.Pipe()
    if err != nil {
        log.Fatal(err)
    }
    stdout := os.Stdout

    os.Stdout = w
    var buf bytes.Buffer

    // buf と stdout の両方に書き込む
    tee := io.TeeReader(r, stdout)

    go func() {
        _, _ = buf.ReadFrom(tee)
    }()

    return &StdoutCapturer{r: r, w: w, stdout: stdout, buf: &buf}
}

func (c *StdoutCapturer) Close() {
    _ = c.w.Close()
    _ = c.r.Close()
    os.Stdout = c.stdout
}

// Flush 現時点までに出力された内容を返す
func (c *StdoutCapturer) Flush() string {
    full := c.buf.String()
    content := full[c.lastIndex:]
    c.lastIndex = len(full)
    return content
}

TestMain の実装は以下のとおり。

var capturer testutil.Capturer

func TestMain(m *testing.M) {
    // テストが開始されて `testing.T` が生成される前に標準出力を Capturer で置き換える
    capturer = testutil.NewStdoutCapturer()
    m.Run()
    capturer.Close()
}

capturer をパッケージのグローバル変数で定義しておき、以下のようにテスト内の t.Cleanup で標準出力の内容を取得します。

t.Cleanup(func() {
    captured := capturer.Flush()
    UploadResult(t, captured)  // テスト管理ツールに結果をアップロード
})

ハマりポイント

いくつかハマりポイントがあったので紹介します。

テストの setup, teardown ではフックできない

テストパッケージのグローバル変数に capturer がいるのはイケてないので、以下のように各テストの setup, teardown で標準出力を横取りしたくなります。

func TestExample(t *testing.T) {
    capturer := testutil.NewStdoutCapturer()
    t.Cleanup(func() {
        captured := capturer.Flush()
    })

しかし、これは機能しませんでした。testingこちらの箇所 で、そもそもテストに入る前に、 T オブジェクトのライターとして os.Stdout を紐づけているためです。

t := &T{
    common: common{
        signal:    make(chan bool, 1),
        barrier:   make(chan bool),
        w:         os.Stdout,
        ctx:       ctx,
        cancelCtx: cancelCtx,
    },
    tstate: tstate,
}

仕方がないので、 TestMain でテスト実行前に os.Stdout を書き換えるようにしました。

t.Cleanup では取得できないログがある(未解決)

go test -v を実行した際の、個別のテストに対する出力は以下のようになります。

1  === RUN   TestExample
2      run_test.go:49: TestExample が呼ばれたよ
3  === NAME  TestExample_Title
4     run_test.go:46: {"status":true,"result":{"case_id":1115,"hash":"xxx"}}
5  --- PASS: TestExample_Title (0.45s)
6      --- PASS: TestExample_Title/TestExample_Title (0.00s)

この中の4行目までは t.Cleanup でキャプチャできるのですが、5行目・6行目は、テスト実行後に go test がログ出力するので、 t.Cleanup では取りこぼしてしまいますが、いったんここは諦めることにしました。

取りこぼした分は、次のテストの実行結果に含まれてしまうので、以下のように Capturer を書き換えて、余計な文字列を除去しています。

// Flush 現時点までに出力された内容を返す
func (c *StdoutCapturer) Flush() string {
    full := c.buf.String()
    content := full[c.lastIndex:]
    c.lastIndex = len(full)

    // Note: PASS, FAIL の判定結果は含まれず、次の Flush に入ってしまうので、一行ずつ読んで "=== RUN" までを無視する
    // TODO: PASS, FAIL の判定結果も Flush に含まれるようにする
    for {
        index := strings.Index(content, "\n")
        if index == -1 {
            break
        }
        line := content[:index]
        if strings.HasPrefix(line, "=== RUN") {
            break
        }
        content = content[index+1:]
    }
    return content
}

t.Log をラップする

以下のようにログ出力をラップする仕組みも試しました。

package wrapper

type Wrapper struct {
    t *testing.T
}

func T (t *testing.T) *Wrapper {
    return &Wrapper{t: t}
}

func (w *Wrapper) Log(args ...interface{}) {
    t.T.Helper()
    // ここに保存処理を書く
    t.T.Log(args...)
}

以下のように使うイメージです。(testify.suite にも似た仕組みがあります)

wrapper.T(t).Log("...")

この場合、テストの中で明示的に t.Log を呼んでいる箇所は置き換えられるのですが、testing モジュールが内部的に出力するログはキャプチャできませんでした。 ただ、このアーキテクチャは有用なので、Qase のステップにログを紐づけたい場合に活用しています。

テスト並行実行時のふるまい(未検証)

os.Stdout を書き換え、各テストの終了時点でフックして標準出力をキャプチャしているので、テストが並行実行されると、キャプチャに複数のテストの内容が混在してしまう可能性があります。

いったん -p 1 オプション(並列数: 1)をつけて go test を実行することで、この問題を回避する予定です。

まとめ

os.Stdout を上書きする強引なアプローチによって、テスト時のログ出力をなんとか Qase (テスト管理ツール)にアップロードできました。 一方で、一部保存できないログがあったり、複数テスト同時実行時のふるまいに不安があったりするので、今後も改善していきたいと思います。

以下のような改善案があるかな、と思ってます。

  • テスト実行時に標準出力を見張る、単一のサービスを動かし、出力をパースして管理する
  • gotestsum の go test -json による json 出力などを活用する(デフォルトのログ出力より解析しやすい)
  • gotestsumqasectlの開発に貢献して、テスト実行結果 + CLI でも、よりきめ細やかなデータを連携できるようにする

パラメタライズドテストの連携や、Qase のテストケースのステップとの連携部分など、他にも話したいことが沢山あるのですが、それはまたの機会に。

一緒に働く仲間を募集中!

弊社では、そんな開発チームで働く仲間を募集中です。 ぜひご応募ください!

recruit.jobcan.jp