An Embedded Engineer’s Blog

とある組み込みエンジニアの備忘録的なブログです。

Python学習メモ - その1

まえがき

最近、Pythonを勉強し始めたので、その学習メモです。


Hello, World

まずは、おなじみの「Hello, World」から。

print('Hello, World')

実行結果

Hello, World

余計なものは何もいらない。そうPythonならね。


文字列処理

続いてはよく使う文字列処理です。


文字列リテラル

Pythonでは、シングルクォート('')とダブルクォート("")のどちらでも文字列と認識されます。

print('Single Quote String')
print("Double Quote String")

実行結果

Single Quote String
Double Quote String


複数行文字列

文字列を3連クォートでくくることによって、複数行の文字列を定義することができます。

multiline_str = """line1
line2
line3
"""

print(multiline_str)

実行結果

line1
line2
line3


もしくは、改行文字("\n")を含めることでも複数行の文字列を定義することができます。

multiline_str = "line1\nline2\nline3"

print(multiline_str)

実行結果

line1
line2
line3


文字列連結

文字列の連結は"+"演算子で行うことができます。

concat_str1 = "con" + "cat" + "_" + "str"

print(concat_str1)

実行結果

concat_str


変数に格納した文字列に、"+"演算子で次々に連結して行くこともできます。

concat_str2 = "con"
concat_str2 = concat_str2 + "cat"
concat_str2 = concat_str2 + "_"
concat_str2 = concat_str2 + "str"

print(concat_str2)

実行結果

concat_str


また、"+="演算子を使用することで、同じ変数に対して次々と連結していくことができます。

concat_str2 = "012"
concat_str2 += "34"
concat_str2 += "567"
concat_str2 += "8"
concat_str2 += "9"

print(concat_str2)

実行結果

0123456789


繰り返し

文字列の繰り返しは"*"演算子で繰り返し回数をかけることで定義できます。

repeat_str1 = "abc" * 3

print(repeat_str1)

実行結果

abcabcabc


文字列の連結("+")と繰り返し("*")を組み合わせて行うこともできます。

repeat_str2 = ("test" + "_") * 5 + "test"

print(repeat_str2)

実行結果

test_test_test_test_test_test


文字列変換

数値などを文字列に変換する場合には、str()関数を使用します。

int_value = 100
int_str = str(int_value) + "%"

print(int_str)

float_value = 3.14
float_value = "π = " + str(float_value)

print(float_value)

実行結果

100%
π = 3.14


大文字/小文字変換

文字列のアルファベットをすべて大文字にするためにはupper()メソッドを使用します。
また、すべて小文字にするためにはlower()メソッドを使用します。

input_str = "TesT"

print(input_str.upper())
print(input_str.lower())

実行結果

TEST
test


置換

文字列を置換するためには、replace()メソッドを使用します。

input_str = "Hello, World"

print(input_str)

replace_str = input_str.replace("World", "Japan")

print(replace_str)

実行結果

Hello, World
Hello, Japan


分割

文字列を特定の区切り文字で分割するためには、split()メソッドを使用します。

分割された文字列はlist 型で返されます。

input_str = "a,b,c,d,e,f"

print(input_str)

split_str = input_str.split(",")

print(split_str)

実行結果

a,b,c,d,e,f
['a', 'b', 'c', 'd', 'e', 'f']


桁揃え

数値などの桁揃えを行いたいときは、rjust()メソッド、ljust()メソッドを使用します。

rjust() / ljust()メソッドの引数には、揃えたい桁数と桁揃え時に埋め込む文字を指定します。

例えば、10桁に満たない数値の左端を0で埋めたい場合には、以下のようにrjust()メソッドを使用します。

input_str = "1234"

print(input_str.rjust(10, "0"))

実行結果

0000001234


0埋めなどをせず、単純な左揃え、右揃えをしたい場合には、空白を指定してrjust()メソッド、ljust()メソッドを使用します。

input_str = "1234"

print("|" + input_str.rjust(5, " ") + "|")

print("|" + input_str.ljust(5, " ") + "|")

実行結果

| 1234|
|1234 |


0埋め

特定の文字埋めをせず、単純な0埋めのみをしたい場合には、zfill()メソッドを使用します。

input_str = "1234"

print(input_str.zfill(5))
print(input_str.zfill(3))

実行結果

01234
1234


検索

ある文字列が、特定の文字列から始まるかどうかを判定する場合には、startswith()メソッドを使用します。

input_str = "Hello, World"

print(input_str.startswith("Hello"))
print(input_str.startswith("World"))

実行結果

True
False


逆に、ある文字列が、特定の文字列で終わるかどうかを判定する場合には、endswith()メソッドを使用します。

input_str = "Hello, World"

print(input_str.endswith("Hello"))
print(input_str.endswith("World"))

実行結果

False
True


また、ある文字列に特定の文字列が含まれているかどうかを判定する場合には、"in"演算子を使用します。

input_str = "test"

print("e" in input_str)
print("a" in input_str)

実行結果

True
Flase


先頭/末尾の削除

文字列の先頭や末尾から特定の文字を削除したい場合には、lstrip()メソッド、rstrip()メソッドを使用します。
メソッドの引数に何も指定しない(引数を省略した)場合は、空白を除去します。
メソッドの引数に特定の文字列を指定した場合は、指定された文字列を除去します(lstrip()メソッドの場合は、指定文字列から始まる場合、rstrip()メソッドの場合は指定文字列で終わる場合のみ)。

input_str = "     1_test_1     "
print("|" + input_str + "|")

input_str = input_str.lstrip()
print("|" + input_str + "|")

input_str = input_str.lstrip("1_")
print("|" + input_str + "|")

input_str = input_str.lstrip("_1")
print("|" + input_str + "|")

print()

input_str = "     1_test_1     "
print("|" + input_str + "|")

input_str = input_str.rstrip()
print("|" + input_str + "|")

input_str = input_str.rstrip("_1")
print("|" + input_str + "|")

input_str = input_str.rstrip("1_")
print("|" + input_str + "|")

実行結果

|     1_test_1     |
|1_test_1     |
|test_1     |
|test_1     |

|     1_test_1     |
|     1_test_1|
|     1_test|
|     1_test|


ある文字列の、先頭および末尾に含まれる余分な空白を除去したい場合には、lstrip()メソッドとrstrip()メソッドを組み合わせて以下のように呼び出します。

input_str = "    test    "
print("|" + input_str + "|")

input_str = input_str.lstrip().rstrip()
print("|" + input_str + "|")

実行結果

|    test    |
|test|


参考文献

Python 公式リファレンス

https://docs.python.org/ja/3.7/index.html

Python-izm

https://www.python-izm.com/

ゲームを作りながら楽しく学べるPythonプログラミング

エキスパートPythonプログラミング

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

Excelの列名 <—> 列番号相互変換

まえがき

Excelの列名(A, B, ..., AA, AB)と列番号(1, 2, ...)を相互変換する方法のメモです。
ついでに、相互変換するWindows用アプリケーションも作成しました。

列名 <--> 列番号相互変換

共通定数

まずは、共通で使用する定数を定義します。

// アルファベットの文字数(26文字)
public const ulong AlphabetNum = ('Z' - 'A' + 1ul);

// 列名判定用正規表現パターン
public const string LabelPattern = @"^[A-Z]+$";

// 列番号判定用正規表現パターン
public const string IndexPattern = @"^[1-9][0-9]*$";


列名 --> 列番号

列名から列番号への変換は、アルファベット(A - Z)を1から26までの数値に置き換え、26進数のような形で計算することで算出できます。


 index = X_{n} * 26^{(n-1)} + X_{(n-1)}  * 26^{(n-2)}  + ... + X_{1} * 26^{0}
 n : \mbox{列名の桁数}
 X : \mbox{アルファベットを数値に変換した値}


Ex)

列名 計算式 結果
A  1 * 26^ 0 1
B  2 * 26^ 0 2
Z  26 * 26^ 0 26
AA  (1 * 26^ 1) + (1 * 26^ 0) 27
AB  (1 * 26^ 1) + (2 * 26^ 0) 28
ZZ  (26 * 26^ 1) + (26 * 26^ 0) 702


public static string ConvertLabelToIndex(string label)
{
    // 入力文字列が列名パターンにマッチしない
    if (!Regex.IsMatch(label, LabelPattern))
    {
        // 入力値エラー
        throw new ArgumentException($"無効なラベル名です : {label}");
    }
    else
    {
        // 出力用列番号初期化
        var index = 0ul;

        // 底の初期化(n = 26^x : x = 0)
        var n = 1ul;

        // 入力列名を文字配列に変換(ex : ABC -> A, B, C)
        var array = label.ToList();

        // 文字配列を反転(ex : A, B, C -> C, B, A)
        array.Reverse();

        // 反転した文字配列を1文字ずつ走査
        foreach (var c in array)
        {
            // 現在のアルファベット文字を数値に変換(A - Z : 1 - 26)
            var x = (ulong)(c - 'A') + 1ul;

            // アルファベットに対応する数値と底を乗算
            var y = x * n;

            // 乗算した結果を列番号に加算
            index += y;

            // 次の桁の底を算出(26^0 -> 26^1 -> 26^2)
            n *= ColumnIndexConverter.AlphabetNum;
        }

        // 列番号を文字列に変換して出力
        return $"{index}";
    }
}


列番号 --> 列名

列番号から列名への変換は、列名から列番号への変換と逆のことをやれば良いということになります。

  1. 列番号を入力値にセット( x = index)
  2. 入力値が0始まりになるように1減算( x = x - 1)
  3. 入力値とアルファベット文字数(26)の剰余を算出( m = Mod(x, 26))
  4. 算出した剰余(0 - 25)をアルファベット文字(A - Z)に変換( A = \mbox{'A'} + m)
  5. 変換したアルファベット文字を列名の先頭に追加([text: label = A + label])
  6. 入力値とアルファベット文字数の商を次の入力値にセット( x = x / 26)
  7. 入力値が0になるまで2〜5を繰り返す


