Assembly Model and Hardware Topology

The AssemblyModel serves as the entry point for accessing and managing the complete hardware structure of a KOS beverage dispenser. It provides a singleton representing the entire device topology, including boards, holders, pumps, and nozzles.

Target Use Cases

The Assembly model hierarchy is designed for crew applications, maintenance tools, and calibration interfaces where direct hardware access and management is required. For consumer-facing applications focused on beverage selection and pouring, see Consumer UI Foundation.

AssemblyModel as Singleton

The AssemblyModel exists as a single instance representing the entire device hardware:

import { Assembly } from "@kosdev-code/kos-dispense-sdk";
// Get the singleton Assembly instance
const assembly = Assembly.factory({});

Since Assembly is a singleton, calling factory() multiple times returns the same instance. Options are ignored after initial creation.

React Applications: In React applications, use the AssemblyProvider and useAssemblyContext() hook instead of calling factory() directly. See React Integration Patterns for details.

Loading Topology

The Assembly model loads device topology during its load() lifecycle phase by calling the getAssembly() service:

// This happens automatically during model lifecycle
// Service call: GET /api/kos/device/assemblies

The service returns the complete device structure including:

  • Boards with their configurations
  • Holders organized by group
  • Pumps mapped to holders and nozzles
  • Nozzle definitions and relationships

Understanding Assembly Data Structure

The assembly data represents the physical hardware layout:

interface AssemblyResponse {
  assemblies: Array<{
    path: string;
    name: string;
    boards: BoardDefinition[];
    holders: Record<string, HolderDefinition[]>; // grouped by holder group
    nozzles: NozzleDefinition[];
    core?: boolean;
  }>;
}

Accessing Child Containers

The Assembly model organizes hardware components into three containers:

HolderContainer

Access all holders or filter by group/nozzle:

import { Assembly } from "@kosdev-code/kos-dispense-sdk";
const assembly = Assembly.factory({});
// Access the holder container
const holderContainer = assembly.holderContainer;
// Get all holders
const allHolders = assembly.allHolders;
// Get holders by group
const cartridgeHolders = assembly.getHoldersByGroup("cartridge");
const bottleHolders = assembly.getHoldersByGroup("bottle");
// Get holders for a specific nozzle
const nozzleHolders = assembly.getHoldersByNozzle("assembly:core:nozzle:nozzle");
// Get list of all holder groups
const groups = assembly.holderGroups; // ["cartridge", "bottle", ...]

PumpContainer

Access all pumps or filter by nozzle/board:

// Access the pump container
const pumpContainer = assembly.pumpContainer;
// Get all pump models
const allPumps = pumpContainer.models.data;
// Get pumps by nozzle (indexed map)
const pumpsByNozzle = assembly.pumpsByNozzle;
// Returns: Record<string, PumpModel[]>
// Use with nozzlePaths to iterate
assembly.nozzlePaths.forEach(nozzlePath => {
  const pumps = pumpsByNozzle[nozzlePath];
  console.log(`Nozzle ${nozzlePath} has ${pumps.length} pumps`);
});
// Get all pump paths
const paths = assembly.pumpPaths; // ["assembly:core:board:micro:pump:S1", ...]

BoardContainer

Access board information:

// Access the board container
const boardContainer = assembly.boardContainer;
// Get all boards
const boards = assembly.boards;
// Access individual board
boards.forEach(board => {
  console.log(`Board: ${board.name}, Path: ${board.path}`);
});

Convenience Methods

The Assembly model provides several convenience methods for common access patterns:

Get Holders by Group

/**
 * Get holders in a specific group (e.g., "cartridge", "bottle")
 */
const holders = assembly.getHoldersByGroup("cartridge");

Get Holders by Nozzle

/**
 * Get all holders mapped to a specific nozzle path
 */
const holders = assembly.getHoldersByNozzle("assembly:core:nozzle:nozzle");

Pumps by Nozzle

/**
 * Get pumps indexed by nozzle path
 * Returns: Record<string, PumpModel[]>
 */
const pumpMap = assembly.pumpsByNozzle;
const nozzlePumps = pumpMap["assembly:core:nozzle:nozzle"];

Nozzle Paths

/**
 * Get list of all nozzle paths on the device
 * Useful for discovering available nozzles on multi-nozzle dispensers
 */
const nozzles = assembly.nozzlePaths;
// ["assembly:core:nozzle:nozzle", "assembly:core:nozzle:nozzle2"]

Holder Groups

/**
 * Get list of unique holder group names
 */
const groups = assembly.holderGroups;
// ["cartridge", "bottle", "bib", ...]

Extension Point: AssemblyMapper

The ExtensionType.AssemblyMapper extension point allows customization of assembly data before it’s processed into models. This is useful when the backend extends the base assembly structure with additional properties.

When to Use

Use the AssemblyMapper when:

  • Your backend extends AssemblyResponse with custom properties
  • You need to add computed properties to the Assembly model
  • You want to create custom child models that participate in Assembly lifecycle

Example Implementation

import { AssemblyTypes } from "@kosdev-code/kos-dispense-sdk";
import { DataMapper } from "@kosdev-code/kos-ui-sdk";
// Define additional backend data
interface CustomAssemblyResponse {
  iceAgitator?: {
    name: "iceAgitator";
    path: string;
  };
  carbAgitator?: {
    name: "carbAgitator";
    path: string;
  };
}
// Define additional Assembly model data
interface CustomAssemblyData {
  iceAgitator: {
    path: string;
  };
  carbAgitator: {
    path: string;
  };
}
// Create the mapper
export const assemblyDataMapper: DataMapper<
  AssemblyTypes.AssemblyResponse<CustomAssemblyResponse>,
  CustomAssemblyData
> = async (data) => {
  const result = data.assemblies.reduce((acc, assembly) => {
    const agitatorKeys = ["iceAgitator", "carbAgitator"];
    agitatorKeys.forEach((key) => {
      const agitator = assembly[key];
      if (agitator) {
        acc[key] = agitator;
      }
    });
    return acc;
  }, {} as CustomAssemblyData);
  return result;
};
// Register the mapper
import { KosExtensionManager } from "@kosdev-code/kos-dispense-sdk";
const extensionManager = new KosExtensionManager();
extensionManager.dispense.registerAssemblyDataMapper(assemblyDataMapper);

The registered mapper transforms the assembly response data during the load lifecycle. The mapped data is merged into the Assembly model, and any child models in the mapped data automatically participate in the model lifecycle.

Complete Example: Accessing Assembly Topology

import { Assembly } from "@kosdev-code/kos-dispense-sdk";
// Get the singleton Assembly instance
const assembly = Assembly.factory({});
// Wait for topology to load (handled by KOS lifecycle)
// After load phase completes:
// Discover nozzles on the device
console.log("Available nozzles:", assembly.nozzlePaths);
// Iterate through holder groups
assembly.holderGroups.forEach(group => {
  const holders = assembly.getHoldersByGroup(group);
  console.log(`Group "${group}" has ${holders.length} holders`);
  holders.forEach(holder => {
    console.log(`  - ${holder.name}: ${holder.ingredientName || 'unassigned'}`);
  });
});
// Check pumps for a specific nozzle
const nozzlePath = assembly.nozzlePaths[0];
const pumps = assembly.pumpsByNozzle[nozzlePath];
console.log(`Nozzle has ${pumps.length} pumps`);
// Access boards
assembly.boards.forEach(board => {
  console.log(`Board: ${board.name} (${board.path})`);
});

Next Steps