/state_machine

A class for using a thread like a finite-state automaton-like program that PLC and automation programmers like

Primary LanguageC++MIT LicenseMIT

State Machine

Tools to use a thread like a finite-state automaton-like program that PLC and automation programmers like.

The class has a basic weakeup period that is set as a constructor argument, in milliseconds. Then, it can be filled with objects whose timing method is activated periodically (the period is set when adding, the parent class' timer must be its divisor). A modification that implements typical functionality of a finite state automaton is available. When all is set up, the resume() method is called to activate it all. Each wakeup gets the same input and time for all automatons, that are fired in the sequence they were added in.

Input and output are be accessed in a synchronised way, either between wakeups or delayed until the wakeup finishes. The returned value is a smart pointer that keeps the wakeup from occurring until destroyed. You may want to copy it.

Classes

Most classes expect two template arguments, one for the input structure given to the state machines, one for the output structures given to the state machines. Class StateMachine that comfortably implements state machines accepts the type describing its state (meant to be an enum) as the third template argument.

template<typename Input, typename Output> class TimedObject

A basic object that can be inserted into the system, it only has the basic interface providing time, input and output. Overload its tick(const Input&, Output&) method for the loop, it doesn't need to be initalised. Use StateMachine for more features.

It provides a makeTimer() method that returns a timer that has a time() method to get the time from its creation in milliseconds and always returns time 0 if default constructed or its method deactivate() was called (this can be checked using its active() method). Its other methods are lastPeriod() that returns the time since last tick in milliseconds and frameTime() that returns the time of that tick.

template<typename Input, typename Output, typename State> class StateMachine

A more advanced object to represent a state machine. Its third template parameter can be anything (intended for an enum) that represents a state. It has a state() method that reads its state and a state(State) method that sets its state. The time spent in the current state can be checked using timeInState().

It also has all the functionality of TimedObject.

template<typename T> class ProtectedReturn

A smart pointer that holds lock over a returned structure until it's destroyed.

template<typename Input, typename Output> class StateMachineManager

A basic class that holds the state machines. It can be paused using the pause() method and resumed using the unpause() method. It starts paused. While it's paused, its contents can be modified with methods addTimedObject() and removeTimedObject().

The input and output structures can be obtained using the input() and output() methods that return ProtectedReturn type smart pointers that hold locks over the structures until destroyed. These methods are therefore thread-safe.

Example

Here is a commeted example of a heating unit program:

// Declare input and output structures
struct Input {
	float temperature;
};
struct Output {
	float power;
};

// Declare a PID controller class
class TemperatureController : public TimedObject<Input, Output> {
	const float proportional_ = 0.3f;
	const float integral_ = 0.02f;
	const float differential_ = -0.2f;
	float integralTotal = 0;
	float previous_ = 0;
public:
	virtual void tick(const Input &in, Output &out)
	{
		float difference = desired_ - in.temperature;
		float needed = difference * proportional_ + integral_ * integralTotal + differential_ * (difference - previous_);
		
		if (needed < 0.0f)
			out.power = 0.0f;
		else if (needed > 100.0f)
			out.power = 100.0f;
		else {
			out.power = needed;
			integralTotal += difference;
		}
		previous_ = difference;
	}
	float desired_ = 0;
};

// Declare states for a class that would control the heating and cooling process
enum TemperatureProgrammerState {
	STARTING = 0,
	HEATING,
	HOT,
	COOLING,
	COOL
};

// Declare the class for controlling the process, using the states set above
class TemperatureProgrammer : public StateMachine<Input, Output, TemperatureProgrammerState> {
	float ramp_ = 0.005f;
	float max_ = 100.0f;
	int hotTime_ = 10000;
	float finish_ = 20.0f;
	float coolRamp_ = 0.005f;
	std::shared_ptr<TemperatureController> controller_;
	
public:
	TemperatureProgrammer(std::shared_ptr<TemperatureController> controller) : controller_(controller)
	{
		state(STARTING);
	}
	
	virtual void tick(const Input &in, Output &out)
	{
		switch(state()) {
			case STARTING:
				state(HEATING);
				break;
			case HEATING: {
				float wanted = timeInState() * ramp_;
				if (wanted > max_) {
					wanted = max_;
					state(HOT);
				}
				controller_->desired_ = wanted;
				break;
			}
			case HOT:
				controller_->desired_ = max_;
				if (timeInState() > hotTime_) {
					state(COOLING);
				}
				break;
			case COOLING: {
				float wanted = max_ - timeInState() * ramp_;
				if (wanted < finish_) {
					wanted = finish_;
					state(COOL);
				}
				controller_->desired_ = wanted;
				break;
			}
			case COOL:
				break;
		}
	}
};

// Create the manager that controls all the state machines, initialise the Input/Output structures, set the minimal period to 100 ms
// It starts in paused state
StateMachineManager<Input, Output> manager(Input{ 20 }, Output{ 0 }, 100);

// Create instances of the classes and insert them into the manager
// It can be done only while the manager is paused
std::shared_ptr<TemperatureController> controller = std::make_shared<TemperatureController>();
manager.addTimedObject(200, controller);
manager.addTimedObject(500, std::make_shared<TemperatureProgrammer>(controller));

// Start the manager, this has to be done from the thread that created it
manager.unpause();

// Periodic reading of output and setting of input for the next turn
for (int i = 0; i < 400; i++) {
	std::this_thread::sleep_for (std::chrono::milliseconds(100));
	auto out = manager.output(); // These two operations are thread-safe because of locks
	auto in = manager.input();
	in->temperature = 20 + (in->temperature - 20) * 0.95f + out->power;
	std::cout << "Power: " << out->power << " temperature " << in->temperature << " desired " << controller->desired_ << std::endl;
	// Destroying the returned structures unlocks the structures
}

// When the manager falls out of scope, it is safely destroyed

Thread safety

Only the thread that created it can pause it, unpause it and destroy it. Reading output and setting input is thread safe and can be done from any thread.