Ex1) 列番号 = 1

  1.  x = 1
  2.  x = x - 1 = 0
  3.  m = Mod(x, 26) = Mod(0, 26) = 0
  4.  A = \mbox{'A'} + m = \mbox{'A'} + 0 = \mbox{'A'}
  5.  label = \mbox{'A'} + label = \mbox{'A'} + \mbox{""} = \mbox{"A"}
  6.  x = x / 26 = 0 / 26 = 0
  7.  x = 0のため終了


Ex2) 列番号 = 2

  1.  x = 2
  2.  x = x - 1 = 1
  3.  m = Mod(x, 26) = Mod(1, 26) = 1
  4.  A = \mbox{'A'} + m = \mbox{'A'} + 1 = \mbox{'B'}
  5.  label = \mbox{'B'} + label = \mbox{'B'} + \mbox{""} = \mbox{"B"}
  6.  x = x / 26 = 0 / 26 = 0
  7.  x = 0のため終了


Ex3) 列番号 = 28

  1.  x = 28
  2.  x = x - 1 = 27
  3.  m = Mod(x, 26) = Mod(27, 26) = 1
  4.  A = \mbox{'A'} + m = \mbox{'A'} + 1 = \mbox{'B'}
  5.  label = \mbox{'B'} + label = \mbox{'B'} + \mbox{""} = \mbox{"B"}
  6.  x = x / 26 = 27 / 26 = 1
  7.  x = x - 1 = 0
  8.  m = Mod(x, 26) = Mod(0, 26) = 0
  9.  A = \mbox{'A'} + m = \mbox{'A'} + 0 = \mbox{'A'}
  10.  label = \mbox{'A'} + label = \mbox{'A'} + \mbox{"B"} = \mbox{"AB"}
  11.  x = x / 26 = 0 / 26 = 0
  12.  x = 0のため終了


public static string ConvertIndexToLabel(string index)
{
    // 入力文字列が列番号パターンにマッチしない
    if (!Regex.IsMatch(index, IndexPattern))
    {
        // 入力値エラー
        throw new ArgumentException($"無効なインデックスです : {index}");
    }
    else
    {
        // 入力文字列を数値に変換
        var value = ulong.Parse(index);

        // 入力数値が0以下
        if (value <= 0)
        {
            // 入力値エラー
            throw new ArgumentException($"無効なインデックスです : {index}");
        }
        else
        {
            // 出力用列名を初期化
            var label = string.Empty;

            // アルファベット文字数(26)を取得
            var a = ColumnIndexConverter.AlphabetNum;

            // 入力値が0より大きい間繰り返す
            while (value > 0)
            {
                // 入力値を0始まりになるようにデクリメント
                value--;

                // 入力値の剰余を文字に変換
                var c = (char)('A' + (value % a));

                // 変換した文字を列名の先頭に追加
                label = $"{c}{label}";

                // 入力値とアルファベット文字数の商を次の入力値にセット
                value /= a;
            }

            // 列名を出力
            return label;
        }
    }
}


相互変換アプリケーション

CUI版と、GUI版を作成しました。
GitHubにアップしていますので、詳細はそちらをご覧ください。

f:id:an-embedded-engineer:20190504174446p:plain:w500
CUIアプリケーション


f:id:an-embedded-engineer:20190504174513p:plain:w500
GUIアプリケーション(列番号 → 列名)


f:id:an-embedded-engineer:20190504174615p:plain:w500
GUIアプリケーション(列名 → 列番号)

UMLのステートマシン図を実装する for C# - まとめ

まとめ

UMLのステートマシン図を実装する for C#」のまとめ記事です。
次のようなステートマシンをC#で実装する方法について紹介しています。

f:id:an-embedded-engineer:20190414174321p:plain
エアコンステートマシン


No. 内容 Link 備考
その1 ステートマシンベースクラス実装 Link
その2 ステートマシンサンプル説明 Link
その3 ステートマシンサンプル実装(1) Link
その4 ステートマシンサンプル実装(2) Link
その5 エアコンモデル実装 Link
その6 エアコンアプリケーション実装(CUI) Link
その7 エアコンアプリケーション実装(GUI) Link


ソースコード一式

ソースコード一式はGitHubにアップしています。
https://github.com/an-embedded-engineer/StateMachineSample


Visual Studio 2017 / 2019にてビルド&動作確認しています。

UMLのステートマシン図を実装する for C# - その7

まえがき

ステートマシンをGUI(WPF)で動作させるアプリケーションを実装していきます。

f:id:an-embedded-engineer:20190414174321p:plain
エアコンステートマシン


事前準備

今回は、WPFMVVMリアクティブプログラミングGUIアプリケーションを実装しようと思います。


MVVMのフレームワークにはLivetを使用します。 こちらからプロジェクトテンプレート(Visual Studio 2017用)をダウンロードして、インストールしてください。


インストールしたLivetプロジェクトテンプレートを使用してWPFアプリケーション用プロジェクトを作成します。


また、リアクティブプログラミングを実現するために、NuGetからReactive Extension(System.Reactive)ReactivePropertyWPFプロジェクトにインストールします。


NotificationObject クラス

MVVMではプロパティ(=データ)が変更されたことをUIに通知するためにINotifyPropertyChanged インタフェースを実装する必要があります。
そのため、INotifyPropertyChanged インタフェースを実装したベースクラスとして、NotificationObject クラスを実装します。

using System.ComponentModel;

public class NotificationObject : INotifyPropertyChanged
{
    // プロパティ変更イベントハンドラ
    public event PropertyChangedEventHandler PropertyChanged;

    // プロパティ変更イベント通知
    protected void RaisePropertyChanged(string name)
    {
        // プロパティ変更イベントハンドラ呼び出し(登録されていたら)
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}


プロパティ変更通知処理

エアコンステートマシンおよびエアコンモデルにおいて、GUIに表示するために、値の変更通知が必要なプロパティを変更します。 値の変更を通知できるようにするために、上記で実装したNotificationObjectを継承します。

StateMachine クラス

StateMachine クラスでは現在の状態を示すCurrentStateプロパティに、プロパティ変更イベント通知処理を組み込みます。

public abstract class StateMachine : NotificationObject


// 現在状態(データ本体)
private State _CurrentState { get; set; }

// 現在状態(プロパティ変更イベント通知用)
public State CurrentState
{
    get { return this._CurrentState; }
    set
    {
        // 現在の状態と異なる値が代入された
        if (this._CurrentState != value)
        {
            // 現在の状態を更新
            this._CurrentState = value;
            // プロパティ値の変更を通知
            this.RaisePropertyChanged(nameof(this.CurrentState));
        }
    }
}


AirConditioner クラス

同様に、AirConditioner クラスにおいても値の変更通知が必要なプロパティに、、プロパティ変更イベント通知処理を組み込みます。

public class AirConditioner : NotificationObject


// 現在温度(本体)
private int _Temperature { get; set; }

// 現在温度(プロパティ変更イベント通知用)
public int Temperature
{
    get { return this._Temperature; }
    set
    { 
        if (this._Temperature != value)
        {
            this._Temperature = value;
            this.RaisePropertyChanged(nameof(this.Temperature));
        }
    }
}

// 目標温度(本体)
private int _TargetTemperature { get; set; }

// 目標温度(プロパティ変更イベント通知用)
public int TargetTemperature
{
    get { return this._TargetTemperature; }
    set
    {
        if (this._TargetTemperature != value)
        {
            this._TargetTemperature = value;
            this.RaisePropertyChanged(nameof(this.TargetTemperature));
        }
    }
}

// 現在湿度(本体)
private int _Humidity { get; set; }

// 現在湿度(プロパティ変更イベント通知用)
public int Humidity
{
    get { return this._Humidity; }
    set
    {
        if (this._Humidity != value)
        {
            this._Humidity = value;
            this.RaisePropertyChanged(nameof(this.Humidity));
        }
    }
}


MainWindow クラス(Xaml)

プロジェクト生成時に自動的に生成されたMainWindow.xamlを編集してGUIの画面設計をしていきます。

<Window x:Class="StateMachineSample.WPF.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:StateMachineSample.WPF.Views"
        xmlns:vm="clr-namespace:StateMachineSample.WPF.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="20"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="20"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
        <Style TargetType="ListBoxItem">
            <Setter Property="FontSize" Value="15"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
        </Style>
        <Style TargetType="StatusBarItem">
            <Setter Property="FontSize" Value="15"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
        </Style>
    </Window.Resources>

    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/> <!-- Command Buttons -->
            <RowDefinition Height="Auto"/> <!-- Target Temp Slider -->
            <RowDefinition Height="Auto"/> <!-- Latest Message -->
            <RowDefinition Height="*"/> <!-- Message Logs -->
            <RowDefinition Height="Auto"/> <!-- Status -->
        </Grid.RowDefinitions>

        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <!-- ストップボタン -->
            <Button Grid.Column="0" Content="Stop" Command="{Binding StopCommand}" />
            <!-- スタートボタン -->
            <Button Grid.Column="1" Content="Start" Command="{Binding StartCommand}" />
            <!-- 冷房ボタン -->
            <Button Grid.Column="2" Content="Cool" Command="{Binding CoolCommand}" />
            <!-- 暖房ボタン -->
            <Button Grid.Column="3" Content="Heat" Command="{Binding HeatCommand}" />
            <!-- 除湿ボタン -->
            <Button Grid.Column="4" Content="Dry" Command="{Binding DryCommand}" />
            <!-- クリーニングボタン -->
            <Button Grid.Column="5" Content="Clean" Command="{Binding CleanCommand}" />
        </Grid>
        <Grid Grid.Row="1">
            <StackPanel Orientation="Horizontal">
                <!-- 目標温度変更スライドバー -->
                <TextBlock Text="Target Temperature : " />
                <Slider Value="{Binding TargetTemperature.Value}" Width="100"
                        VerticalAlignment="Center"
                        Minimum="{Binding MinTargetTemperature}"
                        Maximum="{Binding MaxTargetTemperature}"/>
                <!-- 目標温度アップボタン -->
                <Button Content="Up" Command="{Binding UpCommand}" Width="80" />
                <!-- 目標温度ダウンボタン -->
                <Button Content="Down" Command="{Binding DownCommand}" Width="80" />
            </StackPanel>
        </Grid>
        <Grid Grid.Row="2">
            <StackPanel Orientation="Horizontal">
                <!-- 最新受信メッセージ -->
                <TextBlock Text="Message : " />
                <TextBlock Text="{Binding Message.Value}" />
            </StackPanel>
        </Grid>
        <Grid Grid.Row="3">
            <!-- 受信メッセージログ -->
            <ListBox ItemsSource="{Binding MessageLog}" Margin="5"
                     ScrollViewer.HorizontalScrollBarVisibility="Auto" 
                     ScrollViewer.VerticalScrollBarVisibility="Visible"/>
        </Grid>
        <Grid Grid.Row="4">
            <StatusBar>
                <!-- 現在のステートマシン状態表示 -->
                <StatusBarItem Content="状態:" />
                <StatusBarItem Content="{Binding Status.Value}" />
                <Separator/>
                <!-- 目標温度表示 -->
                <StatusBarItem Content="目標温度:" />
                <StatusBarItem Content="{Binding TargetTemp.Value}" />
                <Separator/>
                <!-- 現在の温度表示 -->
                <StatusBarItem Content="温度:" />
                <StatusBarItem Content="{Binding Temperature.Value}" />
                <Separator/>
                <!-- 現在の湿度表示 -->
                <StatusBarItem Content="湿度:" />
                <StatusBarItem Content="{Binding Humidity.Value}" />
            </StatusBar>
        </Grid>
    </Grid>
</Window>


上記のようにxamlファイルを編集すると、以下のようなデザインの画面になります。

f:id:an-embedded-engineer:20190502232943p:plain:w500
GUIサンプル


MainWindowViewModel クラス

最後に、ViewModelの実装を行っていきます。
ViewModelはView(MainWindow)に描画するための状態やデータの保持と、Viewから受け取った入力(ボタン押下、テキスト入力など)を適切な形式に変換し、Model(エアコンモデル、ステートマシン)に伝達する役割を持ちます。


今回は、ViewとModelの仲介方法として、Reactive ExtensionおよびReactivePropertyを使用したリアクティブプログラミングを使用します。
リアクティブプログラミングを用いる事によって、以下のような処理の扱いが非常に簡単になります。

  • GUIによる入出力
  • 時間経過で状態が変換するもの
  • 非同期の通信処理


Model

MVVMで言うところのModelとは、アプリケーショのドメイン(問題領域)を解決するために、そのアプリケーションが扱う領域のデータと手続き(ビジネスロジック)を表現する要素のことを言います。
今回のサンプルでは、エアコンモデル(AirConditioner)やエアコンステートマシンが該当します。

// エアコンモデル
public AirConditioner Model { get; }

// エアコンステートマシン
public ModelStateMachine StateMachine { get; }


データバインディングプロパティ

WPFのデータバインディングによってGUIに表示するデータを保持するプロパティを定義します。
リアルタイムで値が変化するプロパティはReactivePropertyを使用します。
ReactivePropertyを使用することによって、データの値が変化したときにその変更がUIに通知され、Viewで表示している値に反映されます。

// 受信メッセージ
public ReactiveProperty<string> Message { get; }

// 受信メッセージログ
public ReadOnlyReactiveCollection<string> MessageLog { get; }

// 現在の状態
public ReactiveProperty<State> CurrentState { get; }

// 最大目標温度
public int MaxTargetTemperature { get; }

// 最小目標温度
public int MinTargetTemperature { get; }

// 目標温度
public ReactiveProperty<int> TargetTemperature { get; }

// 現在の状態(ステータスバー表示用)
public ReactiveProperty<string> Status { get; }

// 目標温度(ステータスバー表示用)
public ReactiveProperty<string> TargetTemp { get; }

// 現在の温度(ステータスバー表示用)
public ReactiveProperty<string> Temperature { get; }

// 現在の湿度(ステータスバー表示用)
public ReactiveProperty<string> Humidity { get; }


コマンド

ユーザからの入力を変換してModelに伝達するためのコマンドを定義します。
コマンドはReactiveCommandを使用します。
ReactiveCommandを使用することで、ReactivePropertyで変化したデータの値に応じてコマンドの使用可否(ボタンの有効/無効)などを自動的に切り替えることができます。

// ストップコマンド
public ReactiveCommand StopCommand { get; }

// スタートコマンド
public ReactiveCommand StartCommand { get; }

// 冷房コマンド
public ReactiveCommand CoolCommand { get; }

// 暖房コマンド
public ReactiveCommand HeatCommand { get; }

// 除湿コマンド
public ReactiveCommand DryCommand { get; }

// クリーニングコマンド
public ReactiveCommand CleanCommand { get; }

// 目標温度アップコマンド
public ReactiveCommand UpCommand { get; }

// 目標温度ダウンコマンド
public ReactiveCommand DownCommand { get; }


コンストラク

今回のサンプルは、すべての処理の定義がコンストラクタ内で完結します。 ReactivePropertyやReactiveCommandを使用することで、データの変化に対する処理やユーザからの入力に対する処理を「宣言的」に記述することができます。

// コンストラクタ
public MainWindowViewModel()
{
}


固定値設定

時間的に変化しない固定値プロパティをセットします。
今回は、目標温度のリミッタに使用する最大/最小値をセットします。

// 最大目標温度
this.MaxTargetTemperature = AirConditioner.MaxTargetTemperature;

// 最小目標温度
this.MinTargetTemperature = AirConditioner.MinTargetTemperature;


ステータス情報初期化

ステータスバーに表示する各種情報(状態、温度、湿度など)を空の文字列を初期値とし、string型を保持するReactivePropertyとして宣言します。

this.Status = new ReactiveProperty<string>("");

this.TargetTemp = new ReactiveProperty<string>("");

this.Temperature = new ReactiveProperty<string>("");

this.Humidity = new ReactiveProperty<string>("");


受信メッセージロギング

ステートマシンから送信されるメッセージをロギングする処理を記述します。


最新受信メッセージは初期値空(null)のstring型を保持するReactivePropertyとして宣言します。
初期値をnullとして宣言する理由は、空文字("")にしてしまうと、受信メッセージログに変換した際に、空行が含まれてしまうためです。

// 最新受信メッセージ初期化
this.Message = new ReactiveProperty<string>();


受信メッセージログは、最新受信メッセージを、受け取った順番に保持するReactiveCollectionに変換して使用します。

// 最新受信メッセージをReactive Collectionに変換し、受信メッセージログにする
this.MessageLog = this.Message.ToReadOnlyReactiveCollection();


Messengerの受信イベントハンドラには、受信したメッセージ文字列を最新受信メッセージプロパティにセットする処理を記述します。
こうすることで、ステートマシンからメッセージが送信されると、最新受信メッセージプロパティに受信メッセージがセットされ、受信したメッセージがログとしてリスト化されるようになります。

using StmMessenger = StateMachineSample.Lib.Messenger;

// メッセージ受信イベントハンドラ登録
StmMessenger.OnMessageReceived += (message) =>
{
    // 最新受信メッセージに受信したメッセージ文字列をセット
    this.Message.Value = message;
};


Model初期化

今回のModelにあたるエアコンモデル(AirConditioner)とエアコンステートマシンのインスタンスを生成します。

// エアコンモデルインスタンス生成
this.Model = new AirConditioner();

// エアコンステートマシンインスタンス生成
this.StateMachine = new ModelStateMachine(this.Model);


Model - Viewデータ接続

ReactivePropertyを用いて、Modelの状態を監視し、ModelとViewのデータ接続を行います。


エアコンステートマシンの現在の状態を監視し、ReactivePropertyに変換します。
これによって、ステートマシンの状態が変化するたびにViewModelのCurrentStateが更新されます。
CurrentStateの値は、各種コマンドの実行可否(ボタンの有効/無効)制御に使用します。

// エアコンステートマシンの現在状態を監視し、ReactivePropertyに変換(Model -> View単方向)
this.CurrentState = this.StateMachine.ObserveProperty(stm => stm.CurrentState).ToReactiveProperty();


エアコンモデルの目標温度を監視し、ReactivePropertyに変換します。 目標温度はModelからViewへの変更通知(プログラム上から目標温度変更)とViewからModelへの変更通知(ユーザ入力から目標温度変更)が必要となるため、双方向の監視が必要になります。

// エアコンモデルの目標温度を監視し、ReactivePropertyに変換(Model <-> View双方向)
this.TargetTemperature = this.Model.ToReactivePropertyAsSynchronized(model => model.TargetTemperature);


コマンド宣言

ユーザからの入力を受け付けるコマンドを宣言します。
ReactiveCommandは、宣言時に実行可否を切り替える条件を指定することができます。
今回の場合は、実行可能な場合は、対応するボタンが押下可能な状態になり、実行不可能な状態になっている場合は、ボタンが押下不可能になることで、コマンドを実行できない状態になります。

// 現在の状態がRunning状態またはClean状態の時に実行可能なストップコマンドを宣言
this.StopCommand = this.CurrentState.Select(s => s is RunningState || s is CleanState).ToReactiveCommand();

// 現在の状態がStop状態の時に実行可能なスタートコマンドを宣言
this.StartCommand = this.CurrentState.Select(s => s is StopState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能な冷房コマンドを宣言
this.CoolCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能な暖房コマンドを宣言
this.HeatCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能な除湿コマンドを宣言
this.DryCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能なクリーニングコマンドを宣言
this.CleanCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の目標温度が最大目標温度より小さい場合に実行可能な目標温度アップコマンドを宣言
this.UpCommand = this.TargetTemperature.Select(v => v < this.MaxTargetTemperature).ToReactiveCommand();

// 現在の目標温度が最小目標温度より大きい場合に実行可能な目標温度ダウンコマンドを宣言
this.DownCommand = this.TargetTemperature.Select(v => v > this.MinTargetTemperature).ToReactiveCommand();


コマンド処理定義

コマンドが実行された時の処理を定義します。
今回は、ステートマシンのトリガ送信や、エアコンモデルの目標温度変更を実行するようにします。

// ストップコマンドが実行されたら、エアコンステートマシンにSwitchStopトリガを送信
this.StopCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStopTrigger.Instance));

