🎉

オリジナルのスマートスピーカーを作ってみる 5 目覚まし機能の作成

2024/03/27に公開

この記事は?

この記事は オリジナルのスマートスピーカーを作ってみる シリーズの続き「その5」です。

とりあえず朝9時に起こしてもらおう

一刻も早く働いてもらいたいので、まず目覚まし機能を作ります。
AI とか関係ないですが、まあゆっくり行きます。
文言は 「おはようございます。朝になりました。起きてください。今日も無理しない程度に頑張ってください。」 にします。(ハラスメントを考慮)

アプリをデーモン化する

目覚まし機能を作るにはアプリをデーモン化(常駐アプリ化)する必要があります。
ここでいうデーモン化とは systemd に対応したアプリケーションのこととします。
C# でどのようにデーモンアプリを作成すればよいか調べてみたところ、
どうやら Visual Studio で Worker Service というプロジェクトを使えば systemd 対応のアプリケーションを作ることができるそうです。
Microsoft のテックブログ に How To が書いてありましたので、まずはそっくり真似してみます。
https://devblogs.microsoft.com/dotnet/net-core-and-systemd/

上記の記事の通りにすればデーモンアプリは簡単に作ることができました。
.NET フレームワークが提供する UseSystemd() というメソッドを使用することで、systemd にアプリの起動・停止を通知し、また systemd に対応したロギング機能を提供してくれるそうです。(参考になった記事


ただ一つだけ、アプリケーションを起動するときに以下のエラーメッセージが発生して起動できませんでした。

bash on raspi
$ ./your_app
Process terminated. Couldn't find a valid ICU package installed on the system.
Set the configuration flag System.Globalization.Invariant to true if you want
to run with no globalization support.
...
...
(訳: ICU というパッケージが必要だからインストールしてね!
または System.Globalization.Invariant のフラグを true に設定してね!)

理由としてアプリケーションがホスト OS 依存の動作 (例えば通貨記号、ソート動作、タイムゾーン関連など) をするときに、それが必要だからというらしい。
(詳しいことは Microsoft ドキュメントを参照)
今回はアプリケーションに以下のファイルを加えて解決しました。

runtimeconfig.template.json
{
  "configProperties": {
    "System.Globalization.Invariant": true
  }
}

systemd ユニットファイル

systemd ユニットファイルは以下のものを作成しました。

MyAppName.service
[Unit]
Description=This service is a background service of MyApp.

[Service]
Type=notify
ExecStart=/my_apps/my_app_dirctory/my_app
WorkingDirectory=/my_apps/my_app_dirctory
# サービスが終了してしまったとき自動的に再起動する
Restart=always

[Install]
WantedBy=multi-user.target

Docker と systemd

systemd 対応して分かったことなのですが、Docker コンテナ内で systemd はデフォルトでは働いていないので、systemctl コマンドは使えません。
なるべく実際の使い方と同じ方法で動作確認をしたいので、色々調べてみたのですが、以下の記事に参考になることが書いてありました。
https://zenn.dev/karamawanu/articles/7428686c2217c8
docker のコンセプト・思想は1コンテナ1サービスということらしいです。systemd はサービスマネージャーなので、コンセプトとは外れてきます。
systemd を用いた統合的なテストはテスト用マシンなどで行ったほうが適切ですね。

目覚まし機能をプログラミング

目覚まし機能のプログラムの一部を以下に紹介します。

RootBackgroudService
using NLog;

internal class RootBackgroudService : BackgroundService
{
    private static readonly Logger logger = LogManager.GetCurrentClassLogger();

    public RootBackgroudService(ILogger<RootBackgroudService> systemdLogger)
    {
        NLogExtensions.SetSystemdLogger(systemdLogger);
    }

    public override async Task StartAsync(CancellationToken cancellationToken)
    {
        logger.OutputInfo("Starting background service...");
        ServiceSettingsFetcher.OutputInfo_ServiceSettings();
        await TextSpeaker.Speak_StartApp();

        await base.StartAsync(cancellationToken);
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        logger.OutputInfo("Stoping background service...");
        await TextSpeaker.Speak_StopApp();

        await base.StopAsync(cancellationToken);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var alarmClockTicker = new AlarmClockTicker(stoppingToken);
        await alarmClockTicker.StartTicking();
    }
}
AlarmClockTicker.cs
using NLog;

internal class AlarmClockTicker
{
    private readonly CancellationToken stoppingToken;
    private async Task Sleep30Sec() => await Task.Delay(30000, stoppingToken);
    private static readonly Logger logger = LogManager.GetCurrentClassLogger();

    public AlarmClockTicker(CancellationToken stoppingToken)
    {
        this.stoppingToken = stoppingToken;
    }

    public async Task StartTicking()
    {
        var previousTime_HHmm = "";
        var alarmedAtCurrentTime = false;

        while (!stoppingToken.IsCancellationRequested)
        {
            RegularInfoLog_at_00min_30min();

            var currentTime_HHmm = DateTime.Now.ToString("HH:mm");

            if(currentTime_HHmm != previousTime_HHmm)
                alarmedAtCurrentTime = false;

            if (!alarmedAtCurrentTime)
                alarmedAtCurrentTime = await AlarmClock.Alarm(currentTime_HHmm);

            previousTime_HHmm = currentTime_HHmm;
            await Sleep30Sec();
        }

        logger.OutputInfo("Stop alarm clock ticking.");
    }

    private void RegularInfoLog_at_00min_30min()
    {
        var time_mm = DateTime.Now.ToString("mm");

        if (time_mm == "00" | time_mm == "30")
            logger.OutputInfo("Alarm clock now ticks.");
    }
}
AlarmClock.cs
using NLog;

internal class AlarmClock
{
    private static readonly Logger logger = LogManager.GetCurrentClassLogger();

    /// <summary> アラームを鳴らしたら true を返す </summary>
    public async static Task<bool> Alarm(string currentTime_HHmm)
    {
        if (!ServiceSettings.alarm_setting_list.Any())
            return false;

        var alarm_setting = ServiceSettings.alarm_setting_list
            .FirstOrDefault(x => x.time == currentTime_HHmm);
        var isNotAlarmTime = alarm_setting == null;
        if (isNotAlarmTime)
            return false;

        await TextSpeaker.Speak(alarm_setting.alarm_text);
        logger.OutputInfo($"Completed '{currentTime_HHmm}' alarm.");
        return true;
    }
}

ラズパイ実機で systemctl 起動アプリの音声再生ができない問題が発生

通常起動では、目覚まし音声は再生できました。
しかし、systemctl で起動したとき、音声再生ができませんでした。(音声再生ソフトは sox を利用)
ログには以下の出力がありました。

play FAIL sox: Sorry, there is no default audio device configured
(訳: ごめん、デフォルトのオーディオデバイスが設定されていないよ!)

調べてみたところ、以下リンクの対応方法により解決することができました。
https://forums.raspberrypi.com/viewtopic.php?t=278665
https://raspberrypi.stackexchange.com/questions/120034/python-script-not-playing-audio-when-run-through-systemd

対応方法は以下です。
以下のコマンドによって認識している音声デバイスの "card 番号" を確認する
今回使用したい USB スピーカーの番号は 2 ぽい。

bash on raspi
$ cat /proc/asound/cards
 0 [vc4hdmi0       ]: vc4-hdmi - vc4-hdmi-0
                      vc4-hdmi-0
 1 [vc4hdmi1       ]: vc4-hdmi - vc4-hdmi-1
                      vc4-hdmi-1
 2 [Device         ]: USB-Audio - MosArt USB Audio Device
                      MosArt MosArt USB Audio Device at usb-xhci-hcd.0-1, full speed

/etc/asound.conf というファイルを作成する。
ファイル内容は以下のようにして、番号はデフォルトにしたいデバイスの番号を指定する。

/etc/asound.conf
defaults.pcm.card 2
defaults.ctl.card 2

これで解決できたのですが、linux の音声再生の仕組みについて深堀しないと腹落ちしませんね。
// ToDo: linux の音声再生の仕組みについてまとめる

以下の記事に続く
オリジナルのスマートスピーカーを作ってみる 6 GUI作成

Discussion