This guide explains the individual hardware component models provided by the dispense SDK for crew and maintenance applications. These models represent the physical hardware that makes up a dispense system: holders, pumps, ingredients, and boards.
These models are accessed through the Assembly model hierarchy and are distinct from the consumer UI models. Use these models when building crew applications, maintenance tools, or calibration interfaces that need direct hardware access.
Model Overview
The SDK provides four core hardware models:
- HolderModel: Ingredient holder/cartridge slot with associated hardware
- PumpModel: Individual pump with pour operations
- IngredientModel: Simple ingredient data container
- BoardModel: Board hardware representation
HolderModel
Represents an ingredient cartridge attachment point to a pump. In simple devices this is a hose connecting a BiB (bag-in-box) container to a pump. In complex devices this represents a cartridge slot with associated hardware like RFID antenna, indicator lights, and fuel gauge sensors.
Key Properties
interface HolderModel {
// Identity
id: string;
path: string;
name: string;
group: string; // Holder group (e.g., "A", "B")
// Ingredient Assignment
ingredientId?: string;
ingredientType?: string;
ingredientSource?: string;
ingredient?: IngredientModel; // Reference to IngredientModel
ingredientName: string; // Resolved display name
// Previous Ingredient (for line replacement tracking)
prevIngredientId?: string;
previousIngredient?: IngredientModel;
previousIngredientName: string;
// Fuel Gauge (volume tracking)
maxVolume: number; // Maximum volume in mL
remainingVolume: number; // Current volume in mL
fillPercent: number; // Percentage full (0-100)
hasFuelGuage: boolean; // Whether fuel gauge is available
hasMaxVolume: boolean;
hasRemainingVolume: boolean;
// Expiration Tracking
enjoyByDate?: Date;
hasEnjoyByDate: boolean;
hasEnjoyByWarning: boolean; // Whether enjoy-by trouble exists
// State Flags
unassigned: boolean; // No ingredient assigned
removed: boolean; // Ingredient was removed (prevIngredientId exists)
// Child Models
pumps: IKosModelContainer<PumpModel>; // All pumps for this holder
sortedPumps: PumpModel[]; // Pumps sorted by priority (virtual first)
// Relationships
nozzlePaths: string[]; // Nozzles connected to holder's pumps
pumpPaths: string[]; // Paths of all child pumps
nozzlePath: string; // Primary nozzle path (first pump)
// Pour Operations
canPour: boolean; // Whether holder can pour
isPouring: boolean; // Whether any pump is pouring
pourState: string; // Current pour state
// Trouble Integration (from TroubleAware)
troubles: TroubleModel[];
troublesByType: Record<string, TroubleModel[]>;
shouldDefer: boolean; // Whether pour should be deferred
// Extension Data
data: AdditionalData; // Custom data from extensions
slice: HolderSlice; // Raw assembly data
}Holder-to-Pump Relationship
Each holder has a child container of pumps accessed via holder.pumps:
// Access all pumps
holder.pumps.data.forEach(pump => {
console.log(pump.name, pump.isPouring);
});
// Get specific pump by path
const pump = holder.pumps.getModel(pumpPath);
// Get sorted pumps (virtual pumps first, then by name)
holder.sortedPumps.forEach(pump => {
// Virtual pumps appear first for priority access
});Ingredient Access
Holders provide convenient access to their assigned IngredientModel:
// Get ingredient reference
const ingredient = holder.ingredient;
if (ingredient) {
console.log(ingredient.name, ingredient.type);
}
// Get display name (handles missing ingredients gracefully)
const name = holder.ingredientName; // Returns "" if unassigned, "unknown" if not found
// Check assignment state
if (holder.unassigned) {
console.log("No ingredient assigned");
}
if (holder.removed) {
console.log("Ingredient was removed, line replacement needed");
console.log("Previous ingredient:", holder.previousIngredientName);
}Fuel Gauge
Holders with fuel gauge hardware track ingredient volume:
if (holder.hasFuelGuage) {
console.log(`${holder.fillPercent}% full`);
console.log(`${holder.remainingVolume} of ${holder.maxVolume} mL remaining`);
// Show low-level warning
if (holder.fillPercent < 20) {
showWarning("Ingredient running low");
}
}Assignment Operations
Holders support ingredient assignment and RFID override:
// Assign ingredient to holder
await holder.assignIngredient(ingredientId);
// Unassign ingredient
await holder.assignIngredient(); // No argument unassigns
// Update ingredient without API call (for event handlers)
holder.updateIngredient(ingredientId);
// RFID override for damaged/incorrect RFID tags
await holder.overrideRfidCode({
ingredientId: "12345",
enjoyByDate: "2025-12-31"
});Error handling for RFID override:
try {
await holder.overrideRfidCode(overrides);
} catch (error) {
if (error.type === "errIdLen") {
console.error("Invalid ID length");
} else if (error.type === "errCodeFormat") {
console.error("Invalid code format");
}
// Other error types: errCodeLen, errIdFormat, errVerify, errBadJson
}Pour Operations
Holders delegate pour operations to their child pumps:
// Pour using intent (delegates to first pump)
await holder.performIntent("PRIME");
// Pour using specific pump
await holder.performIntent({
intent: "FLUSH",
pumpPath: "assembly:core:pump:S1"
});
// Check pour state
if (holder.canPour) {
console.log("Ready to pour");
}
if (holder.isPouring) {
console.log("Currently pouring:", holder.pourState);
}
// Cancel all pouring pumps
await holder.cancelPour();
// Get intent volume
const { volume } = await holder.resolveIntentVolume("PRIME");
console.log(`Prime volume: ${volume} mL`);Trouble Integration
Holders use the @kosTroubleAware decorator for automatic trouble tracking:
// Check for troubles
if (holder.troubles.length > 0) {
holder.troubles.forEach(trouble => {
console.log(trouble.type, trouble.message);
});
}
// Get troubles by type
const agitationTroubles = holder.troublesByType["InsufficientAgitationTrouble"] || [];
// Check if pour should be deferred
if (holder.shouldDefer) {
console.log("Cannot pour: troubles require resolution");
}
// Check specific trouble types
if (holder.hasEnjoyByWarning) {
console.log("Ingredient expired:", holder.enjoyByDate);
}
// Line replacement workflow
await holder.replaceLine();For more details on trouble integration, see Trouble Integration.
Extension Data
Holders support custom data through the generic type parameter:
// Define custom holder data
interface MyHolderData {
displayColor: string;
priority: number;
}
// Access typed data
const holder: HolderModel<MyHolderData> = ...;
console.log(holder.data.displayColor);
console.log(holder.data.priority);Extension data is populated via the ExtensionType.AssemblyMapper extension point. See Extension Points for details.
IngredientModel
Simple data container representing an ingredient. Ingredients are referenced by HolderModel and provide display information.
Key Properties
interface IngredientModel {
// Identity
id: string;
ingredientId: string;
name: string;
type: string;
// Extension Data
data: AdditionalData; // Custom data from extensions
}Usage
Ingredients are typically accessed through holders:
const ingredient = holder.ingredient;
if (ingredient) {
console.log("Ingredient:", ingredient.name);
console.log("Type:", ingredient.type);
console.log("ID:", ingredient.ingredientId);
}Extension Data
Ingredients support custom data for brand-specific information:
interface MyIngredientData {
imageUrl: string;
brandColor: string;
nutritionalInfo: object;
}
const ingredient: IngredientModel<MyIngredientData> = ...;
console.log(ingredient.data.imageUrl);
console.log(ingredient.data.brandColor);Extension data is populated via the ExtensionType.IngredientMapper extension point. See Extension Points for details.
Note: IngredientModel contains no business logic—it’s purely a data container. All ingredient-related operations (assignment, pouring, etc.) are handled through HolderModel.
Working with Component Models
Accessing Components via Assembly
All hardware components are accessed through the Assembly singleton:
import { Assembly } from "@kosdev-code/kos-dispense-sdk";
import { getKosModel } from "@kosdev-code/kos-ui-sdk";
// Get assembly singleton
const { model: assembly } = await getKosModel(Assembly.type);
// Access holders
assembly.holderContainer.holders.forEach(holder => {
console.log(holder.name, holder.ingredientName);
});
// Access pumps
assembly.pumpContainer.models.forEach(pump => {
console.log(pump.name, pump.isPouring);
});
// Access boards
assembly.boardContainer.boardList.forEach(board => {
console.log(board.name, board.linked);
});Component Relationships
Components are interconnected through path references:
// Holder → Pumps
holder.pumps.data.forEach(pump => {
console.log("Pump:", pump.name);
});
// Pump → Holder
const holderPath = pump.holderPath;
const holder = assembly.holderContainer.holders.getModel(holderPath);
// Pump → Board
const boardPath = pump.boardPath;
const board = assembly.boardContainer.boardList.find(b => b.path === boardPath);
// Pump → Nozzle
const nozzlePath = pump.nozzlePath;Reactive Updates
All component models are reactive via MobX observables. Use kosAutoEffect in React components for automatic re-rendering:
import { kosAutoEffect } from "@kosdev-code/kos-ui-sdk";
import { useEffect, useState } from "react";
function HolderStatus({ holder }) {
const [isPouring, setIsPouring] = useState(false);
useEffect(() => {
const dispose = kosAutoEffect(() => {
setIsPouring(holder.isPouring);
});
return dispose;
}, [holder]);
return <div>{isPouring ? "Pouring..." : "Idle"}</div>;
}For pre-built React integration patterns, see React Integration.
Next Steps
- Consumer UI Foundation: Learn about DeviceAssembly, Nozzle, and Availability models for consumer applications
- React Integration: Use pre-built hooks and contexts for React applications
- Pouring Operations: Complete guide to pour workflows
- Trouble Integration: Handle troubles and defer logic
- Extension Points: Extend models with custom data and behavior
