Java Extensions

Define a Dispenser

Introduction

One of the core design goals of KOS is scalability, but what does this mean in the context of a smart dispense device?

Scalability in KOS refers to the ability to use the same software stack to build a basic Legacy+ dispenser with an SBC (single board computer), or a dispenser with multiple nozzles, or a dispenser with multiple SBCs, or even a distributed dispenser with parts located in both the front and back rooms. This application note demonstrates how to build a simple legacy dispenser using the KOS building blocks.

This note focuses on the steps required to perform simple ingredient pour. It leaves beverage pouring to a different note. It also ignores issues of brandsets and ingredient assignments, which are covered in a separate note. This note does not cover hardware integration, and simply simulates pump operation, since it’s a hypothetical dispenser.

Part 1: System application

While KOS supports a flexible application model, during the boot process it will look for a special application called the "system" application. This application is packaged in a KAB file and is listed in the NodeManifest. We won’t cover all of the details of how to package and install a system application in this note.

All KOS applications:

  • extend the SystemApplication (primary app) or Application (secondary app) class

  • define the appId and reference the appClass in the descriptor.json file

The system app must have an appId of "system". The sample below is an example of an empty system application named DemoApp:

The start of our dispenser application:
package com.example.DemoApp;

public class DemoApp extends SystemApplication<BaseAppConfig> {

    @Override
    public void load() {
    }

    @Override
    public void start() {
    }
}

Notice this application extends SystemApplication instead of Application. This provides access to several convenience methods typically used by system applications.

When packaged into a KAB file the descriptor.json file needs to populate the app section of the descriptor as follows:

Our descriptor.json file:
{
  "kos": {
    "app": {
      "appClass": "com.example.DemoApp",
      "appId": "system"
    }
  }
}

The appClass entry tells KOS which class to load to start the application, and the appId defines the unique application ID for this application. It must be "system" when it’s a system app.

load() vs start()

Applications have several lifecycle events including:

load()

Called once and only once when the application is loading. Here, you set up any beans and state that should survive until the application is unloaded. This includes tasks like: instantiating your beans and adding them to the bean context; opening all databases; mounting KAB resources; etc. When this method returns:

  • Beans that were added to the BeanContext are autowired

  • All ConfigAware beans are configured

  • All @ApiController beans added to the context are processed (endpoints are enabled when the application is started)

  • Databases are ready to access

start()

Called when the application is starting. This happens both at initial start up and during live updates, which means that start/stop cycles can be performed multiple times. When called, your application can start performing any normal actions. Endpoints and VFS mounts are available when this returns.

stop()

Called with the application is stopping. This is not only called before unloading, but is also called during live system updates. When called, the application should prepare to be unloaded, or to have any other applications unloaded. All endpoints and VFS mounts are disabled before this is called, which prevents external events from triggering while in a stopped state.

unload()

Called once and only once when the application is unloading. This should clean up all resources and references so that the application ClassLoader can be unloaded.

System applications are never stopped or unloaded. In those cases, only load() and start() are implemented.

Part 2: Boards and Pumps

A dispenser pours beverages and ingredients by controlling pumps. This example works with valves, but uses the term pump interchangeably.

Pumps vs Valves

Strictly speaking, pumps have the ability to monitor and control flow rates, while valves are either simply on or off.
→ In KOS, a valve is simply a pump that has a fixed flow rate.
Therefore, KOS uses the generic term "pump" to refer to either actual pumps or valves.

Pumps are usually electro-mechanical devices that have driver circuitry and hardware to turn the pumps on and off. This hardware typically exists in the form of an external board that connects to the SBC using some transport, and implements some protocol for controlling the pumps.

Within KOS, the abstraction for external hardware is the Board class. Applications extend this class to model and interface to the external hardware using a "native code adapter". This app note ignores these details, but assumes we have an external board containing the following:

  • One plain water valve with expected flow rate of 74 ml/sec, named PW

  • One carbonated water valve with expected flow rate of 74 ml/sec, named CW

  • Four syrup valves with expected flow rates of 15 ml/sec, named Sx

