19
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ロボットをインターネット越しにブラウザで動かすプログラムをSkyWay WebRTC Gatewayで実装してみる

Last updated at Posted at 2018-09-07

はじめに

先日フォロを買いました。多脚でガシガシ歩いて可愛い子です。
この子はプラモデルのように組み立てられるようになっていて、足や胴を動かす部分は単三電池に直結してあげるだけで動くようになっていました。
これなら自分で自由に動かすプログラムが書けるんじゃないかと思い、SkyWay WebRTC GatewayのARM版とRaspberry Piを利用して実装してみます。

完成したやつ

この記事では↑のロボットをどうやって動かしているのかを書いていきます。
基本的にはインターネット越しにラズパイを操作する話なので、大体のIoTデバイスにも応用できるはずです。

ラズパイカメラを付けたので、遠隔で映像を見ながら操縦できるようになりました。
電源もモバイルバッテリーで動いたので、
完全に遠隔で動き回れるようになりました。

全体構成

まず全体構成です。せっかくラジコンのように動かすので、PS4コントローラーで自由に動かせるようにします。
PS4コントローラーの信号を取り出し、WebRTCでロボットに送り、Raspberry Piのピンをコントロールすることで回路を動かしてロボットを動かします。
それをモジュール単位で何をしているのか書き出したのが下の流れです。
kairo-Page-6.png

これを順番に分けて記載します。

PS4 Controllerのデータ取得

GamePad APIで簡単に取得できます。
基本的にはAPIの説明ページにあるプログラムをそのまま貼り付ければできます。
全体像はgithubにアップロードしています。


// gamepadが繋がれると発火
window.addEventListener("gamepadconnected", connecthandler);

function connecthandler(e) {
    addgamepad(e.gamepad);
}

function addgamepad(gamepad) {
    // gamepadを管理
    controllers[gamepad.index] = gamepad;
    requestAnimationFrame(updateStatus);
}

function updateStatus() {
    for (key in controllers) {
        const controller = controllers[key];
        for (let i = 0; i < controller.axes.length; i++) {
            // PS4コントローラーの左スティックの値を取得
            const val = controller.axes[i].toFixed(4);
            // WebRTCで転送するプログラムに渡す
            callback(i, controller.axes[i].toFixed(4));
        }
    }

    // 定期的に値の確認をする
    requestAnimationFrame(updateStatus);
}

WebRTC部分

