Skip to content

Instantly share code, notes, and snippets.

@CemraJC
Created September 30, 2019 05:46
Show Gist options
  • Save CemraJC/c67ca8b8ecd5850f6e7ea85bc4180bea to your computer and use it in GitHub Desktop.
Save CemraJC/c67ca8b8ecd5850f6e7ea85bc4180bea to your computer and use it in GitHub Desktop.
FlowTask implementation in C# - see https://decatronics.org/flow_tasks
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