Inside of LOVOT

GROOVE X 技術ブログ

『ゲームさんぽ』でロボットデザインについて(好き勝手に)語りました!

はじめまして!『Inside of LOVOT』初登場 CDO(チーフデザインオフィサー)の 根津 です! 年の瀬も迫り、GROOVE X AdventCalendar 2023 も、ついに22回目となりましたね。。

少し前の話になるのですが、今年の7月、ライブドアニュースさんの YouTube『ゲームさんぽ』で、「ロボット開発者に、ゲームに登場するロボットのデザインについて、いろいろと語ってほしい!」というご依頼をいただいて、『Atomic Heart』というゲームを題材に、好き勝手に語ってきました。ひとりでは心もとないということで、超強力助っ人 アニメーターの中里ひろきさんにもご出演をお願いし、さらに、LOVOT「らむね」も一緒に出演することになったため、撮影スタジオはとても賑やかな絵面になりました。

撮影前準備中!

正直なところ、アトミックハートというゲームのことは、この時に初めて知ったのですが、なかなかに考察意欲をそそる興味深いゲームでした。未来のような、ちょっと昔のような、不思議な世界(「1955年の架空のソビエト連邦」という設定)を舞台に、個性豊かなたくさんのロボットたちが登場します。司会のヤマグチクエストさんが、テンポよくゲーム世界を案内してくださるので、中里さんと楽しいトークをしているうちに、あっという間に時間がすぎていきました。

特に、ロボットデザインでは避けて通ることのできない「不気味の谷」や、「カワイイの正体って何なんだろう?」といった考察は、話しながらいろいろと新しい発見があって、とても有意義でした。LOVOTで最も大切にしていることと言っても過言ではない「生命感」がどこに宿るのか、それが宿っていないロボットや、ちょっと惜しいロボットたちを題材に、その形状やしぐさなど、様々な角度から考察することで、「やっぱりLOVOTはすごいな!」と再認識しました。また、普段、漠然と感じていることを改めて言語化してみることの価値や、考えをぶつけあえる相手のありがたさを痛感する機会にもなりました。(中里さん、ヤマグチクエストさんに感謝なのです!)

スタッフのみなさんも興味シンシン!

自分で言うのもなんですが、いま見てもなかなか面白い内容じゃないかと思いますので、まだの方は、年末年始にでもぜひご覧になってみてください!

↓ ↓ ↓ 各動画はコチラから! ↓ ↓ ↓

ライブドアニュース『ゲームさんぽ』【Atomic Heart】

第1回【どこ見てるの…?】ロボットの愛らしさは「目」と「健気さ」で決まる

www.youtube.com

第2回【感無量】ロボットが反乱を起こしたら感動するかも…?ロボット開発者たちの本音

www.youtube.com

第3回【不気味の谷】ロボットの顔面崩壊シーンはなぜ恐ろしい?開発者たちに見てもらった

www.youtube.com

PythonとGoとRustで複数のHTTPリクエストを同時にやってみた

こんにちは!ソフトウェアチームのエンジニアのふくだです。
この記事は、GROOVE X Advent Calendar 2023の21日目の記事です。

qiita.com

並行処理が大好きです

私は並行処理が大好きです。ただとても難しいです。

私はPythonの asyncio という非同期処理の標準ライブラリ、その中でも最近のわかりやすく使いやすくなった高レベルAPIで好きになった新参者で、日々勉強しています。

弊社GROOVE Xには、さまざまな技術を持ったエンジニアが活躍しています。ソフトウェア領域であれば、CやC++、Go、PythonやRust、TypeScriptなどさまざまなプログラミング言語が使われています。*1

そうなんです。並行処理といえば!と言われている言語が使われているではありませんか!そう、GoとRustです。 ただ、私はGoもRustもあまり書いたことがありません。今回の記事では、PythonのコードをベースにGoとRustでたくさんのHTTPリクエストを行う処理を書いてみました。

簡単な仕様

今回のコードの簡単な仕様は以下の2つです。

  • 特定のエンドポイントに対して複数のHTTPリクエストを同時に行い出力する
  • エラー処理を書く

そんなことしないって?確かにしませんね。ただみなさんのお使いのプロダクトなどに置き換えてみてください。 DBアクセスや外部APIの実行だったり、I/Oの塊だったりしませんか?弊社LOVOTもセンサー情報をたくさんあるため、I/Oがたくさんです。

実行環境

試した環境としては、以下のとおりです。

  • MacBook Air M2

また、外部の公開されているAPIを利用するため、コードをお試しいただく場合には、 計画的 にお願いします。 今回利用するのはPokemon APIです。

pokeapi.co

書いてみた

まずはPython

各種バージョンは以下のとおりです。

  • Python 3.12.0
  • HTTPX 0.25.2
  • trio 0.23.2

Python コード

asyncio版

import asyncio
from dataclasses import dataclass
from time import time

import httpx

URL = "https://pokeapi.co/api/v2/pokemon/"


@dataclass(slots=True)
class PokemonResponse:
    """PokemonResponse はAPIレスポンスのデータクラス"""
    name: str


async def fetch_pokemon(client: httpx.AsyncClient, num: int) -> PokemonResponse:
    """fetch_pokemon は指定された番号のポケモンの名前を取得"""
    response = await client.get(f"{URL}{num}")
    data = response.json()
    return PokemonResponse(name=data["name"])