// スタートコマンドが実行されたら、エアコンステートマシンにSwitchStartトリガを送信
this.StartCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStartTrigger.Instance));

// 冷房コマンドが実行されたら、エアコンステートマシンにSwitchCoolトリガを送信
this.CoolCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCoolTrigger.Instance));

// 暖房コマンドが実行されたら、エアコンステートマシンにSwitchHeatトリガを送信
this.HeatCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchHeatTrigger.Instance));

// 除湿コマンドが実行されたら、エアコンステートマシンにSwitchDryトリガを送信
this.DryCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchDryTrigger.Instance));

// クリーニングコマンドが実行されたら、エアコンステートマシンにSwitchCleanトリガを送信
this.CleanCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCleanTrigger.Instance));

// 目標温度アップコマンドが実行されたら、エアコンモデルに目標温度アップを要求
this.UpCommand.Subscribe(_ => this.Model.Up());

// 目標温度ダウンコマンドが実行されたら、エアコンモデルに目標温度ダウンプを要求
this.DownCommand.Subscribe(_ => this.Model.Down());


周期処理

周期的にModelの状態を監視し、Viewに反映させる処理を実装します。
今回はReactive Extensionを使用して100msごとに呼び出されるインターバルタイマを作成し、ステータスバーに表示する各種状態値を更新と、ステートマシンの定常処理を実行するようにします。
Modelで保持されている状態値をユーザが見やす形式に変換する(単位をつける、いくつかの情報を結合するなど)のもViewModelの役割です。

// 100ms間隔のインターバルタイマ生成
var interval = Observable.Interval(TimeSpan.FromMilliseconds(100));

// タイマで周期実行する処理の定義
var timer_sub = interval.Subscribe(
    i =>
    {
        // ステートマシンの現在の状態がRunning状態
        if (this.StateMachine.CurrentState is RunningState running_state)
        {
            // サブステートマシンを取得
            var sub = running_state.SubContext;

            // 現在の状態(メイン状態 - サブ状態)を文字列に変換してセット
            this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
        }
        // ステートマシンの現在の状態がClean状態
        else if (this.StateMachine.CurrentState is CleanState clean_state)
        {
            // サブステートマシンを取得
            var sub = clean_state.SubContext;

            // 現在の状態(メイン状態 - サブ状態)を文字列に変換してセット
            this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
        }
        else
        {
            // 現在の状態(メイン状態)を文字列に変換してセット
            this.Status.Value = $"{this.StateMachine.CurrentState}";
        }

        // 目標温度を文字列に変換してセット
        this.TargetTemp.Value = $"{this.Model.TargetTemperature}[℃]";
        // 現在の温度を文字列に変換してセット
        this.Temperature.Value = $"{this.Model.Temperature}[℃]";
        // 現在の温度を文字列に変換してセット
        this.Humidity.Value = $"{this.Model.Humidity}[%]";

        // ステートマシンの定常処理実行
        this.StateMachine.Update();
    });


全体

ソース全体を示します。

public class MainWindowViewModel : ViewModel
{
    // エアコンモデル
    public AirConditioner Model { get; }

    // エアコンステートマシン
    public ModelStateMachine StateMachine { get; }

    // 受信メッセージ
    public ReactiveProperty<string> Message { get; }

    // 受信メッセージログ
    public ReadOnlyReactiveCollection<string> MessageLog { get; }

    // 現在の状態
    public ReactiveProperty<State> CurrentState { get; }

    // 最大目標温度
    public int MaxTargetTemperature { get; }

    // 最小目標温度
    public int MinTargetTemperature { get; }

    // 目標温度
    public ReactiveProperty<int> TargetTemperature { get; }

    // 現在の状態(ステータスバー表示用)
    public ReactiveProperty<string> Status { get; }

    // 目標温度(ステータスバー表示用)
    public ReactiveProperty<string> TargetTemp { get; }

    // 現在の温度(ステータスバー表示用)
    public ReactiveProperty<string> Temperature { get; }

    // 現在の湿度(ステータスバー表示用)
    public ReactiveProperty<string> Humidity { get; }

    // ストップコマンド
    public ReactiveCommand StopCommand { get; }

    // スタートコマンド
    public ReactiveCommand StartCommand { get; }

    // 冷房コマンド
    public ReactiveCommand CoolCommand { get; }

    // 暖房コマンド
    public ReactiveCommand HeatCommand { get; }

    // 除湿コマンド
    public ReactiveCommand DryCommand { get; }

    // クリーニングコマンド
    public ReactiveCommand CleanCommand { get; }

    // 目標温度アップコマンド
    public ReactiveCommand UpCommand { get; }

    // 目標温度ダウンコマンド
    public ReactiveCommand DownCommand { get; }

    // コンストラクタ
    public MainWindowViewModel()
    {
        this.MaxTargetTemperature = AirConditioner.MaxTargetTemperature;

        this.MinTargetTemperature = AirConditioner.MinTargetTemperature;

        this.Status = new ReactiveProperty<string>("");

        this.TargetTemp = new ReactiveProperty<string>("");

        this.Temperature = new ReactiveProperty<string>("");

        this.Humidity = new ReactiveProperty<string>("");

        this.Message = new ReactiveProperty<string>();

        this.MessageLog = this.Message.ToReadOnlyReactiveCollection();

        StmMessenger.OnMessageReceived += (message) =>
        {
            this.Message.Value = message;
        };

        this.Model = new AirConditioner();

        this.StateMachine = new ModelStateMachine(this.Model);

        this.CurrentState = this.StateMachine.ObserveProperty(stm => stm.CurrentState).ToReactiveProperty();

        this.TargetTemperature = this.Model.ToReactivePropertyAsSynchronized(model => model.TargetTemperature);

        this.StopCommand = this.CurrentState.Select(s => s is RunningState || s is CleanState).ToReactiveCommand();

        this.StartCommand = this.CurrentState.Select(s => s is StopState).ToReactiveCommand();

        this.CoolCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.HeatCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.DryCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.CleanCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.UpCommand = this.TargetTemperature.Select(v => v < this.MaxTargetTemperature).ToReactiveCommand();

        this.DownCommand = this.TargetTemperature.Select(v => v > this.MinTargetTemperature).ToReactiveCommand();


        this.StopCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStopTrigger.Instance));

        this.StartCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStartTrigger.Instance));

        this.CoolCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCoolTrigger.Instance));

        this.HeatCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchHeatTrigger.Instance));

        this.DryCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchDryTrigger.Instance));

        this.CleanCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCleanTrigger.Instance));

        this.UpCommand.Subscribe(_ => this.Model.Up());

        this.DownCommand.Subscribe(_ => this.Model.Down());


        var interval = Observable.Interval(TimeSpan.FromMilliseconds(100));

        var timer_sub = interval.Subscribe(
            i =>
            {
                if (this.StateMachine.CurrentState is RunningState running_state)
                {
                    var sub = running_state.SubContext;

                    this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
                }
                else if (this.StateMachine.CurrentState is CleanState clean_state)
                {
                    var sub = clean_state.SubContext;

                    this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
                }
                else
                {
                    this.Status.Value = $"{this.StateMachine.CurrentState}";
                }

                this.TargetTemp.Value = $"{this.Model.TargetTemperature}[℃]";
                this.Temperature.Value = $"{this.Model.Temperature}[℃]";
                this.Humidity.Value = $"{this.Model.Humidity}[%]";

                this.StateMachine.Update();
            });
    }
}


まとめ

今回で、「C#UMLのステートマシン図の実装」は完成となります。


ソースコード一式はGitHubにアップしています。
https://github.com/an-embedded-engineer/StateMachineSample

UMLのステートマシン図を実装する for C# - その6

まえがき

ステートマシンをCUI(コマンドライン)で動作させるアプリケーションを実装していきます。

f:id:an-embedded-engineer:20190414174321p:plain
エアコンステートマシン


Messenger クラス

状態遷移など、ステートマシンの内部状態を把握しやすくするために、任意の文字列(ログなど)をアプリケーションに送信するMessengerクラスを定義します。
送信された文字列の表示方法はアプリケーションの種類(CUI / GUI)によって変更できるように、デリゲートとして登録するようにします。

// メッセージ受信ハンドラ(デリゲート)
public delegate void MessageReceivedHandler(string message);

public static class Messenger
{
    // メッセージ受信イベント(デリゲート)
    public static MessageReceivedHandler OnMessageReceived;

    // メッセージ送信
    public static void Send(string message)
    {
        // メッセージ受信ハンドラが登録されていたら呼び出し
        Messenger.OnMessageReceived?.Invoke(message);
    }
}


ステートマシンベースクラス(変更)

Messengerによるログ送信処理をステートマシンの各ベースクラスに追加します。
それぞれの箇所を示します。

StateMachine クラス
public void SendTrigger(Trigger trigger)
{
    Messenger.Send($"Send Trigger : {trigger.Name}");   // ★

    this.CurrentState?.SendTrigger(this, trigger);
}

public void ChangeState(State new_state, Effect effect = null)
{
    if (this.CurrentState != new_state)
    {
        var old_state = this.CurrentState;

        this.CurrentState?.ExecuteExitAction(this);

        this.CurrentState = new_state;
        this.PreviousState = old_state;

        if (old_state != null)                                              // ★
        {                                                                   // ★
            Messenger.Send($"State Changed : {old_state} => {new_state}");  // ★
        }                                                                   // ★

        effect?.Execute(this);

        this.CurrentState?.ExecuteEntryAction(this);
    }
}


State クラス
public void ExecuteEntryAction(StateMachine context)
{
    Messenger.Send($"Entry : {this.Name}");     //★

    this.OnEntry?.Invoke(context);
}

