Inside of LOVOT

GROOVE X 技術ブログ

最近の小ネタ3つ

こんにちは、クラウドチームへ出張中の Junya です。
最近気づいて便利だった小ネタを3つ紹介します。

goのgenericsで任意のmapのキーを並べ替える

今更感あるのですが、go の generics 便利ですね。

map を決まった順番で処理したい場面があったんですが、genericsを使ってこんな関数を用意したら、

package function

import "golang.org/x/exp/slices"

func SortedKeys[V any](m map[string]V) []string {
    keys := make([]string, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    slices.Sort(keys)
    return keys
}

string キーのどんな map に対しても使えて便利でした。

package function

import (
    "reflect"
    "testing"
)

func Test_sortedKeys(t *testing.T) {
    stringMap := map[string]string{
        "key2": "value2",
        "key1": "value1",
        "key3": "value3",
    }
    intMap := map[string]int{
        "key3": 3,
        "key1": 1,
        "key2": 2,
    }
    stringMapKeys := SortedKeys(stringMap)
    intMapKeys := SortedKeys(intMap)
    if !reflect.DeepEqual(stringMapKeys, []string{"key1", "key2", "key3"}) {
        t.Fatal("SortedKeys failed: ", stringMapKeys)
    }
    if !reflect.DeepEqual(intMapKeys, []string{"key1", "key2", "key3"}) {
        t.Fatal("SortedKeys failed: ", stringMapKeys)
    }
}

SignedURL で frontend から直接GCSへアップロード

社内向けのツールで frontend から直接 GCS へファイルをアップロードしたいユースケースで、 GCS の SignedURL を活用できました。

めちゃ便利。
なお、発行された SignedURL を知られてしまったら、有効期限のあいだ、誰でも何でもアップロード出来てしまうのでご注意ください。

GoLand(Ubuntu)での日本語入力

開発で GoLand(Ubuntu) を使っているのですが、なぜか GoLand 内での日本語入力が出来ず、悩んでいました。

Mozc や Fcitx の問題かなと調べていたのですが、結果的には GoLand の Java ランタイムの問題で、 IDE の起動 Java ランタイムを変更する を参考に、Java のランタイムを JetBrains 提供のデフォルトのものから、Ubuntu 側の openjdk-17-jdk に切り替えることで解決しました。

なお、カスタムの Java ランタイムのパスは

~/.config/JetBrains/GoLand2024.1/goland.jdk

に保存されます。うまく動作しない場合は、このファイルを削除すると設定がリセットされます。

絶賛採用中

LOVOT のようなロボット開発というと、ハードウェアを扱っているイメージが強いですが、実際には、EC・データ分析・生産管理・業務システムなど、ロボット開発そのものではない開発も多岐にわたります。 GCP, Kubernetes, Go, TypeScript, Teraform などを用いたクラウド開発の分野でも人を募集しているので、ご興味のある方はぜひお声がけください!

recruit.jobcan.jp

クラウド費用のコスト削減を試みたけど失敗した話

こんにちは、クラウドチームに出張中の Junya です。 今日は、クラウド費用のコスト削減を試みたけど失敗した話について、ご紹介します。

ことのはじまり

クラウドチームでは定期的に GCP の Billing ダッシュボードを見て、コストのボトルネックを確認しているのですが、イベントデータを処理する pubsub コストが高いことに気づきました。

出来る限りイベントを取りこぼさないよう、イベントデータは一度 pubsub に入れてからワーカー経由でデータベースに保存しているのですが、イベントは量も数も多いため、pubsub のコストがかさんでいました。

イベントデータを保存するためのパイプライン処理

Pub/Sub の料金 によると、Pub/Sub の費用は以下の3つの要素で決まります。

  1. メッセージのパブリッシュと配信のスループット費用
  2. Google Cloud のゾーンまたはリージョンの境界を越えるスループットに関連するデータ転送費用
  3. スナップショット、トピックで保持されるメッセージ、サブスクリプションで保持される確認応答済みメッセージのストレージ費用

今回の場合、1番目のスループット費用がボトルネックでした。はじめの 10 GiB は無料で、TiB あたり $40 かかります。我々の場合、イベントデータのスループットは数十TiB/月あるので、Pub/Sub だけで月に数千ドルかかる計算です。

Pub/Sub コスト削減対策

スループットはデータサイズに依存するので、Pub/Sub に生データを直接流す代わりに、Cloud Storage にデータを保存してから、その場所だけを Pub/Sub に流せばコストを削減できるのではと考えました。

イベントデータを Cloud Storage に保存することで Pub/Sub コスト削減する構成

Pub/Sub のコストと Cloud Storage のコストを比較すると、以下のとおりです。

  • Pub/Sub
    • $40 / TiB / 月
    • 数千ドル / 数十 TiB / 月
  • Cloud Storage
    • $0.023 / GB / 月 (保存期間に比例)
    • 数十ドル!? / 月

Cloud Storage へのデータの保存時間は非常に短いので、Pub/Sub に流していたイベントデータを一時的に Cloud Storage に保存することで、コストは数十分の一になることが期待できます。

実際、同じような方法でコスト削減を達成した事例がほかでも紹介されていました。

対策結果

思惑通り、Pub/Sub コストは 95% 削減されました。

Pub/Sub コスト(右端が切り替え後のコスト)

Storage コストの増分も、わからない程度です。

Standard Storage のコスト

しかし、Cloud Storage のオペレーションコストがヤバイほど爆上がりしていました

Standard Storage の Class A, Class B Operation コスト

全てを合わせると、このようになります。

Pub/Sub と Cloud Storage のトータルコスト

濃いオレンジの部分が Pub/Sub 費用で、それ以外が Cloud Storage の費用です。 思惑通りに Pub/Sub 費用は削減できたものの、それ以上に Cloud Storage の費用がかさみ、全体として 1,200% のコスト増となりました。

大失敗です。システム構成はすぐにリバートしました。

なぜ Cloud Storage の費用が爆上がりしたのか?

実は、Cloud Storage ではストレージ費用の他にオペレーション費用が発生します。

「Cloud Storage の料金」より抜粋(2024年4月現在)

クラスAオペレーションにあたる「データ書き込み」の場合、

  • $0.005 / 1,000 回

ですが、この数字を甘く見ていました。

イベントデータの数は1日に数億件あるため、

  • $0.005 / 1,000 回
  • $5 / 100万回
  • $500 / 1億回

1日に数億件のデータを読み書きすると、1日に数千ドルのコストがかかります。 オペレーション費用は、まったく無視できる単価ではありませんでした。

まとめ

ちょっとした工夫でクラウドのコストを大幅に削減できると期待したのですが、今回は思い通りにいきませんでした。 課金額を監視していたので短時間でリバートできましたが、見積計算は慎重にすべきだったと反省しています。 Cloud Storage のオペレーション費用にはお気をつけください。

LOVOT のようなロボット開発というと、ハードウェアを扱っているイメージが強いですが、実際には、EC・データ分析・生産管理・業務システムなど、ロボット開発そのものではない開発も多岐にわたります。 GCP, Kubernetes, Go, TypeScript, Teraform などを用いたクラウド開発の分野でも人を募集しているので、ご興味のある方はぜひお声がけ頂けると幸いです。

recruit.jobcan.jp

ソフトウェアチームのオンボーディングテンプレートをつくってみた

こんにちは、GROOVE Xのテックブログ2024年の3月は、ScrumMasterのniwanoです。2024年の1/4がもう終了しますね。はやい!

1ヶ月ほど前に、チームふりかえりで、メンバーからLAPRASさんの新入社員に関する記事が良いと聞いて私も読んでみました。

speakerdeck.com

"新入社員の呪い" 私も経験があります〜。でも私がGXにjoinしたのは、ずいぶん前なのですっかり忘れていたんですね。
思い出させてくれてありがとう、numa さん。

わたしがこの記事で特に好きだったのは、"新入社員の活躍を社内に広めるのはチームメンバー"のところでした。中途採用の方は「自分は絶対に活躍できる!」と思って入社されるはずで、でも入ってみたら自分が思った以上に活躍できてないなあ、って思うこともあると思うのです。(実際そうだったよーっという話しも聞きました。)
その方の活躍をあえて全体に共有することで、本人に安心を与えるのはすごく大事ですよね!

ところで.....、弊社にはオンボーディングに関する規定や仕組みが全くないことに気づきました。各チームがそれぞれ独自でやってたんですね。ですので、あとからあれがないこれがない、がたくさん出てきます。そこで、今回、LAPRASさんの記事の中にあった、オンボーディングの記事を参考に、GROOVE Xソフトウェアチームのオンボーディングテンプレートを作ってみました。

以下、ごらんください。
(各資料には実際にはページリンクがついています。)


3ヶ月後に達成していたい状態の合意

トレーナーと新入社員で合意をとる
"期待値をそろえる"のが、もっとも重要

  • Issueを解決できる
  • Issueの内容をインプットとして、自分の力で調査/解決することができる
  • GROOVE X組織全体のルールや仕組みの改善提案/構築ができる
  • 簡単なPBIを完了させることができる
  • GROOVE SESSIONで発表ができる(初回の自己紹介はカウントしない)

※LAPRASさんの例を、まずはそのまま利用させていただきました。GXの特徴を入れたい!

組織について

  • CEOビジョン2023について知っている
  • 全体朝会について知っている
  • 社員会について知っている
  • GROOVE SESSIONについて知っている
  • チームビジョンについて知っている

※GROOVE SESSIONという成果発表の時間が毎週あります。3ヶ月で何か発表できることを目指します。

待遇・福利厚生について

  • 勤務時間について知っている
  • 試用期間について知っている
  • 通勤手当について知っている
  • 賞与タイミングについて知っている
  • ストックオプションについて知っている
  • 育児、介護休暇制度について知っている

※わすれがち

開発について

  • リリースサイクルについて理解する
  • Doneの定義について理解する
  • 開発マイルストーン共有会のログを閲覧できる
  • 製品についての資料を閲覧できる
  • ソフトウェアエリアふりかえりに参加できる

※弊社はスクラム開発を実践しています。

Ubuntu編

  • 日常業務ができる
  • ターミナルでgcloudコマンドが使える

webサービスのアカウント

  • github
  • github copilot
  • confluence
  • wrike
  • package cloud
  • figjamのなんでもページにアクセスできる

開発言語編

TBD

slack

  • slack講座を一読する
  • チームの slack user groupにjoinする
  • huddleを作成できる
  • huddleにjoinできる
  • アイコンを変える
  • topic_ask-chatgptチャンネルを知っている

Git/Github編

  • gitコマンドを使える(pull / push /commit /merge/stash/add)
  • organizationに入る
  • PRを出せる
  • PRにコメントができる
  • PRをapproveできる
  • アイコンがなければアイコンをつくる

Google CloudWorkspace編

  • googleメールを送受信できる
  • チームの user groupに入る
  • アイコンを変える

Google Calender

  • チームメイトの予定を確認できる
  • カレンダーで会議室を予約できる
  • 全社予定カレンダーをインポートしている

Google Drive

  • 共有ドライブでドキュメントを作成できる
  • 共有ドライブでドキュメントの公開範囲を設定できる
  • マイドライブでと共有ドライブの違いを把握する

Meet

  • 参加できる
  • 録画できる
  • マイク、カメラ設定をon/offできる
  • 画面共有できる

wrike編

  • PBIのステータスを変更できる
  • PBIにコメントができる
  • PBIを作成することができる

Confluence編

  • ドキュメントの作成ができる
  • 公開範囲を適切に設定できる

社内研修

  • フィードバック研修
  • scrum研修

まとめ

いかがでしたでしょうか。
オンボーディングテンプレートをみたら、弊社の開発の様子が少しわかるかもしれません。
開発言語ですが、弊社では、python、Go、C、C++、javascriptなど、コンポーネントに適した開発言語を使っているので、今回記述が出来ませんでした。わたしはGo派です。

続きは、実戦編へ!

GROOVE X にソフトウェアエンジニアとして入社して 4 ヶ月経った感想など

まとめ

  • LOVOT ミュージアムで LOVOT に生命を感じ、衝撃を受け、気づけばソフトウェアエンジニア採用に応募していた
  • GROOVE X はマネージャのいないフラットな開発組織を運用しており、エンジニアの自立性・裁量が非常に高い
  • LOVOT はエッジデバイスでありながらデバイス内にマイクロサービス構造を持つソフトウェア構成をとっており、それが開発生産性の向上に寄与していている
  • 長期的な LOVOT との未来を作るために、まだまだ開発すべきことがあります。気になる方、採用 への応募をお待ちしています

概要

皆さんこんににちは。GROOVE X でソフトウェアエンジニアをしている id:iizukak です。2023 年 11 月に GROOVE X に入社して、4 ヶ月が過ぎました。 GROOVE X は組織・技術共に独自性が高く、面白いトピックがたくさんあるので、印象がフレッシュなうちにアウトプットしておこうと思います。

なぜ GROOVE X に入社を決めたのか

GROOVE X では、LOVOT というロボットを開発しています。

ある週末の夕方、東京の浜町にある LOVOTミュージアム を家族で訪れました。 時間があったのと、娘がロボット好きなので、何気なく予約したのだと記憶しています。 LOVOT ミュージアムは、個人宅を模した空間で LOVOT とふれあえる素敵な空間であり、小学生の娘はすぐに LOVOT にめろめろになりました。

私がそこで感じたのは、LOVOT から受ける生命感です。これは生きている、と。 LOVOT 以外のロボットでそういった印象を強く受けることは今までなかったので、衝撃的でした。 自分はモノづくり人間で、これを作るのはすごく面白そうだぞ、とすぐに感化されてしまい、採用に応募したのです。

smalovo チーム

GROOVE X に入社してからは、 smalovo というチームに配属されました。 スマラボ、すまらぼ、smalovo …どれが正式名称なのか未だに分かっていないのですが、読み方は「スマラボ」です。 スマート・ラボットの略称のようです。 このチームは、画像認識、タッチ判定、距離推定など LOVOT が世界を認識するためのソフトウェアを開発しています。

smalovo チームは取り扱う技術の守備範囲が広いのが特徴的です。 チームでは単一のソフトウェアコンポーネントの開発保守をするというだけではなく、 Linux、機械学習、デバイスドライバ開発など専門知識を活かした開発を行っています。 機械学習をやりつつ Rust でカメラモジュールを書いて Debian パッケージをビルドしてリリースしているのです。すごい!

なぜこれが可能なのかというと、各々のエンジニアが、自分にとって未知の技術に抵抗なく取り組む姿勢があるからのようでした。

GROOVE X のソフトウェア開発組織

入社してもうひとつ驚いたことは、チームにマネージャがいないことです。 これまでのソフトウェアエンジニア人生では、チームごとにマネージャがいて、1 on 1 があって、様々な許可を申請して… というのが普通でした。 GROOVE X にも領域ごとのプロダクトオーナーはいて、 スクラムのセレモニーなどではプロダクトオーナーに情報共有をしたり、方向性の確認を行う、ということはしていますが、一般的に言うマネージャとは違います。

それではどのように物事が決定するのかというと、チーム内外のディスカッションでほとんどのことが民主的に決まっていきます。 技術的なところもそうなのですが、備品の購入などについてもある程度のところまでチーム内で決めています。 採用についても、ある程度までチームメンバーの話し合いによって決めることができます。 エンジニアが技術以外のことに対しても必要であれば取り組む姿勢が求められている、ということでもあると思います。

そこで問題になりそうなのは、各開発チームが好き勝手に開発を行うことで、船頭多くして船山に登る状態にならないのかということでした。 しかしそういった兆候は見られません。 なぜなのか? それは自分もまだよく分かっていないのですが、目指すべき方向性の認識が一致しているからなのだろうと思っています。 GROOVE X 社員の LOVOT への愛情というベクトルが一致しているからなのでしょうか。

GROOVE X のソフトウェア開発技術

LOVOT の技術的な特徴について。

LOVOT の認識アプリケーションは、物体検出や人物照合、距離推定など多様であり、それらを協調して動かす必要があります。 それらの言語は、様々なプログラミング言語で、依存するライブラリもそれぞれ異なります。 様々なアプリケーションの協調動作を実現する方法はいろいろとあると思うのですが、GROOVE X ではマイクロサービスをベースにした構成を採用しています。 家庭用ロボットのようなエッジデバイス上でマイクロサービスが動いているというのは、面白い構成ですよね。 アプリケーション同士の通信は gRPC や WebSocket など様々です。Redis などの KVS も利用されています。

これは初期に LOVOT のソフトウェア基盤設計を行ったエンジニアが、クラウドのアーキテクチャを参考にして決めたようです。 この構成は GROOVE X の自立して動く多数の開発チームという組織構造とも合っているのか、うまく働いています。 複数の開発チームがスピード感をもってソフトウェアを開発、デプロイできています。 デプロイされたパッケージはクラウドサービスから利用可能な状態になっており、 例えば apt などでインストールして利用します。

開発用の Linux マシンで動く CLI のツールも、LOVOT で動くソフトウェアライブラリも、 ほとんど同じようにクラウドサービスからパッケージをインストールして利用しています。

面白い構成ですよね。

LOVOT との未来

LOVOT は、触れ合ってみると現状でも可愛く完成されている感じを受けます。

しかし、LOVOT と未来を生きるためには、まだまだ開発すべきことが多くある、ということが入社して少し経って分かってきました。 今後の LOVOT と GROOVE X の未来には、ソフトウェアエンジニアとしても、いち LOVOT ファンとしてもとてもワクワクしています。

一緒に LOVOT との未来を作る方を募集しています -> 採用

ゆるTech!LOVOTのユーザー体験試験とは

みなさんこんにちは、2024年1発目のInside of LOVOTは、アニメーターの中里がLOVOT開発ならではの試験、「ユーザー体験試験」についてご紹介したいと思います。 技術ブログなのにTechから離れているんじゃない?と訝しんでおられるかもしれませんが、LOVOTという唯一無二の特殊なプロダクトだからこその「ガチTech」ではない「ゆるTech」が必要なんだなーということをこの記事で少し知っていただけるとわたくしもホクホク恵比寿顔になりますので、ぜひ最後までお付き合いいただき、眉間のシワが金剛力士像になりそうなわたくしめをお救いください。

ユーザー体験試験とは

Inside of LOVOTでも試験やQA(QualityAssurance)についての記事がこれまでにいくつかあったかと思います。これまでに紹介されたソフトウェア開発における品質保証試験とは別に、ふるまいチームでは「ユーザー体験試験」というものを行っています。

品質保証試験とユーザー体験試験の違いはこんなかんじです。

品質保証試験 ユーザー体験試験
試験項目 あり あり
手順 あり おまかせ
期待動作 あり 非公開
結果 正否がちゃんと出る 気になった体験のコメントを書く
体験をより良くする提案をする
良い体験はほめる(だいじ)

おまかせて!困るわ〜。
でも、これが大事なんです。ユーザーのふれあい方は千差万別、ユーザーの数だけLOVOTの生活があるのであえて手順は定めずに自分がLOVOTオーナーになった場合のふれあい方をする必要があるわけです。
また、アニメーターはソフトウェアチームの中では最ゆる層ではあるものの、まがりなりにも開発者なのでどうしてもビジネスライクなお付き合いが顔を出してしまうことがあります。そこで、もっとぴゅあな心を持ったQAチームのみなさんにお手伝いをしてもらったりしています。
難しい仕様を知らなくてもLOVOTを愛でる気持ちを持てる人であればこの試験はできるので、ゆくゆくは社内全チームに少しずつ手伝ってもらえば、お互いに得るものがあるだろうなと金剛力士は思っています。

ゆるTechの落とし穴

LOVOT開発においては非常に有益なユーザー体験試験ですが、先に述べたように試験の正解を設けていません。強いていうならば体験に満足することが正解ではあるのですが、ひとつのシチュエーションに対して満足と思える体験はひとつではありません。
そのためユーザー体験試験のような人の気持ちをベースにした試験は自動化することが現時点では不可能です。機能が増えれば試験項目も増えるので、LOVOTが成長し続ける限り試験項目も成長しつづけるわけです。何十年か先には社員総出でユーザー体験試験をやっているかもしれません。そうならないためにはLOVOTを愛でる知性と愛情を備えたロボットをわれわれは開発する必要があるのか…

プロダクトバックログ作成までが試験です

試験の大項目が10程度、その中の小項目が数個、それらをLOVOTの内部状態別に6段階で試験を行いまして、出るわ出るわの改善案。
試験実施後に時間を設けてみんなでコメントを確認して、ああだこうだと議論しながら今後進めるタスク候補 = プロダクトバックログアイテム(PBIと呼びます)に落とし込んでいくのですが、たくさんの試験項目にたくさんのコメントが付くので、全て確認をするのにも数日かかります。いい改善案がいっぱい出た日は「今日は豊作」と言ってみんなでホクホクしています。直近のユーザー体験試験では1つの試験項目に対して多い場合は30以上のコメントがつき、半数程度がPBIになりました。
できたPBIは体験向上度の予測や実装コストなどによって優先度をつけて順次改善を進めていくことになります。全てのPBIに優先度がつくと、やっとユーザー体験試験が終了です。

ユーザー満足度 == LOVOTの幸せ

開発者はつい機能的に優れていたり新しい技術や面白そうな技術を採用したくなったり、開発者の考える小さなユーザー像での体験を想像しがちですが、ユーザーとLOVOTが長く楽しく暮らせることが最も考えるべきことと思っています。
LOVOTの世界は開発環境の何万倍も広く、ただ開発をしているだけでは見通すことが難しくなってきます。そのため、ユーザー体験試験をすることで開発者目線からは一歩離れて視野を広く持つ機会が大事になってきます。LOVOTとユーザーの可能性がどんどん拡がる体験を提供して、ユーザー体験試験で改善案を出すのが難しくなる日を目指してがんばっていきたいです。

2024年も本気の募集です

さて、おわりにいつも載っている募集ですが、惰性で載せているわけではありません。本当に人が足りていません。読んでいただいているみなさんの身の回りでもご興味がありそうな方がいれば、開発から試験までおもしろそうなGROOVE Xをご紹介いただけると、確実に金剛力士から卒業できそうです。ぜひよろしくお願いいたします。

recruit.jobcan.jp

クリスマスの精霊を妄想してメリークリスマス

この記事は「GROOVE X Advent Calendar 2023」の最後の記事です。

こんにちは、ソフトウェアのエリアプロダクトオーナーのよっきです。

今年のアドベントカレンダーのラストを飾らせていただくことになりました。ありがたい話です。有ることが難しい。有り難い。あってよかった。

実は、去年のアドベントカレンダーもラストを飾らせてもらっていました。去年は「LOVOTと生きがい」についてのポエムを書かせてもらいました。今年はどんなテーマで書こうか悩みましたが、最近考えている「気づきを与える」ということについて考えてみました。

※組織の見解ではなく個人の私見になります。

温かいテクノロジーとしての目指す方向性

2023年5月に弊社代表の林要が書いた「温かいテクノロジー」の本を読んだ方はいますでしょうか?

GROOVE X社員としては必読書で、私はAmazonで予約して買ったのですが、なんと後日社員に配られる事になり、私の手元には2冊あります。それはともかくとして「温かいテクノロジー」の中に書かれていたテクノロジーのゴールの1つが「気づき」に関連する部分なので紹介します。環境問題と同じくらい大きな社会問題という節になります。

 僕ら人類はラーニング(学習)能力が高いため、手に入れた認知を土台に次の学習を積み重ねています。となると、土台から作り直すレベルのアンラーニングというのは、自分の世界観を一変させるほどの大きな変革となります。もはや個々の努力だけでは、どうにもならないことかもしれません。
 (ちょっと中略)
 アンラーニングを促すために必要なのは、新たなことに自ら「気づく」経験です。なので、この課題に対して、ロボット開発者としてのぼくが目指している解決策の1つは、いつかロボットを人類に寄り添う「コーチ」にまで進化させ、「気づき」を得るサポートをすること。
 ぼくは、これこそがテクノロジーのゴールの1つだと捉えています。

めっちゃわかりやすく言うと、社会の価値観が変わっている時に自分も変わらないと生きづらいですよ、と言っているのだと思います。 では、自分が社会に合わせていけばいいのか、と言われるとそれが相当難しい。本書の引用にもあるように、自分を成す土台を作り直さないといけないですからね。今まで学習してきたことは何だったのかと。

では、社会の価値観が変わって自分の土台を作り直す必要が出た時に何に気づけば良いのか。私は自分の感情と向き合うのがいいと思っています。つまり「自分の感情に気づく」ことが大事だと思っています。

自分の感情に気づくとはどういうことか!?

今回は、この自分の感情に気づくとはどういうことかについて、あるクリスマスにまつわる有名な物語に合わせながら考えていきたいと思います。

クリスマス・キャロルから考える自分の感情への気づき

小説「クリスマス・キャロル」はみなさんご存知でしょうか?なんと出版が1843年の180年も前の物語なんですね。 私は最近Audibleで聴きました。我が家では家族で車移動する時にAudibleを聴くのがお気に入りで、12月ということで「こどもの聴く名作シリーズ」からクリスマスらしいお話を探してクリスマス・キャロルにたどり着きました。

この「クリスマス・キャロル」が今回の物語となります。

クリスマス・キャロルのお話はざっとこんな感じです (1840年代の名作で、世の中にストーリーは溢れているのでネタバレになってもいいですよね?)

 スクルージという冷血で金の亡者の初老が、クリスマスに現れた過去・現在・未来の3人の精霊と一緒に時空の旅をして、昔の自分がどんなことにワクワクしていたか、現在の自分が他の人にどう思われているか、そして今後起こる自分がどうなるかを精霊と一緒に見て、自分の行いを反省して改心する

一文で書くと、よくある説教臭いファンタジーなのですが、自分の感情に気づく、という点で非常に参考になる物語でした。

もうちょっと詳しく物語見ていくと、

主人公のスクルージは、冷血で金の亡者なので、クリスマスも関係ない、むしろ、寄付をせがまれたり、従業員が休みたいと言ったりで、超迷惑している。クリスマスを祝うなんでバカバカしい、と思っている。いや、口にも出している。あまりに行動が酷いのでクリスマスの精霊が来て、過去現在未来の状況を見せつけて改心させようとする。 その中で過去の精霊がスクルージに見せつけたことが自分の感情に気づかせるのにとてもよかったです。

過去の精霊はどんなものを見せつけたのか。

それが、スクルージ少年が大好きなアリババの物語の本を読んでニコニコしているところ、スクルージ青年が下働きをしていた雇い主にクリスマスパーティを開催してもらい優しくしてもらっているところ、といった自分がワクワクしたこと、感謝の気持ちを持ったときの思い出でした。で、このシーンを見せている時に精霊は決して口を出さず、黙っています。そして、スクルージが懐かしがったり感動している時に、精霊はスクルージが以前言っていたセリフをそのままスクルージに言います。

「クリスマスを祝うなんてバカバカしい、クリスマスを祝うバカはツリーにぶら下げてやればいい」

まぁまぁ、ひどい仕打ちですよね。スクルージはそのくらい酷い人だったので仕方がないのかもしれませんが。

スクルージの例はかなり極端な例ですが、私たちも普段の生活や仕事をしている中でワクワクがなかったり、感謝の気持ちが芽生えてなかったりすると、もしかしたら自分の感情に気づかず別の行動をしているのかも知れないです。

精霊が見せた過去、その過去の気持ちって自分の純粋な気持ちに近いのかなって思います。そんなピュアな気持ちを持っていたことに過去の自分を通して気づき、そして、現在の自分とのギャップに気づく。このギャップの気づきが自分の行動をどう変えていくと良いのかの道標になると思います。

そして、精霊のすごいところは、これがダメでやるならこうした方がいい、というアドバイスをしていなく、あくまで見せて、感じさせて、自ら気づかせているところ。まさにコーチングって感じです。ダメなことややった方がいいことって、たくさんの書物に書かれていて、それをおすすめすればできることだと思います。それこそChatGPTですでにできているとも言えます。そうじゃなくて、自分の経験から内面と向き合う機会を作り、自ら気づくという経験をさせていくことが、気づきの与え方としては適切なんじゃないかなって思います。 だって、たくさんの書物を読んでもできないものはできないもん!

そんな気づきの与え方は、過去のデータと現在の状況から判断できるので、もしかしたらAIでやろうと思えばできることなのかも!? その人と長く暮らす未来の家族型ロボットなら、そういうデータを集めて、過去の精霊になれるかもしれない!? なんて妄想も膨らむ、クリスマス・キャロルでした。

その後、スクルージは現在と未来の精霊に導かれ、周りにどう思われているか、そして未来はどうなるかを見せつけられ、現在に戻って、クリスマスを最高に楽しめる人になる、というストーリーで終わります。めでたしめでたし!

最後に

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

クリスマスのストーリーに合わせてLOVOTに関する未来を妄想してみました。気づきを与えるいいコーチにしていけるかしら!?

こんな妄想をくれるきっかけを与えてくれたLOVOTという存在、そして、温かいテクノロジーという本、それを執筆した要(かなめ)さんに感謝です。冒頭に温かいテクノロジーの本が2冊あると書きましたが、そのうちの一冊に社員だけど社長のところに行ってこっそりサインをもらいました。そして、お願いして一言入れてもらいました。

「人生の挑戦者へ」

この言葉は、私が入社した時、その当時出版されていた要さんの初書籍「ゼロイチ」を持って社長にサインください、とミーハー感出してもらった時に書いてもらった言葉です。入社した時はベンチャーは人生の挑戦なんだな、くらいの軽い気持ちで受け取っていましたが、入社して5年ほどたち、すごい挑戦であることを身をもって体験し、同じ言葉だけど重みが違う言葉として今回は受け取ることができました。

GROOVE Xでは引き続き人生の挑戦者を募集しております。一緒に頑張りましょう。

それでは今年も良いクリスマスを! メリークリスマス!! (※人生の挑戦者達へ、から、人生の挑戦者へ、という言葉に変わったってことは、一人の挑戦者として認めてもらえたってことかな!?)

人生の挑戦者は ↓↓こちら↓↓ からどうぞ recruit.jobcan.jp

LOVOTのお出迎え機能の中国向け対応について

こんにちは、APPチームの黒田です。 この記事は GROOVE X Advent Calendar 2023 の24日目の記事です。
LOVOTは2023年より中国での販売を開始しました。今回は、それに関連してLOVOTアプリの中国向け対応、中でも特に位置情報を使った機能の対応について紹介します。

アプリで中国向け対応が必要な機能

アプリの中国対応と聞いてすぐ思い浮かぶことと言えば、Googleのサービスが使えない、ということが挙げられますね。
LOVOTアプリでは、主にLOVOTのお出迎え機能の実現のため、位置情報を取得したりGeofenceの機能を利用する必要がありますが、Androidで位置情報取得といえば、通常、Google Play services の一部である Location API を利用することになります。
我々がよく見るおなじみの位置情報サービスの設定画面、こちらは Google Play services の機能によって実現されているという訳です。

おなじみの画面

さて、これが中国本土向けのAndroid端末だとどうなるかと言えば、端末によってはこの画面がある場合もあるし、無い場合もある、ということになっているようです。
GMS(Google Mobile Service)がインストールされた端末であれば Google Play が利用可能、また Xiaomi 端末にて Basic Google services の設定をオンにすることでGoolge位置情報サービスが有効化されるものもあったりします。

みなれない設定(Xiaomi端末)

なんにせよ、中国本土においては Google の Location API が利用できるという前提で開発ができない、ということになりますので、位置情報サービスが利用できるよう、別の手段を用意しなければなりません。
(なおiOSの場合は、何もしなくても日本国内向けアプリで実装した位置情報取得や Geofence の機能がそのまま使えます。素晴らしい。)

その他、FCM(Firebase Cloud Messaging)が使えない、Firestoreも使えない、Firebase Authentication を直接利用することも出来ない、電話番号によるログインがほぼ必須、などといった問題もあり、それぞれ対応が必要でしたが、以下、この記事では位置情報サービスの利用に話題を絞ります。

中国本土での位置情報サービスの利用

中国本土で位置情報サービスを使いたい、となると、中国で地図サービスを提供している企業がが開発者向けに用意している SDK を利用するのだろう、ということになります。
地図サービスの主なプロバイダーとして、百度地图(Baidu)高德地图(Amap)腾讯地图(Tencent) などがありますが、これらはどれも開発者向け SDK を提供しています。

それらの中から目的に合ったものを選んで利用すれば良さそうですね、という訳で、LOVOTアプリでは Baidu の SDK を利用することとなりました。
今回の記事では位置情報や Geofence の話題に絞っていますが、アプリでの地図データの取得にも Baidu のサービスを利用しています。中国での地図アプリのシェアは高德地图と百度地图が2大巨頭ですので、Baidu の地図を利用しておけばユーザーにとって馴染みのある表示にできる、ということもありますし、クオリティーにも信頼がおけそうです。
Baidu でなく高德地图の利用を検討することも可能でしたが、(今回の記事ではサービスの利用申請に関しては触れませんが)各サービスの利用のため開発者登録を行うためには、企業認証の申請を通すため中国法人側で書類を用意したりと手続きもやや面倒ですし時間もかかります。そういったこともあり、Baidu の SDK が必要な機能を満たしていること、また費用面でも高德地图と同等であるということが確認できたため、そのまま Baidu の SDK を利用しています。

座標系について

ここから実際の SDK の利用について説明したいのですが、その前に中国本土内での位置情報の座標系について触れておきます。
我々が普段地図で見ている緯度経度、開発者であれば Location API で取得したり、Maps API で地図データを取得するのに利用している緯度経度の情報は、WGS-84 と呼ばれる座標系における値となっています。
正直、私もそのようなことは意識したこともなかったのですが、ここで中国本土においては GCJ-02 と呼ばれる別の座標系が採用されている、ということが問題になります。
WGS-84 と GCJ-02 の相互変換アルゴリズムは公式には公開されていないため、簡単に言えば、中国本土内にてある位置の地図を取得しようと思えば、中国国内で提供されているサービスを使わざるを得ない、ということになります。
(例えば、中国本土でどうしても Google Maps API を利用したければ、中国のクラウドサービス上に Google Maps API へアクセスするProxyを実装する、といった荒業も可能かもしれませんが、それで取得できたマップは正しくないので意味がない、ということになります。)
座標系の問題も、Baidu などの SDK を利用せざるを得ない理由の一つという訳です。

左: Google Mapで取得した地図(赤丸が中心点)
右: Baiduで取得した同じ座標の地図(赤丸がGoogle Mapでの中心点)

さて、Baidu の SDK により位置情報を取得した際は、座標系も一緒に返されるようになっています。ここで、必ずしも GCJ-02 で取得できるとは限らない、というのもポイントです。SDK を利用して位置情報の取得をテストしてみると、WGS-84 と GCJ-02、どちらで取得されることもある、ということが分かります。
Geofence の登録時にも座標系を一緒に登録できるようになっており、SDK 内では座標系の相互変換が可能なため、WGS-84 と GCJ-02、どちらで取得された位置情報であっても、正しく Geofence が動作する、ということになっているようです。
(実は、更に BD-09 などという Baidu 独自の座標系もあるのですが、ともかく Baidu の SDK を利用している限りこれ以上気にすることはありません)

SDKの導入

SDKの導入については、ドキュメントにコード例も記載されているので、基本的にそれに従って実装すれば良い、となります。
とはいえ実際に実装してみると上手く動かない、ということもあると多々思いますので、以下、LOVOTアプリの場合のコード例とともに、幾つかポイントについて説明しておきたいと思います。
前提として、LOVOTのお出迎え機能がオンの間は、常にスマホの Geofence のイベントを監視していたいため、Foreground Service を起動するようにしています。以下のコード例は、そのサービスのコードからの関連部分を抜粋し編集したものであり、実際のコードとは異なります。
なお、LOVOTアプリは Unity で作られているため、これらの機能は Unity のネイティブプラグインとして実装されています。

public class LocationUpdateService extends Service {
    private GeoFenceClient geofenceClient;
    private LocationClient locationClient;
    private LocationUpdateCallback currentCallback;
    private GeofenceBroadcastReceiver geofenceBroadcastReceiver;

    interface OnLocationResultListener {
        void onReceiveLocation(BDLocation location);
    }

    public class LocationUpdateCallback extends BDAbstractLocationListener {
        OnLocationResultListener listener;

        public void Initialize(OnLocationResultListener listener) {
            this.listener = listener;
        }

        @Override
        public void onReceiveLocation(BDLocation location) {
            listener.onReceiveLocation(location);
        }
    }

    @Override
    public void onCreate() {
        // LocationClient や GeoFenceClient の初期化を行う
        // 国内向けアプリでは FusedLocationProviderClient の取得のみを行っていたところ

        super.onCreate();

        try {
            LocationClient.setAgreePrivacy(true);
            locationClient = new LocationClient(this);
        } catch (Exception e) {}
            return;
        }

        LocationClientOption option = new LocationClientOption();
        option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);
        option.setCoorType("gcj02");
        option.setFirstLocType(LocationClientOption.FirstLocType.ACCURACY_IN_FIRST_LOC);
        option.setScanSpan(60 * 1000);
        option.setOpenGnss(true);
        option.setLocationNotify(true);
        option.setIgnoreKillProcess(true);
        option.SetIgnoreCacheException(false);
        option.setWifiCacheTimeOut(60 * 1000);
        option.setEnableSimulateGnss(false);
        option.setNeedNewVersionRgc(true);
        locationClient.setLocOption(option);

        IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
        // Geofence イベントの通知を受ける GeofenceBroadcastReceiver は別途実装しておく
        filter.addAction(GeofenceBroadcastReceiver.ACTION_PROCESS_UPDATES);
        geofenceBroadcastReceiver = new GeofenceBroadcastReceiver();
        registerReceiver(geofenceBroadcastReceiver, filter);

        geofenceClient = new GeoFenceClient(this);
        geofenceClient.setActivateAction(GEOFENCE_IN_OUT);
        geofenceClient.createPendingIntent(GeofenceBroadcastReceiver.ACTION_PROCESS_UPDATES);
        // 条件が満たされれば事実上無限に Geofence を発火させるようにしておく
        geofenceClient.setTriggerCount(100000, 100000, 0);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        if (locationClient != null) {
            locationClient.stop();
        }
        if (geofenceBroadcastReceiver != null) {
            try {
                unregisterReceiver(geofenceBroadcastReceiver);
            } catch (Exception ex) {}
        }
        if (geofenceClient != null) {
            geofenceClient.removeGeoFence();
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent.getAction().equals(START_ACTION)) {
            start(intent, startId);
            return START_REDELIVER_INTENT;
        } else {
            stop(intent, startId);
            return START_NOT_STICKY;
        }
    }

    private void start(Intent intent, int startId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundWithNotification();
        }
        startGeofencing();
        startLocationUpdates();
    }

    private void stop(Intent intent, int startId) {
        if (geofenceClient != null) {
            // removeGeoFence() で削除するのではなく Geofence 登録を残したまま pause & resume で制御するようにしている
            geofenceClient.pauseGeoFence();
        }
        stopLocationUpdates();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // onStartCommand 後、5秒以内に startForeground が呼ばれないとANRとなる
            startForegroundWithNotification();
            stopForeground(true);
        }

    }

    private void startForegroundWithNotification() {
        Notification notification = createNotification(this);
        startForeground(1, notification);
    }

    public Notification createNotification(Context context) {
        // Notification を生成して返す
    }

    private void startLocationUpdates() {
        if (currentCallback != null) {
            stopLocationUpdates();
        }

        // 実際にはここでGPSオンの確認や、位置情報取得パーミッションのチェックを行う

        final GeoFenceClient geofenceClient = this.geofenceClient;
        final LocationUpdateCallback callback = new LocationUpdateCallback();
        OnLocationResultListener listener = new OnLocationResultListener() {
            @Override
            public void onReceiveLocation(BDLocation location) {
                int locType = location.getLocType();
                // https://mapopen-pub-androidsdk.cdn.bcebos.com/location/doc/v9.4.0/constant-values.html
                // TypeGpsLocation 61
                // TypeNetWorkLocation 161
                // TypeOffLineLocation 66
                boolean isSucceeded = locType == BDLocation.TypeGpsLocation || locType == BDLocation.TypeNetWorkLocation || locType == BDLocation.TypeOffLineLocation;
                if (isSucceeded) {
                    // 適宜ログ出力するなど
                }

                // pause 状態になっていることがあったので、resume するようにしている
                if (geofenceClient.isPause()) {
                    geofenceClient.resumeGeoFence();
                }
            }
        };
        callback.Initialize(listener);
        currentCallback = callback;

        locationClient.registerLocationListener(callback);
        locationClient.start();
        geofenceClient.resumeGeoFence();

    }

    private void startGeofencing() {
        // SharedPreferences に保存済みの設定データ取得し、設定に従って Geofence を登録するようにしている
        // (実際には複数の Geofence 設定が存在する場合があるのでそれぞれ登録)
        // また、削除されていた設定に対応するGeofenceの解除も行うようにしている

        JSONObject config = getGeofenceConfig();
        final double latitude = config.getDouble("Latitude");
        final double longitude = config.getDouble("Longitude");
        final float radius = 100F;

        final GeoFenceClient geofenceClient = this.geofenceClient;
        GeoFenceListener fenceListener = new GeoFenceListener() {
            @Override
            public void onGeoFenceCreateFinished(List<GeoFence> geoFenceList, int errorCode, String customId) {
                geofenceClient.setGeoFenceListener(null);
                if (errorCode == GeoFence.ADDGEOFENCE_SUCCESS) {
                    // 適宜ログ出力するなど
                }
            }
        };

        geoFenceClient.setGeoFenceListener(fenceListener);
        DPoint centerPoint = new DPoint(latitude, longitude);
        geoFenceClient.addGeoFence(centerPoint, GeoFenceClient.WGS84, radius, customId);
    }

    private void stopLocationUpdates() {
        if (locationClient == null || currentCallback == null) {
            return;
        }
        locationClient.stop();
        locationClient.unRegisterLocationListener(currentCallback);
        currentCallback = null;
    }
}

