Introduction
In the product development lifecycle, it’s common to adjust parameters to account for unforeseen or evolving variables. This is especially true when developing software that interacts with hardware, where timings and other settings often need fine-tuning. However, repeatedly modifying these values, rebuilding, and redeploying software can consume valuable time and resources.
To streamline this process, KOS provides a built-in system that lets developers define configurable parameters. These “configs” appear directly in Studio’s “Configuration Viewer” tool, where they can be modified on the fly—eliminating the need for rebuilds and redeploys.
This article explains how to use the configuration tooling in KOS and highlights its key features.
The ConfigService
Like many of KOS’s features, the configuration tooling has its own service which acts as a manager; ConfigService. In this case said service is managing all of the configuration data and applying that data to any object that has been made aware of it. We’ll cover this in more detail later, but for now just note that this service ensures any configuration changes are applied to the correct object.
The ConfigBean
The ConfigBean sits at the core of KOS’s configuration tooling. It is what stores all of the parameters or “configs” that you want to be managed and configured using the ConfigService. It is often the case that in a single application there will be multiple of these beans. For instance, the Application class as well as each board in the Assembly might have its own ConfigBean instance. So long as the ConfigBean is properly defined, and the correct objects are made aware of it, it will appear in the configuration viewer. Here each config is described and available to modify. Changing a config will result in those changes immediately being reflected in the image being run.
To demonstrate how to write a ConfigBean, we have included a basic sample below.
Let’s start by going through this snippet and exploring what each part is doing. First and foremost, we use the @Getter and @Setter Lombok annotations so we can generate the setters and getters for each of the configs. Without the setters and getters the configs will not be usable to ConfigService and thus not visible in the “Configuration Viewer”.
Moving on, we declare our class by extending ConfigBean. By doing so, other objects can be made aware of configuration changes by implementing ConfigAware<T>, and specifying the ConfigBean as the type. This will also allow the class to interact with the ConfigService. The ConfigBean class also implements the Ready interface. This means when the bean is configured, it will be marked ready, and when this happens, the onReady() callback will get called.
Onto the configs themselves. You can see that every config has above it a ConfigDesc annotation. These describe what the config represents and will appear in the configuration viewer. You can see in the third config, a format is specified in the description. Units can be passed via the ConfigFormat class. The use of these adds some extra functionality for unit conversion on the UI end. You can consult the java docs for what units are supported via the ConfigFormat class.
For scenarios where you have multiple instances of the same object, all of which use the same type of ConfigBean, you can use maps to wrap all the beans into a single config stored in a parent ConfigBean. This is good for scenarios where you have many similar components. The String key linked to each child ConfigBean, will be displayed alongside the name of the child ConfigBean in the “Configuration Viewer”. This way you can distinguish between instances of the same ConfigBean.
ConfigAware Objects
To actually attach a ConfigBean to an object so that it can be configured and managed by the ConfigService, you will need to make said object ConfigAware<T>. This can be done in a variety of ways depending on the object. If you are creating an application object, then you will have your ConfigBean extend BaseAppConfig instead, and pass said bean as a type to the Application like so…
or
When the object in question is a Service, you can extend AbstractConfigurableService and similarly pass the ConfigBean like so…
When the object you are trying to make aware is not an Application or Service, there are two remaining ways you can make the object aware. You can either have the class extend AbstractConfigurable<T> like so…
The alternative method is simply implementing the ConfigAware<T> interface and similarly passing it the bean as a type like so…
In this latter case, you will also need to override the getConfig() and setConfig() methods. It will also be necessary to override the various methods associated with HandleAware. In either case, the bean will now be connected to the ConfigService and configurable through the “Configuration Viewer”. Whenever you want to use a config in your code, you can either call getConfig() and then the getter for the specific config you want to access, or call the getter directly on the config attribute stored in the ConfigAware<T> class.
or
Configuration Overrides
Sometimes in cases where you have a single app that is running on multiple devices, changing the default overrides within the config bean for each device can be too much. To remedy this issue, KOS comes packaged with a solution to immediately override the default configurations for a given app. These overrides are applied at a node level, meaning that overrides to an app’s configs will remain so long as that app exists on the same node and is not overridden again. This means that overrides may persist between images if those images share an app and a node. Furthermore, while these new configuration values will override any default values in the ConfigBean, any changes made via the “Configuration Viewer” will take precedence. This way, no matter what, you will still be able to change the configuration values via the viewer. You can supply these default configuration overrides by writing a config.xml file. What follows is an example of one such file.
<?xml version="1.0" encoding="UTF-8"?>
<device>
<configSource>
<beanGroups>
<beanGroup>
<bean path="bean:one:sample:handle:path">
<property name="config1" value="Hello World!"/>
</bean>
<bean path="bean:two:sample:handle:path">
<property name="config1" value="0"/>
</bean>
<bean path="bean:three:sample:handle:path">
<property name="config1" value="true"/>
<property name="config2" value="false"/>
</bean>
</beanGroup>
</beanGroups>
</configSource>
</device>In this example you are configuring three separate beans, specified by their handle paths. It should be noted that these handle paths are those of the ConfigAware<T> objects. Once you specify said path you can set the overrides for that object’s configs by providing the config name, and its new value.
For a situation where multiple objects share the same class of ConfigBean, you can define a configuration and apply it to multiple class instances at once. Take the example below, in which the same override of three configs, is being applied to four instances of the same ConfigAware<T> class.
<?xml version="1.0" encoding="UTF-8"?>
<device>
<configSource>
<configs>
<config name="config.sample.one">
<property name="config1" value="0"/>
<property name="config2" value="11.25"/>
<property name="config3" value="0"/>
</config>
</configs>
<beanGroups>
<beanGroup basePath="sample:base:handle:path">
<config ref="config.sample.one"/>
<bean path="instance1"/>
<bean path="instance2"/>
<bean path="instance3"/>
<bean path="instance4"/>
</beanGroup>
</beanGroups>
</configSource>
</device>In order for an application to make use of a config.xml file, said file must be specified within the pom.xml file alongside the descriptor like so:
<includes>
<include>descriptor.json</include>
<include>config.xml</include>
</includes>Configuration Listeners
There may be a situation in which you will want to listen for when a configuration changes. KOS, built with this in mind, provides a number of ways for users to listen for configuration changes. There are three ways to listen for when a config has changed. The first of these and the simplest is to use is to just implement the callbacks inConfigAwareonConfigSet() and onConfigChanged(). The former will be called when the ConfigBean passed to ConfigAware<T> is first set, and the latter will be called anytime said bean changes. Both are passed as arguments an instance of BeanChanges which stores the changes made to the bean.
The second method of listening would be to implement ConfigBeanListener in a class, create an instance of said class and add to the ConfigBean being monitored via the addListener() method of said class. The interface ConfigBeanListener, has a single method, onConfigChanged(), that takes as arguments the ConfigBean being changed, and BeanChanges. This callback will be called anytime a config change has occurred. These listeners can similarly be removed by calling the removeListener() method. This is useful when a ConfigBean appears in multiple objects where the parent is ConfigAware<T> but the child objects are not, so they lack a natural callback when the config changes.
This is useful if a ConfigBean
- is used in multiple objects where the parent is ConfigAware but the child
- objects are not, so they don’t get a natural callback when the config changes.
The final method of listening would be to use the ConfigChangeListener interface. Just create an instance and add it to the context of the ConfigService. Unlike the previous two, this listener will listen to all configuration changes. The callback, onConfigChanged, takes as arguments the ConfigService and the ConfigChanges. This method provides an easy way to snoop configuration changes to keep other systems in sync.
