Chapter 9: Fields - Get() and Set()#

In chapter 7 we introduced fields – events “on steroids” that always carry a current value. There, however, we only used the notifier part of a field (tag WithNotifier): the consumer subscribed to the field and received value updates event-driven via GetNewSamples().

A field can, in addition to (or instead of) its notifier, offer a request/response style getter (Get()) and setter (Set()). These are enabled via the WithGetter / WithSetter tags. We deferred them to this chapter because – semantically – Get() and Set() on a field are just methods, and methods were only introduced in chapter 8.

This chapter takes the TirePressureService of chapter 7 as its basis, renames it to TirePressureExtendedService (tire_pressure_extended_service.h / .cpp) and extends it to demonstrate Get() and Set().

Warning

Current limitation (please read first): The provider-side handler that serves a field getter is not yet auto-registered by the middleware (see the TODO: Remove this skip once the field Get handler is auto-registered in SkeletonField. in score/mw/com/impl/bindings/lola/skeleton.cpp). As a consequence, while the code of this chapter builds and links cleanly, a provider will currently abort as soon as a consumer subscribes to a field Get(). Set() (i.e. WithSetter) already works end-to-end; the WithGetter/Get() path does not yet. This chapter is therefore written to be complete and correct against the public API, so that it runs as soon as the middleware gap is closed. The “How to run the example” section below reflects this.

Get() vs. the notifier - why does a getter exist at all?#

At first glance a getter looks redundant: with WithNotifier a consumer already receives every field value and can read the latest one via GetNewSamples(). So why would one want Get()?

The key is to understand what the notifier actually requires: whenever the provider updates the field value, that update has to be made visible to a subscribed consumer, i.e. the latest value has to end up in some buffer local to the subscriber, because that is exactly where GetNewSamples() looks. What “local to the subscriber” means, however, is binding-specific:

  • Setup 1 – network binding (separate ECUs): On every field update the provider has to send the new value over the network to the subscriber node, where it is stored in a local buffer that GetNewSamples() then reads.

  • Setup 2 – IPC binding with per-process buffers (same ECU, different processes): On every field update the provider has to copy the new value into the buffer of the subscriber’s process/address space, from where GetNewSamples() reads it.

  • Setup 3 – IPC binding with shared-memory buffers (same ECU, different processes): Here provider and subscriber share the very same buffer for the field samples. There is nothing extra for the provider to do after updating the value – enabling WithNotifier is essentially a no-op on the provider side.

Now assume the provider updates the field at a very high rate. In Setup 1 (and, to a lesser extent, Setup 2) enabling WithNotifier then creates real cost: CPU load in both cases and additional network traffic in Setup 1. If a consumer does not need every single update instantly – e.g. because it only uses the latest value in some rarely scheduled activity – keeping WithNotifier active would just waste resources. Such a consumer would instead disable the notifier, enable WithGetter and call Get() just in time, exactly when it needs the current value.

In Setup 3, on the other hand, where WithNotifier carries no added cost, a consumer could simply keep the notifier and always access the latest sample via GetNewSamples().

From an academic point of view it is somewhat unfortunate when a developer, who implements solely against the (binding agnostic) public API of score::mw::com, lets binding-specific behaviour drive the design of the service interface. Deciding to use WithNotifier and not WithGetter by assuming that the application will always run in a Setup 3 situation is problematic: at least in theory, the person deploying the application (who may be different from the one designing the interface) could change the configuration so that it is not a Setup 3 situation anymore.

Bottom line: there is no real semantic difference between “WithNotifier + read via GetNewSamples()” and “WithGetter + read via Get()”. It is rather a question of efficiency, which depends on the field update rate and the concrete binding being used.

The service interface#

The interface is based on chapter 7’s TirePressureInterface, renamed to TirePressureExtendedInterface. Compared to chapter 7 the following changed:

  • The four per-wheel fields (tire_pressure_front_left, …) are kept, but their tag changed from WithNotifier to WithGetter.

  • A fifth field tire_pressure_thresholds was added. Its data type is a struct TirePressureThreshold with two float members lower_threshold and upper_threshold. It is declared with WithGetter and WithSetter.

  • An event tire_pressure_warning was added, whose data type is an enum identifying a specific tire.

The event data type is a plain enum with a fixed underlying type (so it stays trivially copyable):

Listing 60 tire_pressure_extended_service.h#
enum class Tire : std::uint8_t
{
    front_left,
    front_right,
    rear_left,
    rear_right,
};