Furthermore, we assume that the control board uses a simple protocol that turns a valve on and off based on its position. We assume the valves have the following positions:

  • PW : 0

  • CW : 1

  • S1 : 2

  • S2 : 3

  • S3 : 4

  • S4 : 5

DemoValve class

To build a dispenser, we’re going to need an actual Pump class to work with. Since we’re modeling simple valves, we’ll name our pump class DemoValve. Here is our initial class:

Class to control the dispenser’s valves:
public class DemoValve extends Pump<PumpConfig> {
    @Getter @Setter             // creates getConfig() and setConfig() methods
    private PumpConfig config;  // configuration data for the pump
    private int pos;            // position of the valve on the board

    public DemoValve(Board board, int pos, String name, String category) {
        super(board, name, category);
        this.pos = pos;
        config = new PumpConfig();

        // Default syrup rate of 15ml/sec:
        getConfig().setNominalRate(15);
    }

    @Override
    public FutureWork tpour(int duration, double rate) {
        return null;
    }

    @Override
    public FutureWork vpour(int volume, double rate) {
        return null;
    }

    @Override
    public String getType() {
        return "demo.valve";
    }
}
Lombok

The @Getter and @Setter annotations are part of the open source Lombok project, which is used in our examples to reduce the amount of boilerplate code.

Note that Pump is a generic class that requires a configuration class. We simply use the base PumpConfig class, as it contains a single nominalRate property, which signifies the pump’s normal flow rate. The nominal rate is used when no flow rate is specified in pour operations. As these are valves and the flow rates are mechanically calibrated, it might seem odd to define a nominal flow rate. But even when calibrating mechanically, there is a need to have a target flow rate based on different concentrations of BiB ingredients. The nominal rate in the pump config can serve as the target rate for this use case, as all configuration settings (including pump configurations) can be manipulated using the KOS ConfigService.

A Pump requires a board, name, and category:

board

This is the circuit board the pump is attached to. In a relay system, the board forwards pour requests to the pump’s control hardware.

name

Many objects in KOS require a name, which is used for display and handle path purposes. A "handle path" is a string that uniquely identifies a software object.

category

Used to match PumpIntent definitions. For now, we ignore this by passing in null.

Since Pump implements ConfigAware, we must implement the getConfig() and setConfig() methods. In our code above, this is accomplished using the Lombok annotations. However, we must allocate the config object. Although some valves will have a nominal rate of 15ml/sec and others 74ml/sec, we’ll simply use 15 and override the value for water valves when we create them.

tpour() and vpour()

Every pump must perform these two basic operations, which used as building blocks for more sophisticated operations.

tpour()

Timed pour for the specified duration (in msec) at the specified rate (in ml/sec). For a valve, the rate is effectively ignored. As we will see, since we’re using valves, the rate is usually simply the configured nominal rate.

vpour()

Volume pour for the specified volume (in ml) at the specified rate in (ml/sec). As valves have no flow meter and their rate is fixed, there is seemingly no way to implement this. However, if the pump is calibrated to the nominal rate, then we can compute the time from the volume and rate, thus vpour() can simply call tpour().

Both of these return a FutureWork object, which is a KOS construct for managing asynchronous work. We touch on this briefly, but for a more in-depth description, please see the Javadoc entry.

DemoBoard class

Now that we have a pump class, we need to define a board to interface to the hardware and define the pumps. Based on the list of valves from above, the DemoBoard class is implemented as:

Class that models our circuit board:
@Getter
public class DemoBoard extends Board {
    private DemoValve waterValve;    // plain water valve
    private DemoValve carbValve;     // carbonated water valve
    private List<DemoValve> syrups;  // syrup valves

    public DemoBoard(Assembly assembly, String name) {
        super(assembly, name);

        // Create the water pumps:
        waterValve = new DemoValve(this, 0, "pw", null);
        carbValve  = new DemoValve(this, 1, "cw", null);

        // Adjust the nominal rates of water valves:
        waterValve.getConfig().setNominalRate(74);
        carbValve.getConfig().setNominalRate(74);

        // Create the four syrup pumps:
        syrups = new ArrayList<>();
        for (int i = 1; i <= 4; i++) {
            syrups.add(new DemoValve(this, i + 1, "s" + i, null));
        }
    }

    @Override
    public String getType() {
        return "demo.board";
    }
}

Note that we adjust the nominal rate of the water valves after they are created. Since the pumps are ConfigAware, they will be configured at a later state, but prior to use, so the values set here will be considered the default values and any overrides will be applied automatically.

The board must implement getType(), which returns the type of the board. This can be any string, but it will be matched up with the type returned from the native code adapter that interfaces to the hardware. The HardwareService within KOS matches these together and notifies the board that it is connected to the hardware. We are ignoring this in this app note.

Turning on a pump

As described above, a request to run a pump will ultimately result in the pump forwarding the request to the board, which forwards the request to the underlying hardware. Now that we have our board and pump classes, let’s simulate a pour and connect the pumps to the board.

The first step is to simulate a tpour() in the board since we know we can convert a vpour() to a tpour() in the pump class (described above). Here is an implementation of tpour() that can be added to the board:

Implementation of the tpour() method:
public class DemoBoard extends Board {
    // . . .
    public FutureWork tpour(int pos, int duration) {

        FutureWork future = new FutureWork("tpour", f -> {
            // Open the valve (but just log it now):
            log.info("opened valve: {}", pos);

            // Timer to mark when tpour successfully finishes:
            KosUtil.scheduleCallback(() -> f.success(), duration);
        }, duration);

        // When complete, turn the valve off. This is called for success or cancel:
        future.append("closeValve", FutureEvent.COMPLETE, (f) -> log.info("closed valve: {}", pos));

        return future;
    }
    // . . .
}

This method creates a FutureWork, and in the runnable portion logs that the valve is open (instead of sending a command to hardware). It then schedules a callback for the duration of the pour and in the callback marks the Future "successful". This marks the future "complete", where the callback appended to the Future fires and logs that the valve is closed (instead of sending the command to hardware).

This app note is not an in-depth study of FutureWork, but it is worth noting that the scheduled callback doesn’t turn the valve off: we only turn the valve off as a side effect of the Future completing. This is because a user (or internal process) may decide to cancel the pour before it completes. In this case, the future completes earlier than expected, but we still want to turn the valve off. The original callback still fires and marks the Future "successful", but it has already reached a "cancelled" end state, so the call to success() is ignored. This illustrates that through careful use of FutureWork, it is quite easy to handle success and error recovery with very little effort.

Now that we have tpour() in the board, we can fill in tpour() and vpour() in the DemoValve class:

Adding tpour() and vpour() methods
public class DemoValve extends Pump<PumpConfig> {
    // . . .
    @Override
    public FutureWork tpour(int duration, double rate) {
        return ((DemoBoard)getBoard()).tpour(pos, duration);
    }

    @Override
    public FutureWork vpour(int volume, double rate) {
        // Compute time from volume and then call tpour (multiply by 1000 to get msec from ml/sec):
        return tpour((int)((volume * 1000) / rate), rate);
    }
    // . . .
}

Part 3: Assembly

KOS maintains logical representations of hardware and various physical components as objects. We’ve seen the Board class, which represents an external board that interfaces to the Pump class, which logically represents the physical pumps. KOS also has logical representations of other key components of a dispenser, even if they only exist in logical form for some dispensers:

Nozzle

A nozzle gathers a collection of pumps together and manages various pipelines that can implement different ways to pour from the nozzle.

Container

This is a logical representation of a physical container that holds one or more ingredients (Freestyle has cartidges with more than one ingredient in them). For a dispenser that uses BiB ingredienes, the actual BiB box is the physical equivalent of a container. Water also requires a logical container even though there is no phyisical equivalent. In more complex systems, the container may contain data and be an active component (Freestyle cartridges contain RFID tags, for example).

Holder

This is a logical representation of a receptical that an ingredient container is inserted into. In most systems, this is a logical concept only, as BiB ingredients are simply connected with a passive hose. In some systems, the holder may have a physical form and some capabilities. For example, Freestyle dispensers have cartridge slots, and some of those have indicator lights and agitators.

KOS knows how to manage all these entities, but doesn’t know how everything is connected. Part of the responsibility of the system application is to create these objects and connect them together in a way that reflects the actual hardware of the dispenser.

Another feature of KOS is that groups of hardware can be atomically added and removed on the fly. It also supports the concept of optional hardware. Here is a brief description of optional hardware versus expansion hardware:

optional hardware

Generally speaking, if a board can be added after the fact to an existing dispenser, or a board may come and go (such as a diagnostic device), then it can be defined as optional, and it will just work when discovered.

expansion hardware

Generally speaking, an expansion is a collection of new hardware such as an expansion tower where there can be one or more, may define new nozzle, and have their own group of internal hardware that must exist together.

Optional vs Expansion Hardware

It’s not critical to understand the distinction between "optional" and "expansion" hardware for this app note, but it is worth noting that an expansion is defined by an Assembly, which can define complex collections of hardware, including additional nozzles, boards, pumps, and so on, whereas optional hardware exists logically in the system, but remains hidden until it is detected.

A key requirement of the system application is to create an Assembly, which contains all the logical components of the dispenser, and then install that assembly into KOS. Let’s have a look at creating an assembly class for our demo dispenser:

Create the Assembly class:
public class DemoAssembly extends CoreAssembly {

    public DemoAssembly() {
        // Create a nozzle and add it to the assembly:
        Nozzle nozzle = new Nozzle(this, "myNozzle");
        add(nozzle);

        // Create a board and add it to the assembly:
        DemoBoard board = new DemoBoard(this, "myBoard");
        add(board);

        // Add the pumps from the board to the nozzle:
        nozzle.add(board.getWaterValve());
        nozzle.add(board.getCarbValve());
        nozzle.add(board.getSyrups());

        // Create the water holders:
        add(new Holder(this, "PW", board.getWaterValve()));
        add(new Holder(this, "CW", board.getCarbValve()));

        // Create the syrup holders:
        int i = 1;
        for (Pump<?> pump : board.getSyrups()) {
            add(new Holder(this, "S"+(i++), pump));
        }
    }

    @Override
    public void install() {
    }
}

Notice that in addition to creating the board, we also added a Nozzle and attached the pumps to it. This is because we pour using the nozzle, so it needs to know the connected pumps. We also added the holders which will connect ingredients to the pumps (we will skip ingredient assignment in this app note).

It’s worth noting that holders are "globally" scoped, whereas pumps are "nozzle" scoped. Holders tend to be the objects displayed in dashboards, as they represent the named points in the dispenser that the ingredients are attached to. As a result, the holders are created with display names for the associated pumps.

While this simple dispenser has a single pump per holder, KOS also handles more complex architectures where a single holder may have more than one pump, each routed to a different nozzle.

This assembly extends CoreAssembly instead of Assembly. It is a KOS convention that the main assembly for the dispenser is named "core". By extending CoreAssembly, the name is set according to this convention.

When creating an assembly, the constructor creates the bulk of the objects, and then adds them to the assembly by calling add(). This adds the objects to the BeanContext of the assembly, which inherits from the system application context, which inherits from the root KOS context. This allows objects created in the constructor to be autowired to any components in the assembly, system application, or KOS itself.

Now that we have an assembly, we can go back to our system application and fill in the implementation:

Install the DemoAssembly into our application:
public class DemoApp extends SystemApplication {

