pipes

Store definition

// SPDX-FileCopyrightText: 2020-2023 Jochem Rutgers
//
// SPDX-License-Identifier: CC0-1.0

float sensor
float setpoint

Application

// SPDX-FileCopyrightText: 2020-2023 Jochem Rutgers
//
// SPDX-License-Identifier: CC0-1.0

/*!
 * \file
 * \brief Pipes example.
 *
 * Pipes can be used to compose functionality, such that data streams through a
 * pipe and is modified on the go. Pipes are sequence of objects, which inspect
 * and/or modify the data that is passed through it.
 *
 * Of course, you can write normal functions to implement all this behavior,
 * but the pipe concept turns out to be very useful in case the operations on
 * data become complex and non-centralized. We used it in GUIs, where raw data
 * from sensors are type-converted, unit-converted, checked for boundaries,
 * written changes to the log, multiple views that are synchronized in
 * user-selected units, rate limited for GUI updates, and complex switching of
 * model data below the view logic.
 *
 * It is powerful in the sense that every pipe segment deals with its own
 * functional thing, while the combination of segments can become very complex.
 * Additionally, adding/removing parts of the pipe is easy to do. For example,
 * if you want to add logging afterwards, inserting a Log segment is trivial,
 * without worrying about that some corner cases or code paths do not hit your
 * logging operation, which may be harder in a normal imperative approach with
 * functions.
 *
 * This examples gives an impression what you could do with pipes. This example
 * only uses primitive types as the pipe data type, but actually any type is
 * supported (while moving/copying of data optimized through the pipe). The
 * library provides a series of standard segments.  Writing one yourself is
 * easy; any class/struct can be as segment, as long as it implements the
 * inject() function.
 *
 * Pipes require at least C++14, but C++17 gives you especially the template
 * deduction guides, which simplifies the pipes.  This example requires C++17.
 */

#include "ExamplePipes.h"

#include <algorithm>
#include <stored>

static void measurement()
{
	using namespace stored::pipes;

	stored::ExamplePipes store;

	// In this example, assume we have some measurement data in the store.
	// We want to visualize this on some GUI. Assume that the user can
	// select the unit for this visualization. Let's construct the
	// following pipes:

	// This pipe converts input data in SI units to m.
	auto data_m =
		// Assume that data in the store is in SI units, so m in this
		// case...
		Entry<double>{} >>
		// ...save the last value, which is already in the requested
		// unit m.
		Buffer<double>{} >>
		//
		Exit{};

	// Now, data_m is an object, with consists of a sequence of
	// Entry/Buffer/Exit in this case.  These three pipe segments are
	// combined (at compile time), and cannot be addressed and split
	// separately.  The resulting data_m pipe may be connected dynamically
	// to other pipes, though.

	// This pipe converts input data in SI units to km.
	auto data_km =
		// When SI data received...
		Entry<double>{} >>
		// ...divide by 1000 to convert m into km...
		Convert{Scale<double, std::milli>{}} >>
		// ...and save for later.
		Buffer<double>{} >>
		//
		Exit{};

	// This is the raw input data handling pipe.
	auto data =
		// Data is received from the store...
		Entry<double>{} >>
		// ...and written to the terminal...
		Call{[](double x) { printf("changed %g m\n", x); }} >>
		// ...and forwarded to both pipes for unit conversion.
		Tee{data_m, data_km} >>
		//
		Cap{};

	// This pipe actually reads data from the store.
	auto getter =
		// When something is injected...
		Entry<bool>{} >>
		// ...retrieve data from the store...
		Get{store.sensor} >>
		// ...cast it to double...
		Cast<float, double>{} >>
		// ...upon changes, forward the value to the 'data' pipe.
		Changes{data, similar_to<double>{}} >>
		//
		Cap{};

	// We only have two units in this example.
	enum class Unit { m, km };

	// This pipe converts the enum value to a string.
	auto view_unit =
		// A unit is received...
		Entry<Unit>{} >>
		// ...and we are using a lookup table to convert it to string...
		Mapped(make_random_map<Unit, char const*>({{Unit::m, "m"}, {Unit::km, "km"}})) >>
		// ...and save the output.
		Buffer<char const*>{} >>
		//
		Exit{};

	// Create some class that allows dynamic callbacks to be connected.
	// Something like Qt's signal/slot mechanism.
	stored::Signal<void*, void*, double> sig;

	// This is the view, which allows the unit selection.
	auto view =
		// Upon unit entry...
		Entry<Unit>{} >>
		// ...split of the selected unit for string conversion...
		Tee{view_unit} >>
		// ...map the Unit to an index, corresponding with the Mux below...
		Mapped(make_random_map<Unit, size_t>({{Unit::m, 0U}, {Unit::km, 1U}})) >>
		// ...retrieve the data from the proper unit converted pipe...
		Mux{data_m, data_km} >>
		// ...signal sig to indicate that the data has changed...
		Signal{sig} >>
		//
		Exit{};

	// Let's connect some callback to sig. In case you have Qt, you may
	// trigger some Qt signal to actually update the view.  (However, there
	// are also other ways to do that, like using the Call pipe segment,
	// and probably you want also some RateLimit first.)
	sig.connect([](double x) { printf("signalled %g\n", x); });


	// The following plumbing has be realized:
	//
	//         getter
	//
	//           ||
	//           vv
	//
	//          data
	//
	//           || tee
	//           ||
	//   //======[]======\|
	//   ||              ||
	//   vv              vv
	//
	// data_m          data_km
	//                               ||
	//   ||              ||          vv
	//   ||              ||
	//   ||              \\=====    view  =====> view_unit
	//   \\=====================
	//                       mux     ||
	//                               ||
	//                               vv



	// Let's test:
	store.sensor = 1.F;
	printf("\nUpdate the data from the store:\n");
	true >> getter;

	printf("\nUpdate the data from the store, but without changes:\n");
	// For the Getter(), trigger() is the same as injecting data. This is
	// probably cleaner, though.
	getter.trigger();

	// Assume the data has changed.
	store.sensor = 10.F;
	printf("\nUpdate the data:\n");
	getter.trigger();

	// Now the view is actually updated.
	printf("\nSelect km:\n");
	double x = Unit::km >> view;
	printf("sensor view = %g %s\n", x, view_unit.extract().get());

	printf("\nSelect m:\n");
	x = Unit::m >> view;
	printf("sensor view = %g %s\n", x, view_unit.extract().get());

	printf("\nSensor update:\n");
	store.sensor = 11.F;
	getter.trigger();
	printf("sensor view = %g %s\n", view.extract().get(), view_unit.extract().get());
}