The value type of the new field is a small, trivially copyable struct (a field getter/setter transports the value as a method argument/return value through shared memory, so it must be trivially copyable – exactly the same constraint we saw for method arguments in chapter 8):

Listing 61 tire_pressure_extended_service.h#
struct TirePressureThreshold
{
    float lower_threshold;
    float upper_threshold;
};

The interface itself:

Listing 62 tire_pressure_extended_service.h#
template <typename Trait>
class TirePressureExtendedInterface : public Trait::Base
{
  public:
    using Trait::Base::Base;

    typename Trait::template Field<float, score::mw::com::WithGetter> tire_pressure_front_left{
        *this,
        "tire_pressure_front_left"};
    typename Trait::template Field<float, score::mw::com::WithGetter> tire_pressure_front_right{
        *this,
        "tire_pressure_front_right"};
    typename Trait::template Field<float, score::mw::com::WithGetter> tire_pressure_rear_left{
        *this,
        "tire_pressure_rear_left"};
    typename Trait::template Field<float, score::mw::com::WithGetter> tire_pressure_rear_right{
        *this,
        "tire_pressure_rear_right"};

    // The threshold band a consumer may both read (WithGetter) and adjust (WithSetter).
    typename Trait::template Field<TirePressureThreshold, score::mw::com::WithGetter, score::mw::com::WithSetter>
        tire_pressure_thresholds{*this, "tire_pressure_thresholds"};

    // Notifies consumers about a tire whose current pressure has left the configured threshold band.
    typename Trait::template Event<Tire> tire_pressure_warning{*this, "tire_pressure_warning"};
};

Files/artifacts used#

The bazel project for this chapter is located in score/mw/com/doc/tutorial/chapter_9. It follows the same structure as the previous chapters, with the service interface renamed to tire_pressure_extended_service:


File Name

Description

BUILD

This file contains bazel targets for this example.

consumer/consumer.cpp

Implementation of the service consumer app. The main() for the consumer

consumer/consumer.h

Header (empty - we just always want to have cpp/h pairs) of the service consumer.

consumer/mw_com_config.json

This file contains the configuration for score::mw::com for the consumer app.

consumer/logging.json

This file contains the configuration for the logging system used by score::mw::com

provider/provider.cpp

Implementation of the service provider.

provider/provider.h

Header (empty) of the service provider.

provider/logging.json

This file contains the configuration for the logging system used by score::mw::com

provider/mw_com_config.json

This file contains the configuration for score::mw::com for the provider app.

tire_pressure_extended_service.cpp

This file is empty as the service interface is completely defined in the header.

tire_pressure_extended_service.h

This file contains the definition of the service interface (fields + event).


Configuration#

The service type binding lists the five fields (each with a fieldId) and the new event (with an eventId). Note that a field getter/setter does not need its own methodId: internally the get/set methods are derived from the fieldId of the field they belong to, so the configuration of a field is unchanged whether or not it has a getter/setter.

Listing 63 provider/mw_com_config.json#
          "fields": [
            {
              "fieldName": "tire_pressure_front_left",
              "fieldId": 1
            },
            {
              "fieldName": "tire_pressure_front_right",
              "fieldId": 2
            },
            {
              "fieldName": "tire_pressure_rear_left",
              "fieldId": 3
            },
            {
              "fieldName": "tire_pressure_rear_right",
              "fieldId": 4
            },
            {
              "fieldName": "tire_pressure_thresholds",
              "fieldId": 5
            }
          ],
          "events": [
            {
              "eventName": "tire_pressure_warning",
              "eventId": 6
            }
          ]

On the consumer side there is one important difference to a pure-notifier field (chapter 7): because Get()/Set() are methods, and – just like the regular methods of chapter 8 – a consumer that wants to call them has to list the corresponding fields in its own service instance. The optional useGetIfAvailable / useSetIfAvailable flags declare that the consumer intends to use the getter/setter of the field:

Listing 64 consumer/mw_com_config.json#
          "fields": [
            {
              "fieldName": "tire_pressure_front_left",
              "useGetIfAvailable": true
            },
            {
              "fieldName": "tire_pressure_front_right",
              "useGetIfAvailable": true
            },
            {
              "fieldName": "tire_pressure_rear_left",
              "useGetIfAvailable": true
            },
            {
              "fieldName": "tire_pressure_rear_right",
              "useGetIfAvailable": true
            },
            {
              "fieldName": "tire_pressure_thresholds",
              "useGetIfAvailable": true,
              "useSetIfAvailable": true
            }
          ]

