Trouble System Integration – Managing Device State and User Notifications

Troubles as Device State Indicators

Traditional applications treat errors as exceptional conditions that interrupt normal operation. KOS takes a different approach: troubles are indicators of device state that may require attention, affect operations, or need user awareness.

A trouble doesn’t necessarily mean something is broken. It’s a structured way to communicate device conditions:

  • A pump can’t dispense because it needs priming (PrimeTrouble)
  • The device needs cleaning but can continue operating (CleaningRequiredTrouble)
  • An ingredient is running low and should be reordered soon (IngredientLowTrouble)
  • A cartridge is in the wrong type of holder slot (AgitatedInStaticTrouble)
  • Calibration is due for optimal performance (CalibrationRequiredTrouble)

This state-based approach enables applications to make decisions about what users can do, what they should be notified about, and what actions are available.

Understanding KOS Troubles

KOS troubles are specific, named conditions that follow a consistent naming convention: PascalCase ending with “Trouble” (e.g., CalibrationRequiredTrouble, IngredientEmptyTrouble, BoardNotLinkedTrouble). Each trouble represents a specific device condition with:

  • Severity levels: Indicating operational impact
  • Visibility roles: Who should see the trouble (CREW, TECHNICIAN, NONE)
  • Action roles: Who can resolve the trouble
  • Interface data: Structured data about affected components
  • Resolution pathways: Clear actions for remediation

Troubles vs. Traditional Errors

Traditional Error Handling:

  • Generic error messages
  • No contextual information about affected components
  • Binary working/broken states
  • No role-based visibility

KOS Trouble System:

  • Specific named troubles like PrimeTrouble, PurgeTrouble, CarbTankLockupTrouble
  • Rich interface data linking troubles to device paths
  • Graduated severity with role-based visibility
  • Clear resolution actions with progress tracking

The Extensible Architecture: Backend and Frontend Collaboration

The KOS Trouble System uses an extensible architecture that allows backend and frontend developers to work independently. Backend teams can introduce new trouble types and interfaces without requiring changes to the core trouble infrastructure, while frontend teams handle these new troubles through extension points.

How Backend and Frontend Teams Collaborate

Backend developers can:

  • Create new trouble types (e.g., NewCalibrationRequiredTrouble)
  • Define new interfaces with custom data structures
  • Add troubles with multiple interfaces for rich context
  • Evolve the trouble system without breaking existing code

Frontend/model developers can:

  • Register handlers for new interfaces via extension points
  • Enrich trouble data with UI-specific information
  • Map troubles to device paths for automatic association
  • Add custom business logic without modifying core models

This separation of concerns means the core TroubleModel and TroubleContainerModel never need to change—the extension system handles all customization.

The Handle System and Trouble Associations

KOS uses a Handle system to create unique, hierarchical paths to device objects—similar to filesystem paths but using periods as separators (e.g., board.pump:water, holder.slot:A1). Troubles are associated with these handle paths through their interface data, enabling precise component-level trouble tracking.

How Troubles Map to Device Paths

When the backend sends a trouble, it includes:

  • Type: The specific trouble name (e.g., PrimeTrouble)
  • Interfaces (ifaces): Array of data interfaces the trouble implements (e.g., ["pump", "holder"])
  • Data: Interface-specific data (e.g., { pumpPath: "pump:S1", holderPaths: ["holder.slot:A1"] })

The frontend processes this through two extension points:

  1. EXTENSION_TROUBLE_MAPPER: Maps interface types to handle paths
  2. EXTENSION_TROUBLE_DATA_MAPPER: Enriches trouble data with additional context

Here’s how it works in practice:

Example: Backend Introduces New Trouble Type

When the backend team needs to track a new condition (like temperature sensors drifting out of calibration), they can create a new trouble type that includes:

  • Trouble type name: TemperatureDriftTrouble
  • Interfaces: The trouble implements temperature-sensor and calibration interfaces
  • Interface data: Contains fields like sensorPath (the handle path to the sensor), currentReading, expectedReading, driftAmount, and lastCalibration

The frontend handles this new trouble type without any core changes:

import { ExtensionManager, EXTENSION_TROUBLE_MAPPER, EXTENSION_TROUBLE_DATA_MAPPER } from "@kosdev-code/kos-ui-sdk";
// Register mapper for the new "temperature-sensor" interface
ExtensionManager[EXTENSION_TROUBLE_MAPPER].register(
  "temperature-sensor",
  async (trouble: TroubleModel) => {
    // Map to sensor's handle path for automatic association
    return [trouble.data.sensorPath];
  }
);
// Enrich with UI-specific information
ExtensionManager[EXTENSION_TROUBLE_DATA_MAPPER].register(
  "TemperatureDriftTrouble",  // Can also register by trouble type
  async (trouble: TroubleModel) => {
    const drift = trouble.data.driftAmount;
    const daysSinceCalibration = calculateDaysSince(trouble.data.lastCalibration);
    return {
      driftSeverity: drift > 5 ? "critical" : drift > 2 ? "warning" : "minor",
      calibrationOverdue: daysSinceCalibration > 30,
      recommendedAction: drift > 5 ? "Immediate calibration required" : "Schedule calibration",
      estimatedImpact: `Temperature readings off by ${drift}°F`,
    };
  }
);
// Models with this sensor path automatically receive the trouble
@kosTroubleAware()
class TemperatureSensorModel {
  path = "board.sensor:temp-01";
  // Automatically receives TemperatureDriftTrouble when it occurs
  get needsCalibration(): boolean {
    return this.troubles.some(t => t.type === "TemperatureDriftTrouble");
  }
}

Standard Interface Mappers

Here are the common interface mappers used across KOS:

// Holder interface - maps to holder paths
ExtensionManager[EXTENSION_TROUBLE_MAPPER].register(
  "holder",
  async (trouble: TroubleModel<HolderTroubleData>) => {
    return trouble.data.holderPaths;
  }
);
// Pump interface - maps to pump path
ExtensionManager[EXTENSION_TROUBLE_MAPPER].register(
  "pump",
  async (trouble: TroubleModel<PumpTroubleData>) => {
    return [trouble.data.pumpPath];
  }
);
// Nozzle interface - maps to nozzle path
ExtensionManager[EXTENSION_TROUBLE_MAPPER].register(
  "nozzle",
  async (trouble: TroubleModel<NozzleTroubleData>) => {
    return [trouble.data.nozzlePath];
  }
);
// Generic path interface - used by many troubles
ExtensionManager[EXTENSION_TROUBLE_MAPPER].register(
  "path",
  async (trouble: TroubleModel<HandleTroubleData>) => {
    return [trouble.data.path];
  }
);

Using the SDK’s TroubleContainerModel

The KOS SDK provides a fully-featured TroubleContainerModel that manages all troubles across the device. This singleton model automatically:

  • Handles WebSocket updates for real-time trouble management
  • Maintains indexes for efficient querying by path, type, and group
  • Processes troubles through registered mappers and enrichers
  • Filters troubles by visibility role

Working with the TroubleContainerModel

While individual models use @kosTroubleAware to get their own troubles, the TroubleContainerModel is useful for system-wide queries and special cases:

import { kosModel, kosDependency, TroubleContainerModel, IKosDataModel } from "@kosdev-code/kos-ui-sdk";
// Example: Using existing holder models instead of centralized tracking
@kosModel({ modelTypeId: "holder-management-model", singleton: true })
export class HolderManagementModelImpl implements IKosDataModel {
  @kosDependency({ modelType: HolderContainerModel.type })
  holderContainer!: HolderContainerModel;
  // Get troubles from the holders themselves, not centrally
  get holdersNeedingPrime(): HolderModel[] {
    return this.holderContainer.holders.data.filter(
      holder => holder.troublesByType["PrimeTrouble"]?.length > 0
    );
  }
  get holdersWithIssues(): HolderModel[] {
    return this.holderContainer.holders.data.filter(
      holder => holder.troubles.length > 0
    );
  }
}
// TroubleContainerModel is better used for system-wide concerns
@kosModel({ modelTypeId: "notification-manager-model", singleton: true })
export class NotificationManagerModelImpl implements IKosDataModel {
  @kosDependency({ modelType: TroubleContainerModel.type })
  troubleContainer!: TroubleContainerModel;
  // Use for troubles that don't map to specific models
  get unassociatedTroubles(): TroubleModel[] {
    // Troubles without path associations (system-wide issues)
    return this.troubleContainer.troubles.data.filter(
      trouble => !trouble.affectedPaths || trouble.affectedPaths.length === 0
    );
  }
  // Use for role-based filtering across all troubles
  get technicianOnlyTroubles(): TroubleModel[] {
    return this.troubleContainer.troubles.data.filter(
      trouble => trouble.visibleRole === "TECHNICIAN"
    );
  }
}

Automatic Path-Based Integration with @kosTroubleAware

The @kosTroubleAware decorator enables models to automatically receive troubles associated with their device path. This is particularly powerful when your model represents a specific device component with a handle path.

import { kosModel, kosTroubleAware, TroubleAware, IKosDataModel } from "@kosdev-code/kos-ui-sdk";
// Interface merging for TypeScript support
interface HolderModelImpl extends TroubleAware {}
@kosModel({ modelTypeId: "holder-model", singleton: false })
@kosTroubleAware()
export class HolderModelImpl implements IKosDataModel {
  path: string; // Handle path like "holder.slot:A1"
  constructor(modelId: string, options: HolderOptions) {
    this.path = options.path;
  }
  // Automatically receives troubles mapped to this holder's path
  get hasAgitationIssues(): boolean {
    return (this.troublesByType["AgitatedInStaticTrouble"]?.length || 0) > 0;
  }
  get requiresPrime(): boolean {
    return (this.troublesByType["PrimeTrouble"]?.length || 0) > 0;
  }
  get ingredientTroubles(): TroubleModel[] {
    return [
      ...(this.troublesByType["IngredientEmptyTrouble"] || []),
      ...(this.troublesByType["IngredientLowTrouble"] || []),
      ...(this.troublesByType["NotAllowedTrouble"] || []),
    ];
  }
  // Use trouble data for operational decisions
  get canDispense(): boolean {
    const blockingTroubles = this.troubles.filter(t =>
      t.severity >= 8 ||
      t.type === "BoardNotLinkedTrouble" ||
      t.type === "CalibrationRequiredTrouble"
    );
    return blockingTroubles.length === 0;
  }
  // Generate user-friendly status
  get statusMessage(): string {
    if (this.troubles.length === 0) {
      return `Holder ${this.name} operating normally`;
    }
    const primeRequired = this.troubles.find(t => t.type === "PrimeTrouble");
    if (primeRequired) {
      return `Prime required for ${primeRequired.data.ingredientName}`;
    }
    const agitationIssue = this.troubles.find(t => t.type === "AgitatedInStaticTrouble");
    if (agitationIssue) {
      return `${agitationIssue.data.ingredientName} must be in agitated slot`;
    }
    return `${this.troubles.length} issues detected`;
  }
}

Real-World Trouble Types

KOS defines specific trouble types for different device conditions. Here are common examples:

Ingredient and Holder Troubles

  • IngredientEmptyTrouble: Ingredient container is empty
  • IngredientLowTrouble: Ingredient running low
  • NotAllowedTrouble: Ingredient not allowed in specific holder
  • AgitatedInStaticTrouble: Ingredient requires agitation but is in static slot
  • PrimeTrouble: Line requires priming before use
  • PurgeTrouble: Line requires purging

Hardware and Calibration Troubles

  • CalibrationRequiredTrouble: Component needs calibration
  • BoardNotLinkedTrouble: Hardware board not connected
  • BoardNotLinkedRollupTrouble: Multiple boards offline
  • PumpErrorTrouble: Pump malfunction detected
  • PumpEncoderErrorTrouble: Pump encoder reading issues

System Troubles

  • CarbTankLockupTrouble: Carbonation system locked
  • Co2EmptyTrouble: CO2 supply depleted
  • WaterPressureLowTrouble: Insufficient water pressure
  • AmbientWaterRestrictedTrouble: Water restricted due to pressure

Maintenance Troubles

  • FilterReplacementDueTrouble: Filter needs replacement
  • MaintenanceRequiredTrouble: Scheduled maintenance due
  • CleaningRequiredTrouble: Component requires cleaning

Working with Trouble Resolution

Many troubles support resolution actions that return futures for tracking completion:

// Example: Resolving troubles in React components
function PrimeRequiredAlert({ holderId }: { holderId: string }) {
  const { model: holder } = useModel<HolderModel>(holderId);
  const primeTroubles = holder.troublesByType["PrimeTrouble"] || [];
  const handlePrime = async () => {
    const trouble = primeTroubles[0];
    if (!trouble?._resolvable) return;
    try {
      await trouble.resolve();
      // The trouble model automatically tracks resolution state
    } catch (error) {
      console.error("Failed to resolve prime trouble:", error);
    }
  };
  if (primeTroubles.length === 0) return null;
  const trouble = primeTroubles[0];
  return (
    <Alert severity="warning">
      Prime required for {trouble.data.ingredientName}
      {trouble.progress > 0 && (
        <LinearProgress variant="determinate" value={trouble.progress} />
      )}
      {trouble._resolvable && (
        <Button
          onClick={handlePrime}
          disabled={trouble.isRunning}
        >
          {trouble.isRunning ? "Priming..." : "Prime Now"}
        </Button>
      )}
    </Alert>
  );
}

Enriching Trouble Data for UI Display

The trouble system’s data enrichment capabilities enable rich UI experiences:

// Register enrichment for specific trouble types
ExtensionManager[EXTENSION_TROUBLE_DATA_MAPPER].register(
  "IngredientEmptyTrouble",
  async (trouble: TroubleModel) => {
    // Get detailed ingredient information
    const ingredientId = trouble.data.ingredientId;
    const { model: ingredient } = getKosModelSync<IngredientModel>(ingredientId);
    const holderPath = trouble.data.holderPath;
    const { model: holder } = getKosModelSync<HolderModel>(holderPath);
    return {
      ingredientDisplayName: ingredient?.displayName || "Unknown",
      ingredientType: ingredient?.type,
      holderName: holder?.name || "Unknown",
      holderSlot: holder?.slice,
      lastRefillDate: holder?.data?.lastRefill,
      estimatedEmptyDate: calculateEmptyDate(ingredient, holder),
      reorderUrl: ingredient?.data?.reorderUrl,
    };
  }
);
// UI component can use enriched data
function TroubleCard({ trouble }: { trouble: TroubleModel }) {
  if (trouble.type === "IngredientEmptyTrouble") {
    return (
      <div className="trouble-card">
        <h3>{trouble.data.ingredientDisplayName} Empty</h3>
        <p>Holder: {trouble.data.holderName} (Slot {trouble.data.holderSlot})</p>
        <p>Last refilled: {formatDate(trouble.data.lastRefillDate)}</p>
        {trouble.data.reorderUrl && (
          <button onClick={() => window.open(trouble.data.reorderUrl)}>
            Reorder Ingredient
          </button>
        )}
      </div>
    );
  }
  // ... handle other trouble types
}

Using Troubles in React Components

The trouble system integrates seamlessly with React components to drive UI decisions:

// Component that responds to specific device states
function HolderStatusDisplay({ holderId }: { holderId: string }) {
  const { model: holder } = useModel<HolderModel>(holderId);
  // The @kosTroubleAware decorator provides troublesByType as Record<string, TroubleModel[]>
  const primeTroubles = holder.troublesByType["PrimeTrouble"] || [];
  const cleaningTroubles = holder.troublesByType["CleaningRequiredTrouble"] || [];
  if (primeTroubles.length > 0) {
    const trouble = primeTroubles[0];
    return (
      <Alert severity="warning">
        Prime required for {trouble.data.ingredientName}
        {trouble._resolvable && (
          <Button onClick={() => trouble.resolve("operator")}>
            Prime Now
          </Button>
        )}
      </Alert>
    );
  }
  if (cleaningTroubles.length > 0) {
    // Device can still operate, but show notification
    return (
      <InfoBanner>
        Cleaning recommended for optimal performance
        <Link to="/maintenance/cleaning">View Instructions</Link>
      </InfoBanner>
    );
  }
  return <StatusOk>Holder {holder.name} ready</StatusOk>;
}
// Using trouble status for quick checks
function PumpOperationalStatus({ pumpId }: { pumpId: string }) {
  const { model: pump } = useModel<PumpModel>(pumpId);
  // Models with @kosTroubleAware also provide troubleStatus
  if (pump.troubles.length === 0) {
    return <span>Operating normally</span>;
  }
  // Check specific trouble conditions
  const blockingTroubles = pump.troubles.filter(t =>
    t.type === "PumpErrorTrouble" ||
    t.type === "CalibrationRequiredTrouble"
  );
  if (blockingTroubles.length > 0) {
    return <span className="error">Pump unavailable: {pump.troubleStatus}</span>;
  }
  return <span className="warning">{pump.troubleStatus}</span>;
}

Key Concepts Summary

The KOS Trouble System provides device state management and user notification through:

  1. Named Trouble Types: Specific conditions like PrimeTrouble, CalibrationRequiredTrouble with consistent naming
  2. Handle Path Association: Troubles map to device components via handle paths through interface data
  3. Extension Points:
    • EXTENSION_TROUBLE_MAPPER: Maps interfaces to handle paths
    • EXTENSION_TROUBLE_DATA_MAPPER: Enriches trouble data
  4. Automatic Path Filtering: @kosTroubleAware decorator provides component-specific troubles
  5. Role-Based Visibility: Troubles filtered by CREW, TECHNICIAN, or NONE roles
  6. Resolution Actions: Programmatic trouble resolution with progress tracking
  7. Real-time Updates: WebSocket-based trouble lifecycle management

The system provides a flexible way to communicate device state, trigger user notifications, and manage operational conditions without treating every situation as an error.


Ready to explore more KOS patterns? Continue with React Integration Patterns to learn how trouble information flows into user interfaces, or dive into Model Relationships to understand how troubles coordinate with other KOS systems.