Tutorials

Hardware

Lesson 1: Understanding Hardware Components

The kOS Dispense SDK provides a logical framework to represent and manage the physical workings of a dispenser machine within a software environment. This lesson will break down how the kOS Dispense Extension models the container, slice, holder, pump, and nozzle, and how these components interact to create a seamless dispensing process.

Container

The Container class abstracts the concept of a storage unit that holds one or more ingredients, such as a cartridge or a bag-in-box (BiB) ingredient. While most physical containers are simply cardboard boxes with plastic liners, some containers have additional attributes like printed codes or RFID tags that provide detailed information about the contents.

Slice

A Slice refers to a logical division within the Container, used to manage different types of ingredients within the same system. Each slice is represented by a ContainerSlice object, allowing for the storage and dispensing of multiple sources of ingredient from a single container.

Holder

The Holder abstracts how a ContainerSlice attaches to a Pump. In many cases, this is simply a hose that connects a BiB container to a pump. However, in more complex devices, the Holder represents a cartridge slot that may have associated hardware, such as RFID antennas, indicator lights, and so on. The Holder also maintains the state of availability of a Pump connected ingredient.

Pump

The Pump is a basic abstraction of a pump or valve. One or more pumps are generally created in the constructor of a PumpBoard. A Pump is then assigned to a Holder, which provides the ingredient information for the Pump. Finally, a Pump is also assigned to a Nozzle so that it can logically pour.

Nozzle

The Nozzle is a logical representation of a physical nozzle in a dispenser. A Nozzle has an associated list of pumps (and therefore, ingredients) which are connected to it. A Nozzle can have one or more pipelines, which are used for pouring from the Nozzle.

Lesson 2: Create Assembly with Nozzle

In this lesson, we will create an Assembly for our dispenser system that includes a Nozzle. This Assembly will logically represent the hardware components and their interactions within the system.

What is an Assembly Class?

An Assembly is a logical representation of all the hardware components in a device. It models the physical parts, such as boards, valves, nozzles, containers, holders, and pumps, and provides the functionality to make them work together as a single unit. Being a device-centric operating system, kOS models hardware connections, offering numerous benefits. Within kOS, an Assembly is a collection of hardware that can be added or removed as an atomic unit, allowing for multiple Assemblies if the device supports expansion kits. For example, we create a single core assembly reflecting the device’s core hardware.

Step One: Add Nozzle

We’re going to build our Assembly in stages starting with a Nozzle.

Here is the first iteration of our DispenseAssembly class:

Add Nozzle to Assembly.java
package com.kosdev.samples.dispenser.part1;
import com.tccc.kos.commons.util.json.JsonDescriptor;
import com.tccc.kos.core.service.assembly.CoreAssembly;
import com.tccc.kos.ext.dispense.DispenseAssembly;
import com.tccc.kos.ext.dispense.service.nozzle.Nozzle;
public class DispenserAssembly extends DispenseAssembly implements CoreAssembly {
    public DispenserAssembly(JsonDescriptor descriptor) throws Exception {
        super("core", descriptor);
    }
    @Override
    public void load() throws Exception {
        Nozzle nozzle = new Nozzle(this, "nozzle");
        addNozzle(nozzle);
    }
    @Override
    public void start() {

    }
    @Override
    public void started() {
    }
}

The load() method is where we build our logical device. Here we’ve created a Nozzle named "nozzle" and used the parent’s addNozzle() method to add it to the Assembly.

Step Two: Install the Assembly in the DispenserApp

Now that we have an assembly, we can install it in the start method of the DispenserApp.

Install assembly in DispenserApp.java
...
public class DispenserApp extends SystemApplication<BaseAppConfig> {
   ...
    @Override
    public void start() throws Exception {
    ...
        // Create and install the assembly for the device
        installAssembly(new DispenserAssembly(getDescriptor()));
    }
    ...
}

Lesson 3: Create Valve Class

Introduction

In this lesson, we’ll walk through the process of creating the Valve class for a kOS dispenser system. This class models a valve that manages the flow of ingredients, acting as a logical representation of a physical valve, which is a kind of pump in kOS. This abstraction allows the system to handle valve operations programmatically.

Steps

  1. Create the class signature, extending the Pump class and using ValveConfig to set the nominal flow rate.

  2. Define fields to hold the valve position managed by the control board.

  3. Create the constructor to initialize the valve with a control board, name, and position.

  4. Implement the tpour method to manage the time-based pour operation with asynchronous handling.

  5. Implement the vpour method to manage volume-based pours by calculating the necessary duration.

