Hardware Component Models

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

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