public void ExecuteDoAction(StateMachine context)
{
    Messenger.Send($"Do : {this.Name}");    //★

    this.OnDo?.Invoke(context);
}

public void ExecuteExitAction(StateMachine context)
{
    Messenger.Send($"Exit : {this.Name}");  //★

    this.OnExit?.Invoke(context);
}

public void SendTrigger(StateMachine context, Trigger trigger)
{
    if (this.TriggerActionMap.ContainsKey(trigger.Name) == true)
    {
        Messenger.Send($"Trigger : {trigger.Name}");    //★

        var action = this.TriggerActionMap[trigger.Name];

        var args = new TriggerActionArgs(context, trigger);

        action(args);
    }
}


Effect クラス
public void Execute(StateMachine context)
{
    Messenger.Send($"Execute : {this.Name}");   // ★

    this.ExecuteAction(context);
}


Program クラス

最後に、コンソールアプリケーション部分の実装です。


コンソールアプリケーション用プロジェクトを作成し、Mainメソッドを実装していきます。

class Program
{
    static void Main(string[] args)
    {
    }
}


メッセージ受信ハンドラ

ステートマシンから送信されたメッセージの受信ハンドラを実装します。
今回はコンソールアプリケーションなので、メッセージを受信したらコンソールにメッセージを出力するようにします。

// メッセージ受信イベントハンドラ登録
Messenger.OnMessageReceived += (message) =>
{
    // コンソールにメッセージを出力
    Console.WriteLine(message);
};


インスタンス生成

エアコンモデルおよびエアコンステートマシンのインスタンスを生成します。

// エアコンモデルのインスタンス生成
var model = new AirConditioner();

// エアコンステートマシンのインスタンス生成
var stm = new ModelStateMachine(model);


ループ処理

ステートマシンへのトリガ送信や状態遷移を行うためのループ処理を定義します。
ユーザから終了のリクエストがあるまで、ループし続けるようにします。

// exitフラグ初期化
var exit = false;

// exitフラグがtrueになるまでループ
while(exit == false)
{
}


ステートマシン定常処理

ループ処理内で、ステートマシンの更新処理(Update)を呼び出します。
更新処理内では、ステートマシンの「現在の状態」における「Doイベント」処理が実行されます(定義されていれば)。

// ステートマシンの更新(各状態のDoイベント処理)
stm.Update();


状態表示

ループ処理内で、モデルの現在の状態(温度 / 湿度など)を表示します。
Programクラス内にPrintメソッドを実装し、ループ処理内で呼び出すようにします。

// モデルの状態表示
Print(model);


Printメソッドでは、エアコンの目標温度、現在温度、現在湿度をコンソールに出力します。

static void Print(AirConditioner model)
{
    // 目標温度
    Console.Write($"Target Temp : {model.TargetTemperature}[deg] | ");
    // 現在温度
    Console.Write($"Temp : {model.Temperature}[deg] | ");
    // 現在湿度
    Console.Write($"Humidity : {model.Humidity}[%]");
    // 改行
    Console.WriteLine();
}


ユーザ入力

エアコンを動かすために、ユーザからの入力を受け付けます。 今回は、ユーザにコンソールからコマンド文字列を入力させるさせるようにします。

// 入力プロンプト出力
Console.Write(">");

// コマンド入力
var command = Console.ReadLine();


コマンド制御

ユーザから入力されたコマンドに応じて、エアコンステートマシンやエアコンモデルに対する要求を送信します。
また、ユーザから"exit"を要求されたらループを抜けてアプリケーションを終了させます。

コマンド 制御 備考
start ステートマシンにSwitchStartトリガ送信
stop ステートマシンにSwitchStopトリガ送信
cool ステートマシンにSwitchCoolトリガ送信
heat ステートマシンにSwitchHeatトリガ送信
dry ステートマシンにSwitchDryトリガ送信
clean ステートマシンにSwitchCleanトリガ送信
up エアコンモデルに目標温度アップ要求送信
down エアコンモデルに目標温度ダウン要求送信
exit exitフラグセット(アプリケーション終了要求)
それ以外 何もしない


switch (command)
{
    case "start":
        // ステートマシンにSwitchStartトリガ送信
        stm.SendTrigger(SwitchStartTrigger.Instance);
        break;
    case "stop":
        // ステートマシンにSwitchStopトリガ送信
        stm.SendTrigger(SwitchStopTrigger.Instance);
        break;
    case "cool":
        // ステートマシンにSwitchCoolトリガ送信
        stm.SendTrigger(SwitchCoolTrigger.Instance);
        break;
    case "heat":
        // ステートマシンにSwitchHeatトリガ送信
        stm.SendTrigger(SwitchHeatTrigger.Instance);
        break;
    case "dry":
        // ステートマシンにSwitchDryトリガ送信
        stm.SendTrigger(SwitchDryTrigger.Instance);
        break;
    case "clean":
        // ステートマシンにSwitchDryトリガ送信
        stm.SendTrigger(SwitchCleanTrigger.Instance);
        break;
    case "up":
        // エアコンモデルに目標温度アップ要求送信
        model.Up();
        break;
    case "down":
        // エアコンモデルに目標温度ダウン要求送信
        model.Down();
        break;
    case "exit":
        // exitフラグセット(アプリケーション終了要求)
        exit = true;
        break;
    default:
        // 何もしない
        break;
}


全体

ソース全体を示します。

class Program
{
    static void Main(string[] args)
    {
        Messenger.OnMessageReceived += (message) =>
        {
            Console.WriteLine(message);
        };

        var model = new AirConditioner();

        var stm = new ModelStateMachine(model);

        var exit = false;

        while(exit == false)
        {
            stm.Update();

            Print(model);

            Console.Write(">");

            var command = Console.ReadLine();

            switch (command)
            {
                case "start":
                    stm.SendTrigger(SwitchStartTrigger.Instance);
                    break;
                case "stop":
                    stm.SendTrigger(SwitchStopTrigger.Instance);
                    break;
                case "cool":
                    stm.SendTrigger(SwitchCoolTrigger.Instance);
                    break;
                case "heat":
                    stm.SendTrigger(SwitchHeatTrigger.Instance);
                    break;
                case "dry":
                    stm.SendTrigger(SwitchDryTrigger.Instance);
                    break;
                case "clean":
                    stm.SendTrigger(SwitchCleanTrigger.Instance);
                    break;
                case "up":
                    model.Up();
                    break;
                case "down":
                    model.Down();
                    break;
                case "exit":
                    exit = true;
                    break;
                default:
                    break;
            }
        }
    }

    static void Print(AirConditioner model)
    {
        Console.Write($"Target Temp : {model.TargetTemperature}[deg] | ");
        Console.Write($"Temp : {model.Temperature}[deg] | ");
        Console.Write($"Humidity : {model.Humidity}[%]");
        Console.WriteLine();
    }
}


次回予告

次回は、今回作成したアプリケーションのGUI版を実装していきます。

UMLのステートマシン図を実装する for C# - その5

まえがき

今回は、エアコンモデル(AirConditionerクラス)を実装していきます。
エアコンモデルは、今回のテーマである「ステートマシン図の実装」からは離れてしまうので、それっぽく動作するものに留めておきます。

f:id:an-embedded-engineer:20190414174321p:plain
エアコンステートマシン


StainLevel 列挙体

StainLevel列挙体は、汚れレベル解析処理結果を表し、レベルに応じてクリーニングモードを切り替えるために使用します。

public enum StainLevel
{
    // 汚れレベル未確定
    Unknown,
    // 汚れレベル低
    Low,
    // 汚れレベル高
    High,
}


AirConditioner クラス

エアコンモデルのAirConditionerクラスを実装していきます。

定数定義

定数として、目標温度や湿度の最大/最小値を定義します。

// 最大目標温度[℃]
public const int MaxTargetTemperature = 40;

// 最小目標温度[℃]
public const int MinTargetTemperature = 15;

// 最大湿度[%]
public const int MaxHumidity = 90;

// 最小湿度[%]
public const int MinHumidity = 20;


プロパティ定義

プロパティとして、エアコンの各種状態値(温度、湿度、汚れレベルなど)を定義します。

// 現在温度[℃]
public int Temperature { get; private set; }

// 目標温度[℃]
public int TargetTemperature { get; private set; }

// 湿度[%]
public int Humidity { get; private set; }

// 汚れレベル
public StainLevel StainLevel { get; private set; }

// 前回の汚れレベル
private StainLevel PrevStainLevel { get; set; }

// 汚れレベル解析時間カウント
private int AnalyseCount { get; set; }

// クリーニング時間カウント
private int CleanCount { get; set; }

// 汚れレベル解析完了フラグ : 汚れレベルがUnknown以外でtrue
private bool AnalyseFinished => (this.StainLevel != StainLevel.Unknown);

// クリーニング完了フラグ : 汚れレベルがHighでクリーニング時間20カウント以上 or 汚れレベルがLowでクリーニング時間10カウント以上
private bool CleanFinished => ((this.StainLevel == StainLevel.High && this.CleanCount >= 20) 
                            || (this.StainLevel == StainLevel.Low && this.CleanCount >= 10));


初期化処理

初期化処理では、各種プロパティに初期値をセットします。
今回はサンプルのため、設定値は適当です。
実際には、初期値設定以外にも各種ハードウェアの初期化処理などが実行されます。

public void Initialize()
{
    // 初期温度 = 30[℃]
    this.Temperature = 30;

    // 初期目標温度 = 最低目標温度[℃]
    this.TargetTemperature = AirConditioner.MinTargetTemperature;

    // 初期湿度 = 50[%]
    this.Humidity = 50;

    // 初期汚れレベル = Unknown
    this.StainLevel = StainLevel.Unknown;

    // 初期前回汚れレベル = Unknown
    this.PrevStainLevel = StainLevel.Unknown;

    // 汚れレベル解析時間カウントクリア
    this.AnalyseCount = 0;

    // クリーニング時間カウントクリア
    this.CleanCount = 0;
}


動作開始処理

動作開始処理では、エアコンの温度/湿度制御の開始処理を行います。
今回はサンプルのため、現在温度と湿度を初期値にリセットしています。