Valve.java
package com.kosdev.samples.dispenser.part1.hardware;
import com.tccc.kos.commons.util.KosUtil;
import com.tccc.kos.commons.util.concurrent.future.FutureEvent;
import com.tccc.kos.commons.util.concurrent.future.FutureWork;
import com.tccc.kos.ext.dispense.Pump;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

@Getter
@Slf4j
public class Valve extends Pump<ValveConfig> {
    private final int pos;
    public Valve(ControlBoard controlBoard, String name, int pos) {
        super(controlBoard, name, null);
        this.pos = pos;
    }
    /**
     * Starts a time-based pour operation for this valve.
     */
    @Override
    public FutureWork tpour(int duration, double rate) {
        // Create a future to turn the pump on for the requested duration
        FutureWork future = new FutureWork("tpour" + "-" + getName(), f -> {
            log.info("start: {}", getName());
            KosUtil.scheduleCallback(() -> f.success(), duration);
            // TODO: turn on the valve
        });
        // Add a completion handler to turn off the valve, which is
        // called when the timer fires or if the pour is cancelled
        future.append("stop", FutureEvent.COMPLETE, f -> {
            log.info("stop: {}", getName());
            // TODO: turn off the valve
        });
        return future;
    }
    /**
     * Starts a volume-based pour operation for this valve.
     */
    @Override
    public FutureWork vpour(int volume, double rate) {
        // convert the volume pour to a timed pour
        return tpour((int) (volume / rate), rate);
    }
    @Override
    public String getType() {
        return "valve";
    }
}

Create the ValveConfig class.

ValveConfig.java
package com.kosdev.samples.dispenser.part1.hardware;

import com.tccc.kos.ext.dispense.PumpConfig;

public class ValveConfig extends PumpConfig {
    public ValveConfig() {
        setNominalRate(75);
    }
}
A Word About Futures

FutureWork When working with devices and hardware, there tend to be many process-related tasks. These tend to be asynchronous and can take some time to complete, particularly for physical processes, such as pouring a beverage. This pattern is so common that kOS provides a unique type of future primitive to manage this type of work. FutureWork is one of several building blocks kOS provides to perform long running work in a way that supports sending progress information to UI code and the ability to cleanly cancel or abort tasks. We’ll be seeing much more of FutureWork and related classes in future tutorials.

Lesson 4: Create ControlBoard

In this lesson, we’ll walk through the process of creating the ControlBoard class for our flavored water dispenser. This class models a board that manages multiple valves, including a water valve, a carb valve, and four syrup valves. The ControlBoard class extends the PumpBoard class and integrates with the kOS framework to handle hardware interactions.

PumpBoard

The PumpBoard class is like the brain of a dispenser system that controls the pumps. It acts as a virtual version of a circuit board, making it easier to manage the pumps through software. This class can block pumps if there’s a problem and limit how many pumps can run at the same time to avoid overloading the system. It also handles connecting to the actual hardware, updating firmware, and managing when the board goes online or offline. This ensures everything runs smoothly and efficiently, even if the hardware isn’t always available.

At this step in our tutorial, though, we have simplified this class by delegating the pouring to the Valve class.

ControlBoard.java
package com.kosdev.samples.dispenser.part1.hardware;

import com.tccc.kos.commons.util.KosUtil;
import com.tccc.kos.commons.util.concurrent.future.FutureEvent;
import com.tccc.kos.commons.util.concurrent.future.FutureWork;
import com.tccc.kos.core.service.assembly.Assembly;
import com.tccc.kos.ext.dispense.Pump;
import com.tccc.kos.ext.dispense.PumpBoard;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Getter
@Setter
public class ControlBoard extends PumpBoard {
    private final Valve waterValve;
    private final List<Pump<?>> syrupValves;

    public ControlBoard(Assembly assembly, String name) {
        super(assembly, name);
        // Board contains one water valve and four syrup valves
        waterValve = new Valve(this, "water", 0);
        syrupValves = new ArrayList<>();
        for (int i = 1; i <= 4; i++) {
            syrupValves.add(new Valve(this, "syrup-" + i, i));
        }
    }

    public FutureWork tpour(Valve pump, int duration, double rate) {
        return pump.tpour(duration, rate);
    }

    @Override
    public String getType() {
        return "controlBoard";
    }

    @Override
    public String getInstanceId() {
        return null;
    }
}

Lesson 5: Put it all together

So far in our DispenserAssembly class, we’ve created and added a single Nozzle. Now that we have our ControlBoard and our Valve classes, we can take some more steps in the assembly of our dispenser.

Overview of the Code

The provided code defines a DispenserAssembly class that extends DispenseAssembly and implements CoreAssembly. This class models the physical parts of a dispenser device and provides functionality to make all the parts work together.

  1. Create Control Board: We instantiate ControlBoard and add it to the assembly.

  2. Connect Valves to Nozzle: The valves are added to the nozzle instance, allowing it to control the flow of different ingredients.

  3. Build Holders: The HolderBuilder class is used to build holders for the valves, starting with the water valve, the carb valve, and then iterating over the syrup valves.

  4. Special Insertion: Finally, in the started() method, we perform a special kind of insertion of water and carb.

Intrinsic Insertions

Certain ingredients in a dispenser are "intrinsic", such as plain and carbonated water. These are typically plumbed to dedicated pumps and connected to an essentially infinite ingredient source. Unlike syrups, which may need to pass through certain filters before the InsertService allows their insertion (such as whether the ingredients are known, whether the container is expired, or if calibration is required), intrinsic ingredients need no filters. Once an intrinsic ingredient is inserted, it is locked and cannot be removed, unlike syrups which may need to be replaced.

In a future lesson, we will see this in action!

In short, we have set up the dispenser assembly by creating and connecting all necessary components.

Assemble hardware in DispenserAssembly.java
package com.kosdev.samples.dispenser.part1;

import com.kosdev.samples.dispenser.part1.brandset.Ingredient;
import com.kosdev.samples.dispenser.part1.hardware.ControlBoard;
import com.kosdev.samples.dispenser.part1.pour.VolumeDelegate;
import com.kosdev.samples.dispenser.part1.pour.DispenserPourEngine;
import com.tccc.kos.commons.core.context.annotations.Autowired;
import com.tccc.kos.commons.util.json.JsonDescriptor;
import com.tccc.kos.core.service.assembly.CoreAssembly;
import com.tccc.kos.ext.dispense.DispenseAssembly;
import com.tccc.kos.ext.dispense.HolderBuilder;
import com.tccc.kos.ext.dispense.pipeline.beverage.BeverageNozzlePipeline;
import com.tccc.kos.ext.dispense.service.insertion.InsertionService;
import com.tccc.kos.ext.dispense.service.nozzle.Nozzle;

public class DispenserAssembly extends DispenseAssembly implements CoreAssembly {
    private ControlBoard board;

    public DispenserAssembly(JsonDescriptor descriptor) throws Exception {
        super("core", descriptor);
    }

    @Override
    public void load() throws Exception {
        // Create a nozzle to pour from
        Nozzle nozzle = new Nozzle(this, "nozzle");
        addNozzle(nozzle);

        // Create an instance of a board that controls several valves
        board = new ControlBoard(this, "control");
        addBoard(board);

        // Add the valves from the board to the nozzle
        nozzle.add(board.getSyrupValves());

        // Build holders for the valves starting with the water valve
        HolderBuilder builder = new HolderBuilder(this, nozzle);
        builder.buildWater(board.getWaterValve());

        // Iterate over the remaining valves and name the holders with 'S' prefix
        builder.setPumpIterator(board.getSyrupValves());
        builder.setIncrementNameIterator("S", 1);
        builder.build(board.getSyrupValves().size());
    }

    @Override
    public void start() {
        // Nothing to do after the assembly is configured and autowired
    }

    @Override
    public void started() {
        // Assign water to the water valve so it's always connected
        insertionService.insertIntrinsic(Ingredient.WATER, board.getWaterValve().getHolder());
        // Assign carb to the carb valve so it's always connected
        insertionService.insertIntrinsic(Ingredient.CARB, board.getCarbValve().getHolder());
    }
}

Go ahead and build your app again!

Summary

In this chapter, we delved into the hardware components and assembly of a kOS dispenser system. We explored the key parts like the container, slice, holder, pump, and nozzle, and how they interact within the system. We built a DispenserAssembly class, created a Valve class to manage ingredient flow, and developed a ControlBoard class to handle multiple valves efficiently.

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.