Skip to content

Commit

Permalink
Merge pull request #516 from celloza/celloza/alarmexample
Browse files Browse the repository at this point in the history
Implemented an example for an alarm
  • Loading branch information
mclift authored Jun 2, 2023
2 parents ff6ad59 + 14472a3 commit 0ba05aa
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Stateless.sln
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelephoneCallExample", "exa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonExample", "example\JsonExample\JsonExample.csproj", "{809A7873-DD78-4D5D-A432-9718C929BECA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AlarmExample", "example\AlarmExample\AlarmExample.csproj", "{4E44B325-F791-4C24-872B-D1454DBBA30D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -66,6 +68,10 @@ Global
{809A7873-DD78-4D5D-A432-9718C929BECA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{809A7873-DD78-4D5D-A432-9718C929BECA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{809A7873-DD78-4D5D-A432-9718C929BECA}.Release|Any CPU.Build.0 = Release|Any CPU
{4E44B325-F791-4C24-872B-D1454DBBA30D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E44B325-F791-4C24-872B-D1454DBBA30D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E44B325-F791-4C24-872B-D1454DBBA30D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E44B325-F791-4C24-872B-D1454DBBA30D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -77,6 +83,7 @@ Global
{19ABDDFE-C040-404E-897B-37BE6C248ED7} = {45C09CCA-6C76-4E10-B386-5D95A7610FE0}
{5182CA95-8E6F-4D16-9790-8F7D1C5A9C87} = {45C09CCA-6C76-4E10-B386-5D95A7610FE0}
{809A7873-DD78-4D5D-A432-9718C929BECA} = {45C09CCA-6C76-4E10-B386-5D95A7610FE0}
{4E44B325-F791-4C24-872B-D1454DBBA30D} = {45C09CCA-6C76-4E10-B386-5D95A7610FE0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7A73ADDC-8150-4AFC-AAF0-BA8B4D7A94D7}
Expand Down
175 changes: 175 additions & 0 deletions example/AlarmExample/Alarm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using AlarmExample;
using Stateless;
using System.Diagnostics;

namespace AlarmExample
{
/// <summary>
/// A sample class that implements an alarm as a state machine using Stateless
/// (https://github.com/dotnet-state-machine/stateless).
///
/// It also shows one way that temporary states can be implemented with the use of
/// Timers. PreArmed, PreTriggered, Triggered, and ArmPaused are "temporary" states with
/// a configurable delay (i.e. to allow for an "arm delay"... a delay between Disarmed
/// and Armed). The Triggered state is also considered temporary, since if an alarm
/// sounds for a certain period of time and no-one Acknowledges it, the state machine
/// returns to the Armed state.
///
/// Timers are triggered via OnEntry() and OnExit() methods. Transitions are written to
/// the Trace in order to show what happens.
///
/// The included PNG file shows what the state flow looks like.
///
/// </summary>
public partial class Alarm
{
/// <summary>
/// Moves the Alarm into the provided <see cref="AlarmState" /> via the defined <see cref="AlarmCommand" />.
/// </summary>
/// <param name="command">The <see cref="AlarmCommand" /> to execute on the current <see cref="AlarmState" />.</param>
/// <returns>The new <see cref="AlarmState" />.</returns>
public AlarmState ExecuteTransition(AlarmCommand command)
{
if (_machine.CanFire(command))
{
_machine.Fire(command);
}
else
{
throw new InvalidOperationException($"Cannot transition from {CurrentState} via {command}");
}

return CurrentState();
}

/// <summary>
/// The current <see cref="AlarmState" /> of the alarm.
/// </summary>
public AlarmState CurrentState()
{
if (_machine != null)
return _machine.State;
else
throw new InvalidOperationException("Alarm hasn't been configured yet.");
}

/// <summary>
/// Defines whether the <see cref="Alarm"/> has been configured.
/// </summary>
public bool IsConfigured { get; private set; }

/// <summary>
/// Returns whether the provided command is a valid transition from the Current State.
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
public bool CanFireCommand(AlarmCommand command)
{
return _machine.CanFire(command);
}

/// <summary>
/// Default constructor.
/// </summary>
/// <param name="armDelay">The time (in seconds) the alarm will spend in the
/// Prearmed status before continuing to the Armed status (if not transitioned to
/// Disarmed via Disarm).</param>
/// <param name="pauseDelay">The time (in seconds) the alarm will spend in the
/// ArmPaused status before returning to Armed (if not transitioned to Triggered
/// via Trigger).</param>
/// <param name="triggerDelay">The time (in seconds) the alarm will spend in the
/// PreTriggered status before continuing to the Triggered status (if not
/// transitioned to Disarmed via Disarm).</param>
/// <param name="triggerTimeOut">The time (in seconds) the alarm will spend in the
/// Triggered status before returning to the Armed status (if not transitioned to
/// Disarmed via Disarm).</param>
public Alarm(int armDelay, int pauseDelay, int triggerDelay, int triggerTimeOut)
{
_machine = new StateMachine<AlarmState, AlarmCommand>(AlarmState.Undefined);

preArmTimer = new System.Timers .Timer(armDelay * 1000) { AutoReset = false, Enabled = false };
preArmTimer.Elapsed += TimeoutTimerElapsed;
pauseTimer = new System.Timers.Timer(pauseDelay * 1000) { AutoReset = false, Enabled = false };
pauseTimer.Elapsed += TimeoutTimerElapsed;
triggerDelayTimer = new System.Timers.Timer(triggerDelay * 1000) { AutoReset = false, Enabled = false };
triggerDelayTimer.Elapsed += TimeoutTimerElapsed;
triggerTimeOutTimer = new System.Timers.Timer(triggerTimeOut * 1000) { AutoReset = false, Enabled = false };
triggerTimeOutTimer.Elapsed += TimeoutTimerElapsed;

_machine.OnTransitioned(OnTransition);

_machine.Configure(AlarmState.Undefined)
.Permit(AlarmCommand.Startup, AlarmState.Disarmed)
.OnExit(() => IsConfigured = true);

_machine.Configure(AlarmState.Disarmed)
.Permit(AlarmCommand.Arm, AlarmState.Prearmed);

_machine.Configure(AlarmState.Armed)
.Permit(AlarmCommand.Disarm, AlarmState.Disarmed)
.Permit(AlarmCommand.Trigger, AlarmState.PreTriggered)
.Permit(AlarmCommand.Pause, AlarmState.ArmPaused);

_machine.Configure(AlarmState.Prearmed)
.OnEntry(() => ConfigureTimer(true, preArmTimer, "Pre-arm"))
.OnExit(() => ConfigureTimer(false, preArmTimer, "Pre-arm"))
.Permit(AlarmCommand.TimeOut, AlarmState.Armed)
.Permit(AlarmCommand.Disarm, AlarmState.Disarmed);

_machine.Configure(AlarmState.ArmPaused)
.OnEntry(() => ConfigureTimer(true, pauseTimer, "Pause delay"))
.OnExit(() => ConfigureTimer(false, pauseTimer, "Pause delay"))
.Permit(AlarmCommand.TimeOut, AlarmState.Armed)
.Permit(AlarmCommand.Trigger, AlarmState.PreTriggered);

_machine.Configure(AlarmState.Triggered)
.OnEntry(() => ConfigureTimer(true, triggerTimeOutTimer, "Trigger timeout"))
.OnExit(() => ConfigureTimer(false, triggerTimeOutTimer, "Trigger timeout"))
.Permit(AlarmCommand.TimeOut, AlarmState.Armed)
.Permit(AlarmCommand.Acknowledge, AlarmState.Acknowledged);

_machine.Configure(AlarmState.PreTriggered)
.OnEntry(() => ConfigureTimer(true, triggerDelayTimer, "Trigger delay"))
.OnExit(() => ConfigureTimer(false, triggerDelayTimer, "Trigger delay"))
.Permit(AlarmCommand.TimeOut, AlarmState.Triggered)
.Permit(AlarmCommand.Disarm, AlarmState.Disarmed);

_machine.Configure(AlarmState.Acknowledged)
.Permit(AlarmCommand.Disarm, AlarmState.Disarmed);

_machine.Fire(AlarmCommand.Startup);
}

private void TimeoutTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
_machine.Fire(AlarmCommand.TimeOut);
}

private void ConfigureTimer(bool active, System.Timers.Timer timer, string timerName)
{
if (timer != null)
if (active)
{
timer.Start();
Trace.WriteLine($"{timerName} started.");
}
else
{
timer.Stop();
Trace.WriteLine($"{timerName} cancelled.");
}
}

private void OnTransition(StateMachine<AlarmState, AlarmCommand>.Transition transition)
{
Trace.WriteLine($"Transitioned from {transition.Source} to " +
$"{transition.Destination} via {transition.Trigger}.");
}

private StateMachine<AlarmState, AlarmCommand> _machine;
private System.Timers.Timer? preArmTimer;
private System.Timers.Timer? pauseTimer;
private System.Timers.Timer? triggerDelayTimer;
private System.Timers.Timer? triggerTimeOutTimer;
}
}
13 changes: 13 additions & 0 deletions example/AlarmExample/AlarmCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace AlarmExample
{
public enum AlarmCommand
{
Startup,
Arm,
Disarm,
Trigger,
Acknowledge,
Pause,
TimeOut
}
}
14 changes: 14 additions & 0 deletions example/AlarmExample/AlarmExample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Stateless\Stateless.csproj" />
</ItemGroup>

</Project>
14 changes: 14 additions & 0 deletions example/AlarmExample/AlarmState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace AlarmExample
{
public enum AlarmState
{
Undefined,
Disarmed,
Prearmed,
Armed,
Triggered,
ArmPaused,
PreTriggered,
Acknowledged
}
}
115 changes: 115 additions & 0 deletions example/AlarmExample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
namespace AlarmExample
{
/// <summary>
/// A simple Console Application that allows for interactive input
/// to test the <see cref="Alarm"/> implemented as a Stateless state
/// machine.
/// </summary>
internal class Program
{
static Alarm? _alarm;

static void Main(string[] args)
{
_alarm = new Alarm(10, 10, 10, 10);

string input = "";

WriteHeader();

while (input != "q")
{
Console.Write("> ");

input = Console.ReadLine();

if (!string.IsNullOrWhiteSpace(input))
switch (input.Split(" ")[0])
{
case "q":
Console.WriteLine("Exiting...");
break;
case "fire":
WriteFire(input);
break;
case "canfire":
WriteCanFire();
break;
case "state":
WriteState();
break;
case "h":
case "help":
WriteHelp();
break;
case "c":
case "clear":
Console.Clear();
WriteHeader();
break;
default:
Console.WriteLine("Invalid command. Type 'h' or 'help' for valid commands.");
break;
}
}
}

static void WriteHelp()
{
Console.WriteLine("Valid commands:");
Console.WriteLine("q - Exit");
Console.WriteLine("fire <state> - Tries to fire the provided commands");
Console.WriteLine("canfire <state> - Returns a list of fireable commands");
Console.WriteLine("state - Returns the current state");
Console.WriteLine("c / clear - Clear the window");
Console.WriteLine("h / help - Show this again");
}

static void WriteHeader()
{
Console.WriteLine("Stateless-based alarm test application:");
Console.WriteLine("---------------------------------------");
Console.WriteLine("");
}

static void WriteCanFire()
{
foreach (AlarmCommand command in (AlarmCommand[])Enum.GetValues(typeof(AlarmCommand)))
if (_alarm != null && _alarm.CanFireCommand(command))
Console.WriteLine($"{Enum.GetName(typeof(AlarmCommand), command)}");
}

static void WriteState()
{
if(_alarm != null )
Console.WriteLine($"The current state is {Enum.GetName(typeof(AlarmState), _alarm.CurrentState())}");
}

static void WriteFire(string input)
{
if (input.Split(" ").Length == 2)
{
try
{
if (Enum.TryParse(input.Split(" ")[1], out AlarmCommand command))
{
if (_alarm != null)
_alarm.ExecuteTransition(command);
}
else
{
Console.WriteLine($"{input.Split(" ")[1]} is not a valid AlarmCommand.");
}
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"{input.Split(" ")[1]} is not a valid AlarmCommand to the current state.");
}
}
else
{
Console.WriteLine("fire requires you to specify the command you want to fire.");
}
}
}
}
Binary file added example/AlarmExample/StateDiagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 0ba05aa

Please sign in to comment.