public void Start()
{
    // 現在温度 = 30[℃]
    this.Temperature = 30;

    // 現在湿度 = 50[%]
    this.Humidity = 50;
}


動作停止処理

動作開始処理では、エアコンの温度/湿度制御やクリーニングの停止処理を行います。
今回はサンプルのため、各種クリーニング状態を初期値にリセットしています。

public void Stop()
{
    // 汚れレベルリセット
    this.StainLevel = StainLevel.Unknown;

    // 前回汚れレベルリセット
    this.PrevStainLevel = StainLevel.Unknown;

    // 汚れレベル解析時間カウントクリア
    this.AnalyseCount = 0;

    // クリーニング時間カウントクリア
    this.CleanCount = 0;
}


目標温度設定処理

エアコンの目標温度は、ユーザがアップボタン / ダウンボタンを押下することによって上下させる事ができるようにします。
アップボタン / ダウンボタンそれぞれのインタフェースとして、それぞれUpメソッド、Downメソッドを用意しています。

public void Up()
{
    // 現在の目標温度が最大目標温度より小さい
    if (this.TargetTemperature < AirConditioner.MaxTargetTemperature)
    {
        // 目標温度をインクリメント
        this.TargetTemperature++;
    }
}

public void Down()
{
    // 現在の目標温度が最小目標温度より大きい
    if (this.TargetTemperature > AirConditioner.MinTargetTemperature)
    {
        // 目標温度をデクリメント
        this.TargetTemperature--;
    }
}


温度/湿度制御処理

エアコンが動作状態となっている時の、動作モードに応じた温度/湿度制御処理を実装します。
今回はサンプルのため、単純な温度や湿度の上下とリミット処理のみを実装します。
実際には、センサから取得した現在温度と、設定された目標温度からフィードバック制御などを行います。

public void CoolControl()
{
    // 目標温度が現在温度より低い
    if (this.TargetTemperature < this.Temperature)
    {
        // 現在温度をデクリメント
        this.Temperature--;
    }
}

public void HeatControl()
{
    // 目標温度が現在温度より高い
    if (this.TargetTemperature > this.Temperature)
    {
        // 現在温度をインクリメント
        this.Temperature++;
    }
}

public void DryControl()
{
    // 現在湿度が最小湿度より大きい
    if (this.Humidity > AirConditioner.MinHumidity)
    {
        // 現在湿度をデクリメント
        this.Humidity--;
    }
}


汚れレベル解析処理

クリーニング開始前の、汚れレベル解析処理を実装します。
今回はサンプルのため、時間経過と前回の解析結果に応じて汚れレベルを変化させます。
実際には、センサなどを使用して内部の汚れ具合を判定し、それに応じた汚れレベルを返すようにします。

public StainLevel StainLevelAnalys()
{
    // 汚れレベル解析時間カウントアップ
    this.AnalyseCount++;

    // 解析時間が5カウント以上(メソッドが5回以上呼び出された)
    if (this.AnalyseCount >= 5)
    {
        // 前回の汚れレベル解析結果
        switch (this.PrevStainLevel)
        {
            case StainLevel.Unknown:    // 不明(初回解析)
                // 汚れレベル = 低
                this.StainLevel = StainLevel.Low;
                break;
            case StainLevel.Low:        // 汚れレベル低
                // 汚れレベル = 高
                this.StainLevel = StainLevel.High;
                break;
            case StainLevel.High:       // 汚れレベル高
                // 汚れレベル = 低
                this.StainLevel = StainLevel.Low;
                break;
            default:
                break;
        }
    }
    else
    {
        // 汚れレベル = 未確定
        this.StainLevel = StainLevel.Unknown;
    }

    return this.StainLevel;
}


クリーニング処理

解析した汚れレベルに応じたクリーニング処理を行います。
今回はサンプルのため、単純な時間経過でクリーニング処理完了判定を行います。

public bool DeepCleanControl()
{
    // クリーニング時間カウントアップ
    this.CleanCount++;

    // クリーニング完了判定結果を返す
    return this.CleanFinished;
}

public bool LightCleanControl()
{
    // クリーニング時間カウントアップ
    this.CleanCount++;

    // クリーニング完了判定結果を返す
    return this.CleanFinished;
}


クリーニング完了処理

クリーニング処理が完了したら、その後始末処理を行います。 今回はサンプルのため、汚れレベル解析結果のバックアップと、各クリーニング状態のクリアを行います。

public void CleanEnd()
{
    // 汚れレベルを前回値としてバックアップ
    this.PrevStainLevel = this.StainLevel;

    // 汚れレベル解析結果クリア
    this.StainLevel = StainLevel.Unknown;

    // 汚れレベル解析時間カウントクリア
    this.AnalyseCount = 0;

    // クリーニング時間カウントクリア
    this.CleanCount = 0;
}


ソース全体を示します。

public class AirConditioner
{
    // 最大目標温度[℃]
    public const int MaxTargetTemperature = 40;

    // 最小目標温度[℃]
    public const int MinTargetTemperature = 15;

    // 最大湿度[%]
    public const int MaxHumidity = 90;

    // 最小湿度[%]
    public const int MinHumidity = 20;

    // 現在温度[℃]
    public int Temperature { get; private set; }

    // 目標温度[℃]
    public int TargetTemperature { get; private set; }

    // 現在湿度
    public int Humidity { get; private set; }

    // 汚れレベル
    public StainLevel StainLevel { get; private set; }

    // 前回の汚れレベル
    private StainLevel PrevStainLevel { get; set; }

    // 汚れレベル解析時間カウント
    private int AnalyseCount { get; set; }

    // クリーニング時間カウント
    private int CleanCount { get; set; }

    // 汚れレベル解析完了フラグ
    private bool AnalyseFinished => (this.StainLevel != StainLevel.Unknown);

    // クリーニング完了フラグ
    private bool CleanFinished => ((this.StainLevel == StainLevel.High && this.CleanCount >= 20) 
                                || (this.StainLevel == StainLevel.Low && this.CleanCount >= 10));

    // 初期化処理
    public void Initialize()
    {
        this.Temperature = 30;

        this.TargetTemperature = AirConditioner.MinTargetTemperature;

        this.Humidity = 50;

        this.StainLevel = StainLevel.Unknown;

        this.PrevStainLevel = StainLevel.Unknown;

        this.AnalyseCount = 0;

        this.CleanCount = 0;
    }

    // 動作開始処理
    public void Start()
    {
        this.Temperature = 30;

        this.Humidity = 50;
    }

    // 動作停止処理
    public void Stop()
    {
        this.StainLevel = StainLevel.Unknown;

        this.PrevStainLevel = StainLevel.Unknown;

        this.AnalyseCount = 0;

        this.CleanCount = 0;
    }

    // 目標温度アップ
    public void Up()
    {
        if (this.TargetTemperature < AirConditioner.MaxTargetTemperature)
        {
            this.TargetTemperature++;
        }
    }

    // 目標温度ダウン
    public void Down()
    {
        if (this.TargetTemperature > AirConditioner.MinTargetTemperature)
        {
            this.TargetTemperature--;
        }
    }

    // 冷房制御処理
    public void CoolControl()
    {
        if (this.TargetTemperature < this.Temperature)
        {
            this.Temperature--;
        }
    }

    // 暖房制御処理
    public void HeatControl()
    {
        if (this.TargetTemperature > this.Temperature)
        {
            this.Temperature++;
        }
    }

    // 除湿制御処理
    public void DryControl()
    {
        if (this.Humidity > AirConditioner.MinHumidity)
        {
            this.Humidity--;
        }
    }

    // 汚れレベル解析処理
    public StainLevel StainLevelAnalys()
    {
        this.AnalyseCount++;

        if (this.AnalyseCount >= 5)
        {
            switch (this.PrevStainLevel)
            {
                case StainLevel.Unknown:
                    this.StainLevel = StainLevel.Low;
                    break;
                case StainLevel.Low:
                    this.StainLevel = StainLevel.High;
                    break;
                case StainLevel.High:
                    this.StainLevel = StainLevel.Low;
                    break;
                default:
                    break;
            }
        }
        else
        {
            this.StainLevel = StainLevel.Unknown;
        }

        return this.StainLevel;
    }

    // 入念クリーニング処理
    public bool DeepCleanControl()
    {
        this.CleanCount++;

        return this.CleanFinished;
    }

    // あっさりクリーニング処理
    public bool LightCleanControl()
    {
        this.CleanCount++;

        return this.CleanFinished;
    }

    // クリーニング完了処理
    public void CleanEnd()
    {
        this.PrevStainLevel = this.StainLevel;

        this.StainLevel = StainLevel.Unknown;

        this.AnalyseCount = 0;

        this.CleanCount = 0;
    }
}


次回予告

次回は、ステートマシンをCUI(コマンドライン)で動作させるアプリケーションを実装していきます。

an-embedded-engineer.hateblo.jp

UMLのステートマシン図を実装する for C# - その4

まえがき

今回は、前回に引き続きサンプルのエアコンステートマシンを実装していきます。

f:id:an-embedded-engineer:20190414174321p:plain
エアコンステートマシン


State クラス

State クラスを継承した各状態を実装します。
エアコンステートマシンでは、4 つのメイン状態と、6つのサブ状態を実装する必要があります。

ステートマシン 名称 詳細 備考
ModelStateMachine InitialState 初期状態
ModelStateMachine StopState 停止状態
ModelStateMachine RunningState 動作状態
ModelStateMachine CleanState クリーニング状態
RunningStateMachine CoolState 冷房制御状態
RunningStateMachine HeatState 暖房制御状態
RunningStateMachine DryState 除湿制御状態
CleanStateMachine StainLvelAnalysisState 汚れレベル解析状態
CleanStateMachine DeepCleanState 入念クリーニング状態
CleanStateMachine LightCleanState あっさりクリーニング状態
CleanStateMachine CleanFinalState クリーニング完了状態


各状態はシステム内で 1 つのインスタンスのみを持つべきなので、シングルトンで実装します。


