本文へスキップ
ysk-hack-log
戻る

温度をSignalRでライブグラフ表示する|Blazor Serverのリアルタイム更新【連載⑤】

「温度をSignalRでライブグラフ表示する|Blazor Serverのリアルタイム更新【連載⑤】」の記事アイキャッチ

前回(第4回) で温度が読めるようになりました。ただ、各画面が個別に1秒ごとにポーリングするのは効率が良くありません。今回は 1か所で計測し、全クライアントにリアルタイムで反映する形に作り替え、ライブグラフにします。

Table of contents

Open Table of contents

Blazor ServerとSignalRの関係

実は Blazor Server の画面更新は、最初から SignalR(回線=circuit)の上で動いています。サーバーでUIの差分を計算し、SignalR経由でブラウザに送って描画しているのです。

つまり、サーバー側で状態を変えて StateHasChanged を呼べば、その変化はSignalRで各クライアントに届きます。今回はこの仕組みに乗せて、

  1. バックグラウンドサービスが温度を定期計測
  2. 更新をイベントで通知
  3. 各ページが受け取って再描画 → SignalRで画面反映

という流れを作ります。各画面が個別にポーリングするより効率的で、複数タブ・複数端末が同時に同じ値を見られます。

バックグラウンドで計測するサービス

BackgroundService で1秒ごとに温度を読み、直近の履歴を保持して、更新イベントを発火します。

// Services/TemperatureMonitor.cs
public sealed class TemperatureMonitor : BackgroundService
{
    private const int Capacity = 60;           // 直近60秒ぶん
    private readonly TemperatureService _temp;
    private readonly Queue<double> _history = new();
    private readonly object _gate = new();

    public double Current { get; private set; }
    public event Action? Changed;

    public TemperatureMonitor(TemperatureService temp) => _temp = temp;

    public double[] Snapshot()
    {
        lock (_gate) return _history.ToArray();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            Current = _temp.ReadCelsius();
            lock (_gate)
            {
                _history.Enqueue(Current);
                while (_history.Count > Capacity) _history.Dequeue();
            }
            Changed?.Invoke();
        }
    }
}

Program.cs に、シングルトン兼ホステッドサービスとして登録します。

builder.Services.AddSingleton<TemperatureMonitor>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<TemperatureMonitor>());

Queue は複数スレッドから触るので lock で保護し、描画側には配列のスナップショットを渡します。

ライブグラフを表示する(依存ライブラリ無し)

外部チャートライブラリを使わず、SVGの折れ線で描きます。イベントを購読し、更新のたびに再描画します。

@* Components/Pages/Live.razor *@
@page "/live"
@rendermode InteractiveServer
@implements IDisposable
@inject TemperatureMonitor Monitor

<h1>温度ライブグラフ</h1>
<p style="font-size:2rem"><b>@Monitor.Current.ToString("F1") ℃</b></p>

<svg width="600" height="200" style="border:1px solid #ccc">
    @if (!string.IsNullOrEmpty(_points))
    {
        <polyline points="@_points" fill="none" stroke="#006cac" stroke-width="2" />
    }
</svg>

@code {
    private string _points = "";

    protected override void OnInitialized() => Monitor.Changed += OnChanged;

    private void OnChanged()
    {
        _points = BuildPoints(Monitor.Snapshot());
        InvokeAsync(StateHasChanged);
    }

    private static string BuildPoints(double[] data)
    {
        if (data.Length < 2) return "";
        const int w = 600, h = 200;
        double min = data.Min(), max = data.Max();
        double range = max - min < 0.1 ? 1 : max - min; // 平坦なとき対策

        return string.Join(" ", data.Select((v, i) =>
        {
            double x = (double)i / (data.Length - 1) * w;
            double y = h - (v - min) / range * h;   // 上下反転
            return $"{x:F1},{y:F1}";
        }));
    }

    public void Dispose() => Monitor.Changed -= OnChanged;
}

実機で起動し、/live を開くと、温度が1秒ごとに折れ線で伸びていくはずです。センサーを指で触れて温めると、グラフが上がるのが確認できます。別のタブや別PCで同時に開いても、同じ動きになります(=SignalRで全クライアントに配信)。

ブラウザ画面

発展:明示的なSignalR Hubを使う場合

今回はBlazor Server内で完結しましたが、Blazor以外(素のJavaScript、WASM、モバイル)にも配信したい場合は、明示的なSignalR Hubを立て、IHubContext からブロードキャストします。第7回のMQTTと組み合わせると、他デバイスへの配信も見えてきます。本連載のダッシュボードは Blazor Server なので、まずは上記のシンプルな形で十分です。

つまずきポイント

まとめ

次回(第6回)は、この温度をSQLite+EF Coreで蓄積し、過去の推移を時系列グラフで振り返れるようにします。


前の記事:連載④ MCP3208+LM35DZでアナログ温度を読む


この記事をシェア:

前の記事
MCP3208+LM35DZでアナログ温度を読む|Raspberry PiのSPIとADC【連載④】
次の記事
温度ログをSQLite+EF Coreで蓄積し時系列グラフで振り返る(Raspberry Pi)【連載⑥】