クラウド経由でPaPeRo iを制御する-WebSocket中継4-go版中継クライアント編

 中継クライアント改良編はPython3で実装しましたが、golangでも実装してみました。こちらを利用すればpythonのインストールが不要になりますので、クラウド経由で操作可能なPaPeRo iを短時間でセットアップできる様になります。

WebSocketライブラリ

 WebSocketのライブラリはGorilla WebSocketを利用しました。

import "github.com/gorilla/websocket"

とすることで使えるようになります。サーバへの接続は以下の様になります。

    var dialer *websocket.Dialer
    con, _, err := dialer.Dial(url, nil)

これは同期呼び出しで、errがnilなら接続成功です。返されるconに対しcon.WriteMessage()で送信、con.ReadMessage()で受信ができます。受信も同期呼び出しなのでgoroutineを使います。

受信データの解析

 パペロとアプリを直接接続する場合と同じ動作にするために、クラウド側から初期化メッセージ(Name=”SelectSimRobot”)を受信したことを検知する必要があります。メッセージはJSON形式なのでencoding/jsonパッケージを利用します。golangではどんなjsonでも扱えるようにするのは面倒なので受信メッセージの見たい部分だけ定義したstructを指定してjson.Unmarshal()を呼ぶのが簡単です。structで定義した要素がjsonに含まれていない場合、定義されていない要素が含まれている場合いずれもエラーにはなりません。

type PaperoMessage struct {
    Destination string
    Name        string
    RobotName   string
    SimulatorID string
    RobotID     string
    PaperoID    string
}

    var pm PaperoMessage
    _ = json.Unmarshal(dat, &pm)
    if pm.Name == "SelectSimRobot" {
        ...

送信データの作成

 中継クライアントはアプリとパペロのメッセージを中継する以外に、パペロ識別子登録メッセージを自分から送信する必要があります。送信データの場合にはstructの定義不要でmap[string]stringから自由に生成できます。

    dic := map[string]string{
        "Destination": "RelayServer",
        "Name":        "RegPaperoID",
        "PaperoID":    app.PaperoID,
    }
    bytes, err := json.MarshalIndent(dic, "", "    ")
    if err == nil {
        ...

全体構成

 WebSocketのコネクションの管理と送受信を行うWsConクラスと中継クライアント全体を管理するWsRelayクラスを定義しています。受信はWsCon.Loopをgoroutineで動作させ、受信データはchannelでメインループに上げています。コード全体は以下の様になりました(wsrelaycli.go)。

// PaPeRo i制御用WebSocket通信中継クライアント
// (c)2018 Sophia Planning Inc.
// LICENCE: MIT
package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "os"
    "time"

    "github.com/gorilla/websocket"
)

// WsCon WebSocket connection
type WsCon struct {
    typ     string
    url     string
    con     *websocket.Conn
    ch      chan []byte
    en      bool
    err     error
    sleepms time.Duration
}

// Connect connect websocket connection
func (ws *WsCon) Connect(first bool) bool {
    fmt.Printf("(%s) Connect\n", ws.typ)
    ws.en = false
    ws.con = nil
    var dialer *websocket.Dialer
    con, _, err := dialer.Dial(ws.url, nil)
    if err != nil {
        fmt.Printf("(%s) Connect err\n", ws.typ)
        ws.err = err
        return false
    }
    fmt.Printf("(%s) Connect ok\n", ws.typ)
    ws.con = con
    ws.en = true
    if first {
        go ws.Loop()
    }
    return true
}

// Close close websocket connection
func (ws *WsCon) Close() {
    if ws.en {
        fmt.Printf("(%s) close\n", ws.typ)
        ws.con.Close()
        ws.en = false
    }
}

// SendToWs send to websocket
func (ws *WsCon) SendToWs(dat []byte) bool {
    if ws.en {
        //fmt.Printf("(%s) SendToWs\n", ws.typ)
        ws.con.WriteMessage(websocket.TextMessage, dat)
        return true
    }
    return false
}

// SendDicToWs send dictionary to websocket
func (ws *WsCon) SendDicToWs(dic map[string]string) bool {
    if ws.en {
        fmt.Printf("(%s) SendDicToWs Name=%s\n", ws.typ, dic["Name"])
        bytes, err := json.MarshalIndent(dic, "", "    ")
        if err == nil {
            //fmt.Printf("(%s) SendDictToWs dic=%s", ws.typ, dic)
            return ws.SendToWs(bytes)
        }
    }
    return false
}

// ReconSendToWs reconnect and send to websocket connection
func (ws *WsCon) ReconSendToWs(dat []byte) {
    //fmt.Printf("(%s) ReconSendToWs\n", ws.typ)
    if ws.en {
        ws.Close()
        ws.Connect(false)
    } else {
        ws.Connect(true)
    }
    ws.SendToWs(dat)
}

// Loop loop
func (ws *WsCon) Loop() {
    for {
        if ws.con == nil {
            // connecting
            fmt.Printf("(%s) wait connect\n", ws.typ)
            time.Sleep(ws.sleepms * time.Millisecond)
            continue
        }
        _, dat, err := ws.con.ReadMessage()
        if err != nil {
            fmt.Printf("(%s) read err: %s\n", ws.typ, err)
            ws.err = err
            time.Sleep(ws.sleepms * time.Millisecond)
            continue
        }
        //fmt.Printf("(%s) received:\n%s\n", ws.typ, dat)
        //fmt.Printf("(%s) received\n", ws.typ)
        ws.ch <- dat
    }
}

// NewWsCon new websocket connection
func NewWsCon(typ, url string, ch chan []byte, sleepms time.Duration) *WsCon {
    con := WsCon{typ: typ, url: url, ch: ch, sleepms: sleepms}
    return &con
}

// PaperoMessage receive message format
type PaperoMessage struct {
    Destination string
    Name        string
    RobotName   string
    SimulatorID string
    RobotID     string
    PaperoID    string
}

// WsRelay WebSocket relay class
type WsRelay struct {
    PaperoCh     chan []byte
    CloudCh      chan []byte
    PaperoWs     *WsCon
    CloudWs      *WsCon
    PaperoID     string
    KeepAliveSec time.Duration
}

// Loop loop
func (app *WsRelay) Loop() {
    tick := time.NewTicker(app.KeepAliveSec * time.Second)
    for {
        var dat []byte
        select {
        case dat = <-app.PaperoCh:
            if app.CloudWs.en {
                fmt.Printf("Loop app < papero\n")
                app.CloudWs.SendToWs(dat)
            }
        case <-tick.C:
            if app.CloudWs.en {
                app.SendPing()
            }
        case dat = <-app.CloudCh:
            var pm PaperoMessage
            _ = json.Unmarshal(dat, &pm)
            // fmt.Println("Name:", pm.Name)
            if pm.Name == "SelectSimRobot" {
                app.PaperoWs.ReconSendToWs(dat)
                fmt.Printf("Loop app > papero(reconnect)\n")
            } else {
                if app.PaperoWs.en {
                    fmt.Printf("Loop app > papero\n")
                    app.PaperoWs.SendToWs(dat)
                }
            }
        }
    }
}

// SendPaperoID send paperoID to cloud
func (app *WsRelay) SendPaperoID() bool {
    dic := map[string]string{
        "Destination": "RelayServer",
        "Name":        "RegPaperoID",
        "PaperoID":    app.PaperoID,
    }
    return app.CloudWs.SendDicToWs(dic)
}

// SendPing send ping to cloud
func (app *WsRelay) SendPing() bool {
    err := app.CloudWs.con.WriteControl(websocket.PingMessage, []byte{},
        time.Now().Add(10*time.Second))
    if err != nil {
        fmt.Printf("SendPing error: %s", err)
        return false
    }
    fmt.Println("send ping to cloud")
    return true
}

// Start start relay
func (app *WsRelay) Start(argPaperoURL, argCloudURL, argPaperoID string) {
    var sleepms time.Duration = 500
    //paperoURL := "wss://smilerobo.com:8000/papero"
    paperoURL := "ws://192.168.1.1:8088/papero"
    //cloudURL := "wss://paperoi-relay-server.mybluemix.net/ws/papero"
    cloudURL := "wss://paperoi-relay-server.herokuapp.com/ws/papero"
    paperoID := "12345678"
    if _, err := os.Stat("/Extension"); err == nil {
        paperoURL = "ws://0.0.0.0:8088/papero"
    }
    if 0 < len(argPaperoURL) {
        paperoURL = argPaperoURL
    }
    if 0 < len(argCloudURL) {
        cloudURL = argCloudURL
    }
    if 0 < len(argPaperoID) {
        paperoID = argPaperoID
    }
    fmt.Printf("Start:\npapero url:%s\ncloud url:%s\npapero id:%s\n",
        paperoURL, cloudURL, paperoID)
    app.PaperoID = paperoID
    app.PaperoCh = make(chan []byte)
    app.CloudCh = make(chan []byte)
    app.PaperoWs = NewWsCon("p", paperoURL, app.PaperoCh, sleepms)
    app.CloudWs = NewWsCon("c", cloudURL, app.CloudCh, sleepms)
    if app.CloudWs.Connect(true) {
        app.SendPaperoID()
        app.Loop()
    }
    fmt.Println("error exit")
}

func main() {
    var paperoURL = flag.String("wssvr", "", "papero url")
    var cloudURL = flag.String("cloud", "", "cloud url")
    var paperoID = flag.String("papero", "", "papero id")
    var keepalive = flag.Int("keepalive", 25, "keep alive period in second")
    flag.Parse()
    app := WsRelay{KeepAliveSec: time.Duration(*keepalive)}
    app.Start(*paperoURL, *cloudURL, *paperoID)
}

動作確認

(1) Tornado/Heroku編のサーバを起動します。
(2) 使用するPaPeRo iに、最新のLinuxの/etc/sslをコピーします。電源OFFしても良いようにするためには/Extension以下に置いておき、起動スクリプトで/etc/sslにコピーする必要があります。
(3) PaPeRo iでインターネットへ接続できることを確認します。
(4) PaPeRo iの時刻を合わせます。時刻がずれているとクラウドへの接続ができません。

# date 2018.3.12-10:20

(5) PaPeRo iにgo版中継クライアントwsrelaycliを転送し、起動します。-wssvrでパペロのURL、-cloudでサーバURL(/ws/papero)、-paperoで任意のパペロ識別子を指定します。

# chmod +x wsrelaycli
# ./wsrelaycli -wssvr ws://0.0.0.0:8088/papero -cloud wss://paperoi-relay-server.herokuapp.com/ws/papero -papero 12345678

 但し-cloudには実際は(1)のアドレスを指定してください。
(6) Pythonをインストールしたホストマシンでパペロのアプリを起動します。-wssvrにサーバURL(/ws/controller)、-simに中継クライアントの-paperoと同じ文字列を指定します(pypapero.Papero()呼び出し時に引数arg_ws_server_addrとsimulator_idに渡すようにアプリを作成)。

$ python3 xxxx.py -wssvr wss://paperoi-relay-server.herokuapp.com/ws/controller -sim 12345678

 Python版と同様に別のLANにあるPaPeRo iをクラウド経由で制御できることが確認できました。


0