6.11. Parameters
Parameters allow to view and modify data from running programs. They consist of an input and output signal where the output signal can be overridden either from the ln manager or from python clients.
6.11.1. C++ Parameter Providers
Creating C++ Parameters is a two-step process. You first need to instantiate a parameter block
-
ln::parameters::parameter_block(ln::client *clnt_, std::string parameter_group_name_, bool always_publish_ = false, std::string topic_name_ = "")
and then register parameters to it using
-
template<typename ...Types>
ln::parameters::parameter_block::register_parameters(port_description<Types>... port_descriptions)
Note that this function can only be called once, you cannot register more than one set of parameters to a parameter_block. It is easiest to supply brace-enclosed initializer lists to this functions, e.g. {“sig_name”, &sig_in, &sig_out, before_update_callback, after_update_callback}. The callbacks can also be omitted and are explained in the next section. Please see the following code example for details. Inside your main loop, you then have to cyclically call
-
ln::parameters::parameter_block::update()
to allow overriding parameters.
#include <chrono>
#include <thread>
#include <iostream>
#include <ln/ln.h>
#include <ln/parameters.h>
#include <ln/parameters_eigen.h>
int main(void) {
// Init the client
ln::client clnt("ln_parameters_example");
// Create parameter block
ln::parameters::parameter_block params(&clnt, "ln.parameters.example.cpp");
// Signal
double sig_in = 0, sig_out = 0;
params.register_parameters<double>({"sig", "human-readable sig description", &sig_in, &sig_out});
// Loop
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(5));
sig_in += 1;
params.update();
std::cout << "sig input: " << sig_in << ", sig output: " << sig_out << std::endl;
}
}
6.11.1.1. Parameter Callbacks
As mentioned above, you can supply the two callbacks
-
struct ln::parameters::port_description
-
std::function<bool(void*)> before_update_callback
-
std::function<void(void*)> after_update_callback
-
std::function<bool(void*)> before_update_callback
where the function before_update_callback is called from the service handler when a new override arrives. This function can be used to check the parameter value for validity, if the value is e.g. out of range it can return false; and the update is rejected. Otherwise it has to return true; to accept the update.
The function after_update_callback is called within your main loop’s call to update() after a parameter has been updated. It can be used to recompute dependant values.
6.11.2. C++ Parameter Clients
C++ clients can read and override parameters provided by one or more
parameter blocks. The entry point is
ln::client::get_parameters(group_prefix), which returns a lightweight
ln::parameters::parameter_proxy bound to a namespace prefix. Creating a
proxy does not discover services or allocate parameter service handles. Service
discovery happens lazily on the first access to a signal. Calling
clnt.get_parameters() without an argument creates a proxy for the root
parameter namespace; signal names passed to that proxy must then be full names.
The parameter client API requires C++11 or newer.
Use . as the namespace separator. A prefix is only a name prefix, not a
single provider identity: several parameter blocks may contribute signals below
the same prefix. LN expects each individual signal name to be provided by at
most one block at a time.
#include <array>
#include <iostream>
#include <vector>
#include <ln/ln.h>
#include <ln/parameters.h>
int main(int argc, char** argv)
{
ln::client clnt("parameter user", argc, argv);
ln::parameters::parameter_proxy params =
clnt.get_parameters("example.paramsA");
std::cout << params.get_description("sig1") << std::endl;
double input = params.get_input<double>("sig1");
std::vector<double> q = params.get_input<std::vector<double>>("q");
std::array<std::array<double, 7>, 6> J =
params.get_output<std::array<std::array<double, 7>, 6>>("J");
params.set_override("sig1", input + 1.0);
double new_J[6][7] {};
new_J[0][0] = 1.0;
params.set_override("J", new_J);
params.reset_override("sig1");
auto sub = params.get_parameters("sub1");
double sub_value = sub.get_input<double>("sub_sig1");
(void)q;
(void)J;
(void)sub_value;
}
get_input<T>() returns the provider input value for a signal.
get_output<T>() returns the effective output value, which may be the normal
provider output or an active override. set_override() requests an override,
and reset_override() clears it.
Scalar parameters can be read or written as arithmetic C++ types. Vector
parameters can be read as std::vector<T> or std::array<T, N> and can
also be written from one-dimensional C arrays. Matrix parameters can be read
as std::vector<std::vector<T>> or nested std::array and can also be
written from two-dimensional C arrays. Matrix data is interpreted row-major.
Shape mismatches always raise an exception.
For code that already type-erases values as raw pointers, the proxy also
provides set_override_from_data_*() and get_into_data_*() methods.
They use the existing LN_* parameter data type values, such as
LN_DOUBLE or LN_UINT64. Matrix calls accept
MatrixLayout::row_major or
MatrixLayout::column_major plus an optional
major_stride_bytes value. The stride is a row stride for row-major data
and a column stride for column-major data; 0 means tightly packed.
Generic clients can call get_internal_value() once to discover both the
full provider data type string and the parsed scalar element type:
parameter_value::data_type keeps strings such as double[6, 7], while
parameter_value::element_data_type contains the matching DataType
enumerator for one element.
Warning
The raw-pointer methods trust the caller’s memory description. Keep
data_type and data in exact sync: if data_type says
LN_DOUBLE, the pointer must address storage for one or more
double values. Likewise, n_elements, rows, cols, layout,
and major_stride_bytes must not describe more memory than the buffer
actually contains. If these arguments are wrong, the client can read or
write past the provided object and cause memory corruption or a memory
fault.
By default, the client performs scalar conversions where this is safe enough
for the requested C++ type. For example, an integer parameter may be read as a
floating-point value, and a floating-point parameter may be read as an integer
if the current value fits the requested integer type. Integer reads and writes
raise if the value does not fit the requested C++ type or provider type. If
strict_type_checks is passed as true to get_parameters(), integer
and floating-point parameter types are not converted automatically.
The proxy caches discovered service names and the signal type/shape signature.
For repeated access to a healthy cached signal, get_input(),
get_output(), set_override(), and reset_override() each perform one
parameter-provider service call. If a cached provider no longer has the signal,
or the returned data no longer matches the cached signature, the proxy drops
the cache, performs one rediscovery pass, and retries once.
For a typed handle to one signal, use get_signal<T>():
auto sig1 = params.get_signal<double>("sig1");
double value = sig1.get_output();
sig1.set_override(value + 0.5);
sig1.reset_override();
The lower-level get_internal_value() method returns the raw
ln::parameters::parameter_value structure with data type, shape,
description, override state, and separate floating-point/integer carriers. It
is intended for diagnostics and generic tools, not as the usual application
API.
See Parameter Client API for the full C++ API reference.
6.11.3. Python Parameter Providers
Search keywords: Python Parameter Providers, Python parameter provider,
Python parameter block, client.parameter_block, ParameterBlock,
ParameterSignal, ParameterOutputs.
Python clients can provide parameter blocks with the same update semantics as
the C++ API. Create one block per parameter namespace, add all signals during
setup, then call update() once in each provider loop iteration. The value
passed to add() is used to infer the LN datatype and shape. Scalars,
vectors, and matrices can be given as Python values, Python sequences, or NumPy
arrays. Use dtype= when the inferred type is not the one you want.
Signal names that are valid Python identifiers can be accessed as attributes,
for example paramsA.sig1.input and paramsA.sig1.output. Bracket access
such as paramsA["sig1"].input always works and is useful for names that
are not valid Python attributes. update() returns a ParameterOutputs
object, so outputs.sig1 and outputs["sig1"] both refer to the same
output value.
before_update is called when a parameter user requests a new override. It
receives the requested value and must return True to accept it or False
to reject it. after_update is called from the provider’s update() after
an accepted override has changed the output value. The Python implementation
serializes access to a parameter block, so update() and signal input/output
access may be called from different Python threads. Do not call update()
recursively from a callback.
import sys
import time
import numpy as np
import links_and_nodes as ln
clnt = ln.client("python parameter provider", sys.argv)
def accept_sig1(value):
return 0.0 <= value <= 100.0
def after_sig1(value):
print("sig1 override changed to %.3f" % value)
def accept_q(value):
return np.all(np.abs(value) < 1000.0)
q = np.zeros(43, dtype=np.float64)
J = np.zeros((6, 7), dtype=np.float64)
with clnt.parameter_block("example.paramsA") as paramsA:
paramsA.add(
"sig1",
0.0,
description="measured scalar with override",
before_update=accept_sig1,
after_update=after_sig1,
)
paramsA.add("enabled", True, description="boolean state")
paramsA.add("q", q, description="joint vector", before_update=accept_q)
paramsA.add("J", J, description="matrix")
start = time.time()
while True:
t = time.time() - start
paramsA.sig1.input = t
paramsA.q.input[:] = np.linspace(t, t + 42.0, 43, dtype=np.float64)
J[:, :] = np.arange(42, dtype=np.float64).reshape(6, 7) + t
outputs = paramsA.update(
enabled=True,
J=J,
)
sig1_cmd = paramsA.sig1.output
q_cmd = outputs.q
time.sleep(0.01)
The block is registered automatically when the with block exits without an
exception. If you do not use a context manager, call paramsA.register()
after adding all signals and before the first update(). Supported dtypes
are float64/double, float32/float, signed and unsigned integer
types from 8 to 64 bit, and bool.
6.11.4. Debugging Parameters
The env variable LN_PARAMETER_DEBUG can be set to enable some debug output when using ln parameters (also for Simulink models).
6.11.5. Manager and Web UI Usage
When a client registers a parameter block, it provides manager-visible
ln/parameters/* services. The LN manager GTK UI and web UI use these
services to query parameter values, set or clear overrides, and request
temporary publication of a parameter as a topic for plotting.
In the browser UI, open /lnm/parameters or select the parameters tab.
Use the pattern field and refresh button to query providers. Selecting a
parameter shows its input, override, and output values. Scalars, vectors, and
matrices get editable layouts suited to their shape. set override fixes the
parameter output to the entered value; reset override returns it to normal
provider behavior. open scope requests topic publication for that parameter
and opens ln_scope for the parameter’s .output field.
For scripted access, use the web UI protocol messages documented in
Web UI WebSocket Protocol or the helper methods on
links_and_nodes_manager_client.ManagerClient:
parameters = await client.get_parameters("*")
await client.set_parameter_override(
service,
parameter_name,
True,
"42.0",
provider_id=provider_id,
)
await client.open_parameter_scope(
service,
parameter_name,
provider_id=provider_id,
)