static void setpoint()
{
	using namespace stored::pipes;

	stored::ExamplePipes store;

	// For this example, envision that we have a setpoint in the store.
	// Some GUI visualizes this setpoint. Additionally, the user can open a
	// popup and edit the setpoint.  While the user is editing, the
	// setpoint should not be written to the store, until the user presses
	// some OK button.
	//
	// The pipes we need are the following:

	printf("\n\nInitializing:\n");

	// A pipe that performs the actual store write.
	auto setter =
		// Upon injected data...
		Entry<double>{} >>
		// ...and log that we are going to write to the store...
		Log<double>{"setter setpoint"} >>
		// ...convert to the store's type...
		Cast<double, float>{} >>
		// ...and write to the store.
		Set{store.setpoint} >>
		//
		Exit{};

	// The editor popup, which holds the new data for a while.
	auto editor =
		// Let's say, here is data entered in some text field...
		Entry<double>{} >>
		// ...and it is saved, until trigger() is called. And if so, it
		// is forwarded to the setter pipe...
		Triggered{setter} >>
		// ...and log all changes to this setpoint value.
		Log<double>{"edited setpoint"} >>
		//
		Exit{};

	// The main view of the store's setpoint value.
	auto view =
		// When new data comes in...
		Entry<float>{} >>
		// ...properly convert it...
		Cast<float, double>{} >>
		// ...log all received values...
		Log<double>{"view setpoint"} >>
		// ...and when there are changes, forward these to the
		// editor. Let's say, if the underlaying data changes, you
		// probably want to reflect this in the input field...
		Changes{editor} >>
		// ...and save the data for future extract()s.
		Buffer<double>{} >>
		//
		Exit{};

	// Some mechanism to retrieve data from the underlaying store, if it
	// would be modified concurrently.
	auto getter =
		// Upon any injection...
		Entry<bool>{} >>
		// ...retrieve data from the store (although you could also do
		// trigger()).
		Get{store.setpoint} >>
		//
		Exit{};


	// Forward output of the setter to the view.
	setter >> view;
	// Forward explicitly read data to the view.
	getter >> view;



	// Now, we constructed the following plumbing:
	//
	//                      getter
	//
	//                        ||
	//                        VV
	//
	// setter =============> view
	//
	//  ^^                    || when changed
	//  ||                    VV
	//  ||
	//  \\================= editor
	//   when triggered



	// Let's test it.
	//
	// This will write 1 into the store. Expect also a log entry on the
	// console from the view and the editor.
	printf("\nWrite the store via the setter:\n");
	1 >> setter;
	printf("store.setpoint = %g\n\n", (double)store.setpoint.get());

	// We can do this again. Expect three more log lines on the console.
	2 >> setter;

	printf("\nEdit the store and trigger the getter:\n");
	store.setpoint = 3;
	// This will read the data from the store, and update the view and editor.
	getter.trigger();

	printf("\nEnter data in the editor, but do not write it yet:\n");
	// This data is only saved in the editor pipe. As long as the user does
	// not press OK, do not really write it to the store.
	4 >> editor;

	printf("\nNow, the user accepts the input:\n");
	// Let's say, the user pressed OK.
	editor.trigger();

	printf("\nAgain, but the data has not changed:\n");
	// So, no additional setter/view log lines are expected.
	editor.trigger();
}