Note

The useGetIfAvailable / useSetIfAvailable flags are the field-accessor counterpart of the method use property described in chapter 8: they are pure resource-tweaking hints, only evaluated on the proxy/consumer side, and – just like the method use property – they default to ``true`` (the getter/setter is enabled by default). Enabling a getter/setter makes the proxy set up the resources for it (for the LoLa binding: the shared-memory of the corresponding get/set request/response queue); if a consumer is not going to read or write a field via Get()/Set() at all, set the corresponding flag to false to spare that shared-memory. So they share the exact same default polarity as a method’s use: enabled by default, opt out via useGetIfAvailable: false / useSetIfAvailable: false.

Provider application#

The provider is based on chapter 7’s provider. The essential additions are:

1. A set-handler for tire_pressure_thresholds. Because Set() on a field is a method, its handler has to be registered (via RegisterSetHandler()) before the service is offered. The handler receives the consumer-requested value by (mutable) reference, validates/clamps it in place and thereby determines the value that actually becomes the new field value (the middleware updates the field with whatever the handler leaves behind). The handler clamps lower_threshold to [1.5, 2.5] and upper_threshold to [2.5, 3.5] bar, logging an error whenever it has to adjust a value:

Listing 65 provider/provider.cpp#
    // it.
    const auto set_handler_result =
        service_instance.tire_pressure_thresholds.RegisterSetHandler([](TirePressureThreshold& value) noexcept {
            value.lower_threshold =
                ClampThreshold("lower_threshold", value.lower_threshold, kLowerThresholdMin, kLowerThresholdMax);
            value.upper_threshold =
                ClampThreshold("upper_threshold", value.upper_threshold, kUpperThresholdMin, kUpperThresholdMax);

            {
                std::lock_guard<std::mutex> lock{g_thresholds_mutex};
                g_current_thresholds = value;
            }
            std::cout << "tire_pressure_thresholds set to [lower: " << value.lower_threshold
                      << " bar, upper: " << value.upper_threshold << " bar]" << std::endl;
        });
    SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(set_handler_result.has_value(),

Note that there is no explicit “get-handler”: the getter of a field always returns the field’s current value, so the provider only has to keep the field value up to date via Update() (exactly as in chapter 7). As with every field, the provider must supply an initial value for each field before offering the service:

Listing 66 provider/provider.cpp#
    // call fail.
    std::cout << "Setting initial field values (before offering the service):" << std::endl;
    UpdateField(service_instance.tire_pressure_front_left, "tire_pressure_front_left", kInitialTirePressure);
    UpdateField(service_instance.tire_pressure_front_right, "tire_pressure_front_right", kInitialTirePressure);
    UpdateField(service_instance.tire_pressure_rear_left, "tire_pressure_rear_left", kInitialTirePressure);
    UpdateField(service_instance.tire_pressure_rear_right, "tire_pressure_rear_right", kInitialTirePressure);

    const TirePressureThreshold initial_thresholds{kInitialLowerThreshold, kInitialUpperThreshold};
    const auto thresholds_update_result = service_instance.tire_pressure_thresholds.Update(initial_thresholds);
    SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(thresholds_update_result.has_value(),
                                                "Failed to set initial 'tire_pressure_thresholds' value!");
    std::cout << "  tire_pressure_thresholds = [lower: " << initial_thresholds.lower_threshold
              << " bar, upper: " << initial_thresholds.upper_threshold << " bar]" << std::endl;

    auto offer_result = service_instance.OfferService();
    SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(offer_result.has_value(),
                                                "Failed to offer TirePressureExtendedSkeleton instance!");

2. Cyclic updates and warnings. As in chapter 7 the provider updates the four tire pressures at a randomized cadence, but now the random pressures are drawn from [1.2, 4.0] bar. After each update cycle it reads the current threshold band and, for every tire whose new pressure has left the band, sends a tire_pressure_warning event carrying the affected tire (several warnings may be sent after a single cycle):

Listing 67 provider/provider.cpp#
        std::cout << "Updating tire pressures:" << std::endl;
        std::array<float, 4U> new_pressures{};
        for (std::size_t index = 0U; index < tires.size(); ++index)
        {
            auto& [field, field_name, tire] = tires[index];
            new_pressures[index] = UpdateField(field, field_name, pressure_distribution(random_engine));
        }

        // Read the current threshold band and send a "tire_pressure_warning" event for every tire whose new pressure
        // has left it. Multiple warnings may be sent after a single update cycle.
        TirePressureThreshold thresholds{};
        {
            std::lock_guard<std::mutex> lock{g_thresholds_mutex};
            thresholds = g_current_thresholds;
        }

        for (std::size_t index = 0U; index < tires.size(); ++index)
        {
            const auto& [field, field_name, tire] = tires[index];
            const float pressure = new_pressures[index];
            if ((pressure < thresholds.lower_threshold) || (pressure > thresholds.upper_threshold))
            {
                std::cout << "  '" << field_name << "' (" << pressure
                          << " bar) is outside the threshold band [lower: " << thresholds.lower_threshold
                          << " bar, upper: " << thresholds.upper_threshold << " bar]. Sending tire_pressure_warning."
                          << std::endl;
                const auto send_result = service_instance.tire_pressure_warning.Send(tire);
                if (!send_result)
                {
                    std::cerr << "Failed to send tire_pressure_warning for '" << field_name
                              << "': " << send_result.error() << std::endl;
                }
            }

Consumer application#

The consumer is based on chapter 7’s consumer, but its reception model is inverted: it does not subscribe to the tire pressure fields anymore (they no longer have a notifier). Instead it:

  • subscribes to the tire_pressure_warning event (with max_sample_count 4, one per tire) and registers a receive handler for it,

  • and, whenever a warning arrives, actively reads the values it needs via the field getters.

Registering the receive handler and subscribing looks exactly like the event reception of chapter 5 / the field notifier of chapter 7:

Listing 68 consumer/consumer.cpp#
            // EventReceiveHandler.
            const auto set_receive_handler_result =
                proxy_ptr->tire_pressure_warning.SetReceiveHandler([proxy_ptr]() noexcept {
                    HandleWarnings(*proxy_ptr);
                });
            if (!set_receive_handler_result)
            {
                std::cerr << "Failed to set receive handler for 'tire_pressure_warning': "
                          << set_receive_handler_result.error() << ". Terminating." << std::endl;
                std::exit(EXIT_FAILURE);
            }

            const auto subscribe_result = proxy_ptr->tire_pressure_warning.Subscribe(kMaxSampleCount);
            if (!subscribe_result)
            {
                std::cerr << "Failed to subscribe to 'tire_pressure_warning': " << subscribe_result.error()
                          << ". Terminating." << std::endl;
                std::exit(EXIT_FAILURE);
            }

The receive handler drains all new warning samples via GetNewSamples() and reacts to each of them:

Listing 69 consumer/consumer.cpp#
void HandleWarnings(TirePressureExtendedProxy& proxy)
{
    auto get_new_samples_result = proxy.tire_pressure_warning.GetNewSamples(
        [&proxy](auto&& sample) {
            HandleSingleWarning(proxy, *sample.Get());
        },
        kMaxSampleCount);
    if (!get_new_samples_result)
    {
        std::cerr << "Failed to get new tire_pressure_warning samples: " << get_new_samples_result.error() << std::endl;
    }
}

For each warning the consumer reads – via Get() – the current pressure of the affected tire and the current threshold band, logs both, and then adjusts the threshold band: if the pressure was below the lower threshold it lowers lower_threshold by 0.1 bar; if it was above the upper threshold it raises upper_threshold by 0.1 bar. The new band is written back via Set(), whose return value carries the value the provider actually accepted (the provider may have clamped it):

Listing 70 consumer/consumer.cpp#
void HandleSingleWarning(TirePressureExtendedProxy& proxy, const Tire tire)
{
    // Read the current pressure of the affected tire via the field's Get() (request/response, not a subscription).
    auto pressure_result = GetTireField(proxy, tire).Get();
    if (!pressure_result.has_value())
    {
        std::cerr << "Failed to Get() pressure of '" << TireName(tire) << "': " << pressure_result.error() << std::endl;
        return;
    }
    const float pressure = *(pressure_result.value());

    // Read the current threshold band via the field's Get().
    auto thresholds_result = proxy.tire_pressure_thresholds.Get();
    if (!thresholds_result.has_value())
    {
        std::cerr << "Failed to Get() tire_pressure_thresholds: " << thresholds_result.error() << std::endl;
        return;
    }
    const TirePressureThreshold thresholds = *(thresholds_result.value());

    std::cout << "Received tire_pressure_warning for '" << TireName(tire) << "': current pressure = " << pressure
              << " bar; current thresholds = [lower: " << thresholds.lower_threshold
              << " bar, upper: " << thresholds.upper_threshold << " bar]." << std::endl;

    // Widen the band in the direction the pressure left it.
    TirePressureThreshold new_thresholds = thresholds;
    if (pressure < thresholds.lower_threshold)
    {
        new_thresholds.lower_threshold -= kThresholdAdjustStep;
    }
    else if (pressure > thresholds.upper_threshold)
    {
        new_thresholds.upper_threshold += kThresholdAdjustStep;
    }
    else
    {
        // The pressure is within the band - nothing to adjust.
        return;
    }

    auto set_result = proxy.tire_pressure_thresholds.Set(new_thresholds);
    if (!set_result.has_value())
    {
        std::cerr << "Failed to Set() tire_pressure_thresholds: " << set_result.error() << std::endl;
        return;
    }
    // The set operation returns the value the provider actually accepted (it may have clamped our request).
    const TirePressureThreshold accepted = *(set_result.value());
    std::cout << "  Adjusted tire_pressure_thresholds to [lower: " << accepted.lower_threshold
              << " bar, upper: " << accepted.upper_threshold << " bar]." << std::endl;
}

Note that reading a field via Get() is completely independent of a subscription: Get() is a request/response method call, so the consumer never subscribes to the tire pressure fields at all.

How to run the example#

Warning

As noted at the top of this chapter, the field getter path is not yet functional in the current middleware. The targets build successfully, but running provider + consumer will currently make the provider abort as soon as the consumer subscribes to a field Get(). The instructions below are provided for completeness and will work unchanged once the middleware auto-registers the field get handler.

The steps are the same as in the previous chapters. The consumer uses the implicit configuration loading, so it is started without the --service_instance_manifest argument.

# Build the provider and consumer targets
bazel build //score/mw/com/doc/tutorial/chapter_9:provider-tar
bazel build //score/mw/com/doc/tutorial/chapter_9:consumer-tar

Extract both archives (e.g. in a tmp-directory):

mkdir -p /tmp/tutorial/chapter_9
tar -xf <project dir>/bazel-bin/score/mw/com/doc/tutorial/chapter_9/provider-tar.tar -C /tmp/tutorial/chapter_9/
tar -xf <project dir>/bazel-bin/score/mw/com/doc/tutorial/chapter_9/consumer-tar.tar -C /tmp/tutorial/chapter_9/

… then start the service-provider application in the 1st terminal:

cd /tmp/tutorial/chapter_9/opt/TirePressureExtendedServer
bin/provider_app

and the service-consumer in the 2nd terminal:

cd /tmp/tutorial/chapter_9/opt/TirePressureExtendedClient
bin/consumer_app

Summary#

A short summary of what we have learned in this chapter:

  • A field can, besides (or instead of) its notifier, offer a request/response style getter (Get(), tag WithGetter) and setter (Set(), tag WithSetter). Semantically, Get()/Set() on a field are just methods – which is why they are covered only after chapter 8.

  • There is no semantic difference between “WithNotifier + GetNewSamples()” and “WithGetter + Get()”; the choice is a matter of efficiency, which depends on the field update rate and the concrete binding (network / IPC with per-process buffers / IPC with shared memory). A high-rate field that a consumer only samples occasionally is a typical case for preferring the getter over the notifier.

  • On the provider side, a setter is served by a handler registered via RegisterSetHandler() before OfferService(); the handler validates/adjusts the requested value in place and the middleware then adopts it as the new field value. A getter needs no handler: it always returns the field’s current value, which the provider keeps up to date via Update() (and must initialize before offering, as for any field).

  • On the consumer side, field.Get() and field.Set(value) return a score::Result wrapping a MethodReturnTypePtr, dereferenced to access the value. Reading via Get() is independent of any subscription.

  • A field getter/setter is not configured as a separate method: its method id is derived from the field’s fieldId. However – just like a regular method (chapter 8) – a consumer that wants to call Get()/Set() must list the corresponding fields in its own service instance configuration (optionally with useGetIfAvailable / useSetIfAvailable).

  • Current limitation: the middleware does not yet auto-register the provider-side handler that serves a field getter, so the WithGetter/Get() path is not runnable end-to-end at the time of writing, even though it compiles and links.