中継クライアント改良編は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をクラウド経由で制御できることが確認できました。