InitialState クラス

InitialState クラスでは、Entry イベントハンドラでモデル(AirConditioner)の初期化処理を行います。
初期化完了後、Initialize トリガをステートマシンに送信します。

private void EntryEventHandler(StateMachine context)
{
    // StateMachineをダウンキャスト
    var stm = context.GetAs<ModelStateMachine>();

    // モデル(AirConditionerクラス)を取得
    var model = stm.Model;

    // モデルの初期化処理
    model.Initialize();

    // Initialiedトリガ送信
    stm.SendTrigger(InitializedTrigger.Instance);
}


また、トリガアクションハッシュテーブルに、Initialized トリガに対するアクション(InitializedTriggerHandler)を登録しておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // Initializedトリガに対するアクションの登録
        { InitializedTrigger.Instance.Name, this.InitializedTriggerHandler },
    };
}


InitializedTriggerHandler では、コンテキスト(ステートマシン)に対して Stop 状態への状態遷移を要求します。

private void InitializedTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Stop状態への状態遷移を要求
    context.ChangeState(StopState.Instance);
}


ソース全体を示します。

public sealed class InitialState : State
{
    // シングルトンインスタンス
    public static InitialState Instance { get; private set; } = new InitialState();

    // コンストラクタ
    private InitialState() : base("Initial")
    {
        this.OnEntry += this.EntryEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { InitializedTrigger.Instance.Name, this.InitializedTriggerHandler },
        };
    }

    // Entryイベントハンドラ
    private void EntryEventHandler(StateMachine context)
    {
        var stm = context.GetAs<ModelStateMachine>();

        var model = stm.Model;

        model.Initialize();

        stm.SendTrigger(InitializedTrigger.Instance);
    }

    // Initializedトリガハンドラ
    private void InitializedTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(StopState.Instance);
    }
}


StopState クラス

StopState クラスでは、SwitchStart トリガに対して、RunningState への状態遷移を要求する処理を実装します。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchStartトリガに対するアクション
        { SwitchStartTrigger.Instance.Name, this.SwitchStartTriggerHandler },
    };
}

private void SwitchStartTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Running状態への状態遷移を要求
    context.ChangeState(RunningState.Instance);
}


ソース全体を示します。

public sealed class StopState : State
{
    // シングルトンインスタンス
    public static StopState Instance { get; private set; } = new StopState();

    // コンストラクタ
    private StopState() : base("Stop")
    {
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchStartTrigger.Instance.Name, this.SwitchStartTriggerHandler },
        };
    }

    // SwitchStartトリガハンドラ
    private void SwitchStartTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(RunningState.Instance);
    }
}


RunningState クラス

RunningState クラスでは、サブステートマシンとして RunningStateMachine を持つ必要があります。

// サブステートマシン
public RunningStateMachine SubContext { get; private set; }


RunningStateMachine は、最初に Running 状態に入った(Entry イベント)時のみインスタンスが生成されます。
Hisoty 疑似状態を実現するために、Running 状態を出た際に最後のサブ状態を記憶しておく必要があるためです。

private void EntryEventHandler(StateMachine context)
{
    // 初回入場時(サブステートマシンのインスタンスが生成されていない)
    if (this.SubContext == null)
    {
        // 親ステートマシン(ModelStateMachine)の取得
        var parent = context.GetAs<ModelStateMachine>();

        // サブステートマシン(RunningStateMachine)のインスタンスを生成
        this.SubContext = new RunningStateMachine(parent);
    }
}


また、サブステートマシンの定常的な処理を実行するため、Do イベントハンドラで、サブステートマシンの Update 処理を実行します。

private void DoEventHandler(StateMachine context)
{
    // サブステートマシンの更新
    this.SubContext.Update();
}


RunningStete クラスでは、Running 状態そのものに対するトリガと、サブ状態に対するトリガを受け取れるようにする必要があります。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchStopトリガに対するアクション
        { SwitchStopTrigger.Instance.Name, this.SwitchStopTriggerHandler },
        // SwitchCleanトリガに対するアクション
        { SwitchCleanTrigger.Instance.Name, this.SwitchCleanTriggerHandler },
        // SwitchCoolトリガに対するアクション
        { SwitchCoolTrigger.Instance.Name, this.SubContextTriggerHandler },
        // SwitchHeatトリガに対するアクション
        { SwitchHeatTrigger.Instance.Name, this.SubContextTriggerHandler },
        // SwitchDryトリガに対するアクション
        { SwitchDryTrigger.Instance.Name, this.SubContextTriggerHandler },
    };
}


SwitchStop トリガを受信した場合は、Stop 状態への遷移を要求し、その際に Trigger に設定された Effect をパラメータとして受け渡します。
SwitchClean トリガを受信した場合は、Clean 状態への遷移を要求します。

private void SwitchStopTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // 状態遷移時に実行するEffectを取得
    var effect = args.Trigger.Effect;

    // Stop状態への状態遷移を要求
    context.ChangeState(StopState.Instance, effect);
}

private void SwitchCleanTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Clean状態への状態遷移を要求
    context.ChangeState(CleanState.Instance);
}


サブステートマシン用のトリガ(SwitchCool / SwitchHeat / SwitchDry)を受信した場合は、サブステートマシンに受信したトリガを送信します。

private void SubContextTriggerHandler(TriggerActionArgs args)
{
    // 受信したトリガを取得
    var trigger = args.Trigger;

    // コンテキスト(サブステートマシン)を取得
    var context = this.SubContext;

    // サブステートマシンにトリガを送信
    context.SendTrigger(trigger);
}


ソース全体を示します。

public sealed class RunningState : State
{
    // シングルトンインスタンス
    public static RunningState Instance { get; private set; } = new RunningState();

    // サブステートマシン
    public RunningStateMachine SubContext { get; private set; }

    // コンストラクタ
    private RunningState() : base("Running")
    {
        this.OnEntry += this.EntryEventHandler;
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchStopTrigger.Instance.Name, this.SwitchStopTriggerHandler },
            { SwitchCleanTrigger.Instance.Name, this.SwitchCleanTriggerHandler },
            { SwitchCoolTrigger.Instance.Name, this.SubContextTriggerHandler },
            { SwitchHeatTrigger.Instance.Name, this.SubContextTriggerHandler },
            { SwitchDryTrigger.Instance.Name, this.SubContextTriggerHandler },
        };
    }

    // Entryイベントハンドラ
    private void EntryEventHandler(StateMachine context)
    {
        if (this.SubContext == null)
        {
            var parent = context.GetAs<ModelStateMachine>();

            this.SubContext = new RunningStateMachine(parent);
        }
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        this.SubContext.Update();
    }

    // SwitchStopトリガハンドラ
    private void SwitchStopTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        var effect = args.Trigger.Effect;

        context.ChangeState(StopState.Instance, effect);
    }

    // SwitchCleanトリガハンドラ
    private void SwitchCleanTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(CleanState.Instance);
    }

    // サブステートマシン用トリガハンドラ
    private void SubContextTriggerHandler(TriggerActionArgs args)
    {
        var trigger = args.Trigger;

        var context = this.SubContext;

        context.SendTrigger(trigger);
    }
}


CoolState クラス

CoolState クラスでは、Do イベントハンドラで冷房制御を行うため、モデル(AirConditioner)の冷房制御処理(CoolControl)を呼び出します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(RunningStateMachine)の取得
    var stm = context.GetAs<RunningStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 冷房制御処理呼び出し
    model.CoolControl();
}


CoolState クラスでは、動作モード(暖房 / 除湿)切り替えスイッチ押下によるトリガを受信できるようにしておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchHeatトリガに対するアクション
        { SwitchHeatTrigger.Instance.Name, this.SwitchHeatTriggerHandler },
        // SwitchDryトリガに対するアクション
        { SwitchDryTrigger.Instance.Name, this.SwitchDryTriggerHandler },
    };
}


SwitchHeat トリガを受信した場合は、Heat 状態への遷移を要求します。
SwitchDry トリガを受信した場合は、Dry 状態への遷移を要求します。

private void SwitchHeatTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Heat状態への状態遷移を要求
    context.ChangeState(HeatState.Instance);
}

private void SwitchDryTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Dry状態への状態遷移を要求
    context.ChangeState(DryState.Instance);
}


ソース全体を示します。

public sealed class CoolState : State
{
    // シングルトンインスタンス
    public static CoolState Instance { get; private set; } = new CoolState();

    // コンストラクタ
    private CoolState() : base("Cool")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchHeatTrigger.Instance.Name, this.SwitchHeatTriggerHandler },
            { SwitchDryTrigger.Instance.Name, this.SwitchDryTriggerHandler },
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<RunningStateMachine>();

        var model = stm.Model;

        model.CoolControl();
    }

    // SwitchHeatトリガハンドラ
    private void SwitchHeatTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(HeatState.Instance);
    }

    // SwitchDryトリガハンドラ
    private void SwitchDryTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(DryState.Instance);
    }
}


HeatState クラス

HeatState クラスでは、Do イベントハンドラで暖房制御を行うため、モデル(AirConditioner)の暖房制御処理(HeatControl)を呼び出します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(RunningStateMachine)の取得
    var stm = context.GetAs<RunningStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 暖房制御処理呼び出し
    model.HeatControl();
}


HeatState クラスでは、動作モード(冷房 / 除湿)切り替えスイッチ押下によるトリガを受信できるようにしておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchCoolトリガに対するアクション
        { SwitchCoolTrigger.Instance.Name, this.SwitchCoolTriggerHandler },
        // SwitchDryトリガに対するアクション
        { SwitchDryTrigger.Instance.Name, this.SwitchDryTriggerHandler },
    };
}


SwitchCool トリガを受信した場合は、Cool 状態への遷移を要求します。
SwitchDry トリガを受信した場合は、Dry 状態への遷移を要求します。

private void SwitchCoolTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Cool状態への状態遷移を要求
    context.ChangeState(CoolState.Instance);
}

private void SwitchDryTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Dry状態への状態遷移を要求
    context.ChangeState(DryState.Instance);
}