int main()
{
	measurement();
	setpoint();
}

Output


Update the data from the store:
changed 1 m

Update the data from the store, but without changes:

Update the data:
changed 10 m

Select km:
signalled 0.01
sensor view = 0.01 km

Select m:
signalled 10
sensor view = 10 m

Sensor update:
changed 11 m
signalled 11
sensor view = 11 m


Initializing:
view setpoint = 0
view setpoint = 0

Write the store via the setter:
setter setpoint = 1
view setpoint = 1
edited setpoint = 1
store.setpoint = 1

setter setpoint = 2
view setpoint = 2
edited setpoint = 2

Edit the store and trigger the getter:
view setpoint = 3
edited setpoint = 3

Enter data in the editor, but do not write it yet:
edited setpoint = 4

Now, the user accepts the input:
setter setpoint = 4
view setpoint = 4
edited setpoint = 4
edited setpoint = 4

Again, but the data has not changed:

Store reference

template<typename Base_, typename Implementation_>
class ExamplePipesObjects

All ExamplePipesBase’s objects.

Public Types

typedef Base_ Base
typedef Implementation_ Implementation

Public Members

impl::StoreVariable<Base, Implementation, float, 0u, 4> sensor

sensor

impl::StoreVariable<Base, Implementation, float, 4u, 4> setpoint

setpoint

template<typename Implementation_>
class ExamplePipesBase : public stored::ExamplePipesObjects<ExamplePipesBase<Implementation_>, Implementation_>

Base class with default interface of all ExamplePipes implementations.

Although there are no virtual functions in the base class, subclasses can override them. The (lowest) subclass must pass the Implementation_ template paramater to its base, such that all calls from the base class can be directed to the proper overridden implementation.

The base class cannot be instantiated. If a default implementation is required, which does not have side effects to functions, instantiate stored::ExamplePipes. This class contains all data of all variables, so it can be large. So, be aware when instantiating it on the stack. Heap is fine. Static allocations is fine too, as the constructor and destructor are trivial.

To inherit the base class, you can use the following template:

class ExamplePipes : public stored::store<ExamplePipes, ExamplePipesBase>::type {
    STORE_CLASS(ExamplePipes, ExamplePipesBase)
public:
    // Your class implementation, such as:
    ExamplePipes() is_default
    // ...
};

Some compilers or tools may get confused by the inheritance using stored::store or stored::store_t. Alternatively, use STORE_T(...) instead, providing the template parameters of stored::store as macro arguments.

See also

stored::ExamplePipesData

Subclassed by stored::ExamplePipesDefaultFunctions< ExamplePipesBase< ExamplePipes > >

Public Types

enum [anonymous]

Values:

enumerator ObjectCount

Number of objects in the store.

enumerator VariableCount

Number of variables in the store.

enumerator FunctionCount

Number of functions in the store.

enumerator BufferSize

Buffer size.

typedef Implementation_ Implementation

Type of the actual implementation, which is the (lowest) subclass.

