Firebaseを使い、Scratchで相互通信する

list このエントリーをはてなブックマークに追加

Google の Firebase には Realtime Database というものがあって、複数のアプリケーション(デスクトップPC、スマホ、M5Stack などなど)を使って同期ができます。って記事が、あちこちにあるので、ならば、Scratch でもできるのでは?と思って作ってみたのがこれ。

image

1画面にはなっていますが、デスクトップPCからノートPCにリモートデスクトップ表示しているところです。別のPCでスクラッチを起動しておいて(ひとつのPCでは複数起動できないので)、片方のオレンジ猫を動かすと、もうひとつのオレンジ猫が動きます。

直接通信させることもできるのですが、Google の Firebase を経由させます。

image

Scratch なところは、デスクトップアプリ(WPFとか)でもよいし、スマホアプリを Xamarin で作ることもできます。便利かどうか別として、まあ、こんなことができるということで。

Firebase の基本的なところは、

C#でFirebaseを使ってみよう!(1) FirebaseとEmail-Password認証 – こっちみないで(´・ω・`) http://kmycode.hatenablog.jp/entry/2017/02/09/205655

Firebase Realtime Database のデータ保存、取得、ストリーミング受信実験( ESP32 , M5Stack ) | mgo-tec電子工作
https://www.mgo-tec.com/blog-entry-firebase-realtime-database-sever-sent-events-esp32-m5stack.html

なところを参考にしています。Firebase は API KEY を取得してアクセスするのですが、そのままだと誰でもアクセスできてしまうので、一応ユーザー名(メールアドレス)とパスワードでガードを掛けます。

C# や F# から Firebase を扱うときは、NuGet で次の2つを追加します。

  • FirebaseAuthentication.net
  • FirebaseDatabase.net

内部で Rx が使われているらしく、System.Reactive や System.Reactive.Linq などが同時にインストールされます。

先の記事では、C# でサンプルが書かれているのですが、FireScratch の場合は都合上 F# で書いています。まあ、以前作った NetScrattino が F# だったので、それを踏襲したかっただけなんですけどね。

Firebase 自体は NoSql なので、JSON 形式でごっそりとデータを置きます。

image

こんな風に、/scratch/firecat というパス(フォルダーのようなものか)の下に、データが置かれます。謎な文字は識別子みたいなものですね。プロパティとして、From, To, Text, X, Y を置くためには、下記のようなクラスを作っておきます。

type Data() =
	let mutable _from : string = ""
	let mutable _to : string = ""
	let mutable _x : int = 0
	let mutable _y : int = 0
	let mutable _text = ""

	member x.From with get() = _from and set(v) = _from &<- v
	member x.To with get() = _to and set(v) = _to <- v
	member x.X with get() = _x and set(v) = _x <- v
	member x.Y with get() = _y and set(v) = _y <- v
	member x.Text with get() = _text and set(v) = _text <- v

C# だとこんな感じ

public class Data
{
	public string Text { get; set; }
	public string From { get; set; }
	public string To { get; set; }
	public int X { get; set; }
	public int Y { get; set; }
}

このデータクラスを、Firebase に対してアップロードします。F# で作る場合は、こんな風に、各種の関数を作っておくと便利です。詳しい中身は先の記事の C# コードを読んだほうがよいでしょう。

// ログイン
let singIn() =
     let auth = new FirebaseAuthProvider( new FirebaseConfig( apikey ))
     authLink <- auth.SignInWithEmailAndPasswordAsync( email, passwd ).Result
     printfn "サインインに成功しました"

// クエリを取得
let GetDatabaseQuery( path ) =
     let opt = new FirebaseOptions()
     opt.AuthTokenAsyncFactory <- fun () -> Task.FromResult( authLink.FirebaseToken )
     let client = new FirebaseClient( databaseURL, opt )
     client.Child( path )

// データをアップロード
let upload( data : Data ) =
     let query = GetDatabaseQuery( DatabasePath )
     query.PostAsync( data ) |> ignore
     ()
 // テキストを保存
let uploadText( text: string ) = upload( new Data( Text = text ))


 // テキストを取得
let downloadText() =
     let query = GetDatabaseQuery( DatabasePath )
     let results = query.OnceAsync<Data>().Result
     let items = results.Select( fun o -> o.Object )
     items.First().Text

// リアルタイムデータの監視
let startWatchingRealtime() =
     realtimeDatabaseWatcher <- 
         GetDatabaseQuery(DatabasePath)
             .AsObservable<Data>()
             .Subscribe( fun ev -> 
                 if ev <> null then
                     let text = ev.Object.Text
                     match ev.EventType with
                         | FirebaseEventType.InsertOrUpdate -> fireData <- ev.Object | FirebaseEventType.Delete -> ()
                         | _ -> ()
             )
     ()

