C#でWebSocketを使用してobnizを操作


初めに

Obnizを毎年C#で動かそうとしているのですが、なんか不完全な感じなので今年こそある程度のものを出そうと頑張りました。
ただ忙しい時期にかぶってしまったので自分がやりたいことは完全にできなかったので残念です(毎年同じこと書いてる気がする)
コードは精査しきれなかったたかなり冗長ですが、自分で突き詰める楽しみが残ってると思って大目に見てください笑

今年のテーマ

今年もobnizをC#で動かしてみました。
去年はだいぶやっつけ感がすごかったのですが今年はC#のみでobnizを動かすことができました。
基本的にはobnizをwebsocketを使用してobnizを制御する感じです。
だいぶ昔のアドベントカレンダーで試していた方がいたので参考にしながらC#で操作しました。

以下を参考しました、
https://qiita.com/hmaruyama/items/eee079f035f864638e13

さっそくやってみた

以下のページに送信するjsonのフォーマットが書かれています。(何故か英語で書かれています)
https://obniz.com/ja/doc/reference/websocket/
今回はobnizのディスプレイに文字を表示するものを選びました。
https://obniz.com/ja/doc/reference/websocket/display

例年ならプロジェクトの立ち上げ方から事細かに説明しているのですが、今年は時間がなかったので割愛します。
恐らくそこまで難しくはないので大丈夫だと思います。

まずC#といえばおなじみの”VisualStudio2019”を使っています。無料版もあるのでぜひお試しください。
今回は今もよくつかわれているWindowsFormアプリでプロジェクトを作成しました。コンソールアプリにも簡単に移植はできると思います。(obnizをコンソールで操作するロマンプレイができます)

去年取り上げたblazerやAsp.netは試してないのでわかりませんが、後々試して記事に挙げてみたいと思います。恐らく1年後のアドベントカレンダーです笑

実際にコードを書いている画面はこんな感じになっています。今回はわかりやすくするためForm1にすべてを書き込んでいるのですが、本来はもうちょっと細分化するほうがいいですね。

画面は以下のようになっています。ボタンを2つ、テキストボックスを1つ配置しています。

以下がForm1内のコードです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Windows.Forms;
using System.Net.WebSockets;
using System.Text.Json;

namespace obniz_test
{
    public partial class Form1 : Form
    {
        ClientWebSocket ws2 = null;

        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            Root redirect;

            //クライアント側のWebSocketを定義
            ClientWebSocket ws = new ClientWebSocket();

            //接続先エンドポイントを指定
            var uri = new Uri("wss://obniz.io/obniz/自分のobnizの番号/ws/1");

            //サーバに対し、接続を開始
            await ws.ConnectAsync(uri, CancellationToken.None);
            var buffer = new byte[1024];

            //情報取得待ちループ
            while (true)
            {
                //所得情報確保用の配列を準備
                var segment = new ArraySegment<byte>(buffer);

                //サーバからのレスポンス情報を取得
                var result = await ws.ReceiveAsync(segment, CancellationToken.None);

                //エンドポイントCloseの場合、処理を中断
                if (result.MessageType == WebSocketMessageType.Close)
                {
                    await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "OK",
                      CancellationToken.None);
                    return;
                }

                //バイナリの場合は、当処理では扱えないため、処理を中断
                if (result.MessageType == WebSocketMessageType.Binary)
                {
                    await ws.CloseAsync(WebSocketCloseStatus.InvalidMessageType,
                      "I don't do binary", CancellationToken.None);
                    return;
                }

                //メッセージの最後まで取得
                int count = result.Count;
                while (!result.EndOfMessage)
                {
                    if (count >= buffer.Length)
                    {
                        await ws.CloseAsync(WebSocketCloseStatus.InvalidPayloadData,
                          "That's too long", CancellationToken.None);
                        return;
                    }
                    segment = new ArraySegment<byte>(buffer, count, buffer.Length - count);
                    result = await ws.ReceiveAsync(segment, CancellationToken.None);

                    count += result.Count;
                }

                //メッセージを取得
                var message = Encoding.UTF8.GetString(buffer, 0, count);
                message = message.Replace("[", "");
                message = message.Replace("]", "");
                redirect = JsonSerializer.Deserialize<Root>(message);
                await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "OK",
                  CancellationToken.None);
                break;
            }

            ws2 = new ClientWebSocket();

            //接続先エンドポイントを指定
            uri = new Uri(redirect.ws.redirect + "/obniz/自分のobnizの番号/ws/1");

            //サーバに対し、接続を開始
            await ws2.ConnectAsync(uri, CancellationToken.None);
            buffer = new byte[1024];

            //情報取得待ちループ
            while (true)
            {
                //所得情報確保用の配列を準備
                var segment = new ArraySegment<byte>(buffer);

                //サーバからのレスポンス情報を取得
                var result = await ws2.ReceiveAsync(segment, CancellationToken.None);

                //エンドポイントCloseの場合、処理を中断
                if (result.MessageType == WebSocketMessageType.Close)
                {
                    await ws2.CloseAsync(WebSocketCloseStatus.NormalClosure, "OK",
                      CancellationToken.None);
                    return;
                }

                //バイナリの場合は、当処理では扱えないため、処理を中断
                if (result.MessageType == WebSocketMessageType.Binary)
                {
                    await ws2.CloseAsync(WebSocketCloseStatus.InvalidMessageType,
                      "I don't do binary", CancellationToken.None);
                    return;
                }

                //メッセージの最後まで取得
                int count = result.Count;
                while (!result.EndOfMessage)
                {
                    if (count >= buffer.Length)
                    {
                        await ws2.CloseAsync(WebSocketCloseStatus.InvalidPayloadData,
                          "That's too long", CancellationToken.None);
                        return;
                    }
                    segment = new ArraySegment<byte>(buffer, count, buffer.Length - count);
                    result = await ws2.ReceiveAsync(segment, CancellationToken.None);

                    count += result.Count;
                }

                //メッセージを取得
                var message = Encoding.UTF8.GetString(buffer, 0, count);
                /// 余分な記号を削る
                message = message.Replace("[", "");
                message = message.Replace("]", "");
                break;
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }

        private async void button2_Click(object sender, EventArgs e)
        {
            /// テキストボックスにあるメッセージをobnizに送信する
            string data = "[{\"display\":{\"clear\": true}},{\"display\": {\"text\": \"" + textBox1.text + "\"} }]";

            /// byte型へ変換
            var senddata = Encoding.ASCII.GetBytes(data);

            /// 送信
            await ws2.SendAsync(senddata, WebSocketMessageType.Text, true, CancellationToken.None);
        }
    }

    public class Ws
    {
        public string redirect { get; set; }
    }

    public class Root
    {
        public Ws ws { get; set; }
    }
}

実際に動かしたときの画像が以下のようになります。
テキストボックスに入力されたメッセージが表示されます。現状接続押してから5秒くらいまって文章送信しないとエラーで落ちました。

終わりに

ようやくC#で完結させることができました。
今回は技術的にできることをかければいいなと思ったため細かい点は飛ばしてます。
いずれ細かい説明や、API操作関連をライブラリ化できたらいいなと思っています。