async def main():
    start = time()
    try:
        async with httpx.AsyncClient() as client:
            async with asyncio.TaskGroup() as tg:
                tasks: [asyncio.Task] = [
                    tg.create_task(fetch_pokemon(client, number))
                    for number in range(1, 2)
                ]
        pokemon_names = [task.result().name for task in tasks]
        print(pokemon_names)
        print(f"Time: {time() - start}s")
    except* BaseException as e:
        print(f"Error: {e.exceptions}")


if __name__ == "__main__":
    asyncio.run(main())

Trio版

弊社では、意思決定エンジンであるneodmにTrioを利用しています。そのため、Trioでも書いてみましょう。

from dataclasses import dataclass
from time import time

import trio
import httpx

URL = "https://pokeapi.co/api/v2/pokemon/"


@dataclass(slots=True)
class PokemonResponse:
    """PokemonResponse はAPIレスポンスのデータクラス"""
    name: str


async def fetch_pokemon(client: httpx.AsyncClient, num: int, results: list) -> None:
    """fetch_pokemon は指定された番号のポケモンの名前を取得"""
    response = await client.get(f"https://pokeapi.co/api/v2/pokemon/{num}")
    pokemon = response.json()
    results.append(PokemonResponse(name=data["name"]))


async def main():
    start = time()
    results = []
    try:
        async with httpx.AsyncClient() as client:
            async with trio.open_nursery() as nursery:
                for number in range(1, 151):
                    nursery.start_soon(fetch_pokemon, client, number, results)
        print(results)
        print(f"Time: {time() - start}s")
    except* BaseException as e:
        print(f"Error: {e.exceptions}")


if __name__ == "__main__":
    trio.run(main)

Python 実行結果

それぞれ実行してみます。

