Google の Firebase には Realtime Database というものがあって、複数のアプリケーション(デスクトップPC、スマホ、M5Stack などなど)を使って同期ができます。って記事が、あちこちにあるので、ならば、Scratch でもできるのでは?と思って作ってみたのがこれ。
1画面にはなっていますが、デスクトップPCからノートPCにリモートデスクトップ表示しているところです。別のPCでスクラッチを起動しておいて(ひとつのPCでは複数起動できないので)、片方のオレンジ猫を動かすと、もうひとつのオレンジ猫が動きます。
直接通信させることもできるのですが、Google の Firebase を経由させます。
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 形式でごっそりとデータを置きます。
こんな風に、/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 を読み込ませます。
{ "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"] } }
サーバーを立ち上げて、コマンド待ち状態になると「その他」のところがグリーンになります。
これを2つのPCで起動させて、スクラッチ同士で通信させたのが、https://twitter.com/moonmile/status/1044440471823478784 にある動画になります。
相互通信にしたいところだけど、ひとまず一方向だけのスクリプトを
送信元のスクリプト
送信先のスクリプト