これを、Scratch から呼び出せるように HTTP サーバーを作っておきます。スクラッチからは、/say/me/you/hello のようなスラッシュで区切られてデータが送られてくるので、これをパースして、Firebase に保存します。

もうひとつの PC では、リアルタイム監視(startWatchingRealtimeで登録)をしているので、これを fireData に保存しておいて、スクラッチのポーリング(/poll)に送られるようにします。

// Scratchから受信するためのHTTPサーバー
let Server( port ) =

    // firebase にログイン
     singIn()
     startWatchingRealtime()

    let listener = new System.Net.HttpListener()
     listener.Prefixes.Add("http://127.0.0.1:"+(port |> string)+"/" )
     listener.Start()
     while true do
         let context = listener.GetContext()
         let res = context.Response
         let mutable data = ""
         let path = context.Request.Url.PathAndQuery
         match path with 
             | "/poll" -> 
                 data <- data + String.Format("text {0}\n", fireData.Text )
                 data <- data + String.Format("from {0}\n", fireData.From )
                 data <- data + String.Format("to {0}\n", fireData.To )
                 data <- data + String.Format("x {0}\n", fireData.X )
                 data <- data + String.Format("y {0}\n", fireData.Y ) // printfn "%s" path // printfn "%s" debug | "/reset_all" ->
                 printfn "/reset_all"
                 data <- "ok" | _ ->
                 let pa = path.Split([|'/'|])
                 match pa.[1] with   
                 | "say" ->
                     let me = pa.[2]
                     let you = pa.[3]
                     let text = pa.[4]
                     printfn "say %s %s %s" me you text
                     upload( Data( From = me, To = you, Text = text ))
                 | "sayall" ->
                     let text = pa.[2]
                     printfn "sayall %s" text
                     uploadText( text )
                 | "movex" ->
                     let me = pa.[2]
                     let x = pa.[3] |> int
                     printfn "movex %s %d" me x
                     upload( Data( From = me, X = x ))
                 | "movey" ->
                     let me = pa.[2]
                     let y = pa.[3] |> int
                     printfn "movey %s %d" me y
                     upload( Data( From = me, Y = y ))
                 | "movexy" ->
                     let me = pa.[2]
                     let x = pa.[3] |> int
                     let y = pa.[4] |> int
                     printfn "movexy %s %d %d" me x y 
                     upload( Data( From = me, X = x, Y = y ))
                 | _ ->
                     data <- ""
                 printfn "%s" path
         res.StatusCode <- 200
         let sw = new System.IO.StreamWriter( res.OutputStream )
         sw.Write( data )
         sw.Close()
     ()

スクラッチから送られてくるコマンド(say, sayall, movex など)は自分で定義をします。スクラッチのメニューでシフトキーを押しながら「ファイル」を選択すると「実験的なHTTP拡張の読み込み」が出てくるので、ここで作成した firescratch.json を読み込ませます。

image

{
   "extensionName": "Firebase Scratch",
   "extensionPort": 5411,
   "url": "https://github.com/moonmile/firescratch",
   "blockSpecs": [
     [ " ", "%s と言う", "sayall", "hello world." ],
     [ " ", "%s が %s さんへ %s と言う", "say", "me", "you", "hello" ],
     [ " ", "%s を X座標 %d にする", "movex", "me", 0 ],
     [ " ", "%s を Y座標 %d にする", "movey", "me", 0 ],
     [ " ", "%s を X座標 %d、 Y座標 %d にする", "movexy", "me", 0, 0 ],

    [ "-" ],
     [ "r", "自分", "from" ],
     [ "r", "相手", "to" ],
     [ "r", "X座標", "x" ],
     [ "r", "Y座標", "y" ],
     [ "r", "メッセージ", "text" ],
     [ "-" ]
   ],
   "menus": {
     "OnOffValues": ["ON", "OFF"]
   }
 }

サーバーを立ち上げて、コマンド待ち状態になると「その他」のところがグリーンになります。

image

これを2つのPCで起動させて、スクラッチ同士で通信させたのが、https://twitter.com/moonmile/status/1044440471823478784 にある動画になります。

相互通信にしたいところだけど、ひとまず一方向だけのスクリプトを

送信元のスクリプト

送信先のスクリプト

サンプルコード

https://github.com/moonmile/FireScratch

カテゴリー: Scratch パーマリンク