// Create a pipeline for beverage pouring and add to the nozzle
DispenserPourEngine engine = new DispenserPourEngine();
nozzle.add(new BeverageNozzlePipeline(engine));
Now that we’ve assembled our hardware, let’s dive into how it actually pours drinks. In a kOS dispenser system, nozzle pipelines are essential for managing different types of pouring operations. These pipelines enable the system to efficiently handle tasks such as pouring beverages for consumption and performing ingredient calibration pours for maintenance and accuracy.
A nozzle pipeline is a representation of a specific way to pour from a nozzle. There are pipelines for pouring beverages and for pouring ingredients. By defining different pipelines, the system can tailor the pouring process to meet the requirements of different tasks. Only one pipeline can pour from a nozzle at a time, ensuring that pipelines do not interfere with each other. For the actual pouring implementation, a pipeline must be provided with a BeveragePourEngine
, which is only required by the BeverageNozzlePipeline. This approach allows for the creation of specialized pipelines for different pouring methods, avoiding the complexity of a monolithic code base that handles all pouring.
It is common to pour different ways from a single nozzle. Calibration requires pouring a single ingredient at a fixed rate or volume while a beverage pour mixes multiple ingredients. A nozzle pipeline allows for specialization for a particular type of pouring that is completely isolated. kOS provides highly optimized pipelines for both ingredient pouring and beverage pouring, while still allowing developers to create entirely new pipelines from scratch if needed.
Beverage Pour:
Purpose: To dispense a ready-to-drink beverage to the user.: To dispense a ready-to-drink beverage to the user.
Process: The system mixes the required ingredients in the correct proportions and pours the final beverage through the nozzle.
Example: Pouring a "Cherry Smash" drink, which involves mixing water and cherry flavor.
Ingredient Calibration Pour:
Purpose: To calibrate the dispenser’s ingredient flow rates and ensure accurate measurements.
Process: The system pours a specific ingredient through the nozzle to measure and adjust the flow rate.
Example: Pouring a set amount of lemon flavor to calibrate its flow rate in the system.
When using nozzle pipelines, the system ensures that each pipeline can operate independently and efficiently. Here’s how you might use different pipelines:
BeverageNozzlePipeline: This pipeline supports beverage pouring via a Pourable abstraction. It provides a framework for typical pouring functionality, ensuring that beverages are mixed and dispensed correctly.
IngredientNozzlePipeline: This pipeline supports pump operations via pump intents, used for ingredient pouring and most admin UI-related pour functionality. It can handle tasks like ingredient calibration, ensuring accurate flow rates and measurements.
To set up a BeverageNozzlePipeline in our Assembly, the following code must be added to the load method:
// Create a pipeline for beverage pouring and add to the nozzle
DispenserPourEngine engine = new DispenserPourEngine();
nozzle.add(new BeverageNozzlePipeline(engine));
In a drink dispenser system, a beverage graph helps manage and optimize the pouring process. It represents the relationships between different ingredients and beverages that can be made by them. Additionally, the beverage graph plays a vital role in determining the availability and visibility of drinks.
A beverage graph is a dependency graph used to compute the availability of beverages, brands, and groups based on pump status and other inputs. It consists of nodes that link pumps/ingredients at the bottom to beverages, brands, and groups at the top.
Nodes: Represent different elements like ingredients, pumps, beverages, brands, and groups.
Edges/Links: Show the dependencies and relationships between these nodes.
Ingredients: The basic components that are combined to create beverages.
Pumps: Devices that control the flow of ingredients.
Beverages: Pourable drinks, represented by their IDs.
Brands: Collections of related beverages, e.g., Coke, Diet Coke, etc.
Groups: Categories or functional collections of beverages, such as "low calorie" or "fruit flavored."
Ingredient Availability: Each ingredient in the graph is linked to one or more pumps. The availability of an ingredient depends on whether the pumps assigned to it are operational and have the ingredient in stock. For example, if the pump assigned to water is functioning and has water, the ingredient node for water is marked as available.
Beverage Availability: A beverage is available if all its required ingredients are available. The system checks the ingredient nodes connected to a beverage node. For instance, the beverage “Cherry Cooler” requires water and cherry flavor. If both ingredients are available, “Cherry Cooler” is marked as available. The beverage node uses a logical AND relationship, meaning all connected ingredient nodes must be available for the beverage to be available. If an ingredient or beverage is not available, it might be hidden from the user interface to avoid confusion.
Brands and Groups: Brands and groups help organize beverages and manage their visibility. A brand is visible if any of its beverages are available. Similarly, a group is visible if any beverages within the group are available. This allows the system to provide a clear and organized view of available options to the user.
Ingredients:
Water
Carb
Lemon
Cherry
Lime
Grape
Beverages: Lemon Zip (Carb + Lemon) Cherry Cooler (Water + Cherry) Lime Zap (Carb + Lime) Grape Cooler (Water + Grape)
Visual Representation Ingredients:
Water (available)
Carb (available)
Lemon (available)
Cherry (unavailable)
Lime (available)
Grape (available)
Graph:
Lemon Zip Cherry Cooler Lime Zap Grape Cooler (Available) (Unavailable) (Available) (Available) | | | | v v v v +-----------+ +-----------+ +-----------+ +-----------+ | Carb | | Water | | Carb | | Water | | Available | | Available | | Available | | Available | +-----------+ +-----------+ +-----------+ +-----------+ | | | | v v v v +-----------+ +-----------+ +-----------+ +-----------+ | Lemon | | Cherry | | Lime | | Grape | | Available | |Unavailable| | Available | | Available | +-----------+ +-----------+ +-----------+ +-----------+
In this scenario: Available and Visible Beverages: “Lemon Zip,” “Lime Zap,” and “Grape Cooler” are available and visible because their required ingredients (carb, water, lemon, lime, and grape) are available. Unavailable and Not Visible Beverage: “Cherry Cooler” is unavailable because the cherry flavor is unavailable.
Now that we know more about pouring and beverage graphs, we can build our own BeveragePourEngine.
This engine is responsible for the pouring process of beverages in a dispenser system. It ensures that the correct ingredients are used, checks the availability of beverages, and handles the pouring operation efficiently. When we create our nozzle pipeline, we must construct it with a BeveragePourEngine so that it knows how to pour.
Let’s start by understanding the key components and then build the BeveragePourEngine.
We need to autowire the DispenserApp to access the brandset and other necessary components.
public class DispenserPourEngine extends BeveragePourEngine<BeveragePourEngineConfig> {
@Autowired
private DispenserApp app;
}
The rebuildGraph method constructs the beverage graph using the BevGraphBuilder. It adds ingredient nodes for all pumps and processes beverages from the brandset. kOS calls this method when a change occurs that might impact the contents of the graph. For example, when a new ingredient is assigned, or when a new set of ingredients are installed.
@Override
public void rebuildGraph(BevGraphBuilder builder) {
builder.addIngredientNodes();
Brandset brandset = app.getBrandset();
for (Beverage bev : brandset.getBeverages()) {
builder.addBeverage(new BeverageNode(bev.getId()).setNote(bev.getName()));
builder.addDependency(bev.getId(), Ingredient.WATER);
for (String flavorId : bev.getIngredientIds()) {
builder.addOptionalDependency(bev.getId(), flavorId);
}
}
}
The getPourable method returns a Pourable object for the specified beverage ID.
@Override
public Pourable getPourable(String bevId) throws Exception {
return new BevPourable(bevId);
}
The isPourable method checks if the specified beverage is available for pouring.
@Override
public boolean isPourable(Pourable pourable) throws Exception {
return isAvailable(((BevPourable) pourable).getBevId());
}
The buildFuture method orchestrates the pouring process for a beverage in a kOS dispenser system. It extracts the beverage ID from the Pourable object, uses a RecipeExtractor to identify and validate the required pumps, and calculates the pour duration based on the effective volume and total flow rate. If the setup isn’t valid, it returns a FailedFuture. Otherwise, it sets up a ParallelFuture task to execute the pour using the identified pumps, organizing everything needed and scheduling it for execution by the system.
@Override
protected FutureWork buildFuture(Pourable pourable, Object lock) {
String bevId = ((BevPourable) pourable).getBevId();
// Create recipe extractor to extract the pumps to use
RecipeExtractor extractor = new RecipeExtractor(this).addIngredients(bevId);
// If the extractor didn't find a way to pour, return a failed future
if (!extractor.isValid()) {
return new FailedFuture("pour", "errNotPourable");
}
// Sum the flow rates of all the pumps in the recipe to get the overall flow rate
double totalRate = extractor.getPumps().stream()
.mapToDouble(Pump::getNominalRate)
.sum();
// Compute duration of the pour based on effective volume and total flow rate
int durationMs = (totalRate > 0) ? (int) (pourable.getEffectiveVolume() * 1000 / totalRate) : 0;
// Tpour all the valves in the recipe for the specified duration
ParallelFuture future = new ParallelFuture("pour");
for (Pump<?> pump : extractor.getPumps()) {
future.add(pump.tpour(durationMs, 0));
}
return future;
}
In our custom BeveragePourEngine, we implemented a method called getPourable
which returns a com.tccc.kos.ext.dispense.pipeline.beverage.Pourable
. A Pourable is an object that can be poured using a PourEngine. In this case, we need our engine to pour a beverage.
kOS abstracts how beverages are poured into a Pourable, which is an object that can be poured using a PourEngine. This allows a beverage to be defined as a simple beverageId
or as complex as a dynamic recipe. In this example, we’ll simply use a beverageId
lookup from the brandset.
package com.kosdev.samples.dispenser.part1.pour;
import com.tccc.kos.ext.dispense.pipeline.beverage.Pourable;
import lombok.Getter;
import java.io.IOException;
@Getter
public class BevPourable extends Pourable {
private final String bevId;
public BevPourable(String bevId) throws IOException {
this.bevId = bevId;
}
@Override
public Object getDefinition() {
return bevId;
}
}
This lesson introduces the fundamental concepts of beverage pours, focusing on hold-to-pour, fixed size pours, and split pours.
Hold-to-Pour:
Description: Hold-to-pour is a manual pour method where the user holds down a button to dispense the beverage. The pour continues as long as the button is pressed and stops when the button is released.
Use Case: This type of pour is useful for users who want to control the exact amount of beverage dispensed. It is commonly used in self-service stations where users can fill their cups to their desired level.
Implementation: The dispenser activates the pump as long as the button is held down, allowing the user to control the pour duration manually.
Fixed Size Pours (e.g., Cups):
Description: Fixed size pours dispense a predetermined amount of beverage, typically corresponding to standard cup sizes (e.g., small, medium, large). This ensures that a consistent volume of beverage is dispensed each time.
Use Case: This type of pour is ideal for fast food restaurants and cafes where standard serving sizes are required. It ensures consistency and speed in serving beverages.
Implementation: The dispenser is programmed to pour a specific volume of beverage based on the selected cup size. This can be achieved using volume-based pours (vpour()) or timed pours (tpour()), depending on the system’s capabilities.
Split Pours:
Description: Split pours are designed to handle beverages that tend to foam up, such as carbonated drinks. This type of pour includes delays to allow the foam to settle before continuing the pour.
Use Case: When filling a cup with a carbonated drink, the pour is split into multiple stages with pauses in between. This prevents spillage and ensures the cup is filled to the desired level without overflowing due to excessive foam.
Implementation: The FixedPourAware interface can be used to declare the split percentage and delay for the pour. The pourable beverage specifies how it should be split to manage the foam effectively.
The PourDelegate helps in resolving the maximum pour volume and handling named sizes like cup sizes. This lesson will guide you through creating a VolumeDelegate class and adding it to the context in the DispenserApp.
Let’s create the VolumeDelegate class and integrate it into the DispenserApp.
First, we need to define our VolumeDelegate class and implement the BeveragePipelineDelegate interface.
package com.kosdev.samples.dispenser.part1.pour;
import com.tccc.kos.ext.dispense.pipeline.beverage.BeveragePipelineDelegate;
public class VolumeDelegate implements BeveragePipelineDelegate {
@Override
public int getMaxPourVolume() {
return 946; // 32 oz... kOS uses SI units
}
}
Next, we need to add the VolumeDelegate to the context in the DispenserApp class load method. This ensures that the pour engine can use the delegate to resolve volumes.
@Override
public void load() throws Exception {
// Add a delegate to provide information about pour volumes
addToCtx(new VolumeDelegate());
}
BeanContext
kOS provides a dependency injection framework allowing java objects to autowire dependencies using annotations. Unlike other dependency injection solutions, kOS allows any bean to become a managed bean by simply adding it to a BeanContext. kOS also provides a standardized configuration system which can auto-configure any beans placed in a BeanContext. Many kOS lifecycles are designed to have two distinct phases. The first allows you to create all your objects and add them to a BeanContext. kOS will then autowire and configure all your objects before calling the second phase where all your objects are ready for use. |
In beverage dispensing systems, the tpour method is crucial for controlling the duration and rate of pouring. This lesson will guide you through moving the tpour method from the Valve class to the ControlBoard class, ensuring better organization and control over the pouring process.
Let’s move the tpour method step by step.
First, we need to define the tpour method in the ControlBoard class. This method will centralize the pouring logic for the valves to the board for better coordination.
public FutureWork tpour(Valve pump, int duration, double rate) {
// Create a future to turn the pump on for the requested duration
FutureWork future = new FutureWork("tpour" + "-" + pump.getName(), f -> {
log.info("start: {}", pump.getName());
pump.recordRate(pump.getConfig().getNominalRate());
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: {}", pump.getName());
pump.recordRate(0);
// TODO: turn off the valve
});
return future;
}
Next, we need to update the Valve class to use the tpour method from the ControlBoard.
@Override
public FutureWork tpour(int duration, double rate) {
return ((ControlBoard)getBoard()).tpour(this, duration, rate);
}
By moving the tpour method to the ControlBoard, we centralize the control logic for pouring, making the system more organized and easier to manage. This change ensures that all valves use a consistent method for pouring, improving the reliability and maintainability of the beverage dispensing system.
This chapter unraveled the key aspects of pouring in beverage dispensing systems. It highlighted the role of nozzle pipelines for efficient task management, the utility of beverage graphs for determining beverage availability, and the implementation of the BeveragePourEngine for effective pour operations.
We explored the use of Pourables to abstract the pouring process and the integration of a VolumeDelegate to ensure precise pour volumes. Finally, centralizing the tpour method within the ControlBoard enhanced control and organization.