Created
September 30, 2019 05:46
-
-
Save CemraJC/c67ca8b8ecd5850f6e7ea85bc4180bea to your computer and use it in GitHub Desktop.
FlowTask implementation in C# - see https://decatronics.org/flow_tasks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
namespace flowtasks | |
{ | |
/// <summary> | |
/// Manages performing a set of tasks in a well-defined sequence, allowing holdoff to | |
/// when data or next-state information is available. | |
/// | |
/// The next task is run by the manager, passed the arguments given to the Step method, and | |
/// the task return value is used to determine the next task to run. | |
/// | |
/// Since this is an arbitrary state machine, the run method does not return a value. | |
/// | |
/// HOW TO USE: | |
/// =========== | |
/// Step 1: Design your task flow. Tasks should be self contained, and all information needed by other | |
/// tasks will be stored in a common object that is passed to all the tasks. | |
/// (Tasks are just FlowTask delegates that will be run in sequence) | |
/// | |
/// Tasks have the ability to step forward and backward through the list when they return. | |
/// Tasks can also "hold" for more information, "timeout" to reset after a period, "stop" the | |
/// FlowTaskManager (make it do nothing until it's reset) and jump to arbitary other tasks in | |
/// the task flow (with relative or absolute specificity). | |
/// | |
/// See FlowTaskNextState for the details. | |
/// | |
/// Task Design suggestions: | |
/// 1. Make each task have the same arguments list. Calls to Step() will be easier. | |
/// 2. Try to make the sequence follow state-machine rules, where every possible input | |
/// will produce some form of state transition (or not) | |
/// 3. If input is not what was expected, prefer TIMEOUT over HOLD so the system can reset | |
/// if the situation does not improve. Either that or RESET immediately. | |
/// 4. Try to avoid using JUMP - if the task sequence changes, a lot of updates will need to | |
/// take place to repair the sequencing. Use NEXT, PREV, and the occasional RJUMP instead. | |
/// 5. Draw a flow chart to help you! | |
/// | |
/// Step 2: Create the FlowTaskManager to handle running the tasks | |
/// Step 3: Add the tasks to the manager. Order is important. | |
/// IMPORTANT: The first task is numbered "0", and so on from there | |
/// Step 4: Each time there is new data available, call the manager's "step" method with the | |
/// required objects as arguments. Your methods will be passed these objects and should | |
/// do whatever needs to be done to progress to the next state (or hold in this one) | |
/// | |
/// Note: If needed, you can manually stop and reset the task manager. | |
/// </summary> | |
/// <remarks>Author: Jason Storey</remarks> | |
/// <remarks>Date: 30/09/2019</remarks> | |
public delegate FlowTaskNextState FlowTask(object[] args); | |
public class FlowTaskManager | |
{ | |
private List<FlowTask> tasks; | |
private int taskIndex; | |
/// <summary> | |
/// Indicates if the task flow is stopped (awaiting reset) | |
/// This is default behaviour if index is out of range | |
/// </summary> | |
public bool Stopped | |
{ | |
get | |
{ | |
return taskIndex < 0 || taskIndex >= tasks.Count; | |
} | |
} | |
// Used to handle timeout conditions. | |
// A "completed" task by necessity will move the task index away from itself | |
private DateTime lastCompleted = DateTime.MinValue; | |
public FlowTaskManager() | |
{ | |
tasks = new List<FlowTask>(); // No tasks at the start | |
taskIndex = 0; // Start at the first task | |
} | |
public int TaskCount() | |
{ | |
return tasks.Count; | |
} | |
/// <summary> | |
/// Adds a task on the end of the task list | |
/// </summary> | |
/// <param name="task">Task to add</param> | |
public void AddTask(FlowTask task) | |
{ | |
tasks.Add(task); | |
} | |
/// <summary> | |
/// Insert a task at a position in the list | |
/// </summary> | |
/// <param name="index">Where to insert the task</param> | |
/// <param name="task">The task to insert</param> | |
public void InsertTask(int index, FlowTask task) | |
{ | |
tasks.Insert(index, task); | |
} | |
/// <summary> | |
/// Appends a sequence of tasks to the task list | |
/// </summary> | |
/// <param name="sequence">The sequence to append</param> | |
public void AddTaskSequence(List<FlowTask> sequence) | |
{ | |
tasks.AddRange(sequence); | |
} | |
/// <summary> | |
/// Removes all tasks from the task list | |
/// </summary> | |
public void ClearTasks() | |
{ | |
tasks.Clear(); | |
} | |
/// <summary> | |
/// Run the current task, then set our task index to the next task to run | |
/// based on the return value of the current task. | |
/// </summary> | |
/// <param name="args">Arguments to pass to the task</param> | |
public void Step(object [] args=null) | |
{ | |
// If index is out of range, there is no next task (have been stopped) | |
if (Stopped) | |
{ | |
return; | |
} | |
// Otherwise, run the current task | |
FlowTask currentTask = tasks[taskIndex]; | |
FlowTaskNextState step = currentTask(args); | |
int nextIndex; // For use in JUMP | |
switch (step.state) | |
{ | |
case FlowTaskNextState.State.HOLD: | |
// Do nothing to task index | |
break; | |
case FlowTaskNextState.State.JUMP: | |
nextIndex = step.count; | |
if (nextIndex < 0 || nextIndex >= tasks.Count) | |
{ | |
throw new FormatException("Absolute jump out of bounds"); | |
} | |
else | |
{ | |
taskIndex = nextIndex; | |
} | |
break; | |
case FlowTaskNextState.State.RJUMP: | |
// Jump to next task, if possible | |
nextIndex = taskIndex + step.count; | |
if (nextIndex < 0 || nextIndex >= tasks.Count) | |
{ | |
throw new FormatException("Relative jump out of bounds"); | |
} | |
else | |
{ | |
taskIndex = nextIndex; | |
} | |
break; | |
case FlowTaskNextState.State.NEXT: | |
// Go to the next task, if we can | |
if (taskIndex < tasks.Count - 1) | |
{ | |
taskIndex++; | |
} | |
else | |
{ | |
throw new FormatException("Cannot go to next task from the last task"); | |
} | |
break; | |
case FlowTaskNextState.State.PREV: | |
// Go to the previous task, if we can | |
if (taskIndex > 0) | |
{ | |
taskIndex--; | |
} | |
else | |
{ | |
throw new FormatException("Cannot go to previous task from the first task"); | |
} | |
break; | |
case FlowTaskNextState.State.STOP: | |
StopTasks(); // Don't run any more tasks until we are reset | |
break; | |
case FlowTaskNextState.State.RESET: | |
Reset(); // Back to the start | |
break; | |
case FlowTaskNextState.State.TIMEOUT: | |
// If running the first task, timeout is not allowed (because it's already reset) | |
if (taskIndex == 0) | |
{ | |
break; | |
//throw new FormatException("Timeout not allowed on first task"); | |
} | |
// Otherwise, check timeout and reset if required | |
if (DateTime.Now - lastCompleted > step.time) | |
{ | |
Reset(); | |
} | |
// If no reset, hold task index in place | |
break; | |
default: | |
throw new FormatException("Unrecognised FlowTaskState control"); | |
} | |
// Only the following task returns count as a completion | |
if (step.state == FlowTaskNextState.State.JUMP || | |
step.state == FlowTaskNextState.State.RJUMP || | |
step.state == FlowTaskNextState.State.HOLD || | |
step.state == FlowTaskNextState.State.PREV || | |
step.state == FlowTaskNextState.State.RESET || | |
step.state == FlowTaskNextState.State.NEXT) | |
{ | |
lastCompleted = DateTime.Now; | |
} | |
} | |
/// <summary> | |
/// Start running tasks again from the beginning | |
/// </summary> | |
public void Reset() | |
{ | |
taskIndex = 0; | |
} | |
/// <summary> | |
/// Stop the manager from running (not called "Stop" because it's too similar to "Step") | |
/// </summary> | |
public void StopTasks() | |
{ | |
// Set to -1, out of range, will stop the machine | |
taskIndex = -1; | |
} | |
} | |
public class FlowTaskNextState | |
{ | |
public enum State | |
{ | |
HOLD, // Do this task again | |
JUMP, // Jump to a number task in the list (TRY TO AVOID) | |
RJUMP, // Jump a number of tasks relative to this one | |
NEXT, // Go to next task | |
PREV, // Go to previous task | |
STOP, // Task flow ended | |
RESET, // Equivalent to JUMP(0) | |
TIMEOUT, // Hold, but reset if it takes too long to continue | |
} | |
public TimeSpan time; // For TIMEOUT | |
public int count; // For JUMP and RJUMP | |
public State state; | |
/// <summary> | |
/// Make a keyword state | |
/// </summary> | |
public FlowTaskNextState(State next) | |
{ | |
if (next == State.TIMEOUT) | |
{ | |
throw new ArgumentException("Cannot make timeout without timespan parameter"); | |
} | |
if (next == State.JUMP) | |
{ | |
throw new ArgumentException("Cannot make jump without absolute offset"); | |
} | |
if (next == State.RJUMP) | |
{ | |
throw new ArgumentException("Cannot make jump without relative offset"); | |
} | |
time = default(TimeSpan); | |
count = 0; | |
state = next; | |
} | |
/// <summary> | |
/// Construct a timeout | |
/// </summary> | |
public FlowTaskNextState(State next, TimeSpan period) | |
{ | |
if (next != State.TIMEOUT) | |
{ | |
throw new ArgumentException("Need TIMEOUT with timespan parameter"); | |
} | |
time = period; | |
count = 0; | |
state = next; | |
} | |
/// <summary> | |
/// Construct a jump | |
/// </summary> | |
public FlowTaskNextState(State next, int offset) | |
{ | |
if (next != State.RJUMP && next != State.JUMP) | |
{ | |
throw new ArgumentException("Need JUMP or RJUMP with offset parameter"); | |
} | |
time = default(TimeSpan); | |
count = offset; | |
state = next; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment