Zero4 Backend Tutorial

Welcome to the KOS Zero4 tutorial where we’re going to build a working micro-dosing dispenser completely from scratch. This tutorial is named after custom evaluation board containing a Raspberry Pi Zero and driver circuitry for up to two macro valves and four micro pumps, thus the name Zero4. While not required, this tutorial is best experienced with a KOS Zero4 demo kit which contains a Zero4 board, four micro pumps, a seven inch touch screen and ethernet dongle. This tutorial can still be run in the KOS simulator, but without experiencing the micro pumps running while pouring.

This tutorial assumes the reader is familiar with KOS, at least at a high level. It does not cover the use of KOS Studio which will be required to run any of the code covered by this tutorial.

Chapter 1: System Application

Unlike a desktop or mobile operating system that boots to a desktop, KOS boots into a single special purpose application called the system application. This means we’re going to start with a system application for our Zero4 dispenser.

A system application is just a class that extends SystemApplication so let’s start with a minimal implementation:

//Zero4App.java
@Slf4j
public class Zero4App extends SystemApplication<BaseAppConfig> {
    @Override
    public void start() throws Exception {
        log.info("Start application");
    }
}

Every KOS application has a number of lifecycle callbacks but we only need one for now:

  • start() : The application is ready to start. We can perform application initialization here.

We now have a very basic system application. It doesn’t do much other log that the app started, but it’s a start. Before we can run our application we need to add an application descriptor that tells KOS how to run our application, and package the application as a KAB file.

Creating an application descriptor

Within KOS descriptor.json files are a standard way to package meta data into any KAB file, including where the entry point of our application will be. KOS has many api’s around KAB descriptors that make them particularly useful, but for now we simply need to use it to describe the details of our application so we can package it into a KAB. Here is a sample descriptor.json:

//descriptor.json
{
  "kos": {
    "app": {
      "appClass": "com.kondra.kos.zero4.Zero4App",
      "appId": "system"
    }
  }
}

Notice that there is an app object within the top level kos json object. This is called a descriptor fragment and KOS provides a number of standard fragments for utilizing various KOS features. All KOS fragments are defined within the kos base object. Developers are free to put any data they want into a descriptor.json file, but should avoid putting any data into the kos object other than standard KOS fragments.

The application fragment above populates two properties:

  • appClass : This is the full class name of the application class so KOS knows which class to load as the entry point of the app.
  • appId : This is the unique id of the application. Since this is a system application, we must use the standard system id. There can only be one system application for a KOS device.

Packaging our application

Before we can run this application we need to package it as a KAB file. Within KOS everything is a KAB. Applications, data, even the linux kernel is packaged as a KAB file. KOS Studio includes a command line tool called kabtool, but it’s typically more convenient to use the KOS maven plugin to build a KAB file. To do this you will need to add a section to your pom.xml to include the plugin:

<!-- pom.xml -->
    <build>
        <pluginManagement>
            <plugins>
                <!-- kOS Maven Plugin -->
                <plugin>
                    <groupId>com.kosdev.kos.maven</groupId>
                    <artifactId>kos-maven-plugin</artifactId>
                    <version>1.3.4</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

The plugin can then be used to package the application jar file into a KAB by adding this to your pom.xml:

<!-- pom.xml -->
    <build>
        <plugins>
            <plugin>
                <groupId>com.kosdev.kos.maven</groupId>
                <artifactId>kos-kab-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>kabtool</goal>
                        </goals>
                        <configuration>
                           <type>kos.system</type>
                           <content>
                              <copy>
                                 <include>descriptor.json</include>
                              </copy>
                              <copy>
                                 <include>${project.build.directory}/${project.artifactId}-${project.version}.jar</include>
                                 <dir>lib</dir>
                              </copy>
                           </content>
                        </configuration>                        
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

Here are the key things to note:

  • The type property defines what the type of the KAB. Developers can define any type of KAB they want, but KOS standard KAB types always start with kos. We’re building a system application KAB and the standard KOS type for that is kos.system.
  • The descriptor.json file is copied into the root directory of the KAB. This is the standard location for descriptor.json files.
  • The jar file for the application is copied to the lib directory of the KAB. KOS will automatically add any jar file found in the lib directory to the application classloader.

Chapter 2: Creating an Assembly

Unlike a desktop operating system where hardware-specific applications can only be loaded after the hardware is detected and drivers are loaded. Device-oriented systems need to be able to run even when hardware is not ready or even missing. In fact, device-oriented systems need to be resilient to hardware being rebooted, or even replaced while the system is running. This introduces a great deal of complexity around wiring application code together and reacting correctly when hardware becomes available. KOS provides extensive support for dynamic hardware and firmware update by modeling all the hardware in the device. By creating logical representations of physical components in software, KOS can ensure the system boots in a consistent and controlled way, eliminating complexity and race conditions for application developers.

At the center of this logical abstraction is the Assembly class. An assembly is a collection of logical hardware, ranging from electronics such as boards, to other physical components like pumps and nozzles. By creating these components in an Assembly, applications can configure and wire all the software components together before they are connected to physical hardware. Only when the Assembly is installed does KOS link physical hardware to the logical representations.

In the case of the Zero4, we have several physical components we want to model:

  • Zero4 board : In addition to a Raspberry Pi Zero, the Zero4 board also contains an additional ARM processor that drives the valves and micro pumps. We model this as a Board so that KOS can automatically notify us when the hardware is online, allowing us to pass commands to the hardware to turn the pumps on and off.
  • Nozzle : KOS supports multi-nozzle applications, treating every nozzle as an independent pouring sub-system. We model our physical nozzle with a Nozzle object which allows us to attach pouring infrastructure to the Nozzle.
  • Pumps : The board is capable of driving two valves and four micro pumps. Each of these are modeled as Pump objects, allowing KOS to use standard pouring infrastructure to operate on the pumps.

The first step in creating an assembly is to create a new Assembly class:

// Zero4Assembly.java
public class Zero4Assembly extends DispenseAssembly implements CoreAssembly {
    public Zero4Assembly(JsonDescriptor descriptor) throws Exception {
        super("core", descriptor);
    }
}

Note that our assembly class implements the CoreAssembly interface. KOS allows multiple assemblies which can be dynamically added and removed. This allows KOS to support devices with expansion modules. To distinguish these assemblies from the main assembly for the core device, we tag the core device assembly with the CoreAssembly interface. This indicates to KOS that this assembly can never be removed. Since this is the core assembly, we give it a name of “core” by convention.

Now that we have a basic assembly, we can start filling in the parts!

Adding the Zero4 Board

In KOS terminology, the computer that runs KOS is known as a node and any other hardware is referred to as a board. This can be a little confusing given the Zero4 circuit board appears to be a single circuit board, but let’s review the logical parts of the Zero4 board:

  • Raspberry PI Zero module : Since this is running KOS, this is a node.
  • ARM cpu : This controls the valves and micro pumps and is a board.

This means we should expect to add one board to our assembly:

// Zero4Assembly.java
    private Zero4Board zero4;
    
    @Override
    public void load() throws Exception {
        // Create the logical zero4 board and add to the assembly
        zero4 = new Zero4Board(this);
        addBoard(zero4);
    }

While there isn’t much code, let’s break down what’s going on.

First, notice the Zero4Board class. This is our abstraction of the ARM cpu portion of the physical Zero4 hardware. While we’ll see the details of this class below, the key is that we add the new board to the assembly. When the assembly is installed, this will make this board visible to KOS internal services allowing it to link to physical hardware.

Second, we created the board in a method called load() instead of the constructor of the assembly. This follows a standard KOS lifecycle pattern based on the Loadable interface. Since KOS includes a powerful dependency injection engine and global configuration system, we want to be able to leverage these as easily as possible. The Loadable interface streamlines this through the following callbacks:

  • load() : Create objects and add them to the associated BeanContext. These objects are not yet wired together or configured. This is simply a construction phase. When this method returns all the beans in the context will be dependency injected, configured, and marked ready.
  • start() : Called once all the beans created in load() are fully configured. Within this callback, any objects created in load() are fully functional and ready for use.
  • started() : Called after all integration hooks are enabled. Indicates that the Loadable object is fully functional.

Nozzle and Pumps

The KOS Dispense extension provides extensive support for building arbitrarily complex dispensing devices and models various parts of the fluidic pipeline to describe how your dispenser works. Our Zero4 dispenser is a very simple device, but we still need to model the internals so that KOS can understand how everything is connected.

The basic parts of the fluidic pipeline are as follows:

  • Container : An abstraction of something that provides one or more ingredients. For example, a Freestyle cartridge is an example of a container. Similarly, the cardboard box for a BiB is a container. Even when no physical container exists, such as when water is plumbed to the wall, there is still a logical container in software.
  • ContainerSlice : A slice is part of a container that holds an ingredient. For BiB, you can consider the cardboard exterior the container and the bag inside as the slice. Freestyle supports double cartridges which are two cartridges glued together with a single rfid tag. This is a single container because it’s added / removed as an atomic unit, but it contains two bags of ingredients, each representing a slice. Most containers contain a single slice.
  • Holder : An abstraction of what you insert the container into. In the case of a Freestyle machine, this would be equivalent to the slot you put the cartridge into. Holders have end user visible names that typically correspond to labels in the device. For example a legacy+ dispenser that has lines labeled A1, S1, F1, etc… correspond to holders.
  • Pump : A pump in KOS is a pump or a valve. Pumps pull ingredient from a holder and pour it out a nozzle. A holder can have any number of pumps.
  • Nozzle : Abstraction of a physical nozzle. Each pump is connected to exactly one nozzle.

KOS provides InsertionService which can insert a container into one or more holders (depending on the number of slices) which makes the ingredient pourable. However, we currently don’t have a nozzle or any holders to insert containers into so we need to look at this next, starting with a nozzle:

// Zero4Assembly.java
        // Create a nozzle for the dispenser and add to the assembly
        Nozzle nozzle = new Nozzle(this, "nozzle");
        addNozzle(nozzle);

Now that we have a nozzle we need to create the holders. This isn’t hard, but it’s a lot of little details. For example, for each holder we need to:

  • Create a holder instance
  • Name it in a way that matches the labels on the device
  • Connect the correct pump from the correct board

Again, while simple in concept, this tends to generate a lot of boilerplate loop code. Given how common the pattern is, KOS provides various builders to minimize the effort in creating holders. Let’s have a look at the HolderBuilder class that creates the holders, starting with the water valves:

// Zero4Assembly.java
        // Create a builder to construct holders for the nozzle
        HolderBuilder builder = new HolderBuilder(this, nozzle);
        builder.buildWater(zero4.getWater());
        builder.buildCarb(zero4.getCarb());

This simply asks the builder to create a water holder, wired to the water valve returned by zero4.getWater() and to wire it to the nozzle.

With the waters in place, we turn to the micro pumps:

// Zero4Assembly.java
        builder.setPumpIterator(zero4.getMicros(), 0, 1);
        builder.setNameIterator("M", 1, 1);
        builder.buildMicros(4, 0);

Since micro pumps tend to come in large groups, the builder is designed to use iterators for micros. Let’s look at each line more closely:

        builder.setPumpIterator(zero4.getMicros(), 0, 1);

This tells the builder where to get the next pump. We’re iterating over the with the four micros pumps available from Zero4Board, starting at index 0. The iterator will then increment by 1 on each cycle, thus returning pump 0, 1, 2, 3.

        builder.setNameIterator("M", 1, 1);

This tells the builder what the name of the holder should be. We want to name the micro holders M1, M2 and so on, thus we use a name prefix of “M”, starting with a value of 1 and incrementing by one on each pass.

        builder.buildMicros(4, 0);

Finally, we tell the builder to build four holders and place them all in group zero. Groups are useful for display purposes but otherwise have no implications in the code we’re working with.

Nozzle Pipelines

We now have all our pumps wired to the nozzle and all the holders exist and are properly named. We’re ready to insert and start pouring, or are we? While we have most of the parts in place, we’re not quite there yet. This does get us to the point where we can talk about what pouring really means.

KOS abstracts the concept pouring to a NozzlePipeline. Developers can create new pipelines to define new ways to pour. A single nozzle can contain any number of pipelines and each pipeline can get exclusive use of the nozzle while pouring. The KOS Dispense extension comes with two distinct nozzle pipelines:

  • IngredientNozzlePipeline : Optimized for ingredient pouring, this pipeline can be used for all utility pours.
  • BeverageNozzlePipeline : Designed for beverage pouring, this pipeline provides sophisticated beverage pouring functionality that can be be backed by a custom PourEngine implementation.

The benefit of nozzle pipelines is that developers can design entirely new ways to pour and if they don’t align with the two existing pipelines, can create their own implementation while still supporting the standard approaches. For example, consider a dispenser that can pour standard recipe beverages but can also pour flavored coffee. The steps required to pour coffee may be sequential and require complex actions, such as foaming milk. This can be captured in a new nozzle pipeline without impacting the standard pipelines. KOS also supports multiple nozzles, each with distinct nozzle pipelines, ingredients and so on.

For our Zero4 dispenser we need to be able to perform ingredient pours as part of preparing the micro pumps, such as priming a cartridge when replaced. We also need to be able to pour beverages using micro ingredients. This can be accomplished by adding the two standard pipelines.

Ingredient nozzle pipeline

Let’s start with creating the pipeline and then we’ll dig into the details:

// Zero4Assembly.java
      
        // Load intents used by the ingredient pipeline
        XmlPumpIntentFactory intentFactory = new XmlPumpIntentFactory();
        intentFactory.addLoader(new ClassLoaderResourceLoader(getClass().getClassLoader()));
        intentFactory.load("intents.xml");
        // Add an ingredient pipeline and use water for dilution
        IngredientNozzlePipeline ingredientPipeline = new IngredientNozzlePipeline(intentFactory);
        ingredientPipeline.setDilutionPump(zero4.getWater());
        nozzle.add(ingredientPipeline);

The first step of creating an IngredientNozzlePipeline is to create an intent factory that can resolve named intents for various types of pumps. But what is an intent?

Consider the requirement that when a micro cartridge is replaced, it needs to be primed. We know what our intention is, we want to prime the line, but what’s the definition of prime? Fluidics might say that the definition of a prime is a 15 second pour. We could hard code this every place in our code where a prime is required, but what happens when we change the definition. For example, if the line is empty and we try to pour for 15 seconds, the pour will abort after a few seconds due to an air detect error. This means the user will need to hit the prime button over and over until the line gets enough fluid in it to avoid this error. Instead, fludics may come back and say that prime is defined by two 7.5 second pours where the fist pour ignores air detect errors.

As the definition of particular intents get more complex, it introduces significant software challenges:

  • How do we find all instances of the old code and update it to the latest definition?
  • When a pour has multiple steps, how do we make it an atomic unit of work that has a progress bar and can be cancelled?
  • When a UI needs to show the volume of a pour, such as a calibration intent, how do we keep the label in the ui in sync with the actual pour that’s being delivered?
  • When we add a “prime all” button, how do we orchestrate all the concurrent pours with a single progress bar and the ability to cancel the entire operation?
  • When running multiple concurrent ingredient pours, how do we schedule the pours so that we never exceed the power constraints of the various boards or the device power supply?

The ingredient nozzle pipeline solves all these problems by abstracting the concept of an intent. Within KOS, developers can create low level pump operations, such as timed pour, volume pour, delay, etc… and create named sequences of these operations by describing them via xml file. Applications can then request an intent by name and the pipeline will construct the correct sequence of operations and run it as an atomic unit of work with progress information and support for cancellation.

The base KOS Dispense extension supports three basic operations:

  • tpour : time based pour with rate
  • vpour : volume based pour with rate
  • delay : delay for specified time

Using these basic pump operations, it is easy to build most high level intents. The sample below demonstrates two different intents for micro pumps:

//intents.xml
      <intent name="PRIME">
         <op type="tpour" rate="0.9" duration="15000" diluted="true" />
      </intent>
      <intent name="HOLD_TO_POUR">
         <op type="vpour" rate="0.9" volume="10" diluted="false" />
      </intent>

The PRIME intent is defined as a single timed pour lasting 15 seconds that must also run water to dilute the micro ingredient. The HOLD_TO_POUR intent is defined as a single volume pour of 10ml without water dilution.

Given a pump and the name of an intent, KOS can perform the pour, and even return the total expected volume of the pour before the pour is performed. This just scratches the surface of the ingredient nozzle pipeline, but should provide sufficient background to understand the following fragment:

// Zero4Assembly.java
      
        // Load intents used by the ingredient pipeline
        XmlPumpIntentFactory intentFactory = new XmlPumpIntentFactory();
        intentFactory.addLoader(new ClassLoaderResourceLoader(getClass().getClassLoader()));
        intentFactory.load("intents.xml");

This is simply creating an instance of XmlPumpIntentFactory, which understands the xml format for the standard set of pump operations, and uses it to load intents.xml from the resources folder of the application. This is then used to create the pipeline and install it into the nozzle:

// Zero4Assembly.java
        // Add an ingredient pipeline and use water for dilution
        IngredientNozzlePipeline ingredientPipeline = new IngredientNozzlePipeline(intentFactory);
        ingredientPipeline.setDilutionPump(zero4.getWater());
        nozzle.add(ingredientPipeline);

With this pipeline in place, our dispenser can pour any intent that exists in the intents.xml file.

Beverage nozzle pipeline

Now that we can pour ingredients, it’s time to add support for beverage pouring. As before, we’ll start with the code and then break things down:

// Zero4Assembly.java
        // Create a custom pour engine for our beverage pipeline
        Zero4PourEngine engine = new Zero4PourEngine();
        beveragePipeline = new BeverageNozzlePipeline(engine);
        beveragePipeline.setDelegate(new Zero4PourDelegate());
        nozzle.add(beveragePipeline);

The beverage pipeline is agnostic to how pouring actually works. All the details are captured in our custom Zero4PourEngine implementation. For example, consider the following types of beverage pouring:

  • Simple macro + water / carb
  • Beverage + one or more flavor shots
  • Recipe based pours
  • Mix combinations of standard beverages
  • Fixed volume pours
  • Split pours
  • Percentage pours
  • Layered pours
  • Controlled pour limits across multiple pours

All of these can be handled by the beverage pipeline by simply implementing a pour engine which we’ll examine in more detail below.

Installing the assembly

With the board, nozzle and pumps added to the assembly and both ingredient and beverage pipelines wired to the nozzle, the assembly is almost complete. We simply need to install the assembly into KOS which we do from the Zero4 system application:

//Zero4App.java
    @Override
    public void start() throws Exception {
        // Install the assembly
        installAssembly(new Zero4Assembly(getDescriptor()));
    }

Chapter 3: Modeling the Zero4 hardware

In the previous chapter we added a Zero4Board instance to the assembly and used the pumps provided by this board to create our holders and wire them to the nozzle. In this chapter we’ll look at the Zero4Board class in detail and examine how we can turn the pumps on and off using physical hardware.

Modeling the Zero4 board

KOS provides a base Board class which can be used to model physical hardware. The KOS Dispense extension provides a PumpBoard class that returns a list of pumps associated with a board. By extending PumpBoard, the KOS standard infrastructure automatically identifies associated pumps so we’ll start by extending PumpBoard for our board class:

// Zero4Board.java
public class Zero4Board extends PumpBoard {
    public Zero4Board(Assembly assembly) {
        super(assembly, "zero4");
    }
    @Override
    public String getType() {
        return "tier1.zero4";
    }
    @Override
    public String getInstanceId() {
        return null;
    }
}

When we create a board we need to provide a name for the board. Since the Zero4 board is self contained, it doesn’t make much sense to have more than one in a device so we simply hard code the name in the constructor:

    public Zero4Board(Assembly assembly) {
        super(assembly, "zero4");
    }

If we were building a Board for hardware where there can be more than one instance of the hardware in a single device, we would need to pass in the name so that every board has a unique name.