typedef uintptr_t Key

Type of a key.

See also

bufferToKey()

typedef Map<String::type, Variant<Implementation>>::type ObjectMap

Map as generated by map().

typedef ExamplePipesObjects<ExamplePipesBase, Implementation_> Objects
typedef ExamplePipesBase root

We are the root, as used by STORE_CLASS.

typedef ExamplePipesBase self

Define self for stored::store.

Public Functions

inline ~ExamplePipesBase()
inline Key bufferToKey(void const *buffer) const noexcept

Converts a variable’s buffer to a key.

A key is unique for all variables of the same store, but identical for the same variables across different instances of the same store class. Therefore, the key can be used to synchronize between instances of the same store. A key does not contain meta data, such as type or length. It is up to the synchronization library to make sure that these properties are handled well.

For synchronization, when hookEntryX() or hookEntryRO() is invoked, one can compute the key of the object that is accessed. The key can be used, for example, in a key-to-Variant map. When data arrives from another party, the key can be used to find the proper Variant in the map.

This way, data exchange is type-safe, as the Variant can check if the data format matches the expected type. However, one cannot process data if the key is not set yet in the map.

inline Type::type bufferToType(void const *buffer) noexcept

Return the type of the variable, given its buffer.

inline Variant<Implementation> find(char const *name, size_t len = std::numeric_limits<size_t>::max()) noexcept

Finds an object with the given name.

Returns:

the object, or an invalid stored::Variant if not found.

template<typename T>
inline Function<T, Implementation> function(char const *name, size_t len = std::numeric_limits<size_t>::max()) noexcept

Finds a function with the given name.

The function, when it exists, must have the given (fixed) type.

inline Implementation const &implementation() const noexcept

Returns the reference to the implementation.

inline Implementation &implementation() noexcept

Returns the reference to the implementation.

template<typename F>
inline void list(F &&f) noexcept

Calls a callback for every object in the longDirectory().

See also

stored::list()

template<typename F>
inline void list(F f, void *arg, char const *prefix, String::type *nameBuffer) noexcept

Calls a callback for every object in the longDirectory().

See also

stored::list()

template<typename F>
inline void list(F f, void *arg, char const *prefix = nullptr) noexcept

Calls a callback for every object in the longDirectory().

See also

stored::list()

inline uint8_t const *longDirectory() const noexcept

Retuns the long directory.

When not available, the short directory is returned.

inline ObjectMap map(char const *prefix = nullptr)

Create a name to Variant map for the store.

Generating the map may be expensive and the result is not cached.

inline char const *name() const noexcept

Returns the name of store, which can be used as prefix for stored::Debugger.

inline uint8_t const *shortDirectory() const noexcept

Returns the short directory.

template<typename T>
inline Variable<T, Implementation> variable(char const *name, size_t len = std::numeric_limits<size_t>::max()) noexcept

Finds a variable with the given name.

The variable, when it exists, must have the given (fixed) type.

Public Members

impl::StoreVariable<Base, Implementation, float, 0u, 4> sensor

sensor

impl::StoreVariable<Base, Implementation, float, 4u, 4> setpoint

setpoint

Public Static Functions

template<typename T>
static inline constexpr FreeFunction<T, Implementation> freeFunction(char const *name, size_t len = std::numeric_limits<size_t>::max()) noexcept

Finds a function with the given name.

The function, when it exists, must have the given (fixed) type. It is returned as a free function; it is not bound yet to a specific store instance. This function is constexpr for C++14.

template<typename T>
static inline constexpr FreeVariable<T, Implementation> freeVariable(char const *name, size_t len = std::numeric_limits<size_t>::max()) noexcept

Finds a variable with the given name.

The variable, when it exists, must have the given (fixed) type. It is returned as a free variable; it is not bound yet to a specific store instance. This function is constexpr for C++14.

static inline constexpr char const *hash() noexcept

Returns a unique hash of the store.

Friends

friend class impl::StoreFunction
friend class impl::StoreVariable
friend class impl::StoreVariantF
friend class impl::StoreVariantV
friend class stored::FreeVariable
friend class stored::Variant< void >
class ExamplePipes : public stored::ExamplePipesDefaultFunctions<ExamplePipesBase<ExamplePipes>>

Default ExamplePipesBase implementation.

Public Functions

ExamplePipes() = default

Default constructor.