Skip to content

Commands

Oleksandr Geronime edited this page Jun 27, 2026 · 1 revision

Commands

The Command subsystem provides async sequential orchestration — a way to queue and execute asynchronous operations one at a time, with lifecycle management, timeout enforcement, and result tracking.


Overview

Many embedded and automotive systems need to serialize operations that are individually async: you kick off a request, wait for a hardware acknowledgement, then proceed. Doing this ad-hoc with chains of callbacks is fragile. The Command subsystem gives you a reusable pattern:

  • Wrap each async operation in a serp::Command subclass.
  • Push commands into a serp::CommandQueue or serp::CommandProcessor.
  • The queue executes exactly one command at a time; the next starts only after the current command finishes, is canceled, aborts, or hits the watchdog limit.

Core Classes

serp::Command

Base class for all commands. Subclass it and override the execution hooks.

Method When called Purpose
onExecute() When the command reaches the head of the queue Begin async work. Call finish() when done.
onPostProcessing() After finish() is called, before the next command starts Optional cleanup or transaction finalization.
finish() Called by your code Signals successful completion.
cancel() Called by queue or externally Signals that the command should stop.

States:

Pending ──► Running ──► Finished
                   ├──► Canceled
                   ├──► Aborted
                   └──► Watchdog
State Meaning
Pending Queued, waiting to execute.
Running onExecute() has been called.
Finished finish() was called successfully.
Canceled The command was canceled before or during execution.
Aborted The command signaled an unrecoverable error.
Watchdog The watchdog timeout elapsed before finish() was called.

Result tracking:

enum class Result { Success, Canceled, Aborted, Watchdog };
serp::Command::Result result = cmd->getResult();

Delay before next command:

You can configure a short delay between finish() and the start of the next command — useful to let hardware or protocol state settle before the next operation begins:

cmd->setDelayBeforeNextCommand(std::chrono::milliseconds(50));

serp::ICommand

Pure virtual polymorphic interface. Useful when you need to store or pass commands without knowing their concrete type.


serp::CommandQueue

A FIFO queue that executes commands sequentially. One command runs at a time; it does not start the next until the current one reaches a terminal state.

auto queue = std::make_shared<serp::CommandQueue>();
queue->enqueue(std::make_shared<MyCommand>());
queue->enqueue(std::make_shared<AnotherCommand>());

Commands are executed in the order they are enqueued. Canceling the queue drains all pending commands.


serp::CommandProcessor

A CommandQueue backed by a dedicated EventLoop with integrated watchdog enforcement. The processor owns its own thread; commands execute there regardless of which thread enqueues them.

auto processor = std::make_shared<serp::CommandProcessor>(
    loop,
    std::chrono::seconds(10)   // watchdog timeout
);
processor->enqueue(std::make_shared<FetchCommand>());

The watchdog fires if a command does not call finish() (or abort/cancel) within the configured timeout. The command transitions to the Watchdog state, and the queue continues with the next command.


serp::LambdaCommand

A convenience class that wraps a lambda as a command — no subclass required for simple cases.

processor->enqueue(serp::LambdaCommand::create([](auto cmd) {
    doSomeWork();
    cmd->finish();
}));

The lambda receives a reference to the command itself, allowing you to call finish(), abort(), or check for cancellation inside the lambda body.


serp::CommandPool

A managed pool of reusable command objects. Avoids repeated allocation for high-frequency command patterns. Acquire a command from the pool, use it, and return it when done.

auto pool = std::make_shared<serp::CommandPool<FetchCommand>>(8 /*pool size*/);
auto cmd = pool->acquire();
// configure cmd...
processor->enqueue(cmd);

Listener Notifications

Register a listener to be notified on state transitions:

cmd->addListener([](serp::Command& c) {
    switch (c.getState()) {
        case serp::Command::State::Finished:
            // handle success
            break;
        case serp::Command::State::Watchdog:
            // handle timeout
            break;
        default:
            break;
    }
});

Listeners are called on the EventLoop of the processor that executed the command.


Examples

Subclass pattern

class FetchCommand : public serp::Command {
public:
    Data result;

protected:
    void onExecute() override {
        // Begin async work. finish() will be called from the callback.
        fetchData([this](Data d) {
            result = d;
            finish();
        });
    }

    void onPostProcessing() override {
        // Called after finish(), before the next command starts.
        // Use for transaction cleanup, logging, or post-processing.
        logInfo()->info("FetchCommand completed, result size: " +
                        std::to_string(result.size()));
    }
};

auto processor = std::make_shared<serp::CommandProcessor>(
    loop, std::chrono::seconds(10));

auto cmd = std::make_shared<FetchCommand>();
cmd->addListener([](serp::Command& c) {
    if (c.getResult() == serp::Command::Result::Success) {
        auto& fetch = static_cast<FetchCommand&>(c);
        useData(fetch.result);
    }
});

processor->enqueue(cmd);

Lambda pattern

auto processor = std::make_shared<serp::CommandProcessor>(
    loop, std::chrono::seconds(5));

// Simple fire-and-forget work item
processor->enqueue(serp::LambdaCommand::create([](auto cmd) {
    sendConfigPacket();
    cmd->finish();
}));

// Chained commands — they execute in order, one at a time
processor->enqueue(serp::LambdaCommand::create([](auto cmd) {
    openConnection([cmd]() { cmd->finish(); });
}));

processor->enqueue(serp::LambdaCommand::create([](auto cmd) {
    authenticate([cmd](bool ok) {
        if (ok) cmd->finish();
        else    cmd->abort();
    });
}));

Canceling a command

auto cmd = std::make_shared<LongRunningCommand>();
processor->enqueue(cmd);

// Later:
cmd->cancel();

If the command is still pending, it is removed from the queue. If it is already running, cancel() sets a flag; your onExecute() should check isCanceled() and stop work gracefully.


Watchdog Behavior

The watchdog timeout is set per CommandProcessor. If a command does not reach a terminal state within that duration:

  1. The command transitions to Watchdog state.
  2. Listeners are notified.
  3. onPostProcessing() is skipped.
  4. The queue advances to the next command.

This prevents a stalled async operation from blocking the entire queue indefinitely.


See Also

Clone this wiki locally