Notice that every Board instance also needs to provide a type and instance id. Let’s examine these more closely:

  • type : The type of the board is used by KOS to match physical hardware to the corresponding logical Board object in memory through a process called linking. When the hardware is discovered, it will identify the type of board it is and KOS will search the list of unlinked board objects for an instance of the same type. As with all symbolic constants in KOS, the type is namespaced with the manufacturer name to avoid type collisions with new boards that may be added in the future.
  • instanceId : In the event that there may be more than one of a given piece of hardware in a single device, the instanceId is used to distinguish them from each other. In virtually all cases where two or more instances of a particular piece of hardware exist in a device, the hardware has some way to identify which one it is. This may be through a jump on the board, or a sense pin in the cable harness, but some mechanism exists to identify which one is which. Whatever the mechanism used, the interface to the physical hardware will return this value as the instanceId of the board. The getInstanceId() method in the Board instance simply returns the matching value to allow KOS to link the correct logical board to the physical hardware. Since we only ever have a single instance of a Zero4 board, we simply return null.

Even though our Board class does nothing useful, this is enough information for KOS to match our Zero4Board to physical hardware.

Adapters integrate with physical hardware

We’ve described the KOS process of linking a logical board to physical hardware, but how does this actually work? How do we actually communicate to hardware in KOS? This is done through a native program called an adapter.

While KOS is a java based runtime, java tends to be fairly limited when it comes to interfacing to hardware. The most common approach to java hardware integration is to use Java Native Interface (JNI) which requires creating java to C bindings and compiling native shared libraries that java needs to load. This tends to have several disadvantages:

  • JNI has a relatively steep learning curve.
  • Any bugs in native code will crash the entire java virtual machine.
  • JNI libraries need to be cross compiled for any native platforms used for testing, including the KOS Studio simulator.

KOS decouples java from hardware integration through the use of adapter programs. An adapter is simply a native application that integrates to hardware and communicates to java using the KOS blink network protocol. Blink, short for Binary Link, it an extremely efficient packed binary protocol, allowing data to be sent between adapters and java using native binary data types. The blink protocol provides framing and the concept of request / response messages as well as events. The KOS blink library provides a simple to use callback registration system to allow native callbacks to receive and respond to requests from java and send real-time events back to java.

Being network based, adapters have a few other interesting characteristics:

  • In a device with more than one KOS node, it’s possible to run adapters on different node than where java runs, but still have java control the remote hardware. This even extends to the ability for java on one node to dynamically detect usb insertions on another node and automatically start adapters on that node that are then controlled by java.
  • During development it’s possible to manually run an adapter on real hardware but point the adapter to java running in your development environment, allowing local testing of java code but with full hardware control.

In the case of the Zero4, the zero4Adapter is automatically included in KOS when running on a Zero4 board. We simply need to start it. This can be easily accomplished using the KOS SpawnService, which will launch a program and them monitor it, automatically restarting it if it crashes. We can start the adapter in our application class or as part of installing and starting the assembly. Since the board created in the assembly, we’ll start the adapter there as well:

//Zero4Adapter.java
    @Override
    public void started() {
        // Only start the adapter if running on real hardware
        if (!KosCore.isSimulator()) {
            spawnService.addProcess(new Zero4Adapter());
        }
    }

When an adapter opens a blink connection to java, it describes the various interfaces, or ifaces, that it supports. Each iface has a name and a distinct set of messages and events. A single blink connection can support up to 16 ifaces, allowing a single device to connect to directly to multiple services in KOS.

The java side of a blink iface is implemented as an instance of BinaryMsgIface which is a wrapper class that converts methods calls to blink messages. The zero4Adapter only supports a single api which turns a pump on at a specified rate for a specified duration. We can use this single api call to create start and stop methods in our iface class:

// Zero4BoardIface.java
public class Zero4BoardIface extends BinaryMsgIface {
    // name of this interface
    public static final String NAME = "tier1.zero4";
    // api numbers for the protocol
    private static final int API_PUMP = 2;
    public Zero4BoardIface(BinaryMsgSession session) {
        super(NAME, session, null);
    }
    /**
     * Start a pump.
     *
     * @param pos          0-3 are micros, 4/5 are macros
     * @param rate         rate of to pour (ignored by macros)
     * @param durationMs   how long to pour
     */
    public void startPump(int pos, double rate, int durationMs) throws IOException {
        BinaryMsg msg = msg(API_PUMP);
        msg.writeInt(pos);
        msg.writeInt(Convert.toQ8(rate));
        msg.writeInt(durationMs);
        sendAndRecv(msg);
    }
    /**
     * Stop a pump.
     *
     * @param pos   0-3 are micros, 4/5 are macros
     */
    public void stopPump(int pos) throws IOException {
        // stop the pump by sending a zero rate and duration
        startPump(pos, 0, 0);
    }
}

The zero4Adapter also implements the KOS board iface. This is a standard way to describe the type and instanceId of hardware. If you recall from above, this is enough information to match physical hardware with the corresponding logical Board instance. Since this iface is handled internally in KOS, just the act of starting the adapter is enough to trigger the board linking process. The KOS HardwareService will use the board information from board iface of the connection to find the logical board instance, our Zero4Board instance in this case, and link to the board. By adding link handlers, we can get access to the blink session for our Zero4BoardIface class:

//Zero4Board.java
    private Zero4BoardIface iface;
    
    @Override
    public void onLink(HardwareLink link) {
        // grab the iface from the link
        if (link instanceof BoardIfaceLink blink) {
            iface = new Zero4BoardIface(blink.getSession());
        }
    }
    @Override
    public void onUnlink(HardwareLink link) {
        // remove the iface as session is gone
        iface = null;
    }

We now have an iface property in the Zero4Board class that is null when the adapter isn’t connected, and not-null when the adapter is running. The iface can then be used to start or stop any pump by simply sending a message over blink to the adapter and the adapter performing the necessary hardware operation. Using this iface we can implement a basic timed pour operation in our board class:

//Zero4Board.java
    // Perform a simple timed pour on the specified pump
    public FutureWork tpour(BasePump pump, int duration, double rate) {
        if (iface != null) {
            ...
        }
    }

Performing a timed pour

If we can perform a simple timed pour, we can use this to build up other primitive operations. For example, KOS pump intents only require a minimum of timed pour (tpour) and volume pour (vpour) operations. We can easily convert a volume pour to a timed pour knowing the rate and volume. This means we only need to get our tpour() method working on real hardware to leverage the KOS ingredient pipeline functionality.

A timed pour is a great example of an asynchronous operation. We may want to start multiple pumps at the same time using multiple calls to our tpour() method. If tpour() blocks for the duration of the pour, then this will be impossible, which means tpour() must return immediately. There should also be a mechanism to cancel the pour and even a way to track the progress of the pour as it runs, so we can display a progress bar to the user. This type of asynchronous work is so prevalent that KOS provides FutureWork to specifically address these needs. A FutureWork object can be thought of as a unit of work that can be created but started at a later time. Futures can be cancelled and aborted and allow additional event handlers to be attached so that other code can perform actions as a side effect of the future being started or how it ends. Futures can also be aggregated and run in complex graphs of sequential or parallel logic but still with the ability to run the entire graph as an atomic unit of work that can be cancelled or aborted along with the ability to report progress information. Since the KOS Dispense extension expects basic pump operations to return futures, we need to complete our tpour() implementation using a FutureWork. The first step is to create a FutureWork that will perform the pour:

//Zero4Board.java
    public FutureWork tpour(BasePump pump, int duration, double rate) {
        // Create a new future that will perform the requested pour
        FutureWork future = new FutureWork("tpour-" + pump.getName(), f -> {
            // If there is no iface, we can't turn the pump on
            if (iface == null) {
                f.fail(REASON_errNotConnected);
            } else {
                // Use the iface to run the pump for the specified duration.
                // Since the simple interface to the Zero4 board doesn't send
                // back pump status, use a timer to indicate that the future
                // is complete when the duration is complete. A more robust
                // implementation would send pump status back over the iface.
                log.info("start: {}", pump.getName());
                iface.startPump(pump.getPos(), rate, duration);
                KosUtil.scheduleCallback(() -> f.success(), duration);
            }
        });
        return future;
    }

When the future is started, the lambda will be run which will log that the pump is starting and use the iface to start the specified pump. While this runs the pump as expected, there are two gaps:

  • There is no logging when the pump stops. While not critical, it’s nice to see both a start event and stop event in the logs.
  • If the user cancels the operation, we need to stop the pump.

To handle the cancel situation, we simply need to attach a handler to the future that turns the pump off:

//Zero4Board.java
        // Add a cancel event handler to the future
        future.append("cancel", FutureEvent.CANCEL, f -> {
            // If cancelled, use the iface to stop the pump
            log.info("cancel: {}", pump.getName());
            if (iface != null) {
                iface.stopPump(pump.getPos());
            }
        });

For logging, we can simply attach another handler that logs the stop message when the future is complete:

//Zero4Board.java
        // Add a complete event handler to the future
        future.append("stop",  FutureEvent.COMPLETE, f -> {
            // When the future is complete, regardless if how it ended, log
            // that the pump has stopped
            log.info("stop: {}", pump.getName());
        });

Modeling pumps

The final step in modeling the hardware for our Zero4 dispenser is to model the pumps. The KOS Dispense extension provides a base Pump class which is used to model pumps or valves and can be extended to leverage extensive core functionality. KOS functionality, such as pump intents, can be tied to pump types so it’s important to define different pump types for the valves and the micro pumps. Since both types of pumps have the same hardware interface, we could simply create a single pump class and pass a different type string into the constructor for micro pumps versus macro valves. For the sake of clarity we will create a MicroPump class and MacroPump class, both extending a BasePump class:

//BasePump.java
abstract public class BasePump extends Pump<PumpConfig> {
    @Getter
    private int pos;    // position of the pump on the board
    public BasePump(Zero4Board board, String name, String category, int pos) {
        super(board, name, category);
        this.pos = pos;
    }
    @Override
    public FutureWork tpour(int duration, double rate) {
        // Simply pass the tpour request to the board the pump is attached to
        return ((Zero4Board)getBoard()).tpour(this, duration, rate);
    }
    @Override
    public FutureWork vpour(double volume, double rate) {
        // convert the volume pour to a timed pour
        return tpour((int)(volume / rate), rate);
    }
}

Notice the BasePump class implements the two basic pump operations: tpour() and vpour(). This allows any pumps of this type to work with the ingredient pipeline. We can then extend this class to create MicroPump and MacroPump classes:

//MicroPump.java
public class MicroPump extends BasePump {
    public MicroPump(Zero4Board board, String name, String category, int pos) {
        super(board, name, category, pos);
    }
    @Override
    public String getType() {
        return "micro";
    }
}
//MacroPump.java
public class MacroPump extends BasePump {
    public MacroPump(Zero4Board board, String name, String category, int pos) {
        super(board, name, category, pos);
    }
    @Override
    public String getType() {
        return "valve";
    }
}

With these classes in place, our hardware is fully modeled and KOS understands how pumps are controlled by hardware, how pumps are connected to the nozzle and can perform any ingredient pour defined in intents.xml using standard KOS endpoints. The only remaining gap around ingredient pouring is assigning ingredients to holders, as by default, KOS won’t run pumps that don’t have ingredients assigned.

Chapter 4: Defining a brandset

As we look at beverage pouring, we need to examine what is required to define a beverage. For example, in a system with only fixed rate valves, a typical strategy is to mix a syrup with plain or carbonated water. In such a system, we need to know the following:

  • What syrups are available
  • Does a syrup pour with plain or carbonated water

We refer to the data required to pour a beverage as a brandset, and in the example above it requires some form of recipe, even if it’s just a ingredient name with a flag for the type of water to mix with.

In the case of a Zero4 dispenser we have variable flow rate micro pumps. This means we can mix different ratios of various ingredients to form a beverage. In this context a beverage can be defined using the following java beans:

public class Beverage {
    private String id;               // unique id of the beverage
    private String name;             // display name of the beverage
    private List<RecipePart> recipe; // recipe to pour the beverage
}
public class RecipePart {
    private String ingredientId;   // id of the ingredient
    private double rate;           // rate the ingredient should pour in ml/s
}

This allow a recipe to be defined in the following json format:

    {
      "id": "2",
      "name": "Lemon Zest",
      "recipe": [
        {
          "ingredientId": "water",
          "rate": 66
        },
        {
          "ingredientId": "lemon",
          "rate": 0.25
        }
      ]
    }

