Introduction
The Dispense SDK provides multiple approaches to executing pour operations, each designed for specific use cases. This guide covers pour patterns for both consumer UIs and NCUI/admin applications, including standard nozzle-based pours and generic pour operations for custom endpoints.
Pour Patterns Overview
| Pattern | Model | Use Case | Key Method |
|---|---|---|---|
| Nozzle Pour | NozzleModel | Consumer UI selection-based pours | pour(), fixedPour() |
| Pump Pour | PumpModel | Direct pump operations, calibration, maintenance | performIntent() |
| Holder Pour | HolderModel | Holder-level operations, delegates to pumps | performIntent() |
| Generic Pour | GenericPourModel | Custom endpoints, non-nozzle operations (ice, orchestrated pours) | pour() |
Intent-Based Pouring Concept
KOS dispense operations use intent-based pouring: rather than specifying exact volumes or implementation details, you specify an intent name that represents an operational function. The backend interprets these intents based on the specific hardware involved.
Common Intent Examples
"PRIME"– Prime the pump/line with ingredient"PURGE"– Purge the line"CALIBRATE"– Run calibration operation"FLUSH"– Flush the line with water"HIGH_FLOW_TEST"– High flow rate test"LOW_FLOW_TEST"– Low flow rate test"HOLD_TO_POUR"– User-controlled hold-to-pour
Why Intent-Based?
This abstraction enables:
- Hardware independence: The UI makes normalized intent calls regardless of pump type
- Backend flexibility: Different pump types (LFCV, MSV, FCM, micro, etc.) can implement intents differently
- Complex operations: A single intent can trigger multiple steps (e.g.,
micro:FLUSHruns 3 operations with a 60-second delay) - Simplified UI logic: No need to understand hardware-specific operations or sequences
- Declarative configuration: Intents are defined by the backend application and can vary by device
Intent Implementation Example
For example, a "PRIME" intent on a micro pump might involve:
<intent name="micro:PRIME">
<op type="tpour" rate="0.9" duration="7000" diluted="true" errorHandlerId="blockAirDetect" />
<op type="tpour" rate="0.9" duration="5000" diluted="true" />
</intent>While the same "PRIME" intent on an MSV pump might be:
<intent name="msv:PRIME">
<op type="vpour" rate="11.25" volume="50" diluted="true" />
</intent>The UI simply calls performIntent({ intent: "PRIME" }) and the backend handles the hardware-specific implementation.
Available intents are determined by the device configuration and can vary between dispensers.
NozzleModel Pouring (Consumer UI Pattern)
The NozzleModel provides the primary pour interface for consumer applications where users have selected beverages.
Basic Pour Operation
import { useNozzleContext } from '@kosdev-code/kos-dispense-sdk';
function PourButton() {
const { model: nozzle } = useNozzleContext();
const handlePour = async () => {
if (nozzle?.canPour) {
try {
await nozzle.pour();
} catch (error) {
console.error('Pour failed:', error);
}
}
};
return (
<button
onClick={handlePour}
disabled={!nozzle?.canPour}
>
Pour
</button>
);
}Note: The @kosFuture() decorator automatically generates a unique tracker ID. Do NOT pass a tracker parameter.
Fixed Pour (Named Sizes)
When the backend supports named pour sizes (e.g., “small”, “medium”, “large”), use fixedPour():
const handleFixedPour = async () => {
try {
await nozzle?.fixedPour("medium");
} catch (error) {
console.error('Fixed pour failed:', error);
}
};The fixedPour() method is used for specific backends that translate size names into fixed volume pours. The main usage pattern is the pour() method for selection-based pours.
Pour State Properties
The NozzleModel exposes several properties for tracking pour state:
// Boolean pour state
const isPouring = nozzle?.isPouring; // true during active pour
// Boolean readiness (selected + available + idle)
const canPour = nozzle?.canPour;
// PourState enum value (IDLE, POUR_STARTING, POURING, POUR_CANCELING)
const currentState = nozzle?.currentState;
// Progress from 0-100 (from Future)
const progress = nozzle?.pourProgress;Pour State Lifecycle
IDLE → POUR_STARTING → POURING → IDLE
↓
POUR_CANCELING → IDLECanceling Pours
const handleCancel = async () => {
if (nozzle?.isPouring) {
await nozzle.cancelPour();
}
};Pour Prerequisites
Before calling pour(), ensure:
- A pourable is selected via
setSelectedPourable() - The selected beverage is available
- The nozzle is in IDLE state (
canPour === true)
// Set selection before pouring
nozzle?.setSelectedPourable({
beverage: selectedBeverage,
additional: selectedFlavors
});
// Check readiness
if (nozzle?.canPour) {
await nozzle.pour();
}PumpModel Pouring (Hardware/Maintenance Pattern)
The PumpModel provides direct pump control for maintenance, calibration, and diagnostic operations using intent-based commands.
Performing Intent-Based Operations
import { useAssemblyContext } from '@kosdev-code/kos-dispense-sdk';
function PumpMaintenance({ pumpPath }: { pumpPath: string }) {
const { model: assembly } = useAssemblyContext();
const pump = assembly?.pumpContainer.models.find(p => p.path === pumpPath);
const handlePrimeOperation = async () => {
try {
await pump?.performIntent({ intent: "PRIME" });
} catch (error) {
console.error('Prime operation failed:', error);
}
};
return (
<button onClick={handlePrimeOperation} disabled={!pump?.canPour}>
Prime Pump
</button>
);
}Important: The @kosFuture() decorator automatically generates a unique tracker ID for Future tracking. Do NOT pass the tracker parameter – it’s injected automatically by the decorator.
Intent as String or Object
The performIntent method accepts either a string intent or an IntentAwareProps object:
// IntentAwareProps object (recommended)
await pump?.performIntent({ intent: "PURGE" });
// String intent (convenience)
await pump?.performIntent("PURGE");Pump Pour State
// Boolean state
const isPouring = pump?.isPouring;
const canPour = pump?.canPour;
// String state for display
const pouringState = pump?.pouringState; // "IDLE", "POURING", etc.
// Current PourState enum
const currentState = pump?.currentState;Canceling Pump Operations
if (pump?.isPouring) {
await pump.cancelPour();
}Cancellation is only valid during POURING or POUR_STARTING states. If the pump is already IDLE, the cancellation is ignored.
Intent Volume Resolution
Before executing an intent, you can query the expected volume:
const volume = pump?.resolveIntentVolume("PRIME");
console.log(`Prime operation will dispense ${volume}ml`);This is useful for displaying expected volumes in UI before the user initiates an operation.
HolderModel Pouring (Hardware Pattern)
The HolderModel provides holder-level pour operations that delegate to child pumps.
Basic Holder Operations
function HolderMaintenance({ holderPath }: { holderPath: string }) {
const { model: assembly } = useAssemblyContext();
const holder = assembly?.holderContainer.holders.find(h => h.path === holderPath);
const handleFlushOperation = async () => {
try {
// Delegates to first child pump by default
await holder?.performIntent({ intent: "FLUSH" });
} catch (error) {
console.error('Flush operation failed:', error);
}
};
return (
<button onClick={handleFlushOperation} disabled={!holder?.canPour}>
Flush Line
</button>
);
}Targeting Specific Pump
If a holder has multiple pumps, you can specify which pump to use:
await holder?.performIntent({
intent: "PRIME",
pumpPath: "assembly:core:board:macro:pump:A1"
});Holder Pour State
// Boolean - reflects any child pump pouring
const isPouring = holder?.isPouring;
// Boolean - requires all child pumps can pour AND no shouldDefer
const canPour = holder?.canPour;The canPour property checks:
- All child pumps have
canPour === true - No troubles are present that set
shouldDefer === true
Canceling All Child Pumps
// Cancels all child pumps
await holder?.cancelPour();Intent Volume Resolution
Similar to pumps, holders can resolve intent volumes (delegates to child pump):
const volume = holder?.resolveIntentVolume("PRIME");Generic Pour Operations
The GenericPourModel enables custom pour operations that don’t fit standard nozzle patterns. Use cases include:
- Ice dispensing: Custom ice dispense endpoints
- Orchestrated pours: Backend-coordinated multi-nozzle pours
- Custom hardware: Non-standard dispense points
- Special operations: Cleaning cycles, custom workflows
Why Generic Pour?
Standard nozzle pouring assumes:
- Selection-based workflow (brand → beverage → flavor)
- Standard nozzle endpoints (
/api/ext/dispense/nozzle/{path}/pour) - Single-nozzle, single-ingredient operations
Generic pour is needed when:
- Custom endpoints required: Backend needs different API paths
- Non-nozzle operations: Ice, foam, custom dispensing
- Backend orchestration: Complex multi-step or multi-device pours
- Special hardware: Equipment that doesn’t fit standard topology
Creating a Generic Pour Model
import { GenericPour } from '@kosdev-code/kos-dispense-sdk';
// Define service path factory
const iceServicePathFactory = (selectedId: string) =>
`/api/ice/dispense/${selectedId}`;
// Create model
const icePourModel = GenericPour.instance("ice-pour-001")
.options({
servicePathFactory: iceServicePathFactory,
selectedId: "ice-maker-01",
pourType: "ice" // Optional pour type for analytics
})
.build();Using Generic Pour Hook
The SDK provides a React hook for generic pours:
import { useGenericPour } from '@kosdev-code/kos-dispense-sdk';
function IceDispenser() {
const servicePathFactory = (selectedId: string) =>
`/api/ice/dispense/${selectedId}`;
const model = useGenericPour(
"ice-pour-001",
servicePathFactory,
"ice-maker-01" // itemId parameter
);
const handleIcePour = async () => {
if (model?.canPour) {
try {
await model.pour();
} catch (error) {
console.error('Ice pour failed:', error);
}
}
};
return (
<button onClick={handleIcePour} disabled={!model?.canPour}>
Dispense Ice
</button>
);
}Note: Like other pour operations, GenericPourModel.pour() uses @kosFuture() which automatically generates the tracker. Do NOT pass a tracker parameter.
Service Path Factory Pattern
The servicePathFactory is a function that generates the API endpoint based on a selected ID:
// Example: Different endpoints based on ice type
const servicePathFactory = (iceType: string) => {
switch (iceType) {
case "cubed":
return "/api/ice/dispense/cubed";
case "crushed":
return "/api/ice/dispense/crushed";
default:
return "/api/ice/dispense/default";
}
};The selectedId option sets the initial selected ID, which is passed to the factory to determine the service path.
Generic Pour State
// Boolean state
const isPouring = genericPourModel?.isPouring;
const canPour = genericPourModel?.canPour;
// PourState enum
const currentState = genericPourModel?.currentState;
// Progress from Future
const progress = genericPourModel?.pourProgress;Canceling Generic Pours
await genericPourModel?.cancelPour();Pour Type for Analytics
The optional pourType parameter allows categorization of pour operations for analytics:
.options({
servicePathFactory: factory,
selectedId: "item-01",
pourType: "ice" // Appears in TOPIC_GENERIC_POUR_STARTED events
})Generic Pour Topics
The GenericPourModel publishes events for pour lifecycle:
// Pour started
TOPIC_GENERIC_POUR_STARTED: "/kos/generic/pour/started"
// Event payload: { pourTarget: string, type: string }
// Pour progress
TOPIC_GENERIC_POUR_PROGRESS: "/kos/generic/pour/progress"
// Event payload: { pourTarget, progress, status, futureId, ... }
// Pour cancelled
TOPIC_GENERIC_POUR_CANCEL: "/kos/generic/pour/cancel"
// Event payload: { pourTarget: string }Using usePourTarget Hook
For advanced pour control with gesture handling, use the usePourTarget hook with any PourStrategyAware model (including GenericPourModel):
import { usePourTarget, HoldToPourStrategy } from '@kosdev-code/kos-dispense-sdk';
function AdvancedPourButton({ pourModel }: { pourModel: PourStrategyAware }) {
const {
handlers,
cancel,
updatePourContext,
status
} = usePourTarget({
pourTarget: pourModel,
strategy: HoldToPourStrategy, // Optional, defaults to HoldToPourStrategy
onBeforePour: async (context) => {
// Validation before pour starts
// Access data from context to avoid unnecessary callback re-creation
const selectedItem = context.get("selectedItem");
const isValid = await validatePourConditions(selectedItem);
return isValid; // Return false to cancel
},
onPour: () => {
console.log('Pour started');
},
onPourCancel: () => {
console.log('Pour cancelled');
},
onPourAbort: () => {
console.log('Pour aborted');
}
});
return (
<button {...handlers()}>
Hold to Pour
</button>
);
}The hook returns gesture handlers that can be spread onto DOM elements and automatically detects touch or mouse events.
Pour Context (CUI Kit)
When using CUI Kit, the pour context is used primarily to prevent unnecessary React re-renders by making data available to validation hooks without recreating callback functions.
How It Works
CUI Kit automatically adds the selected beverage to the pour context, making it accessible in onBeforePour hooks without depending on changing props:
const { pour } = useCuiKit();
// CUI Kit automatically does this internally:
// pour.updatePourContext("selectedBeverage", beverageModel);
// Your validation hook can access context without re-creating
const handleBeforePour = async (context) => {
const beverage = context.get("selectedBeverage");
// Perform validation without causing re-renders
return await validateBeverage(beverage);
};Adding Custom Context Data
You can add additional context data for your own validation logic:
const { pour } = useCuiKit();
// Add custom validation data
pour.updatePourContext("userAge", user.age);
pour.updatePourContext("requiresVerification", true);
// Access in validation hooks
const onBeforePour = async (context) => {
const beverage = context.get("selectedBeverage"); // From CUI Kit
const userAge = context.get("userAge"); // Your custom data
const requiresVerification = context.get("requiresVerification");
if (beverage.containsAlcohol && userAge < 21) {
return false; // Cancel pour
}
return true;
};Why Use Context?
The context pattern prevents this common React pitfall:
// ❌ Bad: Creates new callback on every render when selectedBeverage changes
const onBeforePour = useCallback(async () => {
return await validateBeverage(selectedBeverage);
}, [selectedBeverage]); // Dependency causes callback re-creation
// ✅ Good: Stable callback, data accessed from context
const onBeforePour = useCallback(async (context) => {
const beverage = context.get("selectedBeverage");
return await validateBeverage(beverage);
}, []); // No dependencies, stable callbackSee CUI Kit documentation for more details.
Future-Aware Patterns
All pour operations integrate with the KOS Future system for async operation tracking.
Accessing Future State
// Get current Future
const future = nozzle?.future; // or pump.future, genericPourModel.future
if (future) {
console.log('Progress:', future.progress);
console.log('Status:', future.status);
console.log('Remaining time:', future.remainingTimeMs);
console.log('End state:', future.endState); // Set when complete
}Future Update Callbacks
Models expose onFutureUpdate callbacks for custom logic:
pump.onFutureUpdate = (future) => {
console.log(`Operation progress: ${future.progress}%`);
if (future.endState) {
console.log('Operation completed:', future.endState);
}
};Manual Future Cancellation
const future = nozzle?.future;
await future?.cancelFuture();However, prefer using model-level cancelPour() methods as they handle state management correctly.
Error Handling
All pour operations should be wrapped in try/catch blocks:
const handlePour = async () => {
if (!nozzle?.canPour) {
alert('Cannot pour at this time');
return;
}
try {
await nozzle.pour();
} catch (error) {
console.error('Pour failed:', error);
// State automatically resets to IDLE on error
// Show user-friendly error
alert('Pour operation failed. Please try again.');
}
};Common Error Scenarios
- Invalid state: Attempting to pour when not in IDLE state
- No selection: Calling
pour()without a selected pourable (NozzleModel) - Network failure: Device API unreachable
- Device trouble: Hardware issues preventing pour
- Invalid intent: Intent name not supported by hardware type
Pour Validation
Always check canPour before initiating pours:
// NozzleModel validation
if (nozzle?.canPour) {
// Selected + available + idle
await nozzle.pour();
}
// PumpModel validation
if (pump?.canPour) {
// Idle state
await pump.performIntent({ intent: "PRIME" });
}
// HolderModel validation
if (holder?.canPour) {
// All child pumps can pour AND no shouldDefer
await holder.performIntent({ intent: "FLUSH" });
}Complete Pour Examples
Consumer UI with Selection
import { useNozzleContext } from '@kosdev-code/kos-dispense-sdk';
import { useState } from 'react';
function BeverageSelection() {
const { model: nozzle } = useNozzleContext();
const [selectedBeverage, setSelectedBeverage] = useState(null);
const beverages = nozzle?.visibleBeverages || [];
const handleSelectAndPour = async (beverage) => {
// Set selection
nozzle?.setSelectedPourable({ beverage, additional: [] });
setSelectedBeverage(beverage);
// Wait for selection to update
await new Promise(resolve => setTimeout(resolve, 100));
// Pour
if (nozzle?.canPour) {
try {
await nozzle.pour();
} catch (error) {
console.error('Pour failed:', error);
}
}
};
return (
<div>
{beverages.map(beverage => (
<button
key={beverage.id}
onClick={() => handleSelectAndPour(beverage)}
>
{beverage.data.name}
</button>
))}
{nozzle?.isPouring && (
<div>
<p>Pouring: {nozzle.pourProgress}%</p>
<button onClick={() => nozzle.cancelPour()}>
Cancel
</button>
</div>
)}
</div>
);
}Pump Maintenance Operations
import { useAssemblyContext } from '@kosdev-code/kos-dispense-sdk';
import { useState } from 'react';
function PumpMaintenancePanel({ pumpPath }: { pumpPath: string }) {
const { model: assembly } = useAssemblyContext();
const pump = assembly?.pumpContainer.models.find(p => p.path === pumpPath);
const [intent, setIntent] = useState("PRIME");
const handleOperation = async () => {
try {
const expectedVolume = pump?.resolveIntentVolume(intent);
console.log(`${intent} will dispense ${expectedVolume}ml`);
await pump?.performIntent({ intent });
} catch (error) {
console.error('Operation failed:', error);
}
};
return (
<div>
<h3>Pump: {pump?.name}</h3>
<p>State: {pump?.pouringState}</p>
<select value={intent} onChange={e => setIntent(e.target.value)}>
<option value="PRIME">Prime</option>
<option value="PURGE">Purge</option>
<option value="CALIBRATE">Calibrate</option>
<option value="FLUSH">Flush</option>
<option value="HIGH_FLOW_TEST">High Flow Test</option>
<option value="LOW_FLOW_TEST">Low Flow Test</option>
</select>
<button onClick={handleOperation} disabled={!pump?.canPour}>
Execute {intent}
</button>
{pump?.isPouring && (
<button onClick={() => pump.cancelPour()}>
Cancel Operation
</button>
)}
</div>
);
}Generic Pour for Ice
import { useGenericPour } from '@kosdev-code/kos-dispense-sdk';
import { useState } from 'react';
function IceDispenser() {
const servicePathFactory = (iceType: string) =>
`/api/ice/dispense/${iceType}`;
const [iceType, setIceType] = useState("cubed");
const model = useGenericPour(
"ice-dispenser",
servicePathFactory,
iceType
);
const handleIcePour = async () => {
if (model?.canPour) {
try {
await model.pour();
} catch (error) {
console.error('Ice pour failed:', error);
}
}
};
return (
<div>
<h3>Ice Dispenser</h3>
<div>
<button onClick={() => setIceType("cubed")}>Cubed</button>
<button onClick={() => setIceType("crushed")}>Crushed</button>
</div>
<p>Selected: {iceType}</p>
<button onClick={handleIcePour} disabled={!model?.canPour}>
Dispense Ice
</button>
{model?.isPouring && (
<div>
<p>Dispensing: {model.pourProgress}%</p>
<button onClick={() => model.cancelPour()}>Cancel</button>
</div>
)}
</div>
);
}Best Practices
- Always validate before pouring: Check
canPourbefore calling pour methods - Use error handling: Wrap all pour operations in try/catch blocks
- Provide user feedback: Display pour progress and state to users
- Handle cancellation: Provide cancel functionality for better UX
- Don’t pass tracker parameter: The
@kosFuture()decorator auto-generates it - Check prerequisites: Ensure selections are made before nozzle pours
- Respect troubles: Check
shouldDeferon holders before allowing pours - Use appropriate pattern: Choose the right pour model for your use case
- Consumer selection:
NozzleModel.pour() - Named/fixed sizes:
NozzleModel.fixedPour(name)for specific backends - Diagnostics/calibration/maintenance:
PumpModel.performIntent() - Custom endpoints/orchestration:
GenericPourModel.pour()
- Consumer selection:
- Understand intent purpose: Intents represent operational functions (PRIME, PURGE, etc.), not pour sizes
- Device-specific intents: Available intents are defined by backend configuration and may vary
- Intent complexity: Single intents can trigger multi-step operations automatically
- Use pour context wisely: Primary purpose is preventing re-renders in validation hooks, not general analytics
Next Steps
- Trouble Integration – Understanding trouble handling during pours
- CUI Kit – Complete consumer UI patterns with pour integration
- React Integration – React hooks for pour operations
Related Documentation
- Hardware Component Models – Detailed PumpModel and HolderModel APIs
- Consumer UI Foundation – NozzleModel pour capabilities
- Future System Documentation – Understanding KOS async operations