ソース全体を示します。

public sealed class HeatState : State
{
    // シングルトンインスタンス
    public static HeatState Instance { get; private set; } = new HeatState();

    // コンストラクタ
    private HeatState() : base("Heat")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchCoolTrigger.Instance.Name, this.SwitchCoolTriggerHandler },
            { SwitchDryTrigger.Instance.Name, this.SwitchDryTriggerHandler },
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<RunningStateMachine>();

        var model = stm.Model;

        model.HeatControl();
    }

    // SwitchCoolトリガハンドラ
    private void SwitchCoolTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(CoolState.Instance);
    }

    // SwitchDryトリガハンドラ
    private void SwitchDryTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(DryState.Instance);
    }
}


DryState クラス

DryState クラスでは、Do イベントハンドラで除湿制御を行うため、モデル(AirConditioner)の除湿制御処理(DryControl)を呼び出します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(RunningStateMachine)の取得
    var stm = context.GetAs<RunningStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 除湿制御処理呼び出し
    model.DryControl();
}


DryState クラスでは、動作モード(冷房 / 暖房)切り替えスイッチ押下によるトリガを受信できるようにしておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchCoolトリガに対するアクション
        { SwitchCoolTrigger.Instance.Name, this.SwitchCoolTriggerHandler },
        // SwitchHeatトリガに対するアクション
        { SwitchHeatTrigger.Instance.Name, this.SwitchHeatTriggerHandler },
    };
}


SwitchCool トリガを受信した場合は、Cool 状態への遷移を要求します。
SwitchHeat トリガを受信した場合は、Heat 状態への遷移を要求します。

private void SwitchCoolTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Cool状態への状態遷移を要求
    context.ChangeState(CoolState.Instance);
}

private void SwitchHeatTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Heat状態への状態遷移を要求
    context.ChangeState(HeatState.Instance);
}


ソース全体を示します。

public sealed class DryState : State
{
    // シングルトンインスタンス
    public static DryState Instance { get; private set; } = new DryState();

    // コンストラクタ
    private DryState() : base("Dry")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchCoolTrigger.Instance.Name, this.SwitchCoolTriggerHandler },
            { SwitchHeatTrigger.Instance.Name, this.SwitchHeatTriggerHandler },
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<RunningStateMachine>();

        var model = stm.Model;

        model.DryControl();
    }

    // SwitchCoolトリガハンドラ
    private void SwitchCoolTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(CoolState.Instance);
    }

    // SwitchHeatトリガハンドラ
    private void SwitchHeatTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(HeatState.Instance);
    }
}


CleanState クラス

CleanState クラスでは、サブステートマシンとしてCleanStateMachineを持つ必要があります。

// サブステートマシン
public CleanStateMachine SubContext { get; private set; }


CleanStateMachineは、Clena状態に入る(Entryイベント)たびにインスタンスを生成します。
クリーニングモードに入るたびに汚れレベルの解析からやり直すため、Clean状態に入るたび、サブステートマシンの状態を初期状態にリセットする必要があるためです。

private void EntryEventHandler(StateMachine context)
{
    // 親ステートマシン(Model StateMachine)の取得
    var parent = context.GetAs<ModelStateMachine>();

    // サブステートマシン(CleanStateMachine)の生成
    this.SubContext = new CleanStateMachine(parent);
}


また、サブステートマシンの定常的な処理を実行するため、Do イベントハンドラで、サブステートマシンの Update 処理を実行します。
さらに、サブステートマシンが完了状態(CleanFinalState)になっていた場合、クリーニング処理が完了となるため、ステートマシンにRunning状態への状態遷移を要求します。
このとき、エフェクトとしてCleanEndEffectを指定します。

private void DoEventHandler(StateMachine context)
{
    // サブステートマシンの更新
    this.SubContext.Update();

    // サブステートマシンの状態がクリーニング完了状態(CleanFinalState)
    if (this.SubContext.CurrentState is CleanFinalState)
    {
        // 完了遷移で実行するエフェクト(CleanEndEffect)を取得
        var effect = CleanEndEffect.Instance;

        // Running状態への状態遷移を要求
        context.ChangeState(RunningState.Instance, effect);
    }
}


CleanState クラスでは、ストップスイッチ押下によるトリガを受信できるようにしておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchStopトリガに対するアクション
        { SwitchStopTrigger.Instance.Name, this.SwitchStopTriggerHandler },
    };
}


SwitchStop トリガを受信した場合は、Stop 状態への遷移を要求し、その際に Trigger に設定された Effect をパラメータとして受け渡します。

private void SwitchStopTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // 状態遷移時に実行するEffectを取得
    var effect = args.Trigger.Effect;

    // Stop状態への状態遷移を要求
    context.ChangeState(StopState.Instance, effect);
}


ソース全体を示します。

public sealed class CleanState : State
{
    // シングルトンインスタンス
    public static CleanState Instance { get; private set; } = new CleanState();

    // サブステートマシン
    public CleanStateMachine SubContext { get; private set; }

    // コンストラクタ
    private CleanState() : base("Clean")
    {
        this.OnEntry += this.EntryEventHandler;
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchStopTrigger.Instance.Name, this.SwitchStopTriggerHandler },
        };
    }

    // Entryイベントハンドラ
    private void EntryEventHandler(StateMachine context)
    {
        var parent = context.GetAs<ModelStateMachine>();

        this.SubContext = new CleanStateMachine(parent);
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        this.SubContext.Update();

        if (this.SubContext.CurrentState is CleanFinalState)
        {
            var effect = CleanEndEffect.Instance;

            context.ChangeState(RunningState.Instance, effect);
        }
    }

    // SwitchStopトリガハンドラ
    private void SwitchStopTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        var effect = args.Trigger.Effect;

        context.ChangeState(StopState.Instance, effect);
    }
}


StainLevelAnalysisState クラス

StainLevelAnalysisState クラスでは、Doイベントハンドラで汚れレベルを解析し、解析が完了したら汚れレベルに応じたクリーニングモード(LightClean / DeepClean)への遷移を要求します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(CleanStateMachine)の取得
    var stm = context.GetAs<CleanStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 汚れレベル解析処理呼び出し
    var level = model.StainLevelAnalys();

    // 汚れレベル判定
    switch (level)
    {
        case StainLevel.Unknown:    // 未確定
            /* Nothing to do */
            break;
        case StainLevel.Low:    // 汚れレベル低
            // LightClean状態への状態遷移を要求
            stm.ChangeState(LightCleanState.Instance);
            break;
        case StainLevel.High:   // 汚れレベル高
            // DeepClean状態への状態遷移を要求
            stm.ChangeState(DeepCleanState.Instance);
            break;
        default:
            /* Nothing to do */
            break;
    }
}


ソース全体を示します。

public sealed class StainLevelAnalysisState : State
{
    // シングルトンインスタンス
    public static StainLevelAnalysisState Instance { get; private set; } = new StainLevelAnalysisState();

    // コンストラクタ
    private StainLevelAnalysisState() : base("Stain Level AnalysisState")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<CleanStateMachine>();

        var model = stm.Model;

        var level = model.StainLevelAnalys();

        switch (level)
        {
            case StainLevel.Unknown:
                /* Nothing to do */
                break;
            case StainLevel.Low:
                stm.ChangeState(LightCleanState.Instance);
                break;
            case StainLevel.High:
                stm.ChangeState(DeepCleanState.Instance);
                break;
            default:
                /* Nothing to do */
                break;
        }
    }
}


DeepCleanState クラス

DeepCleanState クラスでは、Doイベントハンドラで入念クリーニング処理を実行し、クリーニングが完了したらクリーニング完了(CleanFinal)状態への遷移を要求します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(CleanStateMachine)の取得
    var stm = context.GetAs<CleanStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 入念クリーニング制御処理呼び出し
    var result = model.DeepCleanControl();

    // クリーニング処理完了
    if (result == true)
    {
        // CleanFinal状態への状態遷移を要求
        stm.ChangeState(CleanFinalState.Instance);
    }
}


ソース全体を示します。

public sealed class DeepCleanState : State
{
    // シングルトンインスタンス
    public static DeepCleanState Instance { get; private set; } = new DeepCleanState();

    // コンストラクタ
    private DeepCleanState() : base("Deep Clean")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<CleanStateMachine>();

        var model = stm.Model;

        var result = model.DeepCleanControl();

        if (result == true)
        {
            stm.ChangeState(CleanFinalState.Instance);
        }
    }
}


LightCleanState クラス

LightCleanState クラスでは、Doイベントハンドラであっさりクリーニング処理を実行し、クリーニングが完了したらクリーニング完了(CleanFinal)状態への遷移を要求します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(CleanStateMachine)の取得
    var stm = context.GetAs<CleanStateMachine>();

    // モデル(AirConditioner)の取得

    // あっさりクリーニング制御処理呼び出し
    var result = model.LightCleanControl();

    // クリーニング処理完了
    if (result == true)
    {
        // CleanFinal状態への状態遷移を要求
        stm.ChangeState(CleanFinalState.Instance);
    }
}


ソース全体を示します。

public sealed class LightCleanState : State
{
    // シングルトンインスタンス
    public static LightCleanState Instance { get; private set; } = new LightCleanState();

    // コンストラクタ
    private LightCleanState() : base("Light Clean")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<CleanStateMachine>();

        var model = stm.Model;

        var result = model.LightCleanControl();

        if (result == true)
        {
            stm.ChangeState(CleanFinalState.Instance);
        }
    }
}


CleanFinalState クラス

CleanFinalStateクラスは、CleanStateMachineの最終状態のため、特に何もしません。 ソース全体を示します。

public sealed class CleanFinalState : State
{
    public static CleanFinalState Instance { get; private set; } = new CleanFinalState();

    private CleanFinalState() : base("Clean Final")
    {
    }

    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
        };
    }
}


次回予告

今回で状態遷移部分はすべて実装できたので、次回はエアコンのモデル部分(AirConditionerクラス)の実装を行っていきます。

an-embedded-engineer.hateblo.jp