The KOS Dispense extension also depends on the concept of ingredients which can be assigned to holders and pumps. We can add the following to support ingredients in our json file:

  "ingredients": [
    {
      "id": "water",
      "name": "Water"
    },
    {
      "id": "cherry",
      "name": "Cherry"
    },
    {
      "id": "lime",
      "name": "Lime"
    }

It’s important to understand that KOS does not depend on any particular brandset format or any particular pouring style. The format above is simply a convenient match for the functionality we want to implement in this tutorial. We could just as easily add a concept of flavor shots, dynamically generated recipes and so on.

Packaging as a KAB

We want the brandset data to be independent of the application so we can update the brandset over time. In a following tutorial we’ll also be adding ui related information to our brandset, including images and ui meta-data. We want to make sure all of this data get packaged together and since everything in KOS is a KAB, we should package the brandset as a KAB file.

As with the application, we can rely on the KOS maven plugin to build our KAB file as part of the build:

    <build>
        <plugins>
            <plugin>
                <groupId>com.kosdev.kos.maven</groupId>
                <artifactId>kos-kab-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>kabtool</goal>
                        </goals>
                        <configuration>
                            <type>zero4.brandset</type>
                            <content>
                                <copy>
                                    <includes>
                                        <include>brandset.json</include>
                                        <include>assets</include>
                                    </includes>
                                </copy>
                            </content>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

Notice that we include our brandset.json file as well as every file in the assets directory.

What type of KAB file should we use for the brandset? As there is no concept of a brandset within the KOS Dispense extension, we can pick a type which we’ll use later to find this KAB when the application starts. Following the general namespacing guidance, we use the name zero4.brandset.

Loading the brandset

Now that we have a brandset KAB, we need to include it in our KOS image so it’s available when we deploy our application onto a device. Which section should include the brandset KAB? We can put it in any section so long as we write code in our application to look for the KAB in the specified section. That is, there is no standard, and applications are free to use any convention that is convenient. As KOS provides an api to find a KAB of a particular type in the section the system application was loaded from, it’s simple to put the KAB in the kos.system section. We can then locate the KAB using the following code in the system application:

//Zero4App.java
        // Find the brandset kab by type
        KabFile kab = getSection().getKabByType("zero4.brandset");
        if (kab != null) {
            // Load the brandset json from the KAB into a brandset object
            brandset = KosUtil.getMapper().readValue(kab.getInputStream("brandset.json"), Brandset.class);
            // Make kOS aware of the ingredients from the brandset
            ingredientService.setDefaultSource(brandset);
            // Make assets contained in the KAB available via the web server
            getVfs().mount("/brandset", kab);
        }

While not much code, a lot is going on here so let’s break this down:

KabFile kab = getSection().getKabByType("zero4.brandset");

This utilizes getSection() to return the manifest section the system app was loaded from. We expect our brandset KAB to be in this section so we can search for it using getKabByType(“zero4.brandset”) which is the type we specified when building the brandset KAB. If the KAB file exists, this will return a KabFile object that references the KAB file on disk.

brandset = KosUtil.getMapper().readValue(kab.getInputStream("brandset.json"), Brandset.class);

Since we designed the content of brandset.json to match our brandset java beans, we can simply deserialize the contents of the json file into java beans using an ObjectMapper instance. This is such a common need that KOS provides a pre-created and configured ObjectMapper instance via the KosUtil static class. To read the contents of brandset.json directly from the KAB file we can use kab.getInputStream(“brandset.json”).

ingredientService.setDefaultSource(brandset);

The KOS Dispense extension contains IngredientService which operates on ingredients and needs to know what ingredients are available. Our Brandset object implements the IngredientSource interface allowing us to pass the brandset object directly to IngredientService.

getVfs().mount("/brandset", kab);

Finally, we want the icons and UI meta data to be available via the built in web server so that the UI application running in the browser can access it. Unlike typical web servers, the kOS web server doesn’t serve content directly from disk. Instead, KOS provides a virtual filesystem (vfs) which contains content for the web server. While it’s possible to map directories from local disk to the vfs, since everything in KOS is a KAB file, it’s more common to mount a KAB into the vfs. This is similar to mounting a disk and makes the contents of the KAB available at the specified virtual mount directory in vfs. As every application in KOS has a dedicated base directory in the vfs, we simply mount the brandset KAB in the application vfs using a virtual directory of /brandset.

Inserting ingredients

Now that we have the brandset loaded and ingredients registered with KOS, we can insert some key ingredients, starting with waters. Since plain and carbonated water are typically plumbed directly to a water source, we can bypass all the KOS ingredient insertion logic and simply insert these as intrinsic ingredients:

//Zero4Assembly.java
    @Override
    public void started() {
        // Water and carb are always connected so we can insert them as intrinsic ingredients.
        // Intrinsics are locked in place and cannot be replaced once installed.
        insertionService.insertIntrinsic(Ingredient.WATER, zero4.getWater().getHolder());
        insertionService.insertIntrinsic(Ingredient.CARB, zero4.getCarb().getHolder());
    }

Micro ingredients would normally be inserted using InsertionService endpoints called from a user interface running on the device. As we don’t have an administrative UI application, developers can leverage the ingredient assignment tool packaged with KOS Studio to insert ingredients into the remaining holders for the Zero4 dispenser.

Chapter 5: Custom pour engine

All that remains is beverage pouring. As mentioned earlier, KOS can be used to perform any style of beverage pour. The standard beverage pipeline is designed to work with most any recipe style beverage system. This includes everything from simple syrup + water to complex Freestyle recipes, mixes, flavor shots and anything else described as relationships between groups of ingredients. For entirely different styles of pouring, such as rules based, ai generated, and so on, developers can create custom nozzle pipelines that can work along side the standard KOS pipelines. As our brandset is a simple recipe based system, we can leverage the standard beverage pipeline which requires us to define a Pourable and create a BeveragePourEngine.

What is a Pourable?

To understand what a Pourable is, we start with the question of how do we want to specify a beverage via an endpoint? Our device will eventually have a consumer user interface that allows the user to select a particular beverage to pour and the UI will need to send that selection to the java back end. What the data in that payload. The answer is that it depends on how we want to pour. Consider a few potential ways to pour based on dispensers in the market:

  • A beverage is defined by a recipe in the brandset so we simply need to send the id of the selected beverage.
  • A beverage is defined as a brand id with the addition of optional flavor id’s for selected flavor shots so we need to send multiple id’s in a json structure.
  • A beverage is defined as a mix of other beverages. In this case we need to send a list of beverages + ratios, where each beverage may be one of the previous examples.

The key is that there is no single standard way to specify a beverage to pour. The KOS beverage pipeline abstracts the definition of what to pour into a class called Pourable. It’s up to the developer to override this class and create an implementation that captures the necessary data to pour a beverage. This Pourable object is then passed through KOS to the developer supplied BeveragePourEngine which decodes the information and performs the pour.

The Pourable implementation for our Zero4 project is shown below:

public class BevPourable extends Pourable {
    @Getter
    private BevDef bevDef;             // the definition of what to pour
    /**
     * Create a new pourable from the specified definition string. We expect
     * the string to be a json encoding of a {@code BevDef} object. This could
     * just as easily be a beverage id string, but by wrapping it into an object,
     * we can extend the object later to include additional pour options, such
     * as beverage plus flavors.
     */
    public BevPourable(String definitionStr) throws IOException {
        // Parse the definition using the built-in jackson mapper
        bevDef = KosUtil.getMapper().readValue(definitionStr, BevDef.class);
    }
    @Override
    public Object getDefinition() {
        return bevDef;
    }
    /**
     * A class that represents how the beverage is specified via the
     * kOS select beverage endpoint.
     */
    @Getter @Setter
    public static class BevDef {
        private String bevId;      // beverage id
    }
}

A Pourable is always created from a definition string, which is typically a json object that describes the details of what to pour, but can be as simple as a beverage id. The Pourable simply converts this json string to an internal data class called BevDev which contains a single property of bevId since our brandset is based on beverage id’s.

Beverage availability graph

The standard KOS beverage pipeline is designed around the concept of a beverage availability graph. This single structure provides the following functionality:

  • The ability to determine which beverages and brands are available and visible, including the ability to share this information with a web UI and keep it in sync in real time with push notifications.
  • The ability to project the graph downwards to determine how to pour a given beverage.
  • The ability to apply dependency overrides to block visibility or availability of any portion of the graph.
  • KOS Studio tooling to visualize and debug the graph.

When implementing a BeveragePourEngine, one of the primary roles of the engine is to encode the ingredient to beverage relationships into a dependency graph which is used to compute availability. While this sounds quite complex, it is typically quite simple so we will start with the code and work backwards:

//Zero4PourEngine.java
public class Zero4PourEngine extends BeveragePourEngine<BeveragePourEngineConfig> {
    @Override
    public void rebuildGraph(BevGraphBuilder builder) {
        // Add ingredient nodes for all the pumps. This is a convenience
        // method to build the first couple layers of the beverage graph
        builder.addIngredientNodes();
        // Get the brandset from the app
        Brandset brandset = app.getBrandset();
        // Incorporate all the beverages from the brandset into the graph
        for (Beverage bev : brandset.getBeverages()) {
            // Add the beverage node to the graph
            builder.addBeverage(new BeverageNode(bev.getId()).setNote(bev.getName()));
            // Link the beverage node to ingredient nodes using the recipe data
            for (RecipePart part : bev.getRecipe()) {
                builder.addDependency(bev.getId(), part.getIngredientId());
            }
        }
    }

The goal of rebuildGraph() is to define a collection of graph nodes and link them together with dependencies. Each node in the graph has a particular behavior in propagating availability and visibility information up the graph, typically via a logical AND or OR relationship. For example, consider a lemon ingredient node that depends on any pumps that have lemon inserted. The ingredient node would be an OR node so that it would be available if any pump with lemon is available. For example, if only one pump with lemon exists and it is sold out, no longer available, then the lemon ingredient would not be available. If lemon was added to another pump, then there would be at least one way to pour lemon, thus the lemon ingredient would be available.

Now consider a beverage that depends on lemon and water to form lemonade. A beverage node would be a logical AND node, requiring all dependencies to be available to be available itself. By creating nodes for every pump, every ingredient and every beverage and linking them together with the correct dependencies, we are able to determine every available beverage.

Availability actually has two related aspects: visibility and availability. If an ingredient is inserted into a pump, it is visible. If it is available to pour (not sold out), it’s also available. If the ingredient becomes sold out, it’s still visible since it’s still assigned to the pump, but it’s no longer available since it can’t be poured. In this way we can easily find every beverage that is visible and show it in the UI to the user, while also showing which are actually available to pour. Any changes to the inputs, such as an ingredient being changed or becoming sold out, is simply a change in the state of the graph which ripples up from the bottom to the top, resulting in changes to the beverage availability information.

In order to better visualize this graph, here is a screenshot of the availability graph from the KOS Studio tool:

<<TODO: screenshot goes here>>

If we look at the code pour engine we can see how the graph is being constructed:

builder.addIngredientNodes();

This single line adds a node for every pump with an ingredient assigned, as well as a node for every assigned ingredient. It then adds a dependency link between the pump and the associated ingredient. This is such a common base for the dependency graph, it’s a standard KOS convenience method.

for (Beverage bev : brandset.getBeverages()) {
    builder.addBeverage(new BeverageNode(bev.getId()).setNote(bev.getName()));

For every beverage in the brandset, add a beverage node with the id of the beverage. By creating a BeverageNode and adding it using addBeverage(), KOS knows this is a beverage to show to the user. Since the beverage nodes are shared with the UI via endpoints, the id of a beverage node (which is the id of the beverage) can be used to select the beverage. For convenience, we also attach the name of the beverage to the node as a note so the UI doesn’t need to fetch the brandset data to lookup the beverage names.

    for (RecipePart part : bev.getRecipe()) {
        builder.addDependency(bev.getId(), part.getIngredientId());
    }

For every recipe element of the beverage, create a dependency between the ingredient it references and the beverage. Since beverage nodes are AND nodes, the beverage will only be available if all the ingredients in the recipe are also available.

With this we have a fully constructed beverage availability graph, suitable for display in a consumer UI.

Performing a pour

Now that we have a beverage availability graph which the UI has access to, and the user has selected a beverage id and sent it back to java to create a BevPourable object, how do we actually perform the pour? It all starts here:

//Zero4PourEngine.java
    @Override
    protected FutureWork buildFuture(Pourable pourable, BeveragePumpEventInitiator initiator) {
        // Pour builder to gather pumps and rates. Because Zero4 supports variable rate
        // ingredients, there is a bit more complexity in constructing a pour than simply
        // turning on the correct valves. This is handled by the builder.
        PourBuilder builder = new PourBuilder(pourable, this, app.getBrandset());
        // Return the future for the pour
        return builder.pour(initiator);
    }

This is the standard method in BeveragePourBuilder to convert a Pourable to a FutureWork which will perform the actual pour. Since the only mechanism we have to interact with the Zero4 pumps is via the tpour() or vpour() methods, in order to do a beverage pour we need to do the following:

  • Find all the pumps required to pour the beverage.
  • Lookup the recipe, extract the rates for each pump and sum them to a total flow rate.
  • Convert the pour volume to time using the total flow rate.
  • Turn on each pump for the computed pour duration

If we look at the PourBuilder class we can see each of these steps:

//PourBuilder.java
    public PourBuilder(Pourable  pourable, BeveragePourEngine<?> engine, Brandset brandset) {
        // Grab the beverage definition from the pourable
        BevDef def = ((BevPourable)pourable).getBevDef();
        // Create recipe extractor to extract the pumps to use for the specified beverage.
        // This performs a downward search in the beverage graph to find available pumps
        // to pour the specified beverage.
        extractor = new RecipeExtractor(engine).addIngredients(def.getBevId());
        // If the extractor found a valid way to pour, setup the pour
        if (extractor.isValid()) {
            pumpsByIng = new HashMap<>();
            // Add all the pumps to a map where we can track their required rates
            for (Pump<?> pump : extractor.getPumps()) {
                pumpsByIng.put(pump.getIngredientId(), new PumpInfo(pump));
            }
            // Add recipe rates to the pumps in the map
            addRates(brandset.getBeverageRecipeParts(def.getBevId()));
            // Compute the total duration of the pour based on the requested volume and the total rate
            durationMs = (totalRate > 0) ? (int)(pourable.getEffectiveVolume() * 1000 / totalRate) : 0;
        }
    }

Again, while not much code, there is a lot going on:

BevDef def = ((BevPourable)pourable).getBevDef();

Given the Pourable, extract the BevDef object which has the bevId property in it. Remember, this the BevPourable object that was created when the consumer UI selected a beverage id and sent it via a json payload to record the selected beverage. KOS doesn’t know how the beverage is represented, just that it converted a string to a Pourable and then passed the Pourable to the matching BeveragePourEngine.

extractor = new RecipeExtractor(engine).addIngredients(def.getBevId());

RecipeExtractor is a KOS class that can perform a top down search of the beverage availability graph and find the matching beverage id. It will then continue searching downward and find an available pump for each ingredient required by the beverage and return all this information as a RecipeExtractor object. Since the beverage availability graph contains all the recipe data, there is no need to process the brandset recipes directly as KOS can extract the data directly from the graph.

// Add all the pumps to a map where we can track their required rates
for (Pump<?> pump : extractor.getPumps()) {
    pumpsByIng.put(pump.getIngredientId(), new PumpInfo(pump));
}
// Add recipe rates to the pumps in the map
addRates(brandset.getBeverageRecipeParts(def.getBevId()));

For each pump discovered by the extractor, create a PumpInfo to hold the pump rate information from the recipe.

durationMs = (totalRate > 0) ? (int)(pourable.getEffectiveVolume() * 1000 / totalRate) : 0;

Compute the duration of the pour using the total volume of the pour and the total rate of all the pumps.

Finally, construct a future that turns on all the pumps for the specified duration:

//PourBuilder.java
    public FutureWork pour(BeveragePumpEventInitiator initiator) {
        // Build the pour from a bunch of tpours
        List<Pump<?>> pumps = new ArrayList<>();
        ParallelFuture future = new ParallelFuture("bev-pour");
        for (PumpInfo info : pumpsByIng.values()) {
            pumps.add(info.pump);
            future.add(info.pump.tpour(durationMs, info.rate));
        }
        return future;
    }

Handling fixed volumes

Now that we have a working pour engine, we need one final piece to complete the beverage pouring functionality. KOS doesn’t have any understanding of application-specific fixed pour volumes. For example, if your application uses cups, KOS doesn’t know cup sizes. The final piece of the puzzle is to add a BeveragePipelineDelegate which provides a way for KOS to deal with fixed volume pours.

The BeveragePipelineDelegate interface is quite simple:

public interface BeveragePipelineDelegate {
    /**
     * Return the max pour volume in milliliters for the platform. This is used as the volume
     * when using {@code PourBuilder.addMaxPourVolume()} . It does not limit the volume poured
     * using other builder methods which take volumes as arguments.
     */
    double getMaxPourVolume();
    /**
     * Return a volume in milliliters for a given name. This is typically used to resolve cup
     * names to fixed volumes but can be used for resolving any named volume based on the
     * needs of the application.
     *
     * @param name the name of the volume to return
     * @return the requested volume in milliliters or zero for no pour
     */
    default double getFixedVolumeByName(String name) {
        return 0;
    }
}

It introduces two methods of resolving fixed volumes:

  • Maximum pour volume : This is the platform-specific maximum pour volume for a single pour. When performing a hold-to-pour operation, this is the max volume the system will pour before it stops.
  • Named volumes : This provides a way for applications to pour volumes based on names, such as cup sizes or id’s, and allow KOS to resolve these values to volumes. Once in place, standard KOS endpoints can be used to construct pours using named volumes.

As our application doesn’t use cup sizes, we simply need to provide a delegate that returns a maximum pour volume:

//Zero4PourDelegate.java
public class Zero4PourDelegate implements BeveragePipelineDelegate {
    @Override
    public double getMaxPourVolume() {
        return 350;
    }
}

We can then install the delegate in the assembly while creating the beverage pipeline:

//Zero4Assembly.java
        Zero4PourEngine engine = new Zero4PourEngine();
        beveragePipeline = new BeverageNozzlePipeline(engine);
        beveragePipeline.setDelegate(new Zero4PourDelegate());

Chapter 6: Finishing up

Our Zero4 application is almost complete! As with most applications there are some details that are still missing and we want to make sure we cover them before wrapping things up.

Region Settings

Different regions have their own laws, regulations, and conventions, necessitating a method to specify regional infrastructure. Most devices come equipped with region-specific settings. Common examples include time and date formats, as well as default unit systems; however, more complex devices may have additional data linked to specific regions. KOS offers a standard regional infrastructure that can be easily extended to include any custom regional information. To include region support, we simply add a few lines of code to our system application:

//Zero4App.java
    @Override
    public void start() throws Exception {
        XmlRegionFactory factory = new XmlRegionFactory();
        factory.addLoader(new ClassLoaderResourceLoader(getClass().getClassLoader()));
        factory.load("regions.xml");
        installRegions(factory.getRegions());

This creates a factory that loads region information from regions.xml in the src/main/resources folder of the system application project, and installs the regions loaded by the factory. We’re simply using the base XmlRegionFactory implementation which provides support for time and date formats, timezones and unit systems. Additional information can easily be added by extending this base class to parse additional information from the regions.xml file. For this tutorial we will use a regions.xml file with one region:

//regions.xml
<?xml version="1.0" encoding="UTF-8"?>
<regionDefs>
    <regions>
        <region id="factory"/>
        <region id="us">
            <country>US</country>
            <unitSystemId>us</unitSystemId>
            <timeFormatId>t_hhmma</timeFormatId>
            <dateFormatId>d_MMddyy</dateFormatId>
        </region>
    </regions>
</regionDefs>

Load consumer UI and display it

Although this tutorial is focused on back end functionality, the next tutorial builds a simple consumer user interface for the Zero4 device. We want to add the code required to load this user interface and display it before we wrap things up.

As KOS is a java based platform, developers are free to use standard java display technologies, such as swing or javaFX, to build custom user interfaces for devices. While these are powerful and easy to use, a web browser tends to provide a richer user experience at the expense of building a web application for the user interface. This is such a common approach that KOS provides extensive sdk’s for web development that provide real time data synchronization and events along with standard events for configuration, futures and so on. If you are a web developer, you should review the consumer user interface tutorial to get an introduction into the KOS functionality available for building robust web based experiences with KOS.

As our next tutorial is a web based user interface packaged as a KAB file, we need to add support for finding the KAB, mounting it into the vfs so that the browser can access it, and then tell the web browser to load the application. Let’s start by adding a few properties to our application class:

//Zero4App.java
public class Zero4App extends SystemApplication<BaseAppConfig> {
    // The KAB type for the user interface kab
    private static final String UI_KAB_TYPE = "zero4.ui";
    @Autowired
    private BrowserService browserService;         // used to navigate the browser to our ui
    private VFSSource uiVfsSource;                 // where the ui KAB is mounted in vfs

Notice the use of @Autowired to get access the KOS BrowserService. This is leveraging the KOS dependency injection system to make it easy to access core services.

Next, in our start() method we need to add code to find the KAB that contains our user interface. As with the brandset, we find the KAB by the type which we’ve defined to be zero4.ui.

//Zero4App.java
        KabFile kab = getSection().getKabByType(UI_KAB_TYPE);
        if (kab != null) {
            uiVfsSource = getVfs().mount("/ui", kab);
        }

Once mounted, the contents of the user interface are available in the web server so all that remains is to navigate the browser to the correct url. We can add this code to started() lifecycle method which is called once our application is fully configured and ready for use:

//Zero4App.java
    @Override
    public void started() throws Exception {
        // nav to the ui
        if (uiVfsSource != null) {
            browserService.goToUrl(uiVfsSource.getFullPath("index.html"));
        }
    }