Implementing a Simple State Machine Application for .NET Micro Framework |
Describes how to integrate a StaMa state machine into a .NET Micro Framework based application.
This step by step description shows how to create a simple state machine application with two states and transitions between them.
The Microsoft Visual Studio project and the source code is also available in the release package at StaMa_State_Machine_Controller_Library\Samples\netmf\SampleSimpleStateMachineNETMF\SampleSimpleStateMachineNETMF.csproj.
The following environment is assumed to be installed:
Newer or older versions of the above software will probably not affect the below steps as such, however references to files, paths and versions might be different.
Open Microsoft Visual Studio.
From the File menu select New, then Project.... The New Project dialog appears.
In the New Project dialog select Templates / Visual C# / Windows / Console Application.
Name the new solution and project SampleSimpleStateMachineNETMF. Close the dialog and create the solution by pressing OK.
Add assembly references to Microsoft.SPOT.Hardware.dll and Microsoft.SPOT.TinyCore.dll.
Add a new class with file name GPIOButtonInputProvider.cs to the project.
Copy the following code into the GPIOButtonInputProvider.cs file:
using System; using Microsoft.SPOT; using Microsoft.SPOT.Hardware; using Microsoft.SPOT.Input; namespace SampleSimpleStateMachineNETMF { internal class GPIOButtonInputProvider { public delegate void GPIOButtonInputHandler(InputReportArgs arg); private readonly Dispatcher m_dispatcher; private ButtonPad[] m_buttons; private GPIOButtonInputHandler m_buttonInputHandler; public GPIOButtonInputProvider(GPIOButtonInputHandler buttonInputHandler) { if (buttonInputHandler == null) { throw new ArgumentNullException("buttonInputHandler"); } m_buttonInputHandler = buttonInputHandler; m_dispatcher = Dispatcher.CurrentDispatcher; // Create a hardware provider. HardwareProvider hwProvider = new HardwareProvider(); // Create the pins that are needed for the buttons. // Default their values for the emulator. Cpu.Pin pinLeft = Cpu.Pin.GPIO_Pin0; Cpu.Pin pinRight = Cpu.Pin.GPIO_Pin1; Cpu.Pin pinUp = Cpu.Pin.GPIO_Pin2; Cpu.Pin pinSelect = Cpu.Pin.GPIO_Pin3; Cpu.Pin pinDown = Cpu.Pin.GPIO_Pin4; // Use the hardware provider to get the pins. If the left pin is // not set, assume none of the pins are set, and set the left pin // back to the default emulator value. if ((pinLeft = hwProvider.GetButtonPins(Button.VK_LEFT)) == Cpu.Pin.GPIO_NONE) { pinLeft = Cpu.Pin.GPIO_Pin0; } else { pinRight = hwProvider.GetButtonPins(Button.VK_RIGHT); pinUp = hwProvider.GetButtonPins(Button.VK_UP); pinSelect = hwProvider.GetButtonPins(Button.VK_SELECT); pinDown = hwProvider.GetButtonPins(Button.VK_DOWN); } // Allocate button pads and assign the (emulated) hardware pins as input from specific buttons. m_buttons = new ButtonPad[] { // Associate the buttons to the pins as discovered or set above. new ButtonPad(this, Button.VK_LEFT , pinLeft), new ButtonPad(this, Button.VK_RIGHT , pinRight), new ButtonPad(this, Button.VK_UP , pinUp), new ButtonPad(this, Button.VK_SELECT, pinSelect), new ButtonPad(this, Button.VK_DOWN , pinDown), }; } private class ButtonPad : IDisposable { private Button m_button; private InterruptPort m_port; private GPIOButtonInputProvider m_sink; private ButtonDevice m_buttonDevice; public ButtonPad(GPIOButtonInputProvider sink, Button button, Cpu.Pin pin) { m_sink = sink; m_button = button; m_buttonDevice = InputManager.CurrentInputManager.ButtonDevice; // Do not set an InterruptPort with GPIO_NONE. if (pin != Cpu.Pin.GPIO_NONE) { // When this GPIO pin is true, call the Interrupt method. m_port = new InterruptPort(pin, true, Port.ResistorMode.PullUp, Port.InterruptMode.InterruptEdgeBoth); m_port.OnInterrupt += new NativeEventHandler(this.Interrupt); } } protected virtual void Dispose(bool disposing) { if (disposing) { // Dispose managed resources. if (m_port != null) { m_port.Dispose(); m_port = null; } } // Free native resources. } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } void Interrupt(uint data1, uint data2, DateTime time) { RawButtonActions action = (data2 != 0) ? RawButtonActions.ButtonUp : RawButtonActions.ButtonDown; RawButtonInputReport report = new RawButtonInputReport(null, time, m_button, action); // Queue the button press to the handler. m_sink.m_dispatcher.BeginInvoke(delegate(object arg) { m_sink.m_buttonInputHandler((InputReportArgs)arg); return null; }, new InputReportArgs(m_buttonDevice, report)); } } } }
The code is not related to StaMa. Similar code is created by the .NET Micro Framework code wizard as part of the .NET Micro Framework Window Application default project. The class is a driver for the button pad of the .NET Micro Framework emulator and invokes a handler passed in to the GPIOButtonInputProvider constructor whenever a button connected to the Microsoft.SPOT.Hardware.Cpu.Pin.GPIO_Pin0..GPIO_Pin4 is pressed. The handler is called in the thread where the GPIOButtonInputProvider instance was created.
Add a new class with file name Display.cs to the project.
Copy the following code into the Display.cs file:
using System; using System.Collections; using Microsoft.SPOT; using Microsoft.SPOT.Hardware; namespace SampleSimpleStateMachineNETMF { static class Display { private static readonly Bitmap m_screen; private static readonly string[] m_lines; private static readonly Font m_font; private static readonly int m_fontHeight; private static readonly int m_screenLines; private static readonly char[] m_newLineSeparators; private static int m_linesIndex; private static int m_linesCount; static Display() { int width, height, bitsPerPixel, orientationDeg; HardwareProvider.HwProvider.GetLCDMetrics(out width, out height, out bitsPerPixel, out orientationDeg); m_screen = new Bitmap(width, height); m_newLineSeparators = new char[] { '\n' }; m_font = Resources.GetFont(Resources.FontResources.small); m_fontHeight = m_font.Height; m_screenLines = height / m_fontHeight; m_lines = new string[m_screenLines]; m_linesCount = 0; m_screen.Clear(); m_screen.Flush(); } public static void WriteLine(string text) { if (text == null) { throw new ArgumentNullException("text"); } string[] lines = text.Split(m_newLineSeparators); for (int i = 0; i < lines.Length; i++) { string line = lines[i]; m_lines[m_linesIndex] = (line[line.Length - 1] != '\r') ? line : line.Substring(0, line.Length - 1); m_linesIndex = GetLinesIndexModulo(m_linesIndex + 1); m_linesCount = System.Math.Min(m_linesCount + 1, m_screenLines); } m_screen.Clear(); for (int i = 0; i < m_linesCount; i++) { int index = GetLinesIndexModulo(m_linesIndex + i + (m_screenLines - m_linesCount)); m_screen.DrawText(m_lines[index], m_font, Microsoft.SPOT.Presentation.Media.Color.White, 0, i * m_fontHeight); } m_screen.Flush(); } private static int GetLinesIndexModulo(int index) { return index < m_screenLines ? index : index - m_screenLines; } } }
The code is not related to StaMa. The class provides a driver for the display of the .NET Micro Framework emulator and provides a method to write text line by line onto the display.
Copy the following code into the Program.cs file:
using System; using System.Collections; using Microsoft.SPOT; using Microsoft.SPOT.Hardware; using Microsoft.SPOT.Input; namespace SampleSimpleStateMachineNETMF { public class Program { public static void Main() { GPIOButtonInputProvider buttonInputProvider = new GPIOButtonInputProvider(GPIOButtonInputProvider_ButtonInput); Dispatcher.Run(); } private static void GPIOButtonInputProvider_ButtonInput(InputReportArgs arg) { InputReportArgs args = (InputReportArgs)arg; RawButtonInputReport report = (RawButtonInputReport)args.Report; string info = report.Timestamp.ToLocalTime().ToString("HH:mm:ss.fff") + " Button=" + report.Button.ToString() + " Action=" + report.Actions.ToString(); Debug.Print(info); Display.WriteLine(info); } } }
Compile the solution. No compiler errors appear.
Execute the solution by selecting "Start Debugging" from the "Debug" menu or by pressing F5.
The .NET Micro Framework emulator application appears.
Set a breakpoint to the GPIOButtonInputProvider_ButtonInput method in file Program.cs.
Click the center button of the button pad on the emulator application.
The breakpoint in Program.cs is hit (twice, first for button down, then for button up) Information about the button action is shown in the Microsoft Visual Studio Output panel.
The above project provides generic application frame with basic input capabilities, hosted within the emulator application. The steps in the next section will add a simple state machine based on StaMa.
Add assembly references to StaMa.dll and System.Text.RegularExpressions.dll.
Add a new class with file name SampleSimpleStateMachineNETMF.cs to the project.
Copy the following code into the SampleSimpleStateMachineNETMF.cs file:
using System; using Microsoft.SPOT; using StaMa; namespace SampleSimpleStateMachineNETMF { class SampleSimpleStateMachineNETMF { private StateMachine m_stateMachine; private DispatcherTimer m_timeoutTimer; public SampleSimpleStateMachineNETMF() { StateMachineTemplate t = new StateMachineTemplate(); t.Region("State1", false); t.State("State1", EnterState1, ExitState1); t.Transition("Transition1to2", "State2", "Event1", null, null); t.EndState(); t.State("State2", EnterState2, ExitState2); t.Transition("Transition2to1", "State1", "TimeoutState2", null, null); t.EndState(); t.EndRegion(); m_stateMachine = t.CreateStateMachine(); m_stateMachine.TraceStateChange = this.TraceStateChange; m_timeoutTimer = new DispatcherTimer(); m_timeoutTimer.Tick += TimeoutTimer_Tick; m_stateMachine.Startup(); } private void EnterState1(StateMachine stateMachine, object triggerEvent, EventArgs eventArgs) { Display.WriteLine("EnterState1"); } private void ExitState1(StateMachine stateMachine, object triggerEvent, EventArgs eventArgs) { Display.WriteLine("ExitState1"); } private void EnterState2(StateMachine stateMachine, object triggerEvent, EventArgs eventArgs) { Display.WriteLine("EnterState2"); m_timeoutTimer.Tag = "TimeoutState2"; m_timeoutTimer.Interval = new TimeSpan(0, 0, 2); m_timeoutTimer.Start(); } void TimeoutTimer_Tick(object sender, EventArgs e) { m_timeoutTimer.Stop(); m_stateMachine.SendTriggerEvent(m_timeoutTimer.Tag); } private void ExitState2(StateMachine stateMachine, object triggerEvent, EventArgs eventArgs) { m_timeoutTimer.Stop(); Display.WriteLine("ExitState2"); } private void TraceStateChange(StateMachine stateMachine, StateConfiguration stateConfigurationFrom, StateConfiguration stateConfigurationTo, Transition transition) { string info = DateTime.Now.ToString("HH:mm:ss.fff") + " ActiveState=\"" + stateConfigurationTo.ToString() + "\"" + " Transition=" + ((transition != null) ? "\"" + transition.Name + "\"" : "Startup/Finish"); Debug.Print(info); Display.WriteLine(info); } public void ButtonUp() { m_stateMachine.SendTriggerEvent("Event1"); } } }
The above code adds a state machine with two states "State1" and "State2", a transition "Transition1to2" from "State1" to "State2" which is triggered through signal "Event1" and a transition "Transition2to1" from "State2" to "State1" which is triggered through signal "TimeoutState2" sent from the tick event handler of a Microsoft.Spot.DispatcherTimer after 2 seconds.
"State1" has an entry action EnterState1 and an exit action ExitState1 and "State2" has an entry action EnterState2 and an exit action ExitState2. These are executed when the state machine switches the state from "State1" to "State2" or back from "State2" to "State1" .
The ButtonUp method sends the signal "Event1" to the state machine, triggering "Transition1to2" when the state machine is in state "State1".
Create a new SampleSimpleStateMachineNETMF instance at the beginning of the Main method in file Program.cs.
Invoke the SampleSimpleStateMachine.ButtonUp method from within in the GPIOButtonInputProvider_ButtonInput handler when a button is released.
using System; using System.Collections; using Microsoft.SPOT; using Microsoft.SPOT.Hardware; using Microsoft.SPOT.Input; using SampleSimpleStateMachineNETMF; namespace SampleSimpleStateMachineNETMF { public class Program { private static SampleSimpleStateMachineNETMF m_sampleSimpleStateMachine; public static void Main() { m_sampleSimpleStateMachine = new SampleSimpleStateMachineNETMF(); GPIOButtonInputProvider buttonInputProvider = new GPIOButtonInputProvider(GPIOButtonInputProvider_ButtonInput); Dispatcher.Run(); } private static void GPIOButtonInputProvider_ButtonInput(InputReportArgs arg) { InputReportArgs args = (InputReportArgs)arg; RawButtonInputReport report = (RawButtonInputReport)args.Report; string info = report.Timestamp.ToLocalTime().ToString("HH:mm:ss.fff") + " Button=" + report.Button.ToString() + " Action=" + report.Actions.ToString(); Debug.Print(info); Display.WriteLine(info); if (report.Actions == RawButtonActions.ButtonUp) { m_sampleSimpleStateMachine.ButtonUp(); } } } }
Set a breakpoint at the end of method SampleSimpleStateMachine.TraceStateChange and start the debugger e.g. by pressing F5.
Click the center button of the button pad on the emulator application. Releasing the button on the emulator button panel triggers the GPIOButtonInputProvider which invokes the Program.GPIOButtonInputProvider_ButtonInput method on the main thread which in turn invokes SampleSimpleStateMachineNETMF.ButtonUp method.
The breakpoint in SampleSimpleStateMachine.TraceStateChange is hit (three times):
During startup when the state machine enters its initial state.
Triggered through "Transition1to2" when the state machine transitions from "State1" to "State2" in response to the signal "Event1".
Triggered through "Transition2to1" when the state machine transitions from "State2" to "State1" in response to the timeout signal "TimeoutState2".
The above code provides a minimal executable state machine that can be extended with composite states or orthogonal sub-regions.