Skip to content

frameworklabs/proto_activities

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

proto_activities

Uses the protothreads approach to enable imperative synchronous programming (as promoted by Blech) in C or C++.

With this header-only library you can simplify your embedded programming projects by keeping a delay based approach but still enable multiple things to happen at once in a structured, modular and deterministic way.

Example code

/* This blinks an LED on every other tick. */
pa_activity (FastBlinker, pa_ctx(pa_defer_res), int pin) {
    pa_defer {
        setLED(pin, BLACK);
    };
    while (true) {
        setLED(pin, RED);
        pa_pause;

        setLED(pin, BLACK);
        pa_pause;
    }
} pa_end

/* This blinks an LED on a custom schedule. */
pa_activity (SlowBlinker, pa_ctx_tm(pa_defer_res), int pin, unsigned on_ticks, unsigned off_ticks) {
    pa_defer {
        setLED(pin, BLACK);
    };
    while (true) {
        setLED(pin, RED);
        pa_delay (on_ticks);

        setLED(pin, BLACK);
        pa_delay (off_ticks);
    }
} pa_end

/* An activity which delays for a given number of ticks. */
pa_activity (Delay, pa_ctx_tm(), unsigned ticks) {
    pa_delay (ticks);
} pa_end


/* This drives blinking LEDs and preempts them after 3 and 10 ticks. */
pa_activity (Main, pa_ctx_tm(pa_co_res(3); pa_use(Delay); pa_use(FastBlinker); pa_use(SlowBlinker))) {
    printf("Begin\n");

    /* Blink Fast LED for 3 ticks */
    pa_after_abort (3, FastBlinker, 0);
    
    /* Blink both LED for 10 ticks */
    pa_co(3) {
        pa_with (Delay, 10);
        pa_with_weak (FastBlinker, 0);
        pa_with_weak (SlowBlinker, 1, 3, 2);
    } pa_co_end
    
    printf("Done\n");
} pa_end

In this example, a fast led is blinked for 3 ticks and then both the fast and a slow led are blinked concurrently for 10 ticks.

To trigger the Main activity you need to declare its usage and then tick it until done - either at a specific frequency - or in a free-running style - by repeatedly calling pa_tick:

int main(int argc, char* argv[]) {
    pa_use(Main);

    while (pa_tick(Main) == PA_RC_WAIT) {
        std::this_thread::sleep_for(std::chrono::milliseconds(20)); // Will result in a 50Hz tick frequency.
    }

    return 0;
}

Constructs

As can be seen in the example above, an activity is defined by the pa_activity macro which takes the name of the activity as first parameter.

This is followed by what is called a context (pa_ctx(...)) and which stores the state which should outlive a single tick. Also sub-activities used in the activity are declared here with the pa_use(<SomeActivity>) macro. Separate context elements need to be separated by a semicolon (;). To use delays within an activity, use pa_ctx_tm instead of pa_ctx, which holds an implicit time variable.

After the context, place the input and output parameters of the activity.

At the end of an activity, use pa_activity_end or just pa_end to close it off.

Within an activity you can place normal C control structures and the following synchronous statements.

For a detailed description of the statements, please currently refer to the Blech documentation or look at the proto_activities test and example programs.

  • pa_pause: will pause processing of an activity and resume it at the next tick
  • pa_halt: will pause the activity forever
  • pa_await (cond): will pause the activity and resume it once cond becomes true
  • pa_await_immediate (cond): like pa_await but will not pause if cond is true in the current tick
  • pa_delay (ticks): will pause the activity for the given number of ticks
  • pa_delay_ms (ms): will pause the activity for the given number of milliseconds
  • pa_run (activity, ...): runs the given sub-activity until it returns
  • pa_return: end an activity from within its body - otherwise returns implicitly at the end
  • pa_co(n): starts a concurrent section with n trails - reserve the number of trails with pa_co_res(num_trails) in the activities context - end section with pa_co_end
  • pa_with (activity, ...): runs the given activity concurrently with the others of this section - only applicable within pa_co
  • pa_with_weak (activity, ...): runs the given activity concurrently with the others of this section and can be preempted - only applicable within pa_co
  • pa_when_abort (cond, activity, ...): runs the given activity until cond becomes true in a subsequent tick - unless it ends before
  • pa_when_reset (cond, activity, ...): runs the given activity and restarts it when cond becomes true in a subsequent tick
  • pa_when_suspend (cond, activity, ...): will suspend the given activity while cond is true and lets it continue when cond is false again
  • pa_after_abort (ticks, activity, ...): will abort the given activity after the specified number of ticks
  • pa_after_ms_abort (ms, activity, ...): will abort the given activity after the specified time in milliseconds
  • pa_did_abort (activity): reports whether an activity was aborted in a call before
  • pa_always: will run code on every tick - end block with pa_always_end
  • pa_every (cond): will run code everytime cond is true - end block with pa_every_end
  • pa_every_ms (ms): will run code now and every ms milliseconds thereafter - end block with pa_every_end. Note: Do not use any other construct which uses timing (like pa_delay_ms) in the enclosed block
  • pa_whenever (cond, activity, ...): will run the given activity whenever cond is true and abort it if cond turns false

When compiling wit C++ you could also define the following lifecycle callbacks:

  • pa_defer: defines an instantaneous block of code to run when the activity ends by itself or gets aborted. Add pa_defer_res annotation to the context to enable this feature.
  • pa_enter: defines an instantaneous block of code to run whenever the activity is entered - and initially when defined. Add pa_enter_res annotation to the context to enable this feature.
  • pa_suspend: defines an instantaneous block of code to run when an activity gets suspended by the surrounding pa_when_suspend. Add pa_susres_res annotation to the context to enable this feature.
  • pa_resume: defines an instantaneous block of code to run when an activity gets resumed by the surrounding pa_when_suspend. Add pa_susres_res annotation to the context to enable this feature.

In C++ you can also use signals. Signals can be emitted and checked for presence within a tick. The presence is automatically retreated at the begining of the next tick. Define a signal in a pa_ctx with either pa_def_signal(sig) or pa_def_val_signal(T, sig). The latter can be used to define signals carrying a value in addition to the presence flag. You also need to annotatate the activity defining signals with either pa_signal_res or pa_enter_res. Emit a signal with either pa_emit(sig) for pure signals or pa_emit_val(sig, val) for valued signals and check for presence by operator bool. Extract the value of a valued signal by sig.val(). Note that the value will stay in the next ticks even if not emitted again. This can e.g. be used to model flow values which inform about their update by the presence flag.

Related projects

  • A medium article about proto_activities can be found here.
  • Here is a little robot with proto_activities running on three ESP32 nodes.
  • See running proto_activities code in this online Wokwi simulator.
  • Blech is a new programming language for the embedded domain which inspired proto_activities.
  • Pappe is a sibling project which uses an embedded DSL to allow Blech-style imperative synchronous programming in Swift.