NimでPaPeRo i のアプリをつくる

 GolangRustと同じようにNimでもPaPeRo i 本体で動くアプリを作ることができました。

開発環境

 NimもRustと同じくクロスコンパイルのためには別途Cのクロスコンパイラが必要です。そのためRustの場合と同じくホストにはARMのクロスコンパイラが簡単にインストールできるDebian10(VirtualBox上で動作)を使用しました。Nimは公式サイトで紹介されている方法だとなぜかnim本体しかインストールされずパッケージ管理ツールのnimbleがインストールされなかったので、choosenimでインストールしました。choosenimはnimの複数バージョンを切替えて使える様にするツールですが、choosenimをインストールした時点でnimの最新stableがインストールされるのでそのまま使用します。

$ curl https://nim-lang.org/choosenim/init.sh -sSf | sh
$ echo "export PATH=~/.nimble/bin:\$PATH" >> ~/.bashrc
$ source ~/.bashrc
$ choosenim versions --installed
   Channel: stable
 Installed:  
          * 1.4.4 (latest)

$ nim --version
Nim Compiler Version 1.4.4 [Linux: amd64]
Compiled at 2021-02-23
Copyright (c) 2006-2020 by Andreas Rumpf

$ nimble --version
nimble v0.13.1 compiled at 2021-02-23 00:35:00
git hash: couldn't determine git hash

Cのクロスコンパイラのインストール方法はRustの時と同じです。

$ sudo apt install crossbuild-essential-armhf

パペロアプリプロジェクトの作成と設定

プロジェクトはnimbleコマンドで作成します。ここではpaperoappというプロジェクト名とします。

$ nimble init paperoapp

「Your name」「Package type」などを聞かれますので入力します。Package typeはtabキーでbinaryを選びます。カレントディレクトリ下にpaperoappディレクトリが作られその下に以下のファイルが作成されます。

paperoapp.nimble : プロジェクト設定ファイル
src/paperoapp.nim : ソース(Hello World)

WebSocketパッケージnewsをインストールします。

$ nimble install news

paperoapp.nimbleに使用する依存パッケージを追記します。

# Package

version       = "0.1.0"
author        = "Sophia Planning Inc."
description   = "PaPeRo i sample app"
license       = "MIT"
srcDir        = "src"
bin           = @["paperoapp"]


# Dependencies

requires "nim >= 1.4.4"
requires "news >= 0.5"

クロスコンパイルのためにファイルを追加する必要があります。

paperoapp.nim.cfg

arm.linux.gcc.path = "/usr/bin"
arm.linux.gcc.exe = "arm-linux-gnueabihf-gcc"
arm.linux.gcc.linkerexe = "arm-linux-gnueabihf-gcc"

以上でnimの準備は整いました。
(1) デバッグのためホストで動かすバイナリは

nimble build --threads:on -d:ssl -d:debug --debuginfo --lineDir:on --debugger:native

でビルドできます。
(2) PaPeRo i 本体で動かすバイナリは

nimble build --threads:on --cpu:arm --os:linux --passL:"--static"

でビルドできます。

Visual Studio Codeの設定

エディタ/デバッガとしてVisual Studio Codeを使用する場合、Nim拡張とNative Deubug拡張をインストールした上で以下の設定を行う必要がありました。

(1) ビルド設定 (Ctrl+Shift+Bでビルド実行)

tasks.json:

{
    "version": "2.0.0",
    "tasks": [{
        "label": "Build Debug",
        "type": "shell",
        "command": "nimble build --threads:on -d:ssl -d:debug --debuginfo --lineDir:on --debugger:native",
        "group": {
            "kind": "build",
            "isDefault": true
        }
    },
    {
        "label": "Build Arm Static",
        "type": "shell",
        "command": "nimble build --threads:on --cpu:arm --os:linux --passL:\"--static\"",
        "group": {
            "kind": "build",
            "isDefault": true
        }
    }
    ]
}

(2) デバッグ実行設定

launch.json:

{
    "configurations": [
        {
            "name": "(gdb) 起動",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/paperoapp",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "gdb の再フォーマットを有効にする",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "Build Debug"
        }
    ]
}

デバッグ実行前に毎回ビルドを走らせたくない場合は”preLaunchTask”の行を削除してください。
なおブレークポイントやステップ実行、CALL STACK表示は機能しましたが、変数についてはおそらくC言語に変換されたソースの変数が表示されている様で、Nimソースの変数表示や変更は現状できない様でした。

PaPeRo i アプリ例

 今回作成したPaPeRo i 制御用ライブラリyapapero.nimを使った簡単なアプリの例を紹介します。

yapapero.nimはsrcディレクトリに置き、paperoapp.nimから

import ./yapapero

として読み込んでいます。座布団ボタンで発話とLED点灯、キーボード入力で頭を動かす例です。

paperoapp.nim:

import asyncdispatch
import asyncfile
import json
import logging
import threadpool

#include yapapero
import ./yapapero


var logger* = newConsoleLogger(fmtStr="$datetime $levelname ")
#setLogFilter(lvlDebug)
addHandler(logger)


# アプリクラス
type PaperoApp = ref object of RootObj
  papero: yapapero.Yapapero

proc newPaperoApp(url="", simId="", simName=""): PaperoApp =
  var self = new PaperoApp
  self.papero = yapapero.newYapapero(url, simId, simName)
  return self