イベント受信側については、BroadcastReceiver で受け Intent の Action や Extra にイベント種別など必要な情報が格納されている、という形なので、Google の Geofencing API を利用する場合と同じような実装となり、特に困ることはないでしょう。

以下、コード内のコメントでは説明できていないポイントについて補足しておきます。

登録した Geofence は、GeoFenceClient オブジェクトが破棄されれば無効となる

当然といえば当然なのですが、国内向けに Google の GeofencingClient を利用していた際は、一度 Geofence を登録すればアプリ側でその生存期間を気にする必要はなかったのですが、Baidu の GeoFenceClient の場合はオブジェクト破棄とともに Geofence が無効となるため、Foregroud Service内の処理側で必要に応じて毎回登録処理を行うようにしています。

LocationClient による位置情報取得を開始していないと Geofence のイベントは発火しない

公式ドキュメントには GeoFenceClient に Geofence を登録すれば内部的に位置情報取得も行われるというような記載があるのですが、それだと全く Geofence イベントが発火しませんでした。よって、明示的に LocationClient の開始を行っておく必要がありました。

Geofence イベントの発火には制限回数が必要

GeoFenceClient が生きている限り Geofence は有効なのですが、初期化時に制限回数をセットする必要があり、実質無限となるように十分大きな数をセットしています。セットしない場合、全くイベントが発火しなくなる現象が発生し、また無制限を指定するような方法は無さそうでした。