    @Override
    public void start() {
        installAssembly(new DemoAssembly());
    }
}

When the assembly is installed, KOS autowires all the associated context, configures all ConfigAware beans, and calls install() on the assembly object. At this point, all of the beans are fully wired and configured, so they can be used for any additional device-specific logic. For example, this is where listeners and events are connected so that button presses cause pours, door switches block ice pours, or whatever custom logic is required.

Part 4: Nozzle pipelines

At this point we have boards installed, pumps connected to a nozzle, and the system is ready to go. However, there is currently no way to actually pour. In order to pour from a nozzle, the nozzle needs at least one NozzlePipeline installed.

A NozzlePipeline is an abstraction for a method of pouring. A pipeline focuses on ingredient or beverage pouring. It is possible for a nozzle to have multiple pipelines installed, and each pipline provides unique endpoints for pouring in whatever way that pipeline defines. The nozzle ensures that only one pipeline can pour at a time, which prevents underling hardware from getting mixed signals from the various pipelines.

There are two pipelines included in KOS:

IngredientNozzlePipeline

Supports ingredient-based pouring. This is the primary pipeline used for administrative functions such a priming, purging, and calibrating.

BeverageNozzlePipeline

Supports beverage pouring based on a user-defined brandset. This supports computation of beverage availability based on the state of the dispenser, and is aligned with models provided in the UI SDK which provide simple end-to-end beverage selection and pouring. Higher level pour operations can easily be stacked on top of this pipeline.

For the sake of brevity, this application note will only examine IngredientNozzlePipeline, as it doesn’t require defining a brandset.

IngredientNozzlePipeline

Dispenser administration functions tend to include many operations that result in an ingredient pour. Consider functions like prime, purge, calibration, hold to pour, etc.

In a Freestyle machine, many of these operations are complex sequences of operations, some of which have over 100 discreet steps. Each of these have a logical purpose. For example, a calibration pour has a distinct purpose relative to priming, and the definition of these pours are likely different. A calibration pour may be a 200ml pour, while a prime is something completely different.

In addition, while pours vary based on the intention of the pour, the same intention may differ by pump. For example, calibration for water may be 200ml, whereas calibration for a syrup may be 50ml. This raises the question of how to manage all of these different types of pours in order to minimize the logic required in application code, and to prevent these requirements from leaking into the UI code base.

The IngredientNozzlePipeline solves this problem by introducing the concept of pump "intents". A pump intent corresponds to a particular type of pour, such as a calibration pour.

The intent can be given a name, such as "calibration", and then a data file can be used to define the sequence of operations that defines that intent. It is then possible to define multiple variations of the named intent and differentiate them by pump type, pump handle path, or pump category. In the steps below, we install the pipeline and then define some intents so that we can perform a calibration pour on the water pumps with a volume of 200ml, but for syrup pumps use a volume of 50ml, and see that there is a single endpoint to pour these and the pipeline will sort out the details and perform the necessary pour.

To install a pipeline, we simply add it to the nozzle after we create it in the Assembly.

Add a pump nozzle pipeline to our assembly:
public class DemoAssembly extends CoreAssembly {

    public DemoAssembly() {
        // . . .
        // Add pump pipeline:
        nozzle.add(new IngredientNozzlePipeline(intentFactory));
        // . . .
    }
}

The pipeline is designed to use a factory which returns a PumpIntent, given a pump and intent name. A PumpIntent is simply a sequence of PumpOp objects, each of which describes a single operation that the pump should perform. The sequence of operations then defines the entire intent, which is treated as a single atomic operation at the application layer. Based on the fact that every pump supports tpour() and vpour(), KOS supports the following pump operations out of the box:

  • TpourOp: Performs a tpour on the pump with the specified parameters

  • VpourOp: Performs a vpour on the pump with the specified parameters

  • DelayOp: Performs a delay before starting the next operation

User applications can extend PourOp and define additional operations specific to the underlying hardware. Freestyle hardware supports more than a dozen different operations.