# キーボードから一行読む
proc getKey(): Future[string] {.async.} =
  var inputFile = openAsync("/dev/stdin", fmRead)
  let txt = await inputFile.readline()
  return txt

# 口を光らせる
proc lightMouth(self: PaperoApp) {.async.} =
  info("lightMouth.")
  let ptn = @["NNR1R1R1R1R1NN", "5", "NR2R2R2R2R2R2R2N", "5", "R3R3R3R3R3R3R3R3R3", "5",
  "NG1G1G1G1G1G1G1N", "5", "NNG2G2G2G2G2NN", "5", "NG3G3G3G3G3G3G3N", "5",
  "Y1Y1Y1Y1Y1Y1Y1Y1Y1", "5", "NY2Y2Y2Y2Y2Y2Y2N", "5", "NNY3Y3Y3Y3Y3NN", "5"]
  await self.papero.sendTurnLedOnM(yapapero.PP_MOUTH, ptn, false)

# 頬を光らせる
proc lightCheek(self: PaperoApp) {.async.} =
  info("lightCheek.")
  let ptn = @["R3R3", "10"]
  await self.papero.sendTurnLedOnM(yapapero.PP_CHEEK, ptn, false)

# 頭を動かす
proc moveHead(self: PaperoApp) {.async.} =
  info("moveHead.")
  let v = @["A5T300L", "A-5T300L", "A5T300L", "A-5T300L",
  "A5T300L", "A-5T300L", "A5T300L", "A-5T300L", "A5T300L", "A0T300L"];
  let h = @["A-9T300L", "A-12T300L", "A-3T300L", "A6T300L",
  "A15T300L", "A6T300L", "A-3T300L", "A-12T300L", "A-9T300L", "A0T300L"];
  await self.papero.sendMoveHead(v, h, true);

# 頭を止める
proc stopHead(self: PaperoApp) {.async.} =
  info("stopHead.")
  await self.papero.sendStopHead()

# しゃべる
proc speech(self: PaperoApp, txt: string) {.async.} =
  info("speech.")
  #await self.papero.sendStartSpeech(txt)
  discard await self.papero.speech(txt)  # 終話検知付き

# アプリ開始
proc start*(self: PaperoApp) {.async.} =
  info("app start.")
  await self.papero.start()
  var fk = getKey()
  var fp = self.papero.wsRecv()
  while true:
    # キーボード入力とパペロイベントを待つ
    await fk or fp
    if fk.finished():
      # キーボード入力
      info("event: keyboard")
      case fk.read()
      of "h":
        await self.moveHead()
      of "s":
        await self.stopHead()
      of "m":
        await self.lightMouth()
      of "c":
        await self.lightCheek()
      of "t":
        await self.speech("こんにちは、ごきげんいかがですか?今日はとっても良い天気ですね。明日も晴れるといいですね。")
      fk = getKey()
    if fp.finished():
      # パペロイベント
      let evnt = fp.read()
      case evnt.name
      of yapapero.PP_READY:
        info("event: ", evnt.name)
      of yapapero.PP_DETECT_BUTTON:
        info("event: ", evnt.name)
        var txt: string
        case evnt.msg{yapapero.PP_STATUS}.getStr()
        of "R":
          txt = "こんにちは"
        of "L":
          txt = "さようなら"
        else:
          txt = "元気ですか"
        await self.speech(txt)
      of yapapero.PP_END_OF_SPEECH:
        info("event: ", evnt.name)
        # しゃべり終わったら頬を光らせる
        await self.lightCheek()
      of yapapero.PP_GET_SPEECH_STATUS_RES:
        discard
      else:
        debug("OTHER event: ", evnt.name)
      fp = self.papero.wsRecv()
  info("app end.")

proc main() =
  # コマンドライン指定は下記指定値より優先です
  # USAGE: paperoapp -wssvr url -sim simId -robot simName
  #let url = "" # シミュレータに接続("wss://smilerobo.com:8000/papero")
  let url = "ws://192.168.1.1:8088/papero" # 実機に接続
  let simId = "abcdefgh"  # シミュレータID(実機の場合無視される)
  var app = newPaperoApp(url, simId, "")
  waitFor app.start()

when isMainModule:
  main()

ホストで動作させる場合にはソースのurlの修正またはコマンドライン指定(-sim シミュレータID)でシミュレータにも接続できます。
yapapero.nimの使い方は以下の通りです。

(1) PaPeRo i のURLを指定してオブジェクトを生成(yapapero.newYapapero(“ws://192.168.1.1:8088/papero”))
(2) 初期化 (await papero.start())
(3) パペロのイベントを受信し(papero.wsRecv())必要に応じて発話(papero.sendStartSpeech())等を行う。

nimでは使用したwebsocketパッケージが非同期(async/await)版だったため必然的にyapapero.nimも非同期となり、作成するアプリもasync/awaitで作成する必要があります。パペロ以外のイベントを扱う場合もその流儀に従う必要があります。

(4) ioは非同期版を使用 (ブロックするAPI(readline()等)を使用するとビルド・実行できてしまいますがまともに動きません)
(5) 同時待ちのためには待ちたい全futureに対してawait orを行い、その後各futureに対してfinished()で完了チェック、read()で結果を受け取る

上記例ではキーボードイベントも同時に受け取れるようにしています。パペロのイベントだけで良い場合、futureのメソッドを呼ぶ必要はなくなり、

let evnt = await papero.wsRecv()

と書けます。

ソースはこちらからダウンロードできます。