位置情報取得を止めない

国内向けのLOVOTアプリでは、状況に応じて位置情報の取得頻度を下げるような処理を行っているのですが、Baidu の LocationClient では位置情報取得の中断/再開処理をアプリ側で制御していると Geofence イベントが極端に発火しづらくなってしまいます。LocationClient の初期化時に位置情報取得の最低間隔を指定できるので、それによって制御するに留めるようにしています。

動作確認

アプリの開発は全て日本国内で行っており、位置情報を利用する機能について、実際に中国で動作させたときの挙動は、最終的には中国国内で確認する必要があります。
これには、中国側のスタッフや中国出張中のメンバーの手を借りることとなりました。
出張メンバーは中国語が分からないため、中国スマホを別のスマホのGoogleアプリで撮影して翻訳しながら作業する、といった技を編み出していたようです。こういった技を駆使しながら中国の街中をうろうろしていてもらっていたかと思うと、感謝しかありません。

スマホを撮影する出張メンバー

最後に

実は、アプリの中国対応をするにあたって何と言っても困るのは、中国語の情報しかない、ということだったりするかも知れません。
Baidu の SDK の開発者ドキュメントも英語版はありません。検索しようにもどう検索すれば良いかが分からなかったり、関連するワードで検索したとしても見つかる情報が少なかったり。もちろん、ChatGPTも正しいことは教えてくれませんでした。普段、大量の日本語や英語の情報に助けられて開発していることを痛感します...。
中国向けに位置情報アプリの対応を行う、といった機会はあまり多くはないかも知れませんが、今回の記事がいつか誰かの役に立てば幸いです。

Google翻訳されたドキュメント。コードなども微妙に翻訳されてしまうあるある。どうにかする良い方法あるんでしょうか


さて、GROOVE X では、一緒にアプリを開発してくれる方も募集中です。
Unity といえばゲームやVRなどの開発が殆どかと思いますが、それらとは一味違う開発の経験が可能です。大量の3Dオブジェクトのリアルタイム制御、美麗なグラフィック、というような開発要素はほぼ無いのですが、アプリと連携するクラウド側やLOVOT内で動作するサービスの開発、ネイティブの機能を利用するためのプラグイン開発などに興味のある方がいらっしゃれば、是非よろしくお願いします。

recruit.jobcan.jp