実装内容は基本的に前回の記事(https://qiita.com/nakakura/items/a99c468c56db2a92b32f)と同じですので、
ここは今回は割愛します。そのうち汎用的な形で整理してアップロードしますが、
公式のサンプルプログラムがほぼそのまま使えます。

ロボットの操作

回路部分

このロボットは足を動かす部分と腰を回す部分で2つのモーターが動くようになっており、電池を繋いでやると回るようになっています。直流なので方向の概念があり、逆向きに流し込むと反対向けに動くようになっています。
例えば足なら、逆向きにつないでやるとバックします。
一方で電圧を変えてスピードを変化させるような機能はなさそうです。

従って制御は、ON(前進)、ON(後退)、OFFの3段階あればよさそうです。
これはリレーモジュールで切り替えをしてやることで実現します。

ここで私がハマった落とし穴ですが、Raspberry PiのGPIOピンは3.3Vで16mAまでしか出力できないので、Arduinoのピン(5V)で操作できるリレーでも動かない場合があります。
リレーは電磁石でスイッチを切りかえるような構造になっていますが、スイッチが切り替わるだけの出力が出せないと切り替えられないのです。
どうするかというと、電源ラインと制御ラインが別々になっているものを利用します。
この種のリレーは、スイッチを動かすのに電源ラインから入っている電力を使うため、切り返し時のための制御ラインは小さな出力でも切り替えられるような仕組みになっています。

今回はドクターラボのリレーモジュールを利用しました。
回路は以下のとおりです。

このモジュールは4つのリレーがついていて、足と腰を動かすためにそれぞれ2つずつ利用します。
リレーをうまく切り替えてやることで電流の方向切り替えとOFFにすることが可能です。
リレーの切り替えは、GPIOから3.3V電圧を出す出さないで切り替えます。以下出すときをOn、出さないときをOffと書きます。

前進, 腰を右回転する場合

バック, 腰を左回転する場合

止める場合

作用点(×)に流れる電気の向きが変わっているのがわかると思います。

上の図ではリレー1,2と3,4を同じ動かし方をしていますが、個別制御なので腰だけ止めて足だけ進めることももちろん可能です。

ラズパイ部分

4つのリレーを4つのGPIOピンで制御します。
SkyWay WebRTC GatewayからPS4コントローラーの出力を受け取ったら、状態を制御してラズパイのGPIOを操作して回路を制御します。
例えばRubyだとpi_piperというgemで簡単に操作できます。

基本的には

#26番ピンをoutに設定する場合
pin = PiPiper::Pin.new(:pin => 26, :direction => :out)
#onにする
pin.on
#offにする
pin.off

だけでOn/Offが切り替えられます。

#!/usr/bin/ruby
require "socket"
require "observer"
require "pi_piper"

#(1)
class State
  NOT_OVERRRIDE = 'not override error'
  def pin()
    raise State::NOT_OVERRRIDE
  end

  def update(flag)
    raise DayOfTheWeek::NOT_OVERRRIDE
  end
end

#(2)
class Forward < State
  def initialize(pin1, pin2)
    @pin1 = pin1
    @pin2 = pin2
  end

  def pin()
    @pin1.off
    @pin2.on
  end

  def update(flag)
    if flag == 0
      Stop.new(@pin1, @pin2)
    elsif flag == -1
      Backward.new(@pin1, @pin2)
    end
  end
end

#(3)
class Backward < State
  def initialize(pin1, pin2)
    @pin1 = pin1
    @pin2 = pin2
  end

  def pin()
    @pin1.on
    @pin2.off
  end

  def update(flag)
    if flag == 0
      Stop.new(@pin1, @pin2)
    elsif flag == 1
      Forward.new(@pin1, @pin2)
    end
  end
end

#(4)
class Stop < State
  def initialize(pin1, pin2)
    @pin1 = pin1
    @pin2 = pin2
  end

  def pin()
    @pin1.off
    @pin2.off
  end

  def update(flag)
    if flag == 1
      Forward.new(@pin1, @pin2)
    elsif flag == -1
      Backward.new(@pin1, @pin2)
    end
  end
end

#(5)
class StateManager
  include Observable

  def initialize()
    gpio_in1 = PiPiper::Pin.new(:pin => 26, :direction => :out)
    gpio_in2 = PiPiper::Pin.new(:pin => 19, :direction => :out)
    gpio_in3 = PiPiper::Pin.new(:pin => 20, :direction => :out)
    gpio_in4 = PiPiper::Pin.new(:pin => 21, :direction => :out)

    @body_state = Stop.new(gpio_in1, gpio_in2)
    @leg_state = Stop.new(gpio_in3, gpio_in4)
  end

  def format(message)
    array = message.split(",")
    key = array[0].to_i
    value = array[1].to_f

    #(6)
    if value < -0.5
      [key, 1]
    elsif value > 0.5
      [key, -1]
    else
      [key, 0]
    end
  end

  #(7)
  def feed(message)
    (key, value) = self.format(message)

    if key == 0
      new_state = @body_state.update(value)
      if new_state
        changed
        @body_state = new_state
        notify_observers(key, new_state.pin())
      end
    elsif key == 1
      new_state = @leg_state.update(value)
      if new_state
        changed
        @leg_state = new_state
        notify_observers(key, new_state.pin())
      end
    end
  end
end

if __FILE__ == $0
  #(8)
  stateManager = StateManager.new

  #(9)
  udps = UDPSocket.open()
  udps.bind("0.0.0.0", 10000)
  loop do
    data = udps.recv(65535).chomp
    stateManager.feed(data)
  end

  udps.close
end

ステートモデルでプログラムを書きます。まず状態を定義するための親クラスを作ります(1)

(2)が前進状態、(3)が後退状態、(4)が停止状態で、それぞれのpin()メソッドの中でピンの状態を切り替えています。それぞれピンのOn, Offの状態が異なりますが、↑の回路図に対応しています。

(5)のクラスで状態を切り替えています。
取得したPS4コントローラーのスティックの出力値は-1.0〜1.0なので、

  • -1.0〜-0.5 => 前進
  • -0.5〜0.5 => 停止
  • 0.5〜1.0 => 後退
    としてフラグを立てます。(6)

(7)で状態を実際に切り替え、別の状態に変わる時は該当するクラスのインスタンスを生成してpin()メソッドを呼び出しています。

状態の切り替えとピンの操作するクラスが定期できたら、そのインスタンスを作成し(8)、WebRTC Gatewayから受信するためのUDPポートを作成して、受信した信号を流し込み続けます(9)

これで完成です。

19
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?