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:
EXTENSION_TROUBLE_MAPPER: Maps interface types to handle pathsEXTENSION_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-sensorandcalibrationinterfaces - Interface data: Contains fields like
sensorPath(the handle path to the sensor),currentReading,expectedReading,driftAmount, andlastCalibration
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 emptyIngredientLowTrouble: Ingredient running lowNotAllowedTrouble: Ingredient not allowed in specific holderAgitatedInStaticTrouble: Ingredient requires agitation but is in static slotPrimeTrouble: Line requires priming before usePurgeTrouble: Line requires purging
Hardware and Calibration Troubles
CalibrationRequiredTrouble: Component needs calibrationBoardNotLinkedTrouble: Hardware board not connectedBoardNotLinkedRollupTrouble: Multiple boards offlinePumpErrorTrouble: Pump malfunction detectedPumpEncoderErrorTrouble: Pump encoder reading issues
System Troubles
CarbTankLockupTrouble: Carbonation system lockedCo2EmptyTrouble: CO2 supply depletedWaterPressureLowTrouble: Insufficient water pressureAmbientWaterRestrictedTrouble: Water restricted due to pressure
Maintenance Troubles
FilterReplacementDueTrouble: Filter needs replacementMaintenanceRequiredTrouble: Scheduled maintenance dueCleaningRequiredTrouble: 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:
- Named Trouble Types: Specific conditions like
PrimeTrouble,CalibrationRequiredTroublewith consistent naming - Handle Path Association: Troubles map to device components via handle paths through interface data
- Extension Points:
EXTENSION_TROUBLE_MAPPER: Maps interfaces to handle pathsEXTENSION_TROUBLE_DATA_MAPPER: Enriches trouble data
- Automatic Path Filtering:
@kosTroubleAwaredecorator provides component-specific troubles - Role-Based Visibility: Troubles filtered by CREW, TECHNICIAN, or NONE roles
- Resolution Actions: Programmatic trouble resolution with progress tracking
- 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.