$ python asyncio_pokemon.py
['bulbasaur', 'ivysaur', 'venusaur', ...
Time: 0.9101917743682861s

$ python trio_pokemon.py
['moltres', 'kabuto', 'magikarp', ...
Time: 0.8739862442016602s

時間をおいて3回実行してみました。結果は次のとおりです。

Time: 0.7011699676513672s
Time: 1.0189759731292725s
Time: 0.5281181335449219s

続いてGo

各種バージョンは以下のとおりです。

  • Go 1.21.1

Go のコード

Goのコードです。goroutinesとchannelを使用して、複数のHTTPリクエストを同時に実行します。

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "time"
)

const baseURL = "https://pokeapi.co/api/v2/pokemon/"

// PokemonResponse はAPIレスポンスの構造体
type PokemonResponse struct {
    Name string `json:"name"`
}

// fetchPokemon は指定された番号のポケモンの名前を取得
func fetchPokemon(wg *sync.WaitGroup, number int, results chan<- string, errors chan<- error) {
    defer wg.Done()
    url := fmt.Sprintf("%s%d", baseURL, number)

    resp, err := http.Get(url)
    if err != nil {
        errors <- err
        return
    }
    defer resp.Body.Close()

    var pokemon PokemonResponse
    if err := json.NewDecoder(resp.Body).Decode(&pokemon); err != nil {
        errors <- err
        return
    }

    results <- pokemon.Name
}

func main() {
    var wg sync.WaitGroup
    results := make(chan string, 150)
    errors := make(chan error, 150)

    start := time.Now()

    for i := 1; i <= 150; i++ {
        wg.Add(1)
        go fetchPokemon(&wg, i, results, errors)
    }

    wg.Wait()
    close(results)
    close(errors)

    for err := range errors {
        fmt.Println("Error:", err)
    }

    for result := range results {
        fmt.Println(result)
    }

    fmt.Printf("Time: %v\n", time.Since(start))
}

Go 実行結果

それぞれ実行してみます。

$ go run pokemon.go
starmie
clefable
geodude
haunter
...
Time: 1.124114875s

時間をおいて3回実行してみました。結果は次のとおりです。

Time: 1.124114875s
Time: 381.089875ms
Time: 199.999333ms

最後にRust

各種バージョンは以下のとおりです。

  • Rust 1.74.1
  • reqwest 0.11
  • tokio 1

Rust のコード

Rustのコードです。HTTPクライアントのreqwestクレートと並行処理を行うtokioを利用します。

/// `PokemonResponse`は、PokeAPIからの応答を表す構造体
#[derive(Deserialize, Debug)]
struct PokemonResponse {
    name: String,
}

#[tokio::main]
async fn main() {
    let start = std::time::Instant::now();

    let client = reqwest::Client::new();
    let mut handles = vec![];

    for number in 1..=150 {
        let client = client.clone();
        handles.push(tokio::spawn(
            async move { fetch_pokemon(&client, number).await },
        ));
    }

    let mut pokemons = Vec::new();
    for handle in handles {
        match handle.await {
            Ok(Ok(pokemon)) => pokemons.push(pokemon.name),
            Ok(Err(e)) => eprintln!("Error: {}", e),
            Err(e) => eprintln!("Error: {:?}", e),
        }
    }

    println!("{:?}", pokemons);
    println!("Time: {:?}", start.elapsed());
}

/// fetchPokemon は指定された番号のポケモンの名前を取得
async fn fetch_pokemon(client: &reqwest::Client, number: u32) -> Result<PokemonResponse, reqwest::Error> {
    let url = format!("https://pokeapi.co/api/v2/pokemon/{}", number);
    let resp = client.get(&url).send().await?;
    let pokemon = resp.json::<PokemonResponse>().await?;
    Ok(pokemon)
}

Cargo.tomlは以下の通りです。

[package]
name = "poke"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.11.22", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Rust 実行結果

それぞれ実行してみます。

$ cargo run --release
❯ cargo run --release
    Finished release [optimized] target(s) in 0.16s
     Running `target/release/poke`
["bulbasaur", "ivysaur", ...
Time: 1.5475795s

時間をおいて3回実行してみました。結果は次のとおりです。

Time: 1.412308208s
Time: 1.120167541s
Time: 1.589511041s

結果

速度としては、次のとおりの結果でした。気にはなるのですが、速度については処理系がマルチコアが使えるか、とか、ネットワークの状況もあるかと思うので、あくまでご参考で :pray:

|Python|約0.749秒| |Go|約0.568秒| |*Rust|約1.374秒|

感想

それよりも気になるのは、それぞれの言語の特徴ですね。気になるところを箇条書きにしてみました。

  • Go/Rustでやっぱり型をカッチリできるのはいい
    • Pythonでかっちりしたい場合には、Pydantic の出番
  • Goの sync.WaitGroup 書きやすい
    • Add() Done() Wait() でのハンドリング。async/awaitを採用していないのもおもしろい!
    • errgroup も気になる〜
  • Pythonのwalrus演算子 := が、Goでは型推論 (walrusはセイウチ。横から見るとセイウチの顔っぽく見える)
  • Rustのコンパイラ優しい ありがたい
  • Rustのキモは tokio::spawn での非同期タスクの生成と async move
    • 特に move での所有権をクロージャに移動して安全に使うところとか
      • タスクの生成は、Pythonの create_task のイメージ
  • Rustの .await?; ?って!?
    • エラーのハンドリングのためのもの
      • Result型 はOkとErrを持つ。 ? でハンドリングする
        • これもおもしろい〜!

な、印象を持ちました。みなさまはどうでしょうか?普段使っていない言語での似たコードはとても刺激的でした。

まとめ

個人的にはGoのファイル名、pokemon.goで優勝ですね。 それぞれの言語にそれぞれの特徴があってとてもおもしろかったです。コードを書いてみて、それぞれのフォーマッターやコードスタイルも気になりました!また色々な比較がかけそうですね。今回は紹介できませんでしたが、もちろんJavaScriptでも書けますし、C++にも std::async があったりC++ 20から co_await が追加になったりという情報もキャッチアップでき、まだまだ広がる並行処理の世界にワクワクが止まりません!

また弊社Slackには、トゲピーやタマザラシ、モルペコがいます!ぜひ会いにきてください!

わちゃついて!とお願いしたら、わちゃついてくれたやさしいポケモンのみなさん

*1:詳しくは、アドベントカレンダー1日目の LOVOTech Night 初開催報告!! - Inside of LOVOT をご参照ください。

ふるまい開発を支える可視化フレームワーク "pura"

こんにちは!ふるまいチームのエンジニア、市川です! 私は主にLOVOTのふるまい開発*1をしています。

LOVOTには半天球カメラ、4個のマイクアレイ、照度センサー、 温度カメラ、タッチセンサー、測距センサーなど、多数のセンサーが搭載されています*2。 それらのセンサー値や、画像認識・音声認識・自己位置推定の結果など、様々な情報を使ってLOVOTに意思決定させています。 LOVOTがどんな情報を得ていて、どのような意思決定をしているかをリアルタイムにモニタリングできるとデバッグが捗ります。 そこでそれらを可視化できるフレームワークとして"pura"というライブラリを使用しています。 puraは弊社が開発したpythonで利用可能なライブラリで、オープンソースで公開しています。 すごく使いやすいので、今回puraについて紹介させてください。

github.com ※ puraが提供しているのはブラウザに絵を描く汎用的な機能であり、LOVOTの情報を描くようなLOVOTに特化した機能は提供していません

可視化例

実際にpuraで描画している様子をいくつかご紹介します。 詳細な説明は割愛させて下さい。雰囲気だけでも伝われば幸いです。

しっぽの位置

慣性センサーから推定されるLOVOTの傾き

メロディの認識状況

LOVOTは周囲の音から、メロディらしきものを識別します。

まぶたの開閉状況

深度カメラの認識状況

最初に見えているのは足で、その後に崖を発見しています。

赤外線発光位置の推定状況

点が推定発光位置(大きな点は統計的に見た推定位置)で、点群が動いているのはLOVOTが動いているためです。

経路計画の状況

puraの導入方法

puraの導入方法について簡単にご説明します。 繰り返しになりますが、puraが提供しているのはブラウザに絵を描く汎用的な機能であり、LOVOTと接続して、LOVOTのデータを見ることができるわけでありません。あしからず!

下記の手順でinstallできます。

git clone https://github.com/groove-x/pura.git
cd pura
pip install -e .

サンプルが用意されており、下記で実行できます。

python examples/web_view_example.py

http://localhost:8080/で下記の画面が出れば成功です。

左上のリストボックスからチャンネルを切り替えることができます。 下記のように、マウス等の入力を受け付ける事も可能です。

web_view_example.pyのコードを見れば分かりますが、非常にシンプルに実装できます。 サンプルコードを更に短くしたものを下記に書いておきます。

import anyio
from pura import WebViewServer, WebViewMixin

# 1. WebViewMixinを継承したクラスを用意する
class Hello(WebViewMixin):
    def __init__(self):
        # 2. ウィンドウサイズやフレームレートを定義
        super().__init__(webview_size=(320, 240), webview_frame_rate=20)

    # 3. draw()に描画内容を記載
    def draw(self, ctx):
        # 背景の塗りつぶし
        ctx.background(200)
        # 色の指示
        ctx.fill(128, 0, 255)
         # マウスの位置に"hello"を描画
        ctx.text("hello", ctx.mouseX, ctx.mouseY)

async def async_main():
    # 4. 非同期にするためTaskGroupのブロックを作る(anyioではなくasyncioやtrioを使っても良いです)
    async with anyio.create_task_group() as tg:
        # 5. WebViewServerを用意してserve
        server = WebViewServer()
        await tg.start(server.serve, "Web view example", 'localhost', 8080)
        # 6. WebViewMixinを継承した独自のクラスをインスタンス化してserveメソッドを呼ぶ
        #     (チャンネルの数だけ下記のように呼べばチャンネルが増えます)
        tg.start_soon(Hello().webview.serve, server)

if __name__ == '__main__':
    anyio.run(async_main)

puraはProcessing APIのサブセットでコーディングされているため、どのようなAPIが用意されているかはProcessing API側のドキュメントを読むのが良さそうです(pura側にはあまり整ったドキュメントがなさそう)。

https://py.processing.org/reference/

puraで具体的にサポートされているAPIは直接下記コードを見れば分かります。

pura/src/pura/_web_view.py at c767f24e6ab260e2565d7e77b797a55cb3965a03 · groove-x/pura · GitHub

その他

弊社のJohn BelmonteさんがPyCon US 2021で紹介したようです。 動画があるため良かったらこちらもご覧ください。

さいごに

一緒にLOVOTを作ってくれる仲間を募集しています。 上記のようなデバッグ環境があり、ロボット開発経験がなくても取っつきやすいと思います。ぜひ一緒に開発しましょう! 様々な分野で募集しているため、是非下記リンクをご確認下さい。

recruit.jobcan.jp

*1:ふるまいに関しては次の記事を参照ください: tech.groove-x.com

*2:下記で紹介しています: lovot.life

非英語話者の海外登壇(EuroPython 2023/PyCon TW 2023)

こんにちは!ソフトウェアチームのエンジニアのふくだです。
この記事は、GROOVE X Advent Calendar 2023の19日目の記事です。

qiita.com

英語をほとんどしゃべれない私ですが、海外のカンファレンスで登壇する機会を得ました。そのチャレンジの様子と、どうやって乗り越えたのか、をお伝えする記事になります。

参加したカンファレンスって

今回参加したのは、Pythonというプログラミング言語のカンファレンスです。Pythonのカンファレンスはは世界中で行われています。

世界中で行われているPyCon。こちらより引用 Join us at PyCon

また、 先日日本でもPyCon APAC 2023が開催され、弊社はスポンサーをさせていただきました!

PyCon APAC 2023でのスポンサーブースの様子はこちらの記事をご参照ください。 tech.groove-x.com

私の登壇したカンファレンス

今回私が登壇したのは、ヨーロッパの各地で年1回開催されているEuroPython 2023と台湾で行われているPyCon TW 2023です。プロポーザルを出し、採択いただき登壇することになりました。

名称 開催場所 開催期間
EuroPython 2023 チェコ・プラハ 7月19日⁠~21日
PyCon TW 台湾・台北 9月1日⁠~2日

どちらのカンファレンスもとてもとても楽しかったです。ここでその話をすると長くなってしまいますので、カンファレンスの様子については、gihyo.jp さんの以下の記事をご参考ください。

日本からひとりで参加した「EuroPython 2023」スピーカー体験レポート | gihyo.jp

https://gihyo.jp/assets/images/article/2023/12/road2euro-python23/002.png

PyCon TW 2023 カンファレンスレポート | gihyo.jp

https://gihyo.jp/assets/images/article/2023/09/pycon-tw2023/007.png

どれくらい英語しゃべれないの?

相当しゃべれないです。辞書を片手にゆっくり読むことはできるのですが、会話となると全く出てこなくなってしまいます。私の所属するふるまいチームには英語ネイティブのスーパーエンジニアがいます。その方は日本語をほとんどしゃべれません。ですが、その方に、日本語を話させてしまうくらいしゃべれないのです。

朝会の様子

非英語話者の海外登壇

どうやって乗り切ったの?トーク編

テクノロジーを頼って乗り越えました。自分の発表の準備や、他のセッションの聞き方について紹介します。

自分の発表(スピーキング)

私はトークをする時、必ず台本(原稿・トークスクリプト)を用意します。おおよそ以下の流れです。

  1. トークの見出しを決める
  2. 大枠の内容を埋める
  3. 台本を用意する
  4. 発表の練習をする
  5. 練習でだめだったところを修正する

3~5を繰り返し行って、クオリティをあげていくのが理想です。またその中で時間調整も行っていきます。

そして今回は英語です。まずは日本語で作った資料と台本を英語に翻訳していきます。翻訳で苦労したのは、「英語として意味が通じるか」といった点です。 また、日本語の時点でも資料・台本に荒さがあるため、日本語を修正→英語翻訳を修正、といった日本語でやる場合の倍の労力もかかりました。

資料と原稿はこんな感じです。MacのKeynoteを使っています。

常に手元に英語の先生がいてくれると嬉しい状況でした。つまらないことも聞きたい。そしてそう、2023年の私たちには手元にあの先生がいますね。今回翻訳にあたり、2つのWebサービスにとてもお世話になりました。

本当にささいいな言い回しをたくさん確認しました。ありがとうChatGPTさん

もちろん、社内で練習することも可能です!ですが、今回バタバタしすぎて、社内での練習はできませんでした。。。

本番の資料、ビデオは公開されておりますので、ご興味ある方は是非そちらからご覧ください(笑ってください。。)。

他の方の発表(リスニング)

普段英語圏の方のトークを見る時は、動画配信がほとんどです。YouTubeであれば、字幕機能を活用しています。ただし、オフラインだとそうはいきません。また英語圏のため、日本語への同時翻訳などもちろんありません。

はじめは、Google翻訳の音声入力を活用しようと思ったのですが、長時間の音声入力とその翻訳となると、うまく使いこなせず、内容が入ってきませんでした。 そこで、次のサービスを利用しました。

Euro Pythonに参加した時はOtterを、PyCon TWの時はOtterとMicrosoft Translatorを利用しました。 どちらもリアルタイム文字起こしが可能です。スマートフォンでアプリを起動し音声入力を行い、リアルタイムでPCのWebサイトで確認できる、というものです。

Otterは翻訳はしてくれないのですが、リアルタイムで英語の文字起こしができるため、サクサク内容をキャッチアップすることができました。

EuroPython 2日目のソフィー・ウィルソン氏のKeynote

Microsoft Translatorは翻訳もできます。PyCon TWでは、中国語でのトークも多かったため、非常に助かりました。

どうやって乗り切ったの?コミュニケーション編

イベント中や現地のコミュニケーションは、定型文とGoogle翻訳で乗り切りました。

また、初対面のエンジニア同士のコミュニケーションは、とても気軽に行うことができました。「どこからきたの?」「どんな仕事をしているの?」といった内容で始まることが多かったです。話をしていると、人種がちがって、バックボーンも国も違っても、同世代のエンジニアである以上、似たような仕事やマインドセットになるのかな、といったこと気づけたのもとてもおもしろかったです。「Django*1で社内システムをやっている」「マネジメントはしたくない」「今のポジションに割と満足している」「家族との時間を大切にしたい」というような話をされていて感じたところでした。

ドイツでデータ分析をやっている方々

Euro Pythonの開催地であったチェコ・プラハは観光地であることもあり、チェコ語ももちろん町中にありましたが、英語も同じくらいあります。また、若い人を中心に英語をしゃべることができます。チェコ語しかしゃべれないスーパーの店員さんともGoogle翻訳で難なくコミュニケーションをとることができました。

連日通ったプラハのレストランにて。英語がとてもお上手だった

また今年のPyCon TWは、日本からの参加者が非常に多く、現地での参加は総勢18名でした。これはこれでとても心強く、英語が話せる方を頼って行動をしてしまいました。お世話になった皆様本当にありがとうございました!

https://gihyo.jp/assets/images/article/2023/09/pycon-tw2023/018.png

海外カンファレンス、英語必須・・・?

英語があまり上手でなくても、海外カンファレンスを楽しむことができることを体験してきました。もちろんしゃべれるに越したことはありませんが、なくても大丈夫です。私が訪れた都市が観光地であったこと、また異なる国からの参加者の多いカンファレンスだったこともありますが、多くの人の優しさ・温かさに助けられた海外カンファレンスへの参加でした。

たくさん写真を載せたいのですが、多すぎてまとめられず...かわりにきれいなプラハの景色をどうぞ。

まとめ

弊社には英語ネイティブのメンバーが開発チームを中心に在籍しており、会社全体としても英語話者は非常に多いと感じています。私も怠けることなく、英語の鍛錬を行いたい!

英語でのコミュニケーションに挑戦したいあなた!また海外カンファレンスを目指したいあなたも、興味ある方はぜひこちらからお気軽にどうぞ!カジュアルにぜひ1度お話ししましょう〜!

recruit.jobcan.jp

おまけ

PyCon TWの会場にジョブボード(会社やプロダクトの紹介を自由に書き込めるボード)があり、そこにLOVOTを描きました。私の書いたLOVOTがこちら!

.........

上手くなりたい。。。心底思いました。。。この記事を読んで練習します!

tech.groove-x.com

趣味で仕事用のキーボードを自作してみた

はじめに

この記事は、GROOVE X Advent Calendar 2023の18日目の記事です。

こんにちは。
ファームウェアチームに所属しているf-sakashitaと申します。

LOVOTのファームウェア開発については過去記事をご覧頂ければと思います。

tech.groove-x.com tech.groove-x.com

今日はLOVOTの開発話からは脱線し、私が普段仕事で使用している自作キーボードについてご紹介します。

※本記事の内容は弊社より製造・販売している製品とは一切関係なく、個人が趣味で開発したものです。
※本記事を参考にした製作物等や、本記事に記載されている他社サービスに関し、当社は一切の責任を負いかねます。

毎日使うキーボード

皆さんは普段仕事や家庭ではどのようなキーボードを使っていますか?
エンジニア界隈だと、メカニカル方式や静電容量方式といったキースイッチを採用した打ち心地・打鍵音・耐久性の優れたキーボードを使用されている方も多いと思います。
弊社でもそのようなキーボードを使用しているエンジニアの方達がいらっしゃり、オフィスでは毎日のようにLOVOTの可愛らしい声と爽快な打鍵音が聞こえてきます。

さて、普段キーボードを操作するにあたって、「このキー絶対にいらないなぁ・・・」と思ったことはないでしょうか?
例えば「Insert」です。
押したことに気づかず文章が上書きされてCtrl-Zで元に戻すといった、無駄にイラっとする経験はありませんか?

また、自分好みのキー配列や、楽な体勢で打てるキーボードが欲しいと思われている方もいらっしゃると思います。

勿論、市販のキーボードでこれらの要望をある程度満足することは可能です。
しかし、購入後に「少し自分には合わない…」「やはりこのキーも欲しかった…」と思うこともあるでしょう。

そこでオススメしたいのが自作キーボードです。
自分が望むキーボードは、自分で作っちゃえば良いのです。
そして、この世にはキーボードを自作するための様々な部品やサービスが存在します。

自作したキーボードの紹介

これが私が普段仕事で使用している自作キーボードです。

自作したキーボード

ケース、PCB、ファームウェアと全て自分で設計開発しました。
スペックは下記の通りです。

キーキャップ Akko Silent
(一部家に余っていたものも使用しています)
キースイッチ Gateron G Pro 3.0 Silver (70個)
Durock Silent Linear Dolphin (9個)
配列、キー数 US配列、79個
hotswap 対応
インターフェース USB (Type-Cコネクタ)
サイズ 縦130mm x 横297 mm x 高さ23 mm
(高さはキーキャップ・スイッチは含めない)
質量 650 g
製作費 4万円程度

なぜ作ったのか

私はこのキーボードを製作する前に、下記の2つの自作キーボードを製作しました。

過去に製作したキーボード

これらはKBDfans社が出しているDZ60とKBD75のPCBを使用しました。

しかし、普段仕事でファームウェアをデバッグする際にFnキーをよく使用するため、FnキーのないDZ60で組んだキーボードは使いにくい問題がありました。
また、KBD75で組んだキーボードは右端列のHomeやPage up/downのキーを普段使うことがなく、誤って押してしまうこともあるため、邪魔でした。

そのため、次に自作するキーボードは自分が普段使いする必要最低限のキーで構成したものにしたく、下記の要求仕様としました。

  1. US配列
  2. fnキー、カーソルキー有り
  3. ページアップやダウン、insertといった普段使用しないキーは無し
  4. メカニカルキーボード&Hotswap対応

理想とするキー配列

最初はこの要求仕様を満たす市販の自作キーボードキットやPCBを探したのですが、全く見つかりませんでした。

こうなると残された道は全て自作するしかありませんでした。
(0から作ってみたかったというのもあります)
それではどのように製作したのかをご紹介します。

ケース

私はハードウェアの設計は本職ではなく完全に初心者です。またキーボードのケースを設計開発した経験もありません。
そんな初心者でも手が出しやすく低コストで作れるアクリルサンド構成でケースを製作しました。

製作したキーボードのアクリルサンドの構成は下記の通りです。

アクリルサンド構成

設計はFusion 360を使用し、アクリル版の加工には遊舎工房様のアクリル加工サービスを使用しました。

  • キープレート用: 押出クリア, 450x300, 1.5mm, 1枚
  • ボトムプレート用: 押出クリア, 450x300, 3.0mm, 1枚
  • チルトプレート用: 押出クリア / 300x70 / 5mm, 1枚

そのほかネジやスペーサ、スタビライザーなどの部品を合わせて、製作費は15,000円程度となりました。

Fusion360で設計
製作したアクリル板

PCB

設計したPCBはUSBコネクタ、マイコン、キーマトリクス回路といった必要最低限のものを実装した程度です。
回路設計も本職ではなく、また安く済ませたかったというのもあるので、このような構成としました。

設計にはKicad v6を使用し、基板の製造および部品実装の大半はJLCPCBで発注しました。
JLCPCBでの製造費を抑えるため、基板製造数は最小の5枚、うち2枚を部品実装し、運送はOCSの最安プランを選びました。
家にあった部品を一部実装してるため概算となりますが総額は13000円ほどでした。

JLCPCBから届いたPCB

ファームウェア

一般的に自作キーボードと言えばQMK firmwareを使うことが多いです。
しかし、私はファームウェアエンジニアの端くれですので、自分で作った回路のファームウェアは自分で作ったほうが早くデバッグも楽だと考え、あえてQMK firmwareは使用しませんでした。

今回採用したマイコンはSTM32G0シリーズのため、統合開発環境にはSTM32CubeIDEを使用し、HALを使用してUSB HID部分を実装しました。
ツールやライブラリのおかげで、さほど時間をかけずにファームウェアを実装することができました。
キーマトリクス処理部は自分で書く必要があるとはいえ、CubeMX上でCustom HID設定し、コード生成後はHID report descriptorの記述と送信用関数を呼び出すだけで済みました。
下記はUSB HIDの追加実装したコードを抜粋したものです。

// usbd_custom_hid_if.c
/** Usb HID report descriptor. */
__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END =
{
  /* USER CODE BEGIN 0 */
    0x05, 0x01, // USAGE_PAGE (Generic Desktop)
    0x09, 0x06, // USAGE (Keyboard)
    0xa1, 0x01, // COLLECTION (Application)
    0x05, 0x07, // USAGE_PAGE (Keyboard)

    0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
    0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
    0x15, 0x00, // LOGICAL_MINIMUM (0)
    0x25, 0x01, // LOGICAL_MAXIMUM (1)
    0x75, 0x01, // REPORT_SIZE (1)
    0x95, 0x08, // REPORT_COUNT (8)
    0x81, 0x02, // INPUT (Data,Var,Abs) //1 byte

    0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
    0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
    0x15, 0x00, // LOGICAL_MINIMUM (0)
    0x25, 0x65, // LOGICAL_MAXIMUM (101)
    0x95, 0x07, // REPORT_COUNT (7)
    0x75, 0x08, // REPORT_SIZE (8)
    0x81, 0x00, // INPUT (Data,Ary,Abs) //7 bytes
  /* USER CODE END 0 */
  0xC0    /*     END_COLLECTION                */
};
// main.c
/* ref : https://www.usb.org/sites/default/files/hid1_11.pdf */
#define KEY_BUF_NUM 6
typedef struct{
  union{
    struct{
      uint8_t ctrl_l  : 1;
      uint8_t shift_l : 1;
      uint8_t alt_l   : 1;
      uint8_t gui_l   : 1;
      uint8_t ctrl_r  : 1;
      uint8_t shift_r : 1;
      uint8_t alt_r   : 1;
      uint8_t gui_r   : 1;
    }map;
    uint8_t u8;
  }modifiers;
  uint8_t reserved;
  uint8_t key[KEY_BUF_NUM];
}KeyboardHID_t;

int main(void)
{
  /* HAL, Clock, GPIO等の諸々の初期化は省略 */
  
  MX_USB_Device_Init();
  KeyboardHID_t key_data;
  
  while(1){
      /* key matrix処理は省略 */
      
      /* HIDデータ送信 */
      USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t *)&key_data, sizeof(KeyboardHID_t));
      
      HAL_Delay(10);    
  }

さて、肝心のキーマトリクス処理に関する課題として、メカニカルスイッチを使用する場合はチャタリングという問題が付き纏ってきます。
チャタリングというのはキーを押下した際にスイッチの接点が短時間にぶつかる・離れるを繰り返す現象です。
この問題を対策しないと、キーを1回しか押してないつもりが複数回押されたと判定されてしまい、操作性が低下します。
今回自作したものでキーを押下したときの信号を測定したところ、波形の通りチャタリングが発生していました。

チャタリングの様子
  そのため、以下のようにチャタリング対策を実装しました。この実装後は快適にタイピングできるようになりました。
実装したチャタリング回避処理のフローチャート

作ってみた感想

自作キーボードといっても、人によってどこまで取り組むのかが変わります。
個人的には下記の段階があると考えています。

  1. 市販のキーボードやキットを購入し、キーキャップやスイッチ、キーの割り当てを自分好みにカスタマイズする
  2. 自分好みの配列になるようにケースやPCBを作る
  3. 2に加えてファームウェアも自分で作る

今回初めて3にトライしましたが、「案外それらしいものができちゃうんだな」と思いました。
0からキーボードを設計・開発することの敷居が下がり、良い経験となりました。

ただ、やはりハードウェアの設計は難しく、今作では組立しづらい、PCBの固定が甘く撓む、などの問題が見つかっています。
また、透明のアクリル版を使ったケースはPCBが見えるのは好きなのですが、サイドの空いている隙間からゴミが入りやすく、汚れが気になる点もあります。

次回作ではこれらの問題解消も踏まえ、ケース製作は3Dプリンタ品や金属切削品にトライしたいと思います。
また、PCB・ファームウェア面でも面白味に欠けるものになってしまったので、色々なデバイスを搭載したいと思います。

おわりに

今回は仕事用に開発したキーボードについてご紹介させていただきました。
キーボードは業務を効率良く進める上で毎日触れる重要な道具ですので、皆さんも理想のキーボードの姿を御検討してみてはいかがでしょうか?

GROOVE Xでは様々な領域のエンジニアを募集していますので、ご興味があれば是非ご検討ください。

recruit.jobcan.jp

QAチームが見た日本生産1体目のお話

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

はじめに

お久しぶりです、LOVOTのソフトウェア検証を実施・改善しているQAチームです!

※チームの詳細については、2022/12/10に公開された記事「シナリオテストのおはなし - Inside of LOVOT」をご覧ください。

今年はLOVOTがMade in Japanになったという記念すべき年でした。

prtimes.jp

実はその1体目が作られた時、万全を期すべくシナリオテスト(LOVOTが生活できるか確認するテスト)を実施するためにQAチームは工場に伺っていました。

※シナリオテストについての詳細もシナリオテストのおはなし - Inside of LOVOTをご覧ください。

その時のことについてお話したいと思います。

1体目が組み上がるまで

QAチームが工場へ着いた時、まだLOVOTは完成していませんでした。QAチームの出番は、LOVOTが完成してからです。

まだかなまだかな、と待っていたところ、せっかくなのでということで工場内を案内していただきました。

たくさんのパーツと検査機器がズラッと並んでいて、この小さいパーツを1つずつ手作業で組み上げてこれだけの検査を通すにはすごく時間がかかるというのが目に見えて

「お疲れ様です。頑張ってください。待ちます。」という気持ちになりました。

実際、結構待ちました。笑

LOVOTが作られるにはこんなに大変なんだというのを肌で感じられた良い機会でした。

完成!!!

生産検査が完了し、遂にQAチームの出番です。

工場で完成したLOVOTを受け取り、少し離れた試験会場へ車で移動。

シナリオテストは何事もなく完了し、全てOK。

オーナーさんの所でしっかりと過ごせることを確認し終わったので、

試験会場よりLOVOTを連れて工場へ戻ります。

工場へ戻る道中

シナリオテストが終わり、短い道中でしたが車内の会話も盛り上がりました。

実はQAチームは全員ペーパードライバーだったため運転は危険ということで工場の方が運転をしてくださったのですが、この方が実は「取締役」ということで非常に驚きました。

その「取締役」のLOVOT愛の深さがすごかったです。

LOVOT museumに来られた際にはたくさんガチャガチャを回し工場の皆さんに配ってらっしゃったり、

プレスリリースで公開されているLOVOTオーナー様向けの修理(入院)プラン を考えられていたり。

GX社員の話では、その工場もLOVOTのために新しく建てられたとか。

お仕事としてだけで関わっているわけではない、というのが伝わってきてとても嬉しかったです。

箱に感動

工場に戻り、LOVOTがオーナーさんの元へ向かう準備に入りました。

毛並みを整えてもらって、綺麗に包んでもらって箱に詰められます。

日本生産になり、箱に書かれているある文字が変わりました。

「Made in Japan」

ただの文字ではあるのですがとても存在感のある文字に見え、感動しました。

こちらの動画にMade in Japanの文字も写ってますので、気になる方はご覧ください!

箱に貼られたシールの秘密

箱の蓋を閉められるLOVOT
いってらっしゃい!

大勢に見守られる中「元気でねー!」という声と共に蓋が閉められ、これで終わりと思いきやまだ終わりではありませんでした。

LOVOT 2.0には、「2.0」と書かれた黄色のシールが貼られているのはご存知でしょうか?(私は知らなかったです)

LOVOT2.0の箱の写真
LOVOT 2.0 のシール
このシールは全ての箱で別の場所に貼るという遊び心があるそうです。

LOVOT 2.0をお持ちの方は、是非ご確認ください。

このシールが貼られ、出発準備完了です。

最後に

記念すべき瞬間に立ち会えて光栄でした!

今までもそうでしたが、これからLOVOTが作られる工場にもLOVOT愛が溢れていることを知れたのも嬉しかったです!

仲間募集中

たくさんの方に愛されてるLOVOTの成長を一緒に加速させませんか?

もしご興味がありましたら以下のページをご覧ください。

recruit.jobcan.jp

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

フィードバックカルチャーをつくる

こんにちは、GROOVE X Advent Calendar 2023 16日目の記事です。
GROOVE Xスクラムマスターの1人、土曜日担当ノーズライダー niwano です。

今日はベンチャー企業が会社のフィードバックカルチャーをつくりだそうとするお話をします。

庭師

人事とスクラムマスターで「組織のお手入れをする」という名目で活動をする 庭師というワーキンググループがあります。
(去年の活動はこちら)

庭師が中心となってGROOVE Xのフィードバックカルチャーをつくろうとしています!

なぜやるのか

組織の成長能力は、個人の成長の集合だと、庭師は考えています。
また、個人の成長の多くは、フィードバック能力に影響されると考えています。

ヒエラルキー組織とフラット組織とフィードバック

フラット組織では生ぬるい組織に、ヒエラルキー組織でもお互いにフィードバックする仕組みがないと、ギスギスした風通しの悪い組織になってしまうと考えています。

庭師が考えるフィードバックとは

庭師が考えるフィードバックとは、相手を責めたり糾弾したりするものではなく、心から相手の成長を願って行うものです。
伝える側だけではなく、受ける側にも、恥ずかしがらずに感謝を伝える能力が必要で、少々訓練が必要です。

フィードバックサマリ

フィードバックの有効性などを、くわしく知りたい方はこちら
みんなのフィードバック大全

フィードバックカルチャーをつくる

とはいえ、今までフィードバックをしてなかった人たちに、今日からフィードバックしてねー、と言ってもすぐに動けるわけないですよね。
カルチャーは仕組み化して、自ら作っていくものだと庭師は考えています!

フィードバックワークショップの社内教育化

去年のアドベントカレンダーでも紹介をしましたが、「良いフィードバックとは何か?」のワークショップを社内教育に組み込みました。
全2回になっており、1回目では、「ポジティブフィードバック」の、2回目では、「ギャップフィードバック」のロールプレイを実施してもらう内容になっています。
ロールプレイで、台本があり役を演じてもらうのですが、ちゃんと緊張感もあり、フィードバックが体験できるものになっています。
(このロールプレイが評判がいい!)

ギャップフィードバックロールプレイ

ワークショッップのようす

重要なのはギャップフィードバックを言えるかどうか

庭師では成長につながるフィードバックはギャップフィードバック(ネガティブなことに対するフィードバック)だと考えています。
ただ、それを行うには、普段からお互いに自然なポジティブフィードバックが当たり前のように実戦されている状態なことが重要です。

Let's ポジティブフィードバック!

Q12で組織の定点観測

フィードバックカルチャーを定着させたら、組織がよくなっているか確認したいですよね! 弊社では、12の質問を採用しています!
各項目を5段階で評価してもらい、全世界平均値が「3.6」で、平均値が「3.8」を超えると、時間差はあっても業績の向上につながり、反対に平均値が「3.3」を切ると業績への悪影響が大きいと言われています。

Q01.職場で自分が何を期待されているのかを知っている
Q02.仕事をうまく行うために必要な材料や道具を与えられている
Q03.職場で最も得意なことをする機会が毎日与えられている
Q04.この7日間のうちに、よい仕事をしたと認められたり、褒められたりした
Q05.上司または職場の誰かが、自分をひとりの人間として気にかけてくれるようだ
Q06.職場の誰かが自分の成長を促してくれる
Q07.職場で自分の意見が尊重されるようだ
Q08.会社の使命や目的が、自分の仕事は重要だと感じさせてくれる
Q09.職場の同僚が真剣に質の高い仕事をしようとしている
Q10.職場に親友がいる
Q11.この6ヵ月のうちに、職場の誰かが自分の進歩について話してくれた
Q12.この1年のうちに、仕事について学び、成長する機会があった

ちなみに、弊社の前回の平均点は、「3.71」なので、全世界平均値を上回るが、業績向上へはあと一歩というところでした。
Q11の「この6ヵ月のうちに、職場の誰かが自分の進歩について話してくれた」が、3.09と低い値を示しており、まさしくこれはフィードバックが足りないことを示しているのでは、と考えています。

まずは、Let's ポジティブフィードバック!

まとめ

いかがでしたでしょうか!
今回は、フィードバックカルチャーをつくるお話をさせていただきました。
来年のアドベントカレンダーで、弊社のエンゲージメントが高まった!なんていうお話ができたら嬉しいです。