Any implementation of PumpIntentFactory can be used by the pipeline, but KOS provides XmlPumpIntentFactory, which loads intents and operations from XML files. This class can be extended to allow parsing of custom operations. You can also extend the PumpIntent class itself to hold additional application-specific information, if required. For this application note our pumps only support tpour and vpour, so we can use XmlPumpIntentFactory without any changes.

For this example we will also use the stock PumpSeqenceFactory, which simply converts a PumpIntent to a pump sequence that can be executed by the pipeline.

Add the "Intent" factory:
public class DemoAssembly extends CoreAssembly {

    public DemoAssembly() {
        // . . .
        // "Intent" factory for use by pump pipeline:
        intentFactory = new XmlPumpIntentFactory();
        intentFactory.addLoader(new ClassLoaderResourceLoader(getClass().getClassLoader()));
        intentFactory.load("intents.xml");

        // Add pump pipeline:
        nozzle.add(new IngredientNozzlePipeline(intentFactory));
        // . . .
    }
}

XmlPumpIntentFactory loads intents from XML files (which can include other XML files, allowing for inheritance and other conveniences in managing intents). In the example above we add a resource loader that looks for the specified XML file as a resource file in the JAR. We then specify the file we want to load the intents from the "intents.xml" file.

Next we define the two calibration intents: one for water valves and one for syrup valves. Using the XML file we define these as follows:

"Intent" XML file:
<?xml version="1.0" encoding="UTF-8"?>
<pumpIntents>
    <intents>
        <intent name="water_calibrate">
            <op type="vpour" volume="200"/>
        </intent>
        <intent name="syrup_calibrate">
            <op type="vpour" volume="50"/>
        </intent>
    </intents>
</pumpIntents>

We have now defined two intents, one named "water_calibrate" and the other "syrup_calibrate". Next, we define rules to apply them to the correct pumps. There are three types of rules defined in XmlPumpIntentFactory, although application code can add support for additional rules and change the way the rules are evaluated. These are the three supported rule types:

  • pumpPath: This attaches an intent to a pump using the handle path of the pump. This is the most specific rule available and has the highest priority.

  • pumpCategory: This attaches an intent to a pump using the category of the pump. This is second in priority.

  • pumpType: This attaches an intent to a pump based on the type of the pump. This is the lowest priority.

In our example all pumps are of the same class, which means they all have the same type (see DemoValve.getType()). If we use this type of rule, we’ll pick up all valves. However, the water valves should be different from the syrup valves. We start with this rule and try to address the water valves shortly. Here is the rule added to the XML file:

"Intent" XML file with syrup calibrate rule added:
<?xml version="1.0" encoding="UTF-8"?>
<pumpIntents>
    <intents>
        <intent name="water_calibrate">
            <op type="vpour" volume="200"/>
        </intent>
        <intent name="syrup_calibrate">
            <op type="vpour" volume="50"/>
        </intent>
    </intents>
    <rules>
        <rule type="pumpType" key="demo.valve">
            <intent type="calibrate" ref="syrup_calibrate"/>
        </rule>
    </rules>
</pumpIntents>

This rule attaches "syrup_calibrate" to all pumps of type "demo.valve", which is every valve. We now want to override this rule for the water and carbination pump. Based on the available rule types, we can either: a) add a rule for each pump, specifying the handle path to each pump, or b) use the pump category to pick up both pumps at the same time. First we need to go back to our DemoBoard class where we created the water pumps and specify a category such as "water" instead of using null:

Create the water pumps:
public class DemoBoard extends Board {

    public DemoBoard(Assembly assembly, String name) {
        super(assembly, name);
        // . . .
        // Create the water pumps:
        waterValve = new DemoValve(this, 0, "pw", "water"));  // now passing in "water" instead of null
        carbValve  = new DemoValve(this, 1, "cw", "water"));
        // . . .
    }
}

Now that these pumps have a category, we can add a rule to match them:

Add a rule to match water pumps:
<?xml version="1.0" encoding="UTF-8"?>
<pumpIntents>
    <intents>
        <intent name="water_calibrate">
            <op type="vpour" volume="200"/>
        </intent>
        <intent name="syrup_calibrate">
            <op type="vpour" volume="50"/>
        </intent>
    </intents>
    <rules>
        <rule type="pumpType" key="demo.valve">
            <intent type="calibrate" ref="syrup_calibrate"/>
        </rule>
        <rule type="pumpCategory" key="water">
            <intent type="calibrate" ref="water_calibrate"/>
        </rule>
    </rules>
</pumpIntents>

The pipeline is now fully configured for this one intent.

Part 5: Checking Intents

If we compile all this code and run it in KOS, we can access the IngredientPipelineService within the IngredientNozzlePipeline and query an intent for a given pump. The endpoint is related to the name we provided for the nozzle since the pipeline is scoped to the nozzle:

GET /api/nozzle/{nozzleName}/pipeline/ingredient/intent/{pumpPath}/{intent}

If we want to find the handle paths for our pumps, we can use HandleService to list all the handles in the system:

GET /api/handles

In the list that is returned we find:

...
assembly.core.holder:PW.pump:pw
assembly.core.holder:S1.pump:s1
...
If we query the "calibrate intent" for the plain water pump, we get the following:
GET /api/nozzle/myNozzle/pipeline/ingredient/intent/assembly.core.holder:PW.pump:pw/calibrate
{
  "status": 200,
  "version": {
    "major": 1,
    "minor": 0
  },
  "data": {
    "name": "water_calibrate",
    "ops": [
      {
        "volume": 200,
        "rate": 0
      }
    ],
    "source": "intents.xml"
  }
}

If we perform the same query using a syrup pump, we get the following:

GET /api/nozzle/myNozzle/pipeline/ingredient/intent/assembly.core.holder:S1.pump:s1/calibrate
{
  "status": 200,
  "version": {
    "major": 1,
    "minor": 0
  },
  "data": {
    "name": "syrup_calibrate",
    "ops": [
      {
        "volume": 50,
        "rate": 0
      }
    ],
    "source": "intents.xml"
  }
}

We can now construct a user interface that performs a calibration pour for any valve. The UI button simply needs to pass the pump handle path along with an intent of "calibrate", at which point the pump will perform the correct type of pour. This provides a number of interesting advantages over embedding pour information into the UI:

Code reuse

It is now possible to write code that is independent of the underlying hardware. The pumps can be discovered using endpoints and the intents are fixed by UI function, but the underlying operations performed can be swapped out as data from one platform to another. Within Freestyle, we have a single admin interface that runs on every model of dispenser.

Testability and automation support

With all pump intents visible via endpoints, verifying that a pump is performing the correct operations is as easy to hitting an endpoint in a browser. These same endpoints can be used in test automation to verify all pumps perform the correct operations for all possible intents without ever hitting a pour button.

Delegation of responsibility

It’s pretty rare that the software team defines the specifics of any particular pour. The more types of pours there are and the more complex the UI, the harder it is for the software team to manage the implementation. At Freestyle, the intents files are managed by the fluidics team and are pulled into the build as data.

Hopefully this application note provided insight into using KOS to set up a basic dispenser with ingredient pouring support. If you have any questions or feedback for this note, please contact the KOS development team.

Previous
Next
On this page
Java Development
Seamlessly transition from Legacy+ systems to Freestyle microdosing and advanced distributed dispense systems.
UI Development
Using KOS SDKs, integrating Consumer and Non-consumer facing UIs becomes seamless, giving you less hassle and more time to create.
Video Library
Meet some of our development team, as they lead you through the tools, features, and tips and tricks of various KOS tools.
Resources
Familiarize yourself with KOS terminology, our reference materials, and explore additional resources that complement your KOS journey.
Copyright © 2024 